diff --git a/.gitignore b/.gitignore index ee2a189..ac202f0 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,6 @@ vgcore.* config.h dunst.1 org.knopwob.dunst.service +dunst.systemd.service dunstify +test/test diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..09d80de --- /dev/null +++ b/.travis.yml @@ -0,0 +1,17 @@ +before_install: + - sudo apt-get -qq update + - sudo apt-get install -y libdbus-1-dev libx11-dev libxinerama-dev libxft-dev libxss-dev libxdg-basedir-dev libglib2.0-dev libpango1.0-dev libcairo2-dev libfreetype6-dev libnotify-dev libgtk2.0-dev +dist: trusty +sudo: required +language: c +script: make && make test +compiler: + - clang + - gcc + +notifications: + irc: + channels: + - "chat.freenode.net#dunst" + on_success: always + on_failure: always diff --git a/INSTALL b/INSTALL deleted file mode 100644 index de50361..0000000 --- a/INSTALL +++ /dev/null @@ -1,23 +0,0 @@ -The following dependencies are needed: - - dbus - libxinerama - libxft - libxss - libxdg-basedir - glib - pango/cairo - -On Debian and Debian-based distros you'll probably also need the *-dev packages. - -With all dependencies present you can build dunst with: - - $ make - -If you're getting compilation errors after an update try compiling -with the default config.h. -If you didn't edit your config.h you can just delete it. - -and install with: - - $ sudo make PREFIX=/usr install diff --git a/Makefile b/Makefile index 3d31830..9e6d6a5 100644 --- a/Makefile +++ b/Makefile @@ -3,15 +3,10 @@ include config.mk -SRC = x.c \ - dunst.c \ - dbus.c \ - utils.c \ - option_parser.c \ - settings.c \ - rules.c \ - menu.c \ - notification.c +CFLAGS += -I. +LDFLAGS += -L. + +SRC = $(shell ls src/*.c) OBJ = ${SRC:.c=.o} V ?= 0 @@ -29,7 +24,7 @@ options: .c.o: @echo CC -c $< - ${CC} -c $< ${CFLAGS} + ${CC} -o $@ -c $< ${CFLAGS} ${OBJ}: config.h config.mk @@ -38,52 +33,75 @@ config.h: config.def.h @echo creating $@ from $< @cp $< $@ -dunst: ${OBJ} +dunst: options ${OBJ} main.o @echo "${CC} ${CFLAGS} -o $@ ${OBJ} ${LDFLAGS}" - @${CC} ${CFLAGS} -o $@ ${OBJ} ${LDFLAGS} + @${CC} ${CFLAGS} -o $@ ${OBJ} main.o ${LDFLAGS} dunstify: - @${CC} -o $@ dunstify.c -std=c99 $(shell pkg-config --libs --cflags glib-2.0 libnotify) + @${CC} ${CFLAGS} -o $@ dunstify.c -std=c99 $(shell pkg-config --libs --cflags glib-2.0 libnotify) -debug: ${OBJ} - @echo CC -o $@ - @${CC} ${CFLAGS} -O0 -o dunst ${OBJ} ${LDFLAGS} - -clean: - @echo cleaning - rm -f ${OBJ} - rm -f dunst - rm -f dunst.1 +clean-dunst: + rm -f dunst ${OBJ} main.o rm -f org.knopwob.dunst.service - rm -f core + rm -f dunst.systemd.service + +clean-dunstify: rm -f dunstify -doc: dunst.1 -dunst.1: README.pod +clean-doc: + rm -f docs/dunst.1 + +clean: clean-dunst clean-dunstify clean-doc test-clean + +distclean: clean clean-config + +clean-config: + rm -f config.h + +doc: docs/dunst.1 +docs/dunst.1: docs/dunst.pod pod2man --name=dunst -c "Dunst Reference" --section=1 --release=${VERSION} $< > $@ service: @sed "s|##PREFIX##|$(PREFIX)|" org.knopwob.dunst.service.in > org.knopwob.dunst.service + @sed "s|##PREFIX##|$(PREFIX)|" contrib/dunst.systemd.service.in > dunst.systemd.service -install: all - @echo installing executables to ${DESTDIR}${PREFIX}/bin +install-dunst: dunst doc mkdir -p ${DESTDIR}${PREFIX}/bin - cp -f dunst ${DESTDIR}${PREFIX}/bin - chmod 755 ${DESTDIR}${PREFIX}/bin/dunst - @echo installing manual pages to ${DESTDIR}${MANPREFIX}/man1 + install -m755 dunst ${DESTDIR}${PREFIX}/bin mkdir -p ${DESTDIR}${MANPREFIX}/man1 - cp -f dunst.1 ${DESTDIR}${MANPREFIX}/man1/ - chmod 644 ${DESTDIR}${MANPREFIX}/man1/dunst.1 - mkdir -p "${DESTDIR}${PREFIX}/share/dunst" - cp -f dunstrc ${DESTDIR}${PREFIX}/share/dunst - mkdir -p "${DESTDIR}${PREFIX}/share/dbus-1/services/" - cp -vf org.knopwob.dunst.service "${DESTDIR}${PREFIX}/share/dbus-1/services/org.knopwob.dunst.service" + 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: install-dunst install-doc install-service uninstall: - @echo removing executables from ${DESTDIR}${PREFIX}/bin + @echo Removing executables from ${DESTDIR}${PREFIX}/bin rm -f ${DESTDIR}${PREFIX}/bin/dunst - @echo removing manual page from ${DESTDIR}${MANPREFIX}/man1 - rm -f ${DESTDIR}${MANPREFIX}/man1/dunst - rm -f ${DESTDIR}${PREFIX}/share/dbus-1/service/org.knopwob.dunst.service + @echo Removing manual page from ${DESTDIR}${MANPREFIX}/man1 + rm -f ${DESTDIR}${MANPREFIX}/man1/dunst.1 + @echo Removing service file from ${DESTDIR}${PREFIX}/share/dbus-1/services + rm -f ${DESTDIR}${PREFIX}/share/dbus-1/services/org.knopwob.dunst.service + @echo Removing documentation directory ${DESTDIR}${PREFIX}/share/dunst + rm -rf ${DESTDIR}${PREFIX}/share/dunst + +test: test/test + cd test && ./test + +TEST_SRC = $(shell ls test/*.c) +TEST_OBJ = $(TEST_SRC:.c=.o) + +test/test: ${OBJ} ${TEST_OBJ} + ${CC} ${CFLAGS} -o $@ ${TEST_OBJ} ${OBJ} ${LDFLAGS} + +test-clean: + rm -f test/test test/*.o .PHONY: all options clean dist install uninstall diff --git a/README.md b/README.md new file mode 100644 index 0000000..fbeeb32 --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +## Dunst + + +## 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: + +- dbus +- libxinerama +- libxft +- libxss +- libxdg-basedir +- glib +- pango +- cairo + +On Debian and Debian-based distros you'll probably also need the *-dev packages. + +To build it: +```bash +cd dunst +make +sudo make PREFIX=/usr install +``` + + +## Bug reports + +Please use the [issue tracker][issue-tracker] provided by GitHub to send us bug reports or feature requests. + + +## Author + +written by Sascha Kruse + +[issue-tracker]: https://github.com/knopwob/dunst/issues +## Copyright + +copyright 2013 Sascha Kruse and contributors (see LICENSE for licensing information) + +If you feel that copyrights are violated, please send me an email. diff --git a/README.pod b/README.pod deleted file mode 100644 index 3389f43..0000000 --- a/README.pod +++ /dev/null @@ -1,201 +0,0 @@ -=head1 NAME - -dunst - A customizable and lightweight notification-daemon - -=head1 SYNOPSIS - -dunst [-geometry geom] [-shrink shrink] [-fn font] [-nf/nb/lf/lb/cf/cb color] -[-to/nto/lto/cto secs] [-format fmt] [-key key] [-mod mod] [-mon n] -[-t/title title] [-c/class class] [-v] - -=head1 DESCRIPTION - -Dunst is a highly configurable and lightweight notification daemon. - -=head1 OPTIONS - -=over 4 - -=item B<-h/--help> - -display help message. - -=item B<-fn font> - -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. - -=item B<-lb/nb/cb color> - -defines the background color of low/normal/critical messages. - -=item B<-lf/nf/cf color> - -defines the foreground color of low/normal/critical messages. - -=item B<-to secs> - -defines the default timeout. Can be (partially) overridden by -the following options. - -=item B<-lto/nto/cto secs> - -timeouts for low/normal/critical messages. - -=item B<-key key> - -remove notification by pressing key. - -=item B<-all_key key> - -remove all notifications by pressing key. - -=item B<-history_key key> - -redisplay last notification by pressing key. - -=item B<-format fmt> - -defines the format of the message. See FORMAT. - -=item B<-mon n> - -show the notification on monitor n. - -=item B<-follow mode> - -display notifications on focused monitor. Possible options are "mouse" to follow the mouse cursor, "keyboard" to follow the window with the keyboard focus and "none" to disable focus following. "mouse" and "keyboard" overwrite the -mon option. - -=item B<-s> - -sort messages by urgency. - -=item B<-ns> - -don't sort messages by urgency. - -=item B<-geometry [{width}]x{height}][+/-{x}+/-{y}]> - -The geometry of the message window. The height is measured -in lines 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. -see also EXAMPLES show the notification on monitor n. - -=item B<-t/-title title> - -Define the title of notification windows spawned by dunst. - -=item B<-t/-class class> - -Define the class of notification windows spawned by dunst. - -=item B<-shrink> - -Shrink window if it's smaller than the width. Will be ignored if width is 0. - -=item B<-lh/-line_height> height - -The height of a single line in pixel. If the height is smaller than the font height, it will get raised to the font height. - -=item B<-print> - -Print notifications to stdout. This might be useful for logging, setting up rules or using the output in other scripts. - -=item B<-v/--version> - -print version information. - - -=item B<-config file> - -use alternative config file. - -=item B<-always_run_script> - -Always run rule-defined scripts, even if the notification is suppressed with format = "". - -=back - -=head1 FORMAT - -fmt is a string containing placeholders. The placeholders will be replaced with the corresponding text, or if the text is not present, nothing. Possible placeholders are: - -=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%]) - -=back - -=head1 COLORS - -Every option that needs a color as argument accepts #RGB, #RRGGBB and -X color names. - - -=head1 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 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 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. - -=head1 AUTHOR - -written by Sascha Kruse - -=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/RELEASE_NOTES.1.0.0 b/RELEASE_NOTES similarity index 70% rename from RELEASE_NOTES.1.0.0 rename to RELEASE_NOTES index 8ddc231..47be52d 100644 --- a/RELEASE_NOTES.1.0.0 +++ b/RELEASE_NOTES @@ -1,3 +1,7 @@ +=================================================================================== +Release Notes For v1.0.0 +=================================================================================== + PACKAGE MAINTAINERS: There are new dependencies introduced with this version: *glib @@ -53,3 +57,23 @@ PACKAGE MAINTAINERS: 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/RELEASE_NOTES.0.4.0 b/RELEASE_NOTES.0.4.0 deleted file mode 100644 index 3a7a43c..0000000 --- a/RELEASE_NOTES.0.4.0 +++ /dev/null @@ -1,17 +0,0 @@ -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/config.def.h b/config.def.h index 3f00209..d91e495 100644 --- a/config.def.h +++ b/config.def.h @@ -56,6 +56,8 @@ char *dmenu = "/usr/bin/dmenu"; char *browser = "/usr/bin/firefox"; +int max_icon_size = 32; + /* paths to default icons */ char *icon_folders = "/usr/share/icons/gnome/16x16/status/:/usr/share/icons/gnome/16x16/devices/"; diff --git a/config.mk b/config.mk index 5171442..cd46c19 100644 --- a/config.mk +++ b/config.mk @@ -5,7 +5,7 @@ MANPREFIX = ${PREFIX}/share/man # In dist tarballs, the version is stored in the VERSION files. VERSION := '$(shell [ -f VERSION ] && cat VERSION)' ifeq ('',$(VERSION)) -VERSION := $(shell git describe) +VERSION := $(shell git describe --tags) endif # Xinerama, comment if you don't want it @@ -22,12 +22,12 @@ ifeq (${PKG_CONFIG}, ${EMPTY}) endif # flags -CPPFLAGS += -D_BSD_SOURCE -DVERSION=\"${VERSION}\" ${XINERAMAFLAGS} ${INIFLAGS} +CPPFLAGS += -D_DEFAULT_SOURCE -DVERSION=\"${VERSION}\" ${XINERAMAFLAGS} ${INIFLAGS} CFLAGS += -g --std=gnu99 -pedantic -Wall -Wno-overlength-strings -Os ${STATIC} ${CPPFLAGS} ${EXTRACFLAGS} pkg_config_packs := dbus-1 x11 freetype2 xext xft xscrnsaver \ "glib-2.0 >= 2.36" gio-2.0 \ - pango cairo pangocairo + pango cairo pangocairo gdk-2.0 # check if we need libxdg-basedir ifeq (,$(findstring STATIC_CONFIG,$(CFLAGS))) diff --git a/contrib/dunst.systemd.service.in b/contrib/dunst.systemd.service.in new file mode 100644 index 0000000..9ece8da --- /dev/null +++ b/contrib/dunst.systemd.service.in @@ -0,0 +1,11 @@ +[Unit] +Description=Dunst notification daemon + +[Service] +Type=simple +ExecStart=##PREFIX##/bin/dunst +Environment=DISPLAY=:0 + +[Install] +WantedBy=multi-user.target + diff --git a/docs/dunst.pod b/docs/dunst.pod new file mode 100644 index 0000000..ae54efb --- /dev/null +++ b/docs/dunst.pod @@ -0,0 +1,304 @@ +=head1 NAME + +dunst - A customizable and lightweight notification-daemon + +=head1 SYNOPSIS + +dunst [-conf file] [-fn font] [-geometry geom] [-lf/lb/lfr/nf/nb/nfr/cf/cb/cfr color] +[-lto/nto/cto secs] [-lh/nh/sep_height height] [-format fmt] [-follow mode] [-mon n] +[-history_length n] [-t/title title] [-c/class class] [-v] + +=head1 DESCRIPTION + +Dunst is a highly configurable and lightweight notification daemon. + +=head1 OPTIONS + +=over 4 + +=item B<-h/--help> + +display help message. + +=item B<-conf/-config file> + +use alternative config file. + +=item B<-fn font> + +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. + +=item B<-geometry [{width}]x{height}][+/-{x}+/-{y}]> + +The geometry of the message window. The height is measured +in lines 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. + +=item B<-lf/nf/cf color> + +Defines the foreground color of low/normal/critical messages. + +=item B<-lb/nb/cb color> + +Defines the background color of low/normal/critical messages. + +=item B<-lfr/nfr/cfr color> + +Defines the frame color of low/normal/critical messages. + +=item B<-lto/nto/cto secs> + +Defines timeouts for low/normal/critical messages. + +=item B<-lh/-line_height height> + +Defines the height of a single line in pixel. If the height is smaller than the font height, it will get raised to the font height. + +=item B<-nh/-notification_height height> + +Defines the height of the notification window. + +=item B<-sep_height/-separator_height height> + +Defines the height of the separator line. + +=item B<-format fmt> + +Defines the format of the message. See FORMAT. + +=item B<-follow mode> + +Display notifications on focused monitor. Possible options are "mouse" to follow the mouse cursor, "keyboard" to follow the window +with the keyboard focus and "none" to disable focus following. "mouse" and "keyboard" overwrite the -mon option. + +=item B<-mon n> + +Show the notification on monitor n. + +=item B<-history_length n> + +Max amount of notifications kept in history. + +=item B<-t/-title title> + +Defines the title of notification windows spawned by dunst. + +=item B<-t/-class class> + +Defines the class of notification windows spawned by dunst. + +=item B<-key key> + +Remove notification by pressing key. + +=item B<-all_key key> + +Remove all notifications by pressing key. + +=item B<-history_key key> + +Redisplay last notification by pressing key. + +=item B<-context_key key> + +Shortcut for context menu. + +=item B<-alignment position> + +Defines the align position of notifications to left/center/right. + +=item B<-icon_position position> + +Defines the align position of icons to left/right or off to disable icons. + +=item B<-max_icon_size size> + +Defines the max size for icons to scale. If icon_position is set to off, icons won't be displayed. + +=item B<-icon_folders path> + +Defines the path to the folder with the default icons. + +=item B<-li/ni/ci icon> + +Defines the icon for low/normal/critical messages. + +=item B<-padding pad> + +Defines padding between text and separator. + +=item B<-horizontal_padding pad> + +Defines horizontal padding. + +=item B<-transparency range> + +Defines transparency range between 0-100 + +=item B<-sep_color/-separator_color color> + +Defines color of the separator line. + +=item B<-frame_color color> + +Defines color of the frame around window. + +=item B<-frame_width width> + +Defines width of frame around window. + +=item B<-idle_threshold secs> + +Don't timeout notifications if user is longer idle than threshold. + +=item B<-show_age_threshold secs> + +Show age of message if message is older than show_age_threshold seconds. Set to -1 to disable. + +=item B<-bounce_freq freq> + +The frequency with which text that is longer than the notification window allows bounces back and forth. + +=item B<-markup> + +Allow markups in notifications/formats. + +=item B<-plain> + +Treat incoming notifications as plain text. + +=item B<-indicate_hidden> + +Show how many notifications are hidden. + +=item B<-word_wrap> + +Truncating long lines or do word wrap. + +=item B<-ignore_newline> + +Ignore newline characters in notifications. + +=item B<-stack_duplicates> + +Merge multiple notifications with the same content. + +=item B<-hide_duplicates_count> + +Hide count of the merged notifications with the same content. + +=item B<-sticky_history> + +Don't timeout notifications popped up from history. + +=item B<-show_indicators> + +Show indicators for actions and URLs. + +=item B<-sort> + +Sort messages by urgency. + +=item B<-shrink> + +Shrink window if it's smaller than the width. Will be ignored if width is 0. + +=item B<-print> + +Print notifications to stdout. This might be useful for logging, setting up rules or using the output in other scripts. + +=item B<-v/--version> + +Print version information. + +=item B<-always_run_script> + +Always run rule-defined scripts, even if the notification is suppressed with format = "". + +=back + +=head1 FORMAT + +fmt is a string containing placeholders. +The placeholders will be replaced with the corresponding text, or if the text is not present, nothing. Possible placeholders are: + +=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%]) + +=back + +=head1 COLORS + +Every option that needs a color as argument accepts #RGB, #RRGGBB and +X color names. + + +=head1 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 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 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. + +=head1 AUTHOR + +written by Sascha Kruse + +=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/dunstify.c b/dunstify.c index 5a6fafe..6ae1788 100644 --- a/dunstify.c +++ b/dunstify.c @@ -49,7 +49,7 @@ 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", iter->data); + g_print("%s\n", (char *)iter->data); } } } @@ -171,7 +171,7 @@ int get_id(NotifyNotification *n) return kn->id; } -int put_id(NotifyNotification *n, guint32 id) +void put_id(NotifyNotification *n, guint32 id) { knickers *kn = n->priv; @@ -199,7 +199,7 @@ void closed(NotifyNotification *n, gpointer foo) void add_action(NotifyNotification *n, char *str) { char *action = str; - char *label = strstr(str, ","); + char *label = strchr(str, ','); if (!label || *(label+1) == '\0') { g_printerr("Malformed action. Excpected \"action,label\", got \"%s\"", str); @@ -215,14 +215,14 @@ void add_action(NotifyNotification *n, char *str) void add_hint(NotifyNotification *n, char *str) { char *type = str; - char *name = strstr(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 = strstr(name, ":"); + char *value = strchr(name, ':'); if (!value || *(value+1) == '\0') { g_printerr("Malformed hint. Expected \"type:name:value\", got \"%s\"", str); return; @@ -312,3 +312,5 @@ int main(int argc, char *argv[]) die(0); } + +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/dunstrc b/dunstrc index 61fa321..cd3eb29 100644 --- a/dunstrc +++ b/dunstrc @@ -69,6 +69,12 @@ # screen width minus the width defined in within the geometry option. geometry = "300x5-30+20" + # Define the title of the windows spawned by dunst + title = Dunst + + # Define the class of the windows spawned by dunst + class = Dunst + # Shrink window if it's smaller than the width. Will be ignored if # width is 0. shrink = no @@ -99,6 +105,12 @@ # will be ignored. follow = mouse + # Merge multiple notifications with the same content + stack_duplicates = true + + # Hide count of the merged notifications with the same content + hide_duplicates_count = false + # Should a notification popped up from history be sticky or timeout # as if it would normally do. sticky_history = yes @@ -151,9 +163,15 @@ # 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_folders = /usr/share/icons/gnome/16x16/status/:/usr/share/icons/gnome/16x16/devices/ + # Always run rule-defined scripts, even if the notification is suppressed + always_run_stript = true + [frame] width = 3 color = "#aaaaaa" @@ -173,7 +191,8 @@ # Redisplay last message(s). # On the US keyboard layout "grave" is normally above TAB and left - # of "1". + # of "1". Make sure this key actually exists on your keyboard layout, + # e.g. check output of 'xmodmap -pke' history = ctrl+grave # Context menu. @@ -185,17 +204,23 @@ 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. diff --git a/main.c b/main.c new file mode 100644 index 0000000..e31b420 --- /dev/null +++ b/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/dbus.c b/src/dbus.c similarity index 82% rename from dbus.c rename to src/dbus.c index 27072e4..2d0f53c 100644 --- a/dbus.c +++ b/src/dbus.c @@ -59,21 +59,22 @@ static const char *introspection_xml = " " ""; -static void onGetCapabilities(GDBusConnection * connection, +static void on_get_capabilities(GDBusConnection * connection, const gchar * sender, const GVariant * parameters, GDBusMethodInvocation * invocation); -static void onNotify(GDBusConnection * connection, +static void on_notify(GDBusConnection * connection, const gchar * sender, GVariant * parameters, GDBusMethodInvocation * invocation); -static void onCloseNotification(GDBusConnection * connection, +static void on_close_notification(GDBusConnection * connection, const gchar * sender, GVariant * parameters, GDBusMethodInvocation * invocation); -static void onGetServerInformation(GDBusConnection * connection, +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, @@ -84,13 +85,13 @@ void handle_method_call(GDBusConnection * connection, GDBusMethodInvocation * invocation, gpointer user_data) { if (g_strcmp0(method_name, "GetCapabilities") == 0) { - onGetCapabilities(connection, sender, parameters, invocation); + on_get_capabilities(connection, sender, parameters, invocation); } else if (g_strcmp0(method_name, "Notify") == 0) { - onNotify(connection, sender, parameters, invocation); + on_notify(connection, sender, parameters, invocation); } else if (g_strcmp0(method_name, "CloseNotification") == 0) { - onCloseNotification(connection, sender, parameters, invocation); + on_close_notification(connection, sender, parameters, invocation); } else if (g_strcmp0(method_name, "GetServerInformation") == 0) { - onGetServerInformation(connection, sender, parameters, + on_get_server_information(connection, sender, parameters, invocation); } else { printf("WARNING: sender: %s; unknown method_name: %s\n", sender, @@ -98,7 +99,7 @@ void handle_method_call(GDBusConnection * connection, } } -static void onGetCapabilities(GDBusConnection * connection, +static void on_get_capabilities(GDBusConnection * connection, const gchar * sender, const GVariant * parameters, GDBusMethodInvocation * invocation) @@ -118,7 +119,7 @@ static void onGetCapabilities(GDBusConnection * connection, g_variant_unref(value); } -static void onNotify(GDBusConnection * connection, +static void on_notify(GDBusConnection * connection, const gchar * sender, GVariant * parameters, GDBusMethodInvocation * invocation) { @@ -140,9 +141,11 @@ static void onNotify(GDBusConnection * connection, gchar *fgcolor = NULL; gchar *bgcolor = NULL; gchar *category = NULL; + RawImage *raw_icon = NULL; actions->actions = NULL; actions->count = 0; + actions->dmenu_str = NULL; { GVariantIter *iter = g_variant_iter_new(parameters); @@ -232,6 +235,23 @@ static void onNotify(GDBusConnection * connection, dict_value, NULL); } + 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); + } + dict_value = g_variant_lookup_value(content, "value", @@ -275,14 +295,12 @@ static void onNotify(GDBusConnection * connection, } } - notification *n = malloc(sizeof(notification)); - if(n == NULL) { - die("Unable to allocate memory", EXIT_FAILURE); - } + notification *n = notification_create(); n->appname = appname; n->summary = summary; n->body = body; n->icon = icon; + n->raw_icon = raw_icon; n->timeout = timeout; n->allow_markup = settings.allow_markup; n->plain_text = settings.plain_text; @@ -314,7 +332,7 @@ static void onNotify(GDBusConnection * connection, run(NULL); } -static void onCloseNotification(GDBusConnection * connection, +static void on_close_notification(GDBusConnection * connection, const gchar * sender, GVariant * parameters, GDBusMethodInvocation * invocation) @@ -326,7 +344,7 @@ static void onCloseNotification(GDBusConnection * connection, g_dbus_connection_flush(connection, NULL, NULL, NULL); } -static void onGetServerInformation(GDBusConnection * connection, +static void on_get_server_information(GDBusConnection * connection, const gchar * sender, const GVariant * parameters, GDBusMethodInvocation * invocation) @@ -339,10 +357,10 @@ static void onGetServerInformation(GDBusConnection * connection, g_dbus_connection_flush(connection, NULL, NULL, NULL); } -void notificationClosed(notification * n, int reason) +void notification_closed(notification * n, int reason) { if (!dbus_conn) { - printf("DEBUG: notificationClosed but not (yet) connected\n"); + printf("DEBUG: notification_closed but not (yet) connected\n"); return; } @@ -356,12 +374,12 @@ void notificationClosed(notification * n, int reason) "NotificationClosed", body, &err); if (err) { - printf("notificationClosed ERROR\n"); + printf("notification_closed ERROR\n"); } } -void actionInvoked(notification * n, const char *identifier) +void action_invoked(notification * n, const char *identifier) { GVariant *body = g_variant_new("(us)", n->id, identifier); GError *err = NULL; @@ -412,6 +430,40 @@ static void on_name_lost(GDBusConnection * connection, exit(1); } +static RawImage * get_raw_image_from_data_hint(GVariant *icon_data) +{ + RawImage *image = 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)); + free(image); + return NULL; + } + + image->data = (guchar *) g_memdup (g_variant_get_data (data_variant), + g_variant_get_size (data_variant)); + + return image; +} + int initdbus(void) { guint owner_id; @@ -434,7 +486,10 @@ int initdbus(void) 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 ts=8 sw=8 tw=0: */ +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/dbus.h b/src/dbus.h similarity index 60% rename from dbus.h rename to src/dbus.h index 56f1dc7..0d536e6 100644 --- a/dbus.h +++ b/src/dbus.h @@ -10,9 +10,9 @@ int initdbus(void); void dbus_tear_down(int id); /* void dbus_poll(int timeout); */ -void notificationClosed(notification * n, int reason); -void actionInvoked(notification * n, const char *identifier); +void notification_closed(notification * n, int reason); +void action_invoked(notification * n, const char *identifier); #endif -/* vim: set ts=8 sw=8 tw=0: */ +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/dunst.c b/src/dunst.c similarity index 87% rename from dunst.c rename to src/dunst.c index d8bf621..9364a00 100644 --- a/dunst.c +++ b/src/dunst.c @@ -34,6 +34,7 @@ #include "utils.h" #include "rules.h" #include "notification.h" +#include "menu.h" #include "option_parser.h" #include "settings.h" @@ -272,7 +273,7 @@ gboolean run(void *data) return false; } -gboolean pause_signal (gpointer data) +gboolean pause_signal(gpointer data) { pause_display = true; wake_up(); @@ -280,7 +281,7 @@ gboolean pause_signal (gpointer data) return G_SOURCE_CONTINUE; } -gboolean unpause_signal (gpointer data) +gboolean unpause_signal(gpointer data) { pause_display = false; wake_up(); @@ -288,7 +289,31 @@ gboolean unpause_signal (gpointer data) return G_SOURCE_CONTINUE; } -int main(int argc, char *argv[]) +gboolean quit_signal(gpointer data) +{ + g_main_loop_quit(mainloop); + + return G_SOURCE_CONTINUE; +} + +static void teardown_notification(gpointer data) +{ + notification *n = data; + notification_free(n); +} + +static void teardown(void) +{ + regex_teardown(); + + g_queue_free_full(history, teardown_notification); + g_queue_free_full(displayed, teardown_notification); + g_queue_free_full(queue, teardown_notification); + + x_free(); +} + +int dunst_main(int argc, char *argv[]) { history = g_queue_new(); @@ -318,10 +343,7 @@ int main(int argc, char *argv[]) x_setup(); if (settings.startup_notification) { - notification *n = malloc(sizeof(notification)); - if(n == NULL) { - die("Unable to allocate memory", EXIT_FAILURE); - } + notification *n = notification_create(); n->appname = strdup("dunst"); n->summary = strdup("startup"); n->body = strdup("dunst is up and running"); @@ -330,14 +352,6 @@ int main(int argc, char *argv[]) n->allow_markup = false; n->plain_text = true; n->urgency = LOW; - n->icon = NULL; - n->category = NULL; - n->msg = NULL; - n->dbus_client = NULL; - n->color_strings[0] = NULL; - n->color_strings[1] = NULL; - n->actions = NULL; - n->urls = NULL; notification_init(n, 0); } @@ -364,14 +378,30 @@ int main(int argc, char *argv[]) g_source_attach(x11_source, NULL); - g_unix_signal_add(SIGUSR1, pause_signal, NULL); - g_unix_signal_add(SIGUSR2, unpause_signal, 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; } @@ -392,4 +422,4 @@ void print_version(void) exit(EXIT_SUCCESS); } -/* vim: set ts=8 sw=8 tw=0: */ +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/dunst.h b/src/dunst.h similarity index 85% rename from dunst.h rename to src/dunst.h index c4c7519..b2bc025 100644 --- a/dunst.h +++ b/src/dunst.h @@ -3,6 +3,7 @@ #pragma once #include +#include #include #include "x.h" @@ -12,7 +13,8 @@ #define PERR(msg, errnum) printf("(%d) %s : %s\n", __LINE__, (msg), (strerror(errnum))) #define LENGTH(X) (sizeof X / sizeof X[0]) -#define ColLast 2 +#define ColLast 3 +#define ColFrame 2 #define ColFG 1 #define ColBG 0 @@ -27,12 +29,14 @@ extern GQueue *displayed; extern GQueue *history; extern GSList *rules; extern bool pause_display; -extern const char *color_strings[2][3]; +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 history_pop(void); void history_push(notification *n); @@ -43,4 +47,4 @@ char *extract_urls(const char *str); void context_menu(void); void wake_up(void); void pause_signal_handler(int sig); -/* vim: set ts=8 sw=8 tw=0: */ +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/menu.c b/src/menu.c similarity index 81% rename from menu.c rename to src/menu.c index 8c2962a..06de61e 100644 --- a/menu.c +++ b/src/menu.c @@ -1,6 +1,7 @@ /* copyright 2013 Sascha Kruse and contributors (see LICENSE for licensing information) */ #define _GNU_SOURCE +#include #include #include #include @@ -15,33 +16,50 @@ #include "settings.h" #include "dbus.h" - /* - * Exctract all urls from a given string. - * - * Return: a string of urls separated by \n - * - */ +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) { - static bool is_initialized = false; - static regex_t cregex; - - if (!is_initialized) { - 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) { - printf("failed to compile regex\n"); - return NULL; - } else { - is_initialized = true; - } - } - char *urls = NULL; + if (!regex_init()) + return NULL; + const char *p = to_match; regmatch_t m; @@ -69,10 +87,10 @@ char *extract_urls(const char *to_match) return urls; } - /* - * Open url in browser. - * - */ +/* + * Open url in browser. + * + */ void open_browser(const char *url) { int browser_pid1 = fork(); @@ -93,10 +111,10 @@ void open_browser(const char *url) } } - /* - * Notify the corresponding client - * that an action has been invoked - */ +/* + * Notify the corresponding client + * that an action has been invoked + */ void invoke_action(const char *action) { notification *invoked = NULL; @@ -131,14 +149,14 @@ void invoke_action(const char *action) } if (invoked && action_identifier) { - actionInvoked(invoked, action_identifier); + action_invoked(invoked, action_identifier); } } - /* - * Dispatch whatever has been returned - * by the menu. - */ +/* + * Dispatch whatever has been returned + * by the menu. + */ void dispatch_menu_result(const char *input) { char *in = strdup(input); @@ -164,10 +182,10 @@ void dispatch_menu_result(const char *input) free(in); } - /* - * Open the context menu that let's the user - * select urls/actions/etc - */ +/* + * Open the context menu that let's the user + * select urls/actions/etc + */ void context_menu(void) { char *dmenu_input = NULL; @@ -239,4 +257,4 @@ void context_menu(void) free(dmenu_input); } -/* vim: set ts=8 sw=8 tw=0: */ +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/menu.h b/src/menu.h similarity index 73% rename from menu.h rename to src/menu.h index 805b6a1..86491f1 100644 --- a/menu.h +++ b/src/menu.h @@ -5,3 +5,6 @@ char *extract_urls(const char *to_match); void open_browser(const char *url); void invoke_action(const char *action); +void regex_teardown(void); + +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/notification.c b/src/notification.c similarity index 80% rename from notification.c rename to src/notification.c index 21ccfee..e1e74d3 100644 --- a/notification.c +++ b/src/notification.c @@ -1,7 +1,7 @@ /* copyright 2013 Sascha Kruse and contributors (see LICENSE for licensing information) */ #define _GNU_SOURCE - +#include #include #include #include @@ -9,6 +9,7 @@ #include #include #include +#include #include "dbus.h" #include "x.h" @@ -21,10 +22,10 @@ int next_notification_id = 1; - /* - * print a human readable representation - * of the given notification to stdout. - */ +/* + * print a human readable representation + * of the given notification to stdout. + */ void notification_print(notification * n) { printf("{\n"); @@ -37,6 +38,7 @@ void notification_print(notification * n) 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"); @@ -59,10 +61,10 @@ void notification_print(notification * n) printf("}\n"); } - /* - * Run the script associated with the - * given notification. - */ +/* + * Run the script associated with the + * given notification. + */ void notification_run_script(notification * n) { if (!n->script || strlen(n->script) < 1) @@ -114,10 +116,10 @@ void notification_run_script(notification * n) } } - /* - * Helper function to compare to given - * notifications. - */ +/* + * Helper function to compare to given + * notifications. + */ int notification_cmp(const void *va, const void *vb) { notification *a = (notification *) va; @@ -133,93 +135,83 @@ int notification_cmp(const void *va, const void *vb) } } - /* - * Wrapper for notification_cmp to match glib's - * compare functions signature. - */ +/* + * 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); } - /* - * Free the memory used by the given notification. - */ +int notification_is_duplicate(const notification *a, const notification *b) +{ + 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 memory used by the given notification. + */ void notification_free(notification * n) { - if (n == NULL) - return; + assert(n != NULL); free(n->appname); free(n->summary); free(n->body); free(n->icon); free(n->msg); free(n->dbus_client); + g_free(n->category); + + if (n->text_to_render) + g_free(n->text_to_render); + + if (n->urls) + g_free(n->urls); if (n->actions) { g_strfreev(n->actions->actions); free(n->actions->dmenu_str); } + if (n->raw_icon) { + if (n->raw_icon->data) + free(n->raw_icon->data); + free(n->raw_icon); + } + free(n); } - /* - * Strip any markup from text - */ +/* + * Strip any markup from text + */ char *notification_strip_markup(char *str) { - char *replace_buf, *start, *end; - if (str == NULL) { return NULL; } + /* strip all tags */ + string_strip_delimited(str, '<', '>'); + + /* unquote the remainder */ 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); - /* remove tags */ - 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); - 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); - - while ((start = strstr(str, ""); - if (end != NULL) { - replace_buf = strndup(start, end - start + 1); - str = string_replace(replace_buf, "", str); - free(replace_buf); - } else { - break; - } - } - - while ((start = strstr(str, ""); - if (end != NULL) { - replace_buf = strndup(start, end - start + 2); - str = string_replace(replace_buf, "", str); - free(replace_buf); - } else { - break; - } - } return str; } - /* - * Quote a text string for rendering with pango - */ +/* + * Quote a text string for rendering with pango + */ char *notification_quote_markup(char *str) { if (str == NULL) { @@ -235,10 +227,10 @@ char *notification_quote_markup(char *str) return str; } - /* - * Replace all occurrences of "needle" with a quoted "replacement", - * according to the allow_markup/plain_text settings. - */ +/* + * Replace all occurrences of "needle" with a quoted "replacement", + * according to the allow_markup/plain_text settings. + */ char *notification_replace_format(const char *needle, const char *replacement, char *haystack, bool allow_markup, bool plain_text) { @@ -254,19 +246,25 @@ char *notification_replace_format(const char *needle, const char *replacement, tmp = notification_quote_markup(tmp); ret = string_replace_all(needle, tmp, haystack); free(tmp); - } else if (!allow_markup) { + } else { tmp = strdup(replacement); - if (!settings.ignore_newline) { + if (settings.ignore_newline) { + tmp = string_replace_all("
", " ", tmp); + tmp = string_replace_all("
", " ", tmp); + tmp = string_replace_all("
", " ", tmp); + } else { tmp = string_replace_all("
", "\n", tmp); tmp = string_replace_all("
", "\n", tmp); tmp = string_replace_all("
", "\n", tmp); } - tmp = notification_strip_markup(tmp); - tmp = notification_quote_markup(tmp); + + if (!allow_markup) { + tmp = notification_strip_markup(tmp); + tmp = notification_quote_markup(tmp); + } + ret = string_replace_all(needle, tmp, haystack); free(tmp); - } else { - ret = string_replace_all(needle, replacement, haystack); } return ret; @@ -304,21 +302,48 @@ char *notification_extract_markup_urls(char **str_ptr) { } free(replace_buf); } else { - break; + break; } } *str_ptr = str; return urls; } - /* - * Initialize the given notification and add it to - * the queue. Replace notification with id if id > 0. - */ +/* + * Create notification struct and initialise everything to NULL, + * this function is guaranteed to return a valid pointer. + */ +notification *notification_create(void) +{ + notification *n = malloc(sizeof(notification)); + if(n == NULL) die("Unable to allocate memory", EXIT_FAILURE); + memset(n, 0, sizeof(notification)); + return n; +} + +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 and add it to + * the queue. Replace notification with id if id > 0. + * + * n should be a pointer to a notification allocated with + * notification_create, it is undefined behaviour to pass a notification + * allocated some other way. + */ int notification_init(notification * n, int id) { - if (n == NULL) - return -1; + assert(n != NULL); + + //Prevent undefined behaviour by initialising required fields + notification_init_defaults(n); if (strcmp("DUNST_COMMAND_PAUSE", n->summary) == 0) { pause_display = true; @@ -362,7 +387,7 @@ int notification_init(notification * n, int id) n->msg = string_replace_all("%p", "", n->msg); } - n->msg = g_strstrip(n->msg); + n->msg = g_strchomp(n->msg); if (id == 0) { n->id = ++next_notification_id; @@ -378,9 +403,7 @@ int notification_init(notification * n, int id) for (GList * iter = g_queue_peek_head_link(queue); iter; iter = iter->next) { notification *orig = iter->data; - if (strcmp(orig->appname, n->appname) == 0 - && strcmp(orig->summary, n->summary) == 0 - && strcmp(orig->body, n->body) == 0) { + if (notification_is_duplicate(orig, n)) { /* If the progress differs this was probably intended to replace the notification * but notify-send was used. So don't increment dup_count in this case */ @@ -403,9 +426,7 @@ int notification_init(notification * n, int id) for (GList * iter = g_queue_peek_head_link(displayed); iter; iter = iter->next) { notification *orig = iter->data; - if (strcmp(orig->appname, n->appname) == 0 - && strcmp(orig->summary, n->summary) == 0 - && strcmp(orig->body, n->body) == 0) { + if (notification_is_duplicate(orig, n)) { /* notifications that differ only in progress hints should be expected equal, * but we want the latest message, with the latest hint value */ @@ -438,20 +459,21 @@ int notification_init(notification * n, int id) 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 == -1 ? settings.timeouts[n->urgency] : n->timeout; n->start = 0; - if (n->icon == NULL) { - n->icon = strdup(settings.icons[n->urgency]); - } - else if (strlen(n->icon) <= 0) { + if (n->icon != NULL && strlen(n->icon) <= 0) { free(n->icon); - n->icon = strdup(settings.icons[n->urgency]); + n->icon = NULL; } - if (n->category == NULL) { - n->category = ""; + if (n->raw_icon == NULL && n->icon == NULL) { + n->icon = strdup(settings.icons[n->urgency]); } n->timestamp = time(NULL); @@ -505,15 +527,15 @@ int notification_init(notification * n, int id) return n->id; } - /* - * Close the notification that has id. - * - * reasons: - * -1 -> notification is a replacement, no NotificationClosed signal emitted - * 1 -> the notification expired - * 2 -> the notification was dismissed by the user_data - * 3 -> The notification was closed by a call to CloseNotification - */ +/* + * Close the notification that has id. + * + * reasons: + * -1 -> notification is a replacement, no NotificationClosed signal emitted + * 1 -> the notification expired + * 2 -> the notification was dismissed by the user_data + * 3 -> The notification was closed by a call to CloseNotification + */ int notification_close_by_id(int id, int reason) { notification *target = NULL; @@ -541,20 +563,19 @@ int notification_close_by_id(int id, int reason) } if (reason > 0 && reason < 4 && target != NULL) { - notificationClosed(target, reason); + notification_closed(target, reason); } wake_up(); return reason; } - /* - * Close the given notification. SEE notification_close_by_id. - */ +/* + * Close the given notification. SEE notification_close_by_id. + */ int notification_close(notification * n, int reason) { - if (n == NULL) - return -1; + assert(n != NULL); return notification_close_by_id(n->id, reason); } @@ -567,11 +588,11 @@ void notification_update_text_to_render(notification *n) char *buf = NULL; - char *msg = g_strstrip(n->msg); + char *msg = g_strchomp(n->msg); /* print dup_count and msg */ - if (n->dup_count > 0 && (n->actions || n->urls) - && settings.show_indicators) { + if ((n->dup_count > 0 && !settings.hide_duplicates_count) + && (n->actions || n->urls) && settings.show_indicators) { buf = g_strdup_printf("(%d%s%s) %s", n->dup_count, n->actions ? "A" : "", @@ -580,7 +601,7 @@ void notification_update_text_to_render(notification *n) buf = g_strdup_printf("(%s%s) %s", n->actions ? "A" : "", n->urls ? "U" : "", msg); - } else if (n->dup_count > 0) { + } else if (n->dup_count > 0 && !settings.hide_duplicates_count) { buf = g_strdup_printf("(%d) %s", n->dup_count, msg); } else { buf = g_strdup(msg); @@ -627,4 +648,4 @@ int notification_get_ttl(notification *n) { int notification_get_age(notification *n) { return time(NULL) - n->timestamp; } -/* vim: set ts=8 sw=8 tw=0: */ +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/notification.h b/src/notification.h similarity index 78% rename from notification.h rename to src/notification.h index 71422ea..e6d42dc 100644 --- a/notification.h +++ b/src/notification.h @@ -7,6 +7,16 @@ #define NORM 1 #define CRIT 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; @@ -18,6 +28,7 @@ typedef struct _notification { char *summary; char *body; char *icon; + RawImage *raw_icon; char *msg; /* formatted message */ char *category; char *text_to_render; @@ -33,7 +44,7 @@ typedef struct _notification { int id; int dup_count; int displayed_height; - const char *color_strings[2]; + const char *color_strings[3]; bool first_render; int progress; /* percentage + 1, 0 to hide */ @@ -43,11 +54,13 @@ typedef struct _notification { Actions *actions; } notification; +notification *notification_create(void); int notification_init(notification * n, int id); void notification_free(notification * n); int notification_close_by_id(int id, int reason); 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); int notification_close(notification * n, int reason); void notification_print(notification * n); @@ -56,3 +69,5 @@ char *notification_quote_markup(char *str); void notification_update_text_to_render(notification *n); int notification_get_ttl(notification *n); int notification_get_age(notification *n); + +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/option_parser.c b/src/option_parser.c similarity index 95% rename from option_parser.c rename to src/option_parser.c index 6de5a76..48d92e0 100644 --- a/option_parser.c +++ b/src/option_parser.c @@ -175,7 +175,7 @@ int ini_get_bool(char *section, char *key, int def) case '0': return false; default: - return false; + return def; } } } @@ -214,7 +214,7 @@ int load_ini_file(FILE * fp) continue; if (*start == '[') { - char *end = strstr(start + 1, "]"); + char *end = strchr(start + 1, ']'); if (!end) { printf ("Warning: invalid config file at line %d\n", @@ -232,7 +232,7 @@ int load_ini_file(FILE * fp) continue; } - char *equal = strstr(start + 1, "="); + char *equal = strchr(start + 1, '='); if (!equal) { printf("Warning: invalid config file at line %d\n", line_num); @@ -244,9 +244,9 @@ int load_ini_file(FILE * fp) char *key = g_strstrip(start); char *value = g_strstrip(equal + 1); - char *quote = strstr(value, "\""); + char *quote = strchr(value, '"'); if (quote) { - char *closing_quote = strstr(quote + 1, "\""); + char *closing_quote = strchr(quote + 1, '"'); if (!closing_quote) { printf ("Warning: invalid config file at line %d\n", @@ -257,9 +257,7 @@ int load_ini_file(FILE * fp) closing_quote = '\0'; } else { - char *comment = strstr(value, "#"); - if (!comment) - comment = strstr(value, ";"); + char *comment = strpbrk(value, "#;"); if (comment) comment = '\0'; } @@ -291,7 +289,7 @@ int cmdline_find_option(char *key) return -1; } char *key1 = g_strdup(key); - char *key2 = strstr(key1, "/"); + char *key2 = strchr(key1, '/'); if (key2) { *key2 = '\0'; @@ -421,7 +419,7 @@ double option_get_double(char *ini_section, char *ini_key, char *cmdline_key, double val = cmdline_get_double(cmdline_key, def, description); if (!str) - return ini_get_int(ini_section, ini_key, def); + return ini_get_double(ini_section, ini_key, def); else return val; } @@ -473,4 +471,4 @@ char *cmdline_create_usage(void) return g_strdup(usage_str); } -/* vim: set ts=8 sw=8 tw=0: */ +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/option_parser.h b/src/option_parser.h similarity index 96% rename from option_parser.h rename to src/option_parser.h index 873897f..1b1d9df 100644 --- a/option_parser.h +++ b/src/option_parser.h @@ -33,4 +33,4 @@ int option_get_bool(char *ini_section, char *ini_key, char *cmdline_key, */ char *next_section(char *section); -/* vim: set ts=8 sw=8 tw=0: */ +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/rules.c b/src/rules.c similarity index 97% rename from rules.c rename to src/rules.c index 8687f07..dc8e465 100644 --- a/rules.c +++ b/src/rules.c @@ -78,4 +78,4 @@ bool rule_matches_notification(rule_t * r, notification * n) && (!r->category || !fnmatch(r->category, n->category, 0)) && (r->msg_urgency == -1 || r->msg_urgency == n->urgency)); } -/* vim: set ts=8 sw=8 tw=0: */ +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/rules.h b/src/rules.h similarity index 92% rename from rules.h rename to src/rules.h index f7fc734..218ce37 100644 --- a/rules.h +++ b/src/rules.h @@ -34,3 +34,5 @@ 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); + +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/settings.c b/src/settings.c similarity index 92% rename from settings.c rename to src/settings.c index c05545e..8baeffb 100644 --- a/settings.c +++ b/src/settings.c @@ -1,6 +1,8 @@ /* copyright 2013 Sascha Kruse and contributors (see LICENSE for licensing information) */ #include +#include +#include #ifndef STATIC_CONFIG #include @@ -169,6 +171,10 @@ void load_settings(char *cmdline_config_path) option_get_int("global", "show_age_threshold", "-show_age_threshold", show_age_threshold, "When should the age of the notification be displayed?"); + settings.hide_duplicates_count = + option_get_bool("global", "hide_duplicates_count", + "-hide_duplicates_count", false, + "Hide count of the merged notifications with the same content"); settings.sticky_history = option_get_bool("global", "sticky_history", "-sticky_history", sticky_history, @@ -179,8 +185,8 @@ void load_settings(char *cmdline_config_path) "Max amount of notifications kept in history"); settings.show_indicators = option_get_bool("global", "show_indicators", "-show_indicators", - show_indicators, - "Show indicators for actions \"(A)\" and URLs \"(U)\""); + show_indicators, + "Show indicators for actions \"(A)\" and URLs \"(U)\""); settings.separator_height = option_get_int("global", "separator_height", "-sep_height/-separator_height", separator_height, @@ -249,6 +255,10 @@ void load_settings(char *cmdline_config_path) } } + settings.max_icon_size = + option_get_int("global", "max_icon_size", "-max_icon_size", max_icon_size, + "Scale larger icons down to this size, set to 0 to disable"); + settings.icon_folders = option_get_string("global", "icon_folders", "-icon_folders", icon_folders, "paths to default icons"); @@ -267,6 +277,9 @@ void load_settings(char *cmdline_config_path) settings.lowfgcolor = option_get_string("urgency_low", "foreground", "-lf", 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[LOW] = option_get_int("urgency_low", "timeout", "-lto", timeouts[LOW], "Timeout for notifications with low urgency"); @@ -281,6 +294,9 @@ void load_settings(char *cmdline_config_path) option_get_string("urgency_normal", "foreground", "-nf", 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[NORM] = option_get_int("urgency_normal", "timeout", "-nto", timeouts[NORM], "Timeout for notifications with normal urgency"); @@ -295,6 +311,9 @@ void load_settings(char *cmdline_config_path) option_get_string("urgency_critical", "foreground", "-cf", 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[CRIT] = option_get_int("urgency_critical", "timeout", "-cto", timeouts[CRIT], "Timeout for notifications with critical urgency"); @@ -363,6 +382,7 @@ void load_settings(char *cmdline_config_path) 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_int(cur_section, "timeout", r->timeout); r->allow_markup = ini_get_bool(cur_section, "allow_markup", r->allow_markup); r->plain_text = ini_get_bool(cur_section, "plain_text", r->plain_text); @@ -372,7 +392,7 @@ void load_settings(char *cmdline_config_path) 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->script = ini_get_string(cur_section, "script", NULL); + r->script = ini_get_string(cur_section, "script", NULL); } #ifndef STATIC_CONFIG @@ -383,4 +403,4 @@ void load_settings(char *cmdline_config_path) } #endif } -/* vim: set ts=8 sw=8 tw=0: */ +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/settings.h b/src/settings.h similarity index 87% rename from settings.h rename to src/settings.h index c8b9631..290291f 100644 --- a/settings.h +++ b/src/settings.h @@ -6,13 +6,17 @@ typedef struct _settings { bool allow_markup; bool plain_text; bool stack_duplicates; + bool hide_duplicates_count; char *font; char *normbgcolor; char *normfgcolor; + char *normframecolor; char *critbgcolor; char *critfgcolor; + char *critframecolor; char *lowbgcolor; char *lowfgcolor; + char *lowframecolor; char *format; int timeouts[3]; char *icons[3]; @@ -29,7 +33,7 @@ typedef struct _settings { float bounce_freq; int sticky_history; int history_length; - int show_indicators; + int show_indicators; int verbosity; int word_wrap; int ignore_newline; @@ -49,6 +53,7 @@ typedef struct _settings { char **dmenu_cmd; char *browser; enum icon_position_t icon_position; + int max_icon_size; char *icon_folders; enum follow_mode f_mode; bool always_run_script; @@ -61,3 +66,5 @@ typedef struct _settings { extern settings_t settings; void load_settings(char *cmdline_config_path); + +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/utils.c b/src/utils.c similarity index 73% rename from utils.c rename to src/utils.c index 0ab4b3a..1e51da3 100644 --- a/utils.c +++ b/src/utils.c @@ -11,10 +11,10 @@ #include "dunst.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 *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) @@ -25,13 +25,21 @@ char *string_replace_at(char *buf, int pos, int len, const char *repl) buf_len = strlen(buf); repl_len = strlen(repl); size = (buf_len - len) + repl_len + 1; - tmp = malloc(size); + + if (repl_len <= len) { + tmp = buf; + } else { + tmp = malloc(size); + } memcpy(tmp, buf, pos); memcpy(tmp + pos, repl, repl_len); - memcpy(tmp + pos + repl_len, buf + pos + len, buf_len - (pos + len) + 1); + memmove(tmp + pos + repl_len, buf + pos + len, buf_len - (pos + len) + 1); + + if(tmp != buf) { + free(buf); + } - free(buf); return tmp; } @@ -105,17 +113,19 @@ char **string_to_argv(const char *s) return argv; } -int digit_count(int i) +void string_strip_delimited(char *str, char a, char b) { - i = ABS(i); - int len = 1; - - while (i > 0) { - len++; - i /= 10; + 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]; + } } - - return len; + str[iwrite] = 0; } void die(char *text, int exit_value) @@ -124,4 +134,4 @@ void die(char *text, int exit_value) exit(exit_value); } -/* vim: set ts=8 sw=8 tw=0: */ +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/utils.h b/src/utils.h similarity index 81% rename from utils.h rename to src/utils.h index 9617301..cf193cc 100644 --- a/utils.h +++ b/src/utils.h @@ -17,10 +17,11 @@ char *string_append(char *a, const char *b, const char *sep); char **string_to_argv(const char *s); +/* 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); - -int digit_count(int i); #endif -/* vim: set ts=8 sw=8 tw=0: */ +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/x.c b/src/x.c similarity index 81% rename from x.c rename to src/x.c index c645925..4316853 100644 --- a/x.c +++ b/src/x.c @@ -14,6 +14,7 @@ #include #include #include +#include #include "x.h" #include "utils.h" @@ -39,15 +40,15 @@ 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; -static color_t frame_color; - /* FIXME refactor setup teardown handlers into one setup and one teardown */ static void x_follow_setup_error_handler(void); static int x_follow_tear_down_error_handler(void); @@ -59,8 +60,6 @@ static void x_handle_click(XEvent ev); static void x_screen_info(screen_info * scr); static void x_win_setup(void); - - static color_t x_color_hex_to_double(int hexValue) { color_t color; @@ -68,7 +67,7 @@ static color_t x_color_hex_to_double(int hexValue) color.g = ((hexValue >> 8) & 0xFF) / 255.0; color.b = ((hexValue) & 0xFF) / 255.0; - return color; + return color; } static color_t x_string_to_color_t(const char *str) @@ -111,20 +110,23 @@ static color_t calculate_foreground_color(color_t bg) } -static color_t x_get_separator_color(color_t fg, color_t bg) +static color_t x_get_separator_color(colored_layout *cl, colored_layout *cl_next) { switch (settings.sep_color) { case FRAME: - return x_string_to_color_t(settings.frame_color); + 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 fg; + return cl->fg; case AUTO: - return calculate_foreground_color(bg); + return calculate_foreground_color(cl->bg); default: printf("Unknown separator color type. Please file a Bugreport.\n"); - return fg; + return cl->fg; } } @@ -137,8 +139,6 @@ static void x_cairo_setup(void) cairo_ctx.context = cairo_create(cairo_ctx.surface); cairo_ctx.desc = pango_font_description_from_string(settings.font); - - frame_color = x_string_to_color_t(settings.frame_color); } static void r_setup_pango_layout(PangoLayout *layout, int width) @@ -191,6 +191,17 @@ static bool have_dynamic_width(void) return (xctx.geometry.mask & WidthValue && xctx.geometry.w == 0); } +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; @@ -217,6 +228,7 @@ static dimension_t calculate_dimensions(GSList *layouts) 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; @@ -274,11 +286,41 @@ static dimension_t calculate_dimensions(GSList *layouts) return dim; } -static cairo_surface_t *get_icon_surface(char *icon_path) +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 && settings.icon_position != icons_off) { + 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) { @@ -287,14 +329,10 @@ static cairo_surface_t *get_icon_surface(char *icon_path) } /* absolute path? */ if (icon_path[0] == '/' || icon_path[0] == '~') { - icon_surface = cairo_image_surface_create_from_png(icon_path); - if (cairo_surface_status(icon_surface) != CAIRO_STATUS_SUCCESS) { - cairo_surface_destroy(icon_surface); - icon_surface = NULL; - } + pixbuf = get_pixbuf_from_file(icon_path); } /* search in icon_folders */ - if (icon_surface == NULL) { + if (pixbuf == NULL) { char *start = settings.icon_folders, *end, *current_folder, *maybe_icon_path; do { @@ -305,19 +343,16 @@ static cairo_surface_t *get_icon_surface(char *icon_path) maybe_icon_path = g_strconcat(current_folder, "/", icon_path, ".png", NULL); free(current_folder); - icon_surface = cairo_image_surface_create_from_png(maybe_icon_path); + pixbuf = get_pixbuf_from_file(maybe_icon_path); free(maybe_icon_path); - if (cairo_surface_status(icon_surface) == CAIRO_STATUS_SUCCESS) { - return icon_surface; - } else { - cairo_surface_destroy(icon_surface); - icon_surface = NULL; + if (pixbuf != NULL) { + return pixbuf; } start = end + 1; } while (*(end) != '\0'); } - if (icon_surface == NULL) { + if (pixbuf == NULL) { fprintf(stderr, "Could not load icon: '%s'\n", icon_path); } @@ -325,7 +360,24 @@ static cairo_surface_t *get_icon_surface(char *icon_path) g_free(uri_path); } } - return icon_surface; + 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 colored_layout *r_init_shared(cairo_t *c, notification *n) @@ -340,10 +392,51 @@ static colored_layout *r_init_shared(cairo_t *c, notification *n) pango_layout_set_ellipsize(cl->l, PANGO_ELLIPSIZE_MIDDLE); } - cl->icon = get_icon_surface(n->icon); + 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; @@ -427,10 +520,10 @@ static GSList *r_create_layouts(cairo_t *c) 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)); + 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; @@ -441,11 +534,15 @@ 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, dimension_t dim, bool first, bool last) +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 = MAX(cairo_image_surface_get_height(cl->icon), 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; @@ -454,11 +551,21 @@ static dimension_t x_render_layout(cairo_t *c, colored_layout *cl, dimension_t d 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) @@ -473,9 +580,15 @@ static dimension_t x_render_layout(cairo_t *c, colored_layout *cl, dimension_t d 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, cairo_image_surface_get_width(cl->icon) + 2 * settings.h_padding, dim.y); - else cairo_move_to(c, settings.h_padding, dim.y); + + 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); @@ -484,13 +597,16 @@ static dimension_t x_render_layout(cairo_t *c, colored_layout *cl, dimension_t d else dim.y += (int) (floor(bg_half_height) + pango_offset); - color_t sep_color = x_get_separator_color(cl->fg, cl->bg); 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); - cairo_rectangle(c, settings.frame_width, dim.y, - dim.w - 2 * settings.frame_width - , settings.separator_height); + 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; @@ -503,8 +619,8 @@ static dimension_t x_render_layout(cairo_t *c, colored_layout *cl, dimension_t d image_x, image_y = bg_y + settings.padding; - if (settings.icon_position == icons_left) image_x = settings.h_padding; - else image_x = bg_width - settings.h_padding - image_width; + 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); @@ -523,7 +639,7 @@ void x_win_draw(void) int width = dim.w; int height = dim.h; - if ((have_dynamic_width() || settings.shrink) && settings.align != left) { + if ((have_dynamic_width() || settings.shrink) && settings.align != left) { r_update_layouts_width(layouts, width); } @@ -534,16 +650,15 @@ void x_win_draw(void) x_win_move(width, height); cairo_xlib_surface_set_size(cairo_ctx.surface, width, height); - cairo_set_source_rgb(c, frame_color.r, frame_color.g, frame_color.b); - cairo_rectangle(c, 0.0, 0.0, width, height); - cairo_fill(c); - cairo_move_to(c, 0, 0); bool first = true; for (GSList *iter = layouts; iter; iter = iter->next) { - colored_layout *cl = iter->data; - dim = x_render_layout(c, cl, dim, first, iter->next == NULL); + 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; } @@ -591,7 +706,6 @@ static void x_win_move(int width, int height) xctx.window_dim.w = width; } - static void setopacity(Window win, unsigned long opacity) { Atom _NET_WM_WINDOW_OPACITY = @@ -600,15 +714,9 @@ static void setopacity(Window win, unsigned long opacity) PropModeReplace, (unsigned char *)&opacity, 1L); } - - - - - - - /* - * Returns the modifier which is NumLock. - */ +/* + * Returns the modifier which is NumLock. + */ static KeySym x_numlock_mod() { static KeyCode nl = 0; @@ -659,10 +767,10 @@ end: return sym; } - /* - * Helper function to use glib's mainloop mechanic - * with Xlib - */ +/* + * Helper function to use glib's mainloop mechanic + * with Xlib + */ gboolean x_mainloop_fd_prepare(GSource * source, gint * timeout) { if (timeout) @@ -672,18 +780,18 @@ gboolean x_mainloop_fd_prepare(GSource * source, gint * timeout) return false; } - /* - * Helper function to use glib's mainloop mechanic - * with Xlib - */ +/* + * 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 - */ +/* + * Main Dispatcher for XEvents + */ gboolean x_mainloop_fd_dispatch(GSource * source, GSourceFunc callback, gpointer user_data) { @@ -747,9 +855,9 @@ gboolean x_mainloop_fd_dispatch(GSource * source, GSourceFunc callback, return true; } - /* - * Check whether the user is currently idle. - */ +/* + * Check whether the user is currently idle. + */ bool x_is_idle(void) { XScreenSaverQueryInfo(xctx.dpy, DefaultRootWindow(xctx.dpy), @@ -761,9 +869,9 @@ bool x_is_idle(void) } /* TODO move to x_mainloop_* */ - /* - * Handle incoming mouse click events - */ +/* + * Handle incoming mouse click events + */ static void x_handle_click(XEvent ev) { if (ev.xbutton.button == Button3) { @@ -791,10 +899,10 @@ static void x_handle_click(XEvent ev) } } - /* - * Return the window that currently has - * the keyboard focus. - */ +/* + * Return the window that currently has + * the keyboard focus. + */ static Window get_focused_window(void) { Window focused = 0; @@ -818,10 +926,10 @@ static Window get_focused_window(void) } #ifdef XINERAMA - /* - * Select the screen on which the Window - * should be displayed. - */ +/* + * Select the screen on which the Window + * should be displayed. + */ static int select_screen(XineramaScreenInfo * info, int info_len) { int ret = 0; @@ -884,10 +992,10 @@ sc_cleanup: } #endif - /* - * Update the information about the monitor - * geometry. - */ +/* + * Update the information about the monitor + * geometry. + */ static void x_screen_info(screen_info * scr) { #ifdef XINERAMA @@ -921,9 +1029,18 @@ static void x_screen_info(screen_info * scr) } } - /* - * Setup X11 stuff - */ +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) { @@ -956,6 +1073,19 @@ void x_setup(void) xctx.color_strings[ColBG][NORM] = settings.normbgcolor; xctx.color_strings[ColBG][CRIT] = settings.critbgcolor; + if (settings.lowframecolor) + xctx.color_strings[ColFrame][LOW] = settings.lowframecolor; + else + xctx.color_strings[ColFrame][LOW] = settings.frame_color; + if (settings.normframecolor) + xctx.color_strings[ColFrame][NORM] = settings.normframecolor; + else + xctx.color_strings[ColFrame][NORM] = settings.frame_color; + if (settings.critframecolor) + xctx.color_strings[ColFrame][CRIT] = settings.critframecolor; + else + xctx.color_strings[ColFrame][CRIT] = settings.frame_color; + /* parse and set xctx.geometry and monitor position */ if (settings.geom[0] == '-') { xctx.geometry.negative_width = true; @@ -976,7 +1106,6 @@ void x_setup(void) } - static void x_set_wm(Window win) { @@ -1018,9 +1147,9 @@ static void x_set_wm(Window win) PropModeReplace, (unsigned char *) data, 1L); } - /* - * Setup the window - */ +/* + * Setup the window + */ static void x_win_setup(void) { @@ -1064,9 +1193,9 @@ static void x_win_setup(void) } } - /* - * Show the window and grab shortcuts. - */ +/* + * Show the window and grab shortcuts. + */ void x_win_show(void) { /* window is already mapped or there's nothing to show */ @@ -1089,9 +1218,9 @@ void x_win_show(void) xctx.visible = true; } - /* - * Hide the window and ungrab unused keyboard_shortcuts - */ +/* + * Hide the window and ungrab unused keyboard_shortcuts + */ void x_win_hide() { x_shortcut_ungrab(&settings.close_ks); @@ -1104,9 +1233,9 @@ void x_win_hide() xctx.visible = false; } - /* - * Parse a string into a modifier mask. - */ +/* + * Parse a string into a modifier mask. + */ KeySym x_shortcut_string_to_mask(const char *str) { if (!strcmp(str, "ctrl")) { @@ -1128,9 +1257,9 @@ KeySym x_shortcut_string_to_mask(const char *str) } - /* - * Error handler for grabbing mouse and keyboard errors. - */ +/* + * Error handler for grabbing mouse and keyboard errors. + */ static int GrabXErrorHandler(Display * display, XErrorEvent * e) { dunst_grab_errored = true; @@ -1157,9 +1286,9 @@ static int FollowXErrorHandler(Display * display, XErrorEvent * e) return 0; } - /* - * Setup the Error handler. - */ +/* + * Setup the Error handler. + */ static void x_shortcut_setup_error_handler(void) { dunst_grab_errored = false; @@ -1176,9 +1305,9 @@ static void x_follow_setup_error_handler(void) XSetErrorHandler(FollowXErrorHandler); } - /* - * Tear down the Error handler. - */ +/* + * Tear down the Error handler. + */ static int x_shortcut_tear_down_error_handler(void) { XFlush(xctx.dpy); @@ -1195,9 +1324,9 @@ static int x_follow_tear_down_error_handler(void) return dunst_follow_errored; } - /* - * Grab the given keyboard shortcut. - */ +/* + * Grab the given keyboard shortcut. + */ int x_shortcut_grab(keyboard_shortcut * ks) { if (!ks->is_valid) @@ -1222,9 +1351,9 @@ int x_shortcut_grab(keyboard_shortcut * ks) return 0; } - /* - * Ungrab the given keyboard shortcut. - */ +/* + * Ungrab the given keyboard shortcut. + */ void x_shortcut_ungrab(keyboard_shortcut * ks) { Window root; @@ -1235,9 +1364,9 @@ void x_shortcut_ungrab(keyboard_shortcut * ks) } } - /* - * Initialize the keyboard shortcut. - */ +/* + * Initialize the keyboard shortcut. + */ void x_shortcut_init(keyboard_shortcut * ks) { if (ks == NULL || ks->str == NULL) @@ -1254,7 +1383,7 @@ void x_shortcut_init(keyboard_shortcut * ks) if (str == NULL) die("Unable to allocate memory", EXIT_FAILURE); - while (strstr(str, "+")) { + while (strchr(str, '+')) { char *mod = str; while (*str != '+') str++; @@ -1291,4 +1420,4 @@ void x_shortcut_init(keyboard_shortcut * ks) free(str_begin); } -/* vim: set ts=8 sw=8 tw=0: */ +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/x.h b/src/x.h similarity index 93% rename from x.h rename to src/x.h index aeb48f6..767190b 100644 --- a/x.h +++ b/src/x.h @@ -13,8 +13,6 @@ #endif #include -#include - #define BUTTONMASK (ButtonPressMask|ButtonReleaseMask) #define FONT_HEIGHT_BORDER 2 #define DEFFONT "Monospace-11" @@ -48,10 +46,9 @@ typedef struct _xctx { Window win; bool visible; dimension_t geometry; - const char *color_strings[2][3]; + const char *color_strings[3][3]; XScreenSaverInfo *screensaver_info; dimension_t window_dim; - unsigned long framec; unsigned long sep_custom_col; } xctx_t; @@ -77,6 +74,7 @@ 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); @@ -84,4 +82,4 @@ gboolean x_mainloop_fd_check(GSource * source); gboolean x_mainloop_fd_prepare(GSource * source, gint * timeout); #endif -/* vim: set ts=8 sw=8 tw=0: */ +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/test/data/test-ini b/test/data/test-ini new file mode 100644 index 0000000..4afb1d2 --- /dev/null +++ b/test/data/test-ini @@ -0,0 +1,38 @@ +#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"" + +[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/test/dunstrc.default b/test/functional-tests/dunstrc.default similarity index 100% rename from test/dunstrc.default rename to test/functional-tests/dunstrc.default diff --git a/test/dunstrc.ignore_newline b/test/functional-tests/dunstrc.ignore_newline similarity index 100% rename from test/dunstrc.ignore_newline rename to test/functional-tests/dunstrc.ignore_newline diff --git a/test/dunstrc.ignore_newline_no_wrap b/test/functional-tests/dunstrc.ignore_newline_no_wrap similarity index 100% rename from test/dunstrc.ignore_newline_no_wrap rename to test/functional-tests/dunstrc.ignore_newline_no_wrap diff --git a/test/dunstrc.markup b/test/functional-tests/dunstrc.markup similarity index 100% rename from test/dunstrc.markup rename to test/functional-tests/dunstrc.markup diff --git a/test/dunstrc.nomarkup b/test/functional-tests/dunstrc.nomarkup similarity index 100% rename from test/dunstrc.nomarkup rename to test/functional-tests/dunstrc.nomarkup diff --git a/test/dunstrc.nowrap b/test/functional-tests/dunstrc.nowrap similarity index 100% rename from test/dunstrc.nowrap rename to test/functional-tests/dunstrc.nowrap diff --git a/test/dunstrc.run_script b/test/functional-tests/dunstrc.run_script similarity index 100% rename from test/dunstrc.run_script rename to test/functional-tests/dunstrc.run_script diff --git a/test/dunstrc.show_age b/test/functional-tests/dunstrc.show_age similarity index 100% rename from test/dunstrc.show_age rename to test/functional-tests/dunstrc.show_age diff --git a/test/script_test.sh b/test/functional-tests/script_test.sh similarity index 100% rename from test/script_test.sh rename to test/functional-tests/script_test.sh diff --git a/test/functional-tests/test.sh b/test/functional-tests/test.sh new file mode 100755 index 0000000..ae3ca17 --- /dev/null +++ b/test/functional-tests/test.sh @@ -0,0 +1,195 @@ +#!/bin/bash + +function keypress { + echo "press enter to continue..." + read key +} + +function basic_notifications { + ../../dunstify -a "dunst tester" "normal" "italic body" + ../../dunstify -a "dunst tester" -u c "critical" "bold body" + ../../dunstify -a "dunst tester" "long body" "This is a notification with a very long body" + ../../dunstify -a "dunst tester" "duplucate" + ../../dunstify -a "dunst tester" "duplucate" + ../../dunstify -a "dunst tester" "duplucate" + ../../dunstify -a "dunst tester" "url" "www.google.de" + +} + +function show_age { + echo "###################################" + echo "show age" + echo "###################################" + killall dunst + ../../dunst -config dunstrc.show_age & + ../../dunstify -a "dunst tester" -u c "Show Age" "These should print their age after 2 seconds" + basic_notifications + keypress +} + +function run_script { + echo "###################################" + echo "run script" + echo "###################################" + killall dunst + PATH=".:$PATH" ../../dunst -config dunstrc.run_script & + ../../dunstify -a "dunst tester" -u c \ + "Run Script" "After Keypress, 2 other notification should pop up. THis needs notify-send installed" + keypress + ../../dunstify -a "dunst tester" -u c "trigger" "this should trigger a notification" + keypress +} + +function ignore_newline { + echo "###################################" + echo "ignore newline" + echo "###################################" + killall dunst + ../../dunst -config dunstrc.ignore_newline_no_wrap & + ../../dunstify -a "dunst tester" -u c "Ignore Newline No Wrap" "There should be no newline anywhere" + ../../dunstify -a "dunst tester" -u c "Th\nis\n\n\n is\n fu\nll of \n" "\nnew\nlines" + basic_notifications + keypress + + killall dunst + ../../dunst -config dunstrc.ignore_newline & + ../../dunstify -a "dunst tester" -u c "Ignore Newline" \ + "The only newlines you should encounter here are wordwraps. That's why I'm so long." + ../../dunstify -a "dunst tester" -u c "Th\nis\n\n\n is\n fu\nll of \n" "\nnew\nlines" + basic_notifications + keypress +} + +function replace { + echo "###################################" + echo "replace" + echo "###################################" + killall dunst + ../../dunst -config dunstrc.default & + id=$(../../dunstify -a "dunst tester" -p "Replace" "this should get replaces after keypress") + keypress + ../../dunstify -a "dunst tester" -r $id "Success?" "I hope this is not a new notification" + keypress + +} + +function markup { + echo "###################################" + echo "markup" + echo "###################################" + killall dunst + ../../dunst -config dunstrc.markup "200x0+10+10" & + ../../dunstify -a "dunst tester" "Markup Tests" -u "c" + ../../dunstify -a "dunst tester" "bold italic" + ../../dunstify -a "dunst tester" "broken markup" + keypress + + killall dunst + ../../dunst -config dunstrc.nomarkup "200x0+10+10" & + ../../dunstify -a "dunst tester" -u c "NO Markup Tests" + ../../dunstify -a "dunst tester" "bolditalic" + ../../dunstify -a "dunst tester" "broken markup" + keypress + +} + +function corners { + echo "###################################" + echo "corners" + echo "###################################" + killall dunst + ../../dunst -config dunstrc.default -geom "200x0+10+10" & + ../../dunstify -a "dunst tester" -u c "upper left" + basic_notifications + keypress + + killall dunst + ../../dunst -config dunstrc.default -geom "200x0-10+10" & + ../../dunstify -a "dunst tester" -u c "upper right" + basic_notifications + keypress + + killall dunst + ../../dunst -config dunstrc.default -geom "200x0-10-10" & + ../../dunstify -a "dunst tester" -u c "lower right" + basic_notifications + keypress + + killall dunst + ../../dunst -config dunstrc.default -geom "200x0+10-10" & + ../../dunstify -a "dunst tester" -u c "lower left" + basic_notifications + keypress + +} + +function geometry { + echo "###################################" + echo "geometry" + echo "###################################" + killall dunst + ../../dunst -config dunstrc.default -geom "0x0" & + ../../dunstify -a "dunst tester" -u c "0x0" + basic_notifications + keypress + + + killall dunst + ../../dunst -config dunstrc.default -geom "200x0" & + ../../dunstify -a "dunst tester" -u c "200x0" + basic_notifications + keypress + + killall dunst + ../../dunst -config dunstrc.default -geom "200x2" & + ../../dunstify -a "dunst tester" -u c "200x2" + basic_notifications + keypress + + killall dunst + ../../dunst -config dunstrc.default -geom "200x1" & + ../../dunstify -a "dunst tester" -u c "200x1" + basic_notifications + keypress + + killall dunst + ../../dunst -config dunstrc.default -geom "0x1" & + ../../dunstify -a "dunst tester" -u c "0x1" + basic_notifications + keypress + + killall dunst + ../../dunst -config dunstrc.default -geom "-300x1" & + ../../dunstify -a "dunst tester" -u c "-300x1" + basic_notifications + keypress + + killall dunst + ../../dunst -config dunstrc.default -geom "-300x1-20-20" & + ../../dunstify -a "dunst tester" -u c "-300x1-20-20" + basic_notifications + keypress + + killall dunst + ../../dunst -config dunstrc.default -geom "x1" & + ../../dunstify -a "dunst tester" -u c "x1-20-20" "across the screen" + basic_notifications + keypress +} + +if [ -n "$1" ]; then + while [ -n "$1" ]; do + $1 + shift + done +else + geometry + corners + show_age + run_script + ignore_newline + replace + markup +fi + +killall dunst diff --git a/test/greatest.h b/test/greatest.h new file mode 100644 index 0000000..bc48e15 --- /dev/null +++ b/test/greatest.h @@ -0,0 +1,1035 @@ +/* + * Copyright (c) 2011-2016 Scott Vokes + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef GREATEST_H +#define GREATEST_H + +#ifdef __cplusplus +extern "C" { +#endif + +/* 1.2.1 */ +#define GREATEST_VERSION_MAJOR 1 +#define GREATEST_VERSION_MINOR 2 +#define GREATEST_VERSION_PATCH 1 + +/* A unit testing system for C, contained in 1 file. + * It doesn't use dynamic allocation or depend on anything + * beyond ANSI C89. + * + * An up-to-date version can be found at: + * https://github.com/silentbicycle/greatest/ + */ + + +/********************************************************************* + * Minimal test runner template + *********************************************************************/ +#if 0 + +#include "greatest.h" + +TEST foo_should_foo(void) { + PASS(); +} + +static void setup_cb(void *data) { + printf("setup callback for each test case\n"); +} + +static void teardown_cb(void *data) { + printf("teardown callback for each test case\n"); +} + +SUITE(suite) { + /* Optional setup/teardown callbacks which will be run before/after + * every test case. If using a test suite, they will be cleared when + * the suite finishes. */ + SET_SETUP(setup_cb, voidp_to_callback_data); + SET_TEARDOWN(teardown_cb, voidp_to_callback_data); + + RUN_TEST(foo_should_foo); +} + +/* Add definitions that need to be in the test runner's main file. */ +GREATEST_MAIN_DEFS(); + +/* Set up, run suite(s) of tests, report pass/fail/skip stats. */ +int run_tests(void) { + GREATEST_INIT(); /* init. greatest internals */ + /* List of suites to run (if any). */ + RUN_SUITE(suite); + + /* Tests can also be run directly, without using test suites. */ + RUN_TEST(foo_should_foo); + + GREATEST_PRINT_REPORT(); /* display results */ + return greatest_all_passed(); +} + +/* main(), for a standalone command-line test runner. + * This replaces run_tests above, and adds command line option + * handling and exiting with a pass/fail status. */ +int main(int argc, char **argv) { + GREATEST_MAIN_BEGIN(); /* init & parse command-line args */ + RUN_SUITE(suite); + GREATEST_MAIN_END(); /* display results */ +} + +#endif +/*********************************************************************/ + + +#include +#include +#include +#include + +/*********** + * Options * + ***********/ + +/* Default column width for non-verbose output. */ +#ifndef GREATEST_DEFAULT_WIDTH +#define GREATEST_DEFAULT_WIDTH 72 +#endif + +/* FILE *, for test logging. */ +#ifndef GREATEST_STDOUT +#define GREATEST_STDOUT stdout +#endif + +/* Remove GREATEST_ prefix from most commonly used symbols? */ +#ifndef GREATEST_USE_ABBREVS +#define GREATEST_USE_ABBREVS 1 +#endif + +/* Set to 0 to disable all use of setjmp/longjmp. */ +#ifndef GREATEST_USE_LONGJMP +#define GREATEST_USE_LONGJMP 1 +#endif + +#if GREATEST_USE_LONGJMP +#include +#endif + +/* Set to 0 to disable all use of time.h / clock(). */ +#ifndef GREATEST_USE_TIME +#define GREATEST_USE_TIME 1 +#endif + +#if GREATEST_USE_TIME +#include +#endif + +/* Floating point type, for ASSERT_IN_RANGE. */ +#ifndef GREATEST_FLOAT +#define GREATEST_FLOAT double +#define GREATEST_FLOAT_FMT "%g" +#endif + +/********* + * Types * + *********/ + +/* Info for the current running suite. */ +typedef struct greatest_suite_info { + unsigned int tests_run; + unsigned int passed; + unsigned int failed; + unsigned int skipped; + +#if GREATEST_USE_TIME + /* timers, pre/post running suite and individual tests */ + clock_t pre_suite; + clock_t post_suite; + clock_t pre_test; + clock_t post_test; +#endif +} greatest_suite_info; + +/* Type for a suite function. */ +typedef void (greatest_suite_cb)(void); + +/* Types for setup/teardown callbacks. If non-NULL, these will be run + * and passed the pointer to their additional data. */ +typedef void (greatest_setup_cb)(void *udata); +typedef void (greatest_teardown_cb)(void *udata); + +/* Type for an equality comparison between two pointers of the same type. + * Should return non-0 if equal, otherwise 0. + * UDATA is a closure value, passed through from ASSERT_EQUAL_T[m]. */ +typedef int greatest_equal_cb(const void *exp, const void *got, void *udata); + +/* Type for a callback that prints a value pointed to by T. + * Return value has the same meaning as printf's. + * UDATA is a closure value, passed through from ASSERT_EQUAL_T[m]. */ +typedef int greatest_printf_cb(const void *t, void *udata); + +/* Callbacks for an arbitrary type; needed for type-specific + * comparisons via GREATEST_ASSERT_EQUAL_T[m].*/ +typedef struct greatest_type_info { + greatest_equal_cb *equal; + greatest_printf_cb *print; +} greatest_type_info; + +typedef struct greatest_memory_cmp_env { + const unsigned char *exp; + const unsigned char *got; + size_t size; +} greatest_memory_cmp_env; + +/* Callbacks for string and raw memory types. */ +extern greatest_type_info greatest_type_info_string; +extern greatest_type_info greatest_type_info_memory; + +typedef enum { + GREATEST_FLAG_FIRST_FAIL = 0x01, + GREATEST_FLAG_LIST_ONLY = 0x02 +} greatest_flag_t; + +/* Struct containing all test runner state. */ +typedef struct greatest_run_info { + unsigned char flags; + unsigned char verbosity; + unsigned int tests_run; /* total test count */ + + /* overall pass/fail/skip counts */ + unsigned int passed; + unsigned int failed; + unsigned int skipped; + unsigned int assertions; + + /* currently running test suite */ + greatest_suite_info suite; + + /* info to print about the most recent failure */ + const char *fail_file; + unsigned int fail_line; + const char *msg; + + /* current setup/teardown hooks and userdata */ + greatest_setup_cb *setup; + void *setup_udata; + greatest_teardown_cb *teardown; + void *teardown_udata; + + /* formatting info for ".....s...F"-style output */ + unsigned int col; + unsigned int width; + + /* only run a specific suite or test */ + const char *suite_filter; + const char *test_filter; + +#if GREATEST_USE_TIME + /* overall timers */ + clock_t begin; + clock_t end; +#endif + +#if GREATEST_USE_LONGJMP + jmp_buf jump_dest; +#endif +} greatest_run_info; + +struct greatest_report_t { + /* overall pass/fail/skip counts */ + unsigned int passed; + unsigned int failed; + unsigned int skipped; + unsigned int assertions; +}; + +/* Global var for the current testing context. + * Initialized by GREATEST_MAIN_DEFS(). */ +extern greatest_run_info greatest_info; + +/* Type for ASSERT_ENUM_EQ's ENUM_STR argument. */ +typedef const char *greatest_enum_str_fun(int value); + +/********************** + * Exported functions * + **********************/ + +/* These are used internally by greatest. */ +void greatest_do_pass(const char *name); +void greatest_do_fail(const char *name); +void greatest_do_skip(const char *name); +int greatest_pre_test(const char *name); +void greatest_post_test(const char *name, int res); +void greatest_usage(const char *name); +int greatest_do_assert_equal_t(const void *exp, const void *got, + greatest_type_info *type_info, void *udata); + +/* These are part of the public greatest API. */ +void GREATEST_SET_SETUP_CB(greatest_setup_cb *cb, void *udata); +void GREATEST_SET_TEARDOWN_CB(greatest_teardown_cb *cb, void *udata); +int greatest_all_passed(void); +void greatest_set_test_filter(const char *name); +void greatest_set_suite_filter(const char *name); +void greatest_get_report(struct greatest_report_t *report); +unsigned int greatest_get_verbosity(void); +void greatest_set_verbosity(unsigned int verbosity); +void greatest_set_flag(greatest_flag_t flag); + + +/******************** +* Language Support * +********************/ + +/* If __VA_ARGS__ (C99) is supported, allow parametric testing +* without needing to manually manage the argument struct. */ +#if __STDC_VERSION__ >= 19901L || _MSC_VER >= 1800 +#define GREATEST_VA_ARGS +#endif + + +/********** + * Macros * + **********/ + +/* Define a suite. */ +#define GREATEST_SUITE(NAME) void NAME(void); void NAME(void) + +/* Declare a suite, provided by another compilation unit. */ +#define GREATEST_SUITE_EXTERN(NAME) void NAME(void) + +/* Start defining a test function. + * The arguments are not included, to allow parametric testing. */ +#define GREATEST_TEST static enum greatest_test_res + +/* PASS/FAIL/SKIP result from a test. Used internally. */ +typedef enum greatest_test_res { + GREATEST_TEST_RES_PASS = 0, + GREATEST_TEST_RES_FAIL = -1, + GREATEST_TEST_RES_SKIP = 1 +} greatest_test_res; + +/* Run a suite. */ +#define GREATEST_RUN_SUITE(S_NAME) greatest_run_suite(S_NAME, #S_NAME) + +/* Run a test in the current suite. */ +#define GREATEST_RUN_TEST(TEST) \ + do { \ + if (greatest_pre_test(#TEST) == 1) { \ + enum greatest_test_res res = GREATEST_SAVE_CONTEXT(); \ + if (res == GREATEST_TEST_RES_PASS) { \ + res = TEST(); \ + } \ + greatest_post_test(#TEST, res); \ + } else if (GREATEST_LIST_ONLY()) { \ + fprintf(GREATEST_STDOUT, " %s\n", #TEST); \ + } \ + } while (0) + +/* Ignore a test, don't warn about it being unused. */ +#define GREATEST_IGNORE_TEST(TEST) (void)TEST + +/* Run a test in the current suite with one void * argument, + * which can be a pointer to a struct with multiple arguments. */ +#define GREATEST_RUN_TEST1(TEST, ENV) \ + do { \ + if (greatest_pre_test(#TEST) == 1) { \ + int res = TEST(ENV); \ + greatest_post_test(#TEST, res); \ + } else if (GREATEST_LIST_ONLY()) { \ + fprintf(GREATEST_STDOUT, " %s\n", #TEST); \ + } \ + } while (0) + +#ifdef GREATEST_VA_ARGS +#define GREATEST_RUN_TESTp(TEST, ...) \ + do { \ + if (greatest_pre_test(#TEST) == 1) { \ + int res = TEST(__VA_ARGS__); \ + greatest_post_test(#TEST, res); \ + } else if (GREATEST_LIST_ONLY()) { \ + fprintf(GREATEST_STDOUT, " %s\n", #TEST); \ + } \ + } while (0) +#endif + + +/* Check if the test runner is in verbose mode. */ +#define GREATEST_IS_VERBOSE() ((greatest_info.verbosity) > 0) +#define GREATEST_LIST_ONLY() \ + (greatest_info.flags & GREATEST_FLAG_LIST_ONLY) +#define GREATEST_FIRST_FAIL() \ + (greatest_info.flags & GREATEST_FLAG_FIRST_FAIL) +#define GREATEST_FAILURE_ABORT() \ + (greatest_info.suite.failed > 0 && GREATEST_FIRST_FAIL()) + +/* Message-less forms of tests defined below. */ +#define GREATEST_PASS() GREATEST_PASSm(NULL) +#define GREATEST_FAIL() GREATEST_FAILm(NULL) +#define GREATEST_SKIP() GREATEST_SKIPm(NULL) +#define GREATEST_ASSERT(COND) \ + GREATEST_ASSERTm(#COND, COND) +#define GREATEST_ASSERT_OR_LONGJMP(COND) \ + GREATEST_ASSERT_OR_LONGJMPm(#COND, COND) +#define GREATEST_ASSERT_FALSE(COND) \ + GREATEST_ASSERT_FALSEm(#COND, COND) +#define GREATEST_ASSERT_EQ(EXP, GOT) \ + GREATEST_ASSERT_EQm(#EXP " != " #GOT, EXP, GOT) +#define GREATEST_ASSERT_EQ_FMT(EXP, GOT, FMT) \ + GREATEST_ASSERT_EQ_FMTm(#EXP " != " #GOT, EXP, GOT, FMT) +#define GREATEST_ASSERT_IN_RANGE(EXP, GOT, TOL) \ + GREATEST_ASSERT_IN_RANGEm(#EXP " != " #GOT " +/- " #TOL, EXP, GOT, TOL) +#define GREATEST_ASSERT_EQUAL_T(EXP, GOT, TYPE_INFO, UDATA) \ + GREATEST_ASSERT_EQUAL_Tm(#EXP " != " #GOT, EXP, GOT, TYPE_INFO, UDATA) +#define GREATEST_ASSERT_STR_EQ(EXP, GOT) \ + GREATEST_ASSERT_STR_EQm(#EXP " != " #GOT, EXP, GOT) +#define GREATEST_ASSERT_STRN_EQ(EXP, GOT, SIZE) \ + GREATEST_ASSERT_STRN_EQm(#EXP " != " #GOT, EXP, GOT, SIZE) +#define GREATEST_ASSERT_MEM_EQ(EXP, GOT, SIZE) \ + GREATEST_ASSERT_MEM_EQm(#EXP " != " #GOT, EXP, GOT, SIZE) +#define GREATEST_ASSERT_ENUM_EQ(EXP, GOT, ENUM_STR) \ + GREATEST_ASSERT_ENUM_EQm(#EXP " != " #GOT, EXP, GOT, ENUM_STR) + +/* The following forms take an additional message argument first, + * to be displayed by the test runner. */ + +/* Fail if a condition is not true, with message. */ +#define GREATEST_ASSERTm(MSG, COND) \ + do { \ + greatest_info.assertions++; \ + if (!(COND)) { GREATEST_FAILm(MSG); } \ + } while (0) + +/* Fail if a condition is not true, longjmping out of test. */ +#define GREATEST_ASSERT_OR_LONGJMPm(MSG, COND) \ + do { \ + greatest_info.assertions++; \ + if (!(COND)) { GREATEST_FAIL_WITH_LONGJMPm(MSG); } \ + } while (0) + +/* Fail if a condition is not false, with message. */ +#define GREATEST_ASSERT_FALSEm(MSG, COND) \ + do { \ + greatest_info.assertions++; \ + if ((COND)) { GREATEST_FAILm(MSG); } \ + } while (0) + +/* Fail if EXP != GOT (equality comparison by ==). */ +#define GREATEST_ASSERT_EQm(MSG, EXP, GOT) \ + do { \ + greatest_info.assertions++; \ + if ((EXP) != (GOT)) { GREATEST_FAILm(MSG); } \ + } while (0) + +/* Fail if EXP != GOT (equality comparison by ==). + * Warning: EXP and GOT will be evaluated more than once on failure. */ +#define GREATEST_ASSERT_EQ_FMTm(MSG, EXP, GOT, FMT) \ + do { \ + const char *greatest_FMT = ( FMT ); \ + greatest_info.assertions++; \ + if ((EXP) != (GOT)) { \ + fprintf(GREATEST_STDOUT, "\nExpected: "); \ + fprintf(GREATEST_STDOUT, greatest_FMT, EXP); \ + fprintf(GREATEST_STDOUT, "\n Got: "); \ + fprintf(GREATEST_STDOUT, greatest_FMT, GOT); \ + fprintf(GREATEST_STDOUT, "\n"); \ + GREATEST_FAILm(MSG); \ + } \ + } while (0) + +/* Fail if EXP is not equal to GOT, printing enum IDs. */ +#define GREATEST_ASSERT_ENUM_EQm(MSG, EXP, GOT, ENUM_STR) \ + do { \ + int greatest_EXP = (int)(EXP); \ + int greatest_GOT = (int)(GOT); \ + greatest_enum_str_fun *greatest_ENUM_STR = ENUM_STR; \ + if (greatest_EXP != greatest_GOT) { \ + fprintf(GREATEST_STDOUT, "\nExpected: %s", \ + greatest_ENUM_STR(greatest_EXP)); \ + fprintf(GREATEST_STDOUT, "\n Got: %s\n", \ + greatest_ENUM_STR(greatest_GOT)); \ + GREATEST_FAILm(MSG); \ + } \ + } while (0) \ + +/* Fail if GOT not in range of EXP +|- TOL. */ +#define GREATEST_ASSERT_IN_RANGEm(MSG, EXP, GOT, TOL) \ + do { \ + GREATEST_FLOAT greatest_EXP = (EXP); \ + GREATEST_FLOAT greatest_GOT = (GOT); \ + GREATEST_FLOAT greatest_TOL = (TOL); \ + greatest_info.assertions++; \ + if ((greatest_EXP > greatest_GOT && \ + greatest_EXP - greatest_GOT > greatest_TOL) || \ + (greatest_EXP < greatest_GOT && \ + greatest_GOT - greatest_EXP > greatest_TOL)) { \ + fprintf(GREATEST_STDOUT, \ + "\nExpected: " GREATEST_FLOAT_FMT \ + " +/- " GREATEST_FLOAT_FMT \ + "\n Got: " GREATEST_FLOAT_FMT \ + "\n", \ + greatest_EXP, greatest_TOL, greatest_GOT); \ + GREATEST_FAILm(MSG); \ + } \ + } while (0) + +/* Fail if EXP is not equal to GOT, according to strcmp. */ +#define GREATEST_ASSERT_STR_EQm(MSG, EXP, GOT) \ + do { \ + GREATEST_ASSERT_EQUAL_Tm(MSG, EXP, GOT, \ + &greatest_type_info_string, NULL); \ + } while (0) \ + +/* Fail if EXP is not equal to GOT, according to strcmp. */ +#define GREATEST_ASSERT_STRN_EQm(MSG, EXP, GOT, SIZE) \ + do { \ + size_t size = SIZE; \ + GREATEST_ASSERT_EQUAL_Tm(MSG, EXP, GOT, \ + &greatest_type_info_string, &size); \ + } while (0) \ + +/* Fail if EXP is not equal to GOT, according to memcmp. */ +#define GREATEST_ASSERT_MEM_EQm(MSG, EXP, GOT, SIZE) \ + do { \ + greatest_memory_cmp_env env; \ + env.exp = (const unsigned char *)EXP; \ + env.got = (const unsigned char *)GOT; \ + env.size = SIZE; \ + GREATEST_ASSERT_EQUAL_Tm(MSG, env.exp, env.got, \ + &greatest_type_info_memory, &env); \ + } while (0) \ + +/* Fail if EXP is not equal to GOT, according to a comparison + * callback in TYPE_INFO. If they are not equal, optionally use a + * print callback in TYPE_INFO to print them. */ +#define GREATEST_ASSERT_EQUAL_Tm(MSG, EXP, GOT, TYPE_INFO, UDATA) \ + do { \ + greatest_type_info *type_info = (TYPE_INFO); \ + greatest_info.assertions++; \ + if (!greatest_do_assert_equal_t(EXP, GOT, \ + type_info, UDATA)) { \ + if (type_info == NULL || type_info->equal == NULL) { \ + GREATEST_FAILm("type_info->equal callback missing!"); \ + } else { \ + GREATEST_FAILm(MSG); \ + } \ + } \ + } while (0) \ + +/* Pass. */ +#define GREATEST_PASSm(MSG) \ + do { \ + greatest_info.msg = MSG; \ + return GREATEST_TEST_RES_PASS; \ + } while (0) + +/* Fail. */ +#define GREATEST_FAILm(MSG) \ + do { \ + greatest_info.fail_file = __FILE__; \ + greatest_info.fail_line = __LINE__; \ + greatest_info.msg = MSG; \ + return GREATEST_TEST_RES_FAIL; \ + } while (0) + +/* Optional GREATEST_FAILm variant that longjmps. */ +#if GREATEST_USE_LONGJMP +#define GREATEST_FAIL_WITH_LONGJMP() GREATEST_FAIL_WITH_LONGJMPm(NULL) +#define GREATEST_FAIL_WITH_LONGJMPm(MSG) \ + do { \ + greatest_info.fail_file = __FILE__; \ + greatest_info.fail_line = __LINE__; \ + greatest_info.msg = MSG; \ + longjmp(greatest_info.jump_dest, GREATEST_TEST_RES_FAIL); \ + } while (0) +#endif + +/* Skip the current test. */ +#define GREATEST_SKIPm(MSG) \ + do { \ + greatest_info.msg = MSG; \ + return GREATEST_TEST_RES_SKIP; \ + } while (0) + +/* Check the result of a subfunction using ASSERT, etc. */ +#define GREATEST_CHECK_CALL(RES) \ + do { \ + enum greatest_test_res greatest_RES = RES; \ + if (greatest_RES != GREATEST_TEST_RES_PASS) { \ + return greatest_RES; \ + } \ + } while (0) \ + +#if GREATEST_USE_TIME +#define GREATEST_SET_TIME(NAME) \ + NAME = clock(); \ + if (NAME == (clock_t) -1) { \ + fprintf(GREATEST_STDOUT, \ + "clock error: %s\n", #NAME); \ + exit(EXIT_FAILURE); \ + } + +#define GREATEST_CLOCK_DIFF(C1, C2) \ + fprintf(GREATEST_STDOUT, " (%lu ticks, %.3f sec)", \ + (long unsigned int) (C2) - (long unsigned int)(C1), \ + (double)((C2) - (C1)) / (1.0 * (double)CLOCKS_PER_SEC)) +#else +#define GREATEST_SET_TIME(UNUSED) +#define GREATEST_CLOCK_DIFF(UNUSED1, UNUSED2) +#endif + +#if GREATEST_USE_LONGJMP +#define GREATEST_SAVE_CONTEXT() \ + /* setjmp returns 0 (GREATEST_TEST_RES_PASS) on first call */ \ + /* so the test runs, then RES_FAIL from FAIL_WITH_LONGJMP. */ \ + ((enum greatest_test_res)(setjmp(greatest_info.jump_dest))) +#else +#define GREATEST_SAVE_CONTEXT() \ + /*a no-op, since setjmp/longjmp aren't being used */ \ + GREATEST_TEST_RES_PASS +#endif + +/* Include several function definitions in the main test file. */ +#define GREATEST_MAIN_DEFS() \ + \ +/* Is FILTER a subset of NAME? */ \ +static int greatest_name_match(const char *name, \ + const char *filter) { \ + size_t offset = 0; \ + size_t filter_len = strlen(filter); \ + while (name[offset] != '\0') { \ + if (name[offset] == filter[0]) { \ + if (0 == strncmp(&name[offset], filter, filter_len)) { \ + return 1; \ + } \ + } \ + offset++; \ + } \ + \ + return 0; \ +} \ + \ +int greatest_pre_test(const char *name) { \ + if (!GREATEST_LIST_ONLY() \ + && (!GREATEST_FIRST_FAIL() || greatest_info.suite.failed == 0) \ + && (greatest_info.test_filter == NULL || \ + greatest_name_match(name, greatest_info.test_filter))) { \ + GREATEST_SET_TIME(greatest_info.suite.pre_test); \ + if (greatest_info.setup) { \ + greatest_info.setup(greatest_info.setup_udata); \ + } \ + return 1; /* test should be run */ \ + } else { \ + return 0; /* skipped */ \ + } \ +} \ + \ +void greatest_post_test(const char *name, int res) { \ + GREATEST_SET_TIME(greatest_info.suite.post_test); \ + if (greatest_info.teardown) { \ + void *udata = greatest_info.teardown_udata; \ + greatest_info.teardown(udata); \ + } \ + \ + if (res <= GREATEST_TEST_RES_FAIL) { \ + greatest_do_fail(name); \ + } else if (res >= GREATEST_TEST_RES_SKIP) { \ + greatest_do_skip(name); \ + } else if (res == GREATEST_TEST_RES_PASS) { \ + greatest_do_pass(name); \ + } \ + greatest_info.suite.tests_run++; \ + greatest_info.col++; \ + if (GREATEST_IS_VERBOSE()) { \ + GREATEST_CLOCK_DIFF(greatest_info.suite.pre_test, \ + greatest_info.suite.post_test); \ + fprintf(GREATEST_STDOUT, "\n"); \ + } else if (greatest_info.col % greatest_info.width == 0) { \ + fprintf(GREATEST_STDOUT, "\n"); \ + greatest_info.col = 0; \ + } \ + if (GREATEST_STDOUT == stdout) fflush(stdout); \ +} \ + \ +static void report_suite(void) { \ + if (greatest_info.suite.tests_run > 0) { \ + fprintf(GREATEST_STDOUT, \ + "\n%u test%s - %u passed, %u failed, %u skipped", \ + greatest_info.suite.tests_run, \ + greatest_info.suite.tests_run == 1 ? "" : "s", \ + greatest_info.suite.passed, \ + greatest_info.suite.failed, \ + greatest_info.suite.skipped); \ + GREATEST_CLOCK_DIFF(greatest_info.suite.pre_suite, \ + greatest_info.suite.post_suite); \ + fprintf(GREATEST_STDOUT, "\n"); \ + } \ +} \ + \ +static void update_counts_and_reset_suite(void) { \ + greatest_info.setup = NULL; \ + greatest_info.setup_udata = NULL; \ + greatest_info.teardown = NULL; \ + greatest_info.teardown_udata = NULL; \ + greatest_info.passed += greatest_info.suite.passed; \ + greatest_info.failed += greatest_info.suite.failed; \ + greatest_info.skipped += greatest_info.suite.skipped; \ + greatest_info.tests_run += greatest_info.suite.tests_run; \ + memset(&greatest_info.suite, 0, sizeof(greatest_info.suite)); \ + greatest_info.col = 0; \ +} \ + \ +static void greatest_run_suite(greatest_suite_cb *suite_cb, \ + const char *suite_name) { \ + if (greatest_info.suite_filter && \ + !greatest_name_match(suite_name, greatest_info.suite_filter)) { \ + return; \ + } \ + update_counts_and_reset_suite(); \ + if (GREATEST_FIRST_FAIL() && greatest_info.failed > 0) { return; } \ + fprintf(GREATEST_STDOUT, "\n* Suite %s:\n", suite_name); \ + GREATEST_SET_TIME(greatest_info.suite.pre_suite); \ + suite_cb(); \ + GREATEST_SET_TIME(greatest_info.suite.post_suite); \ + report_suite(); \ +} \ + \ +void greatest_do_pass(const char *name) { \ + if (GREATEST_IS_VERBOSE()) { \ + fprintf(GREATEST_STDOUT, "PASS %s: %s", \ + name, greatest_info.msg ? greatest_info.msg : ""); \ + } else { \ + fprintf(GREATEST_STDOUT, "."); \ + } \ + greatest_info.suite.passed++; \ +} \ + \ +void greatest_do_fail(const char *name) { \ + if (GREATEST_IS_VERBOSE()) { \ + fprintf(GREATEST_STDOUT, \ + "FAIL %s: %s (%s:%u)", \ + name, greatest_info.msg ? greatest_info.msg : "", \ + greatest_info.fail_file, greatest_info.fail_line); \ + } else { \ + fprintf(GREATEST_STDOUT, "F"); \ + greatest_info.col++; \ + /* add linebreak if in line of '.'s */ \ + if (greatest_info.col != 0) { \ + fprintf(GREATEST_STDOUT, "\n"); \ + greatest_info.col = 0; \ + } \ + fprintf(GREATEST_STDOUT, "FAIL %s: %s (%s:%u)\n", \ + name, \ + greatest_info.msg ? greatest_info.msg : "", \ + greatest_info.fail_file, greatest_info.fail_line); \ + } \ + greatest_info.suite.failed++; \ +} \ + \ +void greatest_do_skip(const char *name) { \ + if (GREATEST_IS_VERBOSE()) { \ + fprintf(GREATEST_STDOUT, "SKIP %s: %s", \ + name, \ + greatest_info.msg ? \ + greatest_info.msg : "" ); \ + } else { \ + fprintf(GREATEST_STDOUT, "s"); \ + } \ + greatest_info.suite.skipped++; \ +} \ + \ +int greatest_do_assert_equal_t(const void *exp, const void *got, \ + greatest_type_info *type_info, void *udata) { \ + int eq = 0; \ + if (type_info == NULL || type_info->equal == NULL) { \ + return 0; \ + } \ + eq = type_info->equal(exp, got, udata); \ + if (!eq) { \ + if (type_info->print != NULL) { \ + fprintf(GREATEST_STDOUT, "\nExpected: "); \ + (void)type_info->print(exp, udata); \ + fprintf(GREATEST_STDOUT, "\n Got: "); \ + (void)type_info->print(got, udata); \ + fprintf(GREATEST_STDOUT, "\n"); \ + } else { \ + fprintf(GREATEST_STDOUT, \ + "GREATEST_ASSERT_EQUAL_T failure at %s:%u\n", \ + greatest_info.fail_file, \ + greatest_info.fail_line); \ + } \ + } \ + return eq; \ +} \ + \ +void greatest_usage(const char *name) { \ + fprintf(GREATEST_STDOUT, \ + "Usage: %s [-hlfv] [-s SUITE] [-t TEST]\n" \ + " -h, --help print this Help\n" \ + " -l List suites and their tests, then exit\n" \ + " -f Stop runner after first failure\n" \ + " -v Verbose output\n" \ + " -s SUITE only run suites containing string SUITE\n" \ + " -t TEST only run tests containing string TEST\n", \ + name); \ +} \ + \ +static void greatest_parse_args(int argc, char **argv) { \ + int i = 0; \ + for (i = 1; i < argc; i++) { \ + if (0 == strncmp("-t", argv[i], 2)) { \ + if (argc <= i + 1) { \ + greatest_usage(argv[0]); \ + exit(EXIT_FAILURE); \ + } \ + greatest_info.test_filter = argv[i+1]; \ + i++; \ + } else if (0 == strncmp("-s", argv[i], 2)) { \ + if (argc <= i + 1) { \ + greatest_usage(argv[0]); \ + exit(EXIT_FAILURE); \ + } \ + greatest_info.suite_filter = argv[i+1]; \ + i++; \ + } else if (0 == strncmp("-f", argv[i], 2)) { \ + greatest_info.flags |= GREATEST_FLAG_FIRST_FAIL; \ + } else if (0 == strncmp("-v", argv[i], 2)) { \ + greatest_info.verbosity++; \ + } else if (0 == strncmp("-l", argv[i], 2)) { \ + greatest_info.flags |= GREATEST_FLAG_LIST_ONLY; \ + } else if (0 == strncmp("-h", argv[i], 2) || \ + 0 == strncmp("--help", argv[i], 6)) { \ + greatest_usage(argv[0]); \ + exit(EXIT_SUCCESS); \ + } else if (0 == strncmp("--", argv[i], 2)) { \ + break; \ + } else { \ + fprintf(GREATEST_STDOUT, \ + "Unknown argument '%s'\n", argv[i]); \ + greatest_usage(argv[0]); \ + exit(EXIT_FAILURE); \ + } \ + } \ +} \ + \ +int greatest_all_passed(void) { return (greatest_info.failed == 0); } \ + \ +void greatest_set_test_filter(const char *name) { \ + greatest_info.test_filter = name; \ +} \ + \ +void greatest_set_suite_filter(const char *name) { \ + greatest_info.suite_filter = name; \ +} \ + \ +void greatest_get_report(struct greatest_report_t *report) { \ + if (report) { \ + report->passed = greatest_info.passed; \ + report->failed = greatest_info.failed; \ + report->skipped = greatest_info.skipped; \ + report->assertions = greatest_info.assertions; \ + } \ +} \ + \ +unsigned int greatest_get_verbosity(void) { \ + return greatest_info.verbosity; \ +} \ + \ +void greatest_set_verbosity(unsigned int verbosity) { \ + greatest_info.verbosity = (unsigned char)verbosity; \ +} \ + \ +void greatest_set_flag(greatest_flag_t flag) { \ + greatest_info.flags |= flag; \ +} \ + \ +void GREATEST_SET_SETUP_CB(greatest_setup_cb *cb, void *udata) { \ + greatest_info.setup = cb; \ + greatest_info.setup_udata = udata; \ +} \ + \ +void GREATEST_SET_TEARDOWN_CB(greatest_teardown_cb *cb, \ + void *udata) { \ + greatest_info.teardown = cb; \ + greatest_info.teardown_udata = udata; \ +} \ + \ +static int greatest_string_equal_cb(const void *exp, const void *got, \ + void *udata) { \ + size_t *size = (size_t *)udata; \ + return (size != NULL \ + ? (0 == strncmp((const char *)exp, (const char *)got, *size)) \ + : (0 == strcmp((const char *)exp, (const char *)got))); \ +} \ + \ +static int greatest_string_printf_cb(const void *t, void *udata) { \ + (void)udata; /* note: does not check \0 termination. */ \ + return fprintf(GREATEST_STDOUT, "%s", (const char *)t); \ +} \ + \ +greatest_type_info greatest_type_info_string = { \ + greatest_string_equal_cb, \ + greatest_string_printf_cb, \ +}; \ + \ +static int greatest_memory_equal_cb(const void *exp, const void *got, \ + void *udata) { \ + greatest_memory_cmp_env *env = (greatest_memory_cmp_env *)udata; \ + return (0 == memcmp(exp, got, env->size)); \ +} \ + \ +static int greatest_memory_printf_cb(const void *t, void *udata) { \ + greatest_memory_cmp_env *env = (greatest_memory_cmp_env *)udata; \ + unsigned char *buf = (unsigned char *)t, diff_mark = ' '; \ + FILE *out = GREATEST_STDOUT; \ + size_t i, line_i, line_len = 0; \ + int len = 0; /* format hexdump with differences highlighted */ \ + for (i = 0; i < env->size; i+= line_len) { \ + diff_mark = ' '; \ + line_len = env->size - i; \ + if (line_len > 16) { line_len = 16; } \ + for (line_i = i; line_i < i + line_len; line_i++) { \ + if (env->exp[line_i] != env->got[line_i]) diff_mark = 'X'; \ + } \ + len += fprintf(out, "\n%04x %c ", (unsigned int)i, diff_mark); \ + for (line_i = i; line_i < i + line_len; line_i++) { \ + int m = env->exp[line_i] == env->got[line_i]; /* match? */ \ + len += fprintf(out, "%02x%c", buf[line_i], m ? ' ' : '<'); \ + } \ + for (line_i = 0; line_i < 16 - line_len; line_i++) { \ + len += fprintf(out, " "); \ + } \ + fprintf(out, " "); \ + for (line_i = i; line_i < i + line_len; line_i++) { \ + unsigned char c = buf[line_i]; \ + len += fprintf(out, "%c", isprint(c) ? c : '.'); \ + } \ + } \ + len += fprintf(out, "\n"); \ + return len; \ +} \ + \ +greatest_type_info greatest_type_info_memory = { \ + greatest_memory_equal_cb, \ + greatest_memory_printf_cb, \ +}; \ + \ +greatest_run_info greatest_info + +/* Init internals. */ +#define GREATEST_INIT() \ + do { \ + /* Suppress unused function warning if features aren't used */ \ + (void)greatest_run_suite; \ + (void)greatest_parse_args; \ + \ + memset(&greatest_info, 0, sizeof(greatest_info)); \ + greatest_info.width = GREATEST_DEFAULT_WIDTH; \ + GREATEST_SET_TIME(greatest_info.begin); \ + } while (0) \ + +/* Handle command-line arguments, etc. */ +#define GREATEST_MAIN_BEGIN() \ + do { \ + GREATEST_INIT(); \ + greatest_parse_args(argc, argv); \ + } while (0) + +/* Report passes, failures, skipped tests, the number of + * assertions, and the overall run time. */ +#define GREATEST_PRINT_REPORT() \ + do { \ + if (!GREATEST_LIST_ONLY()) { \ + update_counts_and_reset_suite(); \ + GREATEST_SET_TIME(greatest_info.end); \ + fprintf(GREATEST_STDOUT, \ + "\nTotal: %u test%s", \ + greatest_info.tests_run, \ + greatest_info.tests_run == 1 ? "" : "s"); \ + GREATEST_CLOCK_DIFF(greatest_info.begin, \ + greatest_info.end); \ + fprintf(GREATEST_STDOUT, ", %u assertion%s\n", \ + greatest_info.assertions, \ + greatest_info.assertions == 1 ? "" : "s"); \ + fprintf(GREATEST_STDOUT, \ + "Pass: %u, fail: %u, skip: %u.\n", \ + greatest_info.passed, \ + greatest_info.failed, greatest_info.skipped); \ + } \ + } while (0) + +/* Report results, exit with exit status based on results. */ +#define GREATEST_MAIN_END() \ + do { \ + GREATEST_PRINT_REPORT(); \ + return (greatest_all_passed() ? EXIT_SUCCESS : EXIT_FAILURE); \ + } while (0) + +/* Make abbreviations without the GREATEST_ prefix for the + * most commonly used symbols. */ +#if GREATEST_USE_ABBREVS +#define TEST GREATEST_TEST +#define SUITE GREATEST_SUITE +#define SUITE_EXTERN GREATEST_SUITE_EXTERN +#define RUN_TEST GREATEST_RUN_TEST +#define RUN_TEST1 GREATEST_RUN_TEST1 +#define RUN_SUITE GREATEST_RUN_SUITE +#define IGNORE_TEST GREATEST_IGNORE_TEST +#define ASSERT GREATEST_ASSERT +#define ASSERTm GREATEST_ASSERTm +#define ASSERT_FALSE GREATEST_ASSERT_FALSE +#define ASSERT_EQ GREATEST_ASSERT_EQ +#define ASSERT_EQ_FMT GREATEST_ASSERT_EQ_FMT +#define ASSERT_IN_RANGE GREATEST_ASSERT_IN_RANGE +#define ASSERT_EQUAL_T GREATEST_ASSERT_EQUAL_T +#define ASSERT_STR_EQ GREATEST_ASSERT_STR_EQ +#define ASSERT_STRN_EQ GREATEST_ASSERT_STRN_EQ +#define ASSERT_MEM_EQ GREATEST_ASSERT_MEM_EQ +#define ASSERT_ENUM_EQ GREATEST_ASSERT_ENUM_EQ +#define ASSERT_FALSEm GREATEST_ASSERT_FALSEm +#define ASSERT_EQm GREATEST_ASSERT_EQm +#define ASSERT_EQ_FMTm GREATEST_ASSERT_EQ_FMTm +#define ASSERT_IN_RANGEm GREATEST_ASSERT_IN_RANGEm +#define ASSERT_EQUAL_Tm GREATEST_ASSERT_EQUAL_Tm +#define ASSERT_STR_EQm GREATEST_ASSERT_STR_EQm +#define ASSERT_STRN_EQm GREATEST_ASSERT_STRN_EQm +#define ASSERT_MEM_EQm GREATEST_ASSERT_MEM_EQm +#define ASSERT_ENUM_EQm GREATEST_ASSERT_ENUM_EQm +#define PASS GREATEST_PASS +#define FAIL GREATEST_FAIL +#define SKIP GREATEST_SKIP +#define PASSm GREATEST_PASSm +#define FAILm GREATEST_FAILm +#define SKIPm GREATEST_SKIPm +#define SET_SETUP GREATEST_SET_SETUP_CB +#define SET_TEARDOWN GREATEST_SET_TEARDOWN_CB +#define CHECK_CALL GREATEST_CHECK_CALL + +#ifdef GREATEST_VA_ARGS +#define RUN_TESTp GREATEST_RUN_TESTp +#endif + +#if GREATEST_USE_LONGJMP +#define ASSERT_OR_LONGJMP GREATEST_ASSERT_OR_LONGJMP +#define ASSERT_OR_LONGJMPm GREATEST_ASSERT_OR_LONGJMPm +#define FAIL_WITH_LONGJMP GREATEST_FAIL_WITH_LONGJMP +#define FAIL_WITH_LONGJMPm GREATEST_FAIL_WITH_LONGJMPm +#endif + +#endif /* USE_ABBREVS */ + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/test/option_parser.c b/test/option_parser.c new file mode 100644 index 0000000..b45788f --- /dev/null +++ b/test/option_parser.c @@ -0,0 +1,227 @@ +#include "greatest.h" + +#include +#include + +#include "src/option_parser.h" + +TEST test_next_section(void) +{ + char *section = NULL; + ASSERT_STR_EQ("bool", (section = next_section(section))); + ASSERT_STR_EQ("string", (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"; + ASSERT_STR_EQ("A simple string", ini_get_string(string_section, "simple", "")); + + ASSERT_STR_EQ("A quoted string", ini_get_string(string_section, "quoted", "")); + ASSERT_STR_EQ("A string \"with quotes\"", ini_get_string(string_section, "quoted_with_quotes", "")); + + ASSERT_STR_EQ("default value", ini_get_string(string_section, "nonexistent", "default value")); + + 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_string(void) +{ + ASSERT_STR_EQ("A simple string from the cmdline", cmdline_get_string("-string", "", "")); + ASSERT_STR_EQ("Single_word_string", cmdline_get_string("-str/-s", "", "")); + ASSERT_STR_EQ("Default", cmdline_get_string("-nonexistent", "Default", "")); + 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) +{ + 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"); + char *usage = cmdline_create_usage(); + ASSERT_FALSE(strstr(usage, "-msgstring/-ms") == NULL); + ASSERT_FALSE(strstr(usage, "A string to test usage creation") == NULL); + ASSERT_FALSE(strstr(usage, "-msgint/-mi") == NULL); + ASSERT_FALSE(strstr(usage, "An int to test usage creation") == NULL); + ASSERT_FALSE(strstr(usage, "-msgdouble/-md") == NULL); + ASSERT_FALSE(strstr(usage, "A double to test usage creation") == NULL); + ASSERT_FALSE(strstr(usage, "-msgbool/-mb") == NULL); + ASSERT_FALSE(strstr(usage, "A bool to test usage creation") == NULL); + free(usage); + PASS(); +} + +TEST test_option_get_string(void) +{ + char *string_section = "string"; + + ASSERT_STR_EQ("A simple string", option_get_string(string_section, "simple", "-nonexistent", "", "")); + ASSERT_STR_EQ("Single_word_string", option_get_string(string_section, "simple", "-str/-s", "", "")); + ASSERT_STR_EQ("A simple string from the cmdline", option_get_string(string_section, "simple", "-string", "", "")); + ASSERT_STR_EQ("A simple string from the cmdline", option_get_string(string_section, "simple", "-string/-s", "", "")); + ASSERT_STR_EQ("Single_word_string", option_get_string(string_section, "simple", "-s", "", "")); + ASSERT_STR_EQ("Default", option_get_string(string_section, "nonexistent", "-nonexistent", "Default", "")); + 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_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 " + "-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_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_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/test/test.c b/test/test.c new file mode 100644 index 0000000..6de63cd --- /dev/null +++ b/test/test.c @@ -0,0 +1,14 @@ +#include "greatest.h" + +SUITE_EXTERN(suite_utils); +SUITE_EXTERN(suite_option_parser); + +GREATEST_MAIN_DEFS(); + +int main(int argc, char *argv[]) { + GREATEST_MAIN_BEGIN(); + RUN_SUITE(suite_utils); + RUN_SUITE(suite_option_parser); + GREATEST_MAIN_END(); +} +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/test/test.sh b/test/test.sh deleted file mode 100755 index 7883e1e..0000000 --- a/test/test.sh +++ /dev/null @@ -1,195 +0,0 @@ -#!/bin/bash - -function keypress { - echo "press enter to continue..." - read key -} - -function basic_notifications { - ../dunstify -a "dunst tester" "normal" "italic body" - ../dunstify -a "dunst tester" -u c "critical" "bold body" - ../dunstify -a "dunst tester" "long body" "This is a notification with a very long body" - ../dunstify -a "dunst tester" "duplucate" - ../dunstify -a "dunst tester" "duplucate" - ../dunstify -a "dunst tester" "duplucate" - ../dunstify -a "dunst tester" "url" "www.google.de" - -} - -function show_age { - echo "###################################" - echo "show age" - echo "###################################" - killall dunst - ../dunst -config dunstrc.show_age & - ../dunstify -a "dunst tester" -u c "Show Age" "These should print their age after 2 seconds" - basic_notifications - keypress -} - -function run_script { - echo "###################################" - echo "run script" - echo "###################################" - killall dunst - PATH=".:$PATH" ../dunst -config dunstrc.run_script & - ../dunstify -a "dunst tester" -u c \ - "Run Script" "After Keypress, 2 other notification should pop up. THis needs notify-send installed" - keypress - ../dunstify -a "dunst tester" -u c "trigger" "this should trigger a notification" - keypress -} - -function ignore_newline { - echo "###################################" - echo "ignore newline" - echo "###################################" - killall dunst - ../dunst -config dunstrc.ignore_newline_no_wrap & - ../dunstify -a "dunst tester" -u c "Ignore Newline No Wrap" "There should be no newline anywhere" - ../dunstify -a "dunst tester" -u c "Th\nis\n\n\n is\n fu\nll of \n" "\nnew\nlines" - basic_notifications - keypress - - killall dunst - ../dunst -config dunstrc.ignore_newline & - ../dunstify -a "dunst tester" -u c "Ignore Newline" \ - "The only newlines you should encounter here are wordwraps. That's why I'm so long." - ../dunstify -a "dunst tester" -u c "Th\nis\n\n\n is\n fu\nll of \n" "\nnew\nlines" - basic_notifications - keypress -} - -function replace { - echo "###################################" - echo "replace" - echo "###################################" - killall dunst - ../dunst -config dunstrc.default & - id=$(../dunstify -a "dunst tester" -p "Replace" "this should get replaces after keypress") - keypress - ../dunstify -a "dunst tester" -r $id "Success?" "I hope this is not a new notification" - keypress - -} - -function markup { - echo "###################################" - echo "markup" - echo "###################################" - killall dunst - ../dunst -config dunstrc.markup "200x0+10+10" & - ../dunstify -a "dunst tester" "Markup Tests" -u "c" - ../dunstify -a "dunst tester" "bold italic" - ../dunstify -a "dunst tester" "broken markup" - keypress - - killall dunst - ../dunst -config dunstrc.nomarkup "200x0+10+10" & - ../dunstify -a "dunst tester" -u c "NO Markup Tests" - ../dunstify -a "dunst tester" "bolditalic" - ../dunstify -a "dunst tester" "broken markup" - keypress - -} - -function corners { - echo "###################################" - echo "corners" - echo "###################################" - killall dunst - ../dunst -config dunstrc.default -geom "200x0+10+10" & - ../dunstify -a "dunst tester" -u c "upper left" - basic_notifications - keypress - - killall dunst - ../dunst -config dunstrc.default -geom "200x0-10+10" & - ../dunstify -a "dunst tester" -u c "upper right" - basic_notifications - keypress - - killall dunst - ../dunst -config dunstrc.default -geom "200x0-10-10" & - ../dunstify -a "dunst tester" -u c "lower right" - basic_notifications - keypress - - killall dunst - ../dunst -config dunstrc.default -geom "200x0+10-10" & - ../dunstify -a "dunst tester" -u c "lower left" - basic_notifications - keypress - -} - -function geometry { - echo "###################################" - echo "geometry" - echo "###################################" - killall dunst - ../dunst -config dunstrc.default -geom "0x0" & - ../dunstify -a "dunst tester" -u c "0x0" - basic_notifications - keypress - - - killall dunst - ../dunst -config dunstrc.default -geom "200x0" & - ../dunstify -a "dunst tester" -u c "200x0" - basic_notifications - keypress - - killall dunst - ../dunst -config dunstrc.default -geom "200x2" & - ../dunstify -a "dunst tester" -u c "200x2" - basic_notifications - keypress - - killall dunst - ../dunst -config dunstrc.default -geom "200x1" & - ../dunstify -a "dunst tester" -u c "200x1" - basic_notifications - keypress - - killall dunst - ../dunst -config dunstrc.default -geom "0x1" & - ../dunstify -a "dunst tester" -u c "0x1" - basic_notifications - keypress - - killall dunst - ../dunst -config dunstrc.default -geom "-300x1" & - ../dunstify -a "dunst tester" -u c "-300x1" - basic_notifications - keypress - - killall dunst - ../dunst -config dunstrc.default -geom "-300x1-20-20" & - ../dunstify -a "dunst tester" -u c "-300x1-20-20" - basic_notifications - keypress - - killall dunst - ../dunst -config dunstrc.default -geom "x1" & - ../dunstify -a "dunst tester" -u c "x1-20-20" "across the screen" - basic_notifications - keypress -} - -if [ -n "$1" ]; then - while [ -n "$1" ]; do - $1 - shift - done -else - geometry - corners - show_age - run_script - ignore_newline - replace - markup -fi - -killall dunst diff --git a/test/utils.c b/test/utils.c new file mode 100644 index 0000000..1b42146 --- /dev/null +++ b/test/utils.c @@ -0,0 +1,136 @@ +#include "greatest.h" +#include "src/utils.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) +{ + SKIP(); //TODO: Implement this + PASS(); +} + +TEST test_string_to_argv(void) +{ + char **argv = string_to_argv("argv"); + ASSERT_STR_EQ("argv", argv[0]); + ASSERT_EQ( NULL, argv[1]); + free(argv); + argv = NULL; + + argv = string_to_argv("echo test"); + ASSERT_STR_EQ("echo", argv[0]); + ASSERT_STR_EQ("test", argv[1]); + ASSERT_EQ( NULL, argv[2]); + free(argv); + argv = NULL; + + argv = string_to_argv(""); + ASSERT_EQ( NULL, argv[0]); + free(argv); + + PASS(); +} + +TEST test_string_strip_delimited(void) +{ + char *text = malloc(128 * sizeof(char)); + + strcpy(text, "A string_strip_delimited test"); + string_strip_delimited(text, '<', '>'); + ASSERT_STR_EQ("A string_strip_delimited test", text); + + strcpy(text, "Remove html tags"); + string_strip_delimited(text, '<', '>'); + ASSERT_STR_EQ("Remove html tags", text); + + strcpy(text, "Calls|with|identical|delimiters|are|handled|properly"); + string_strip_delimited(text, '|', '|'); + ASSERT_STR_EQ("Calls", text); + + strcpy(text, ""); + string_strip_delimited(text, '<', '>'); + ASSERT_STR_EQ("", text); + + strcpy(text, "Nothing is done if there are no delimiters in the string"); + string_strip_delimited(text, '<', '>'); + ASSERT_STR_EQ("Nothing is done if there are no delimiters in the string", text); + + free(text); + PASS(); +} + +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_to_argv); + RUN_TEST(test_string_strip_delimited); +} +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */