├── .github └── workflows │ ├── main.yml │ └── release.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.org ├── README.org.img ├── more_notifications.png ├── org_20200223_193345_VhlbOf.jpg ├── org_20200223_193450_1en7sh.jpg ├── org_20200223_200131_4WWV2Y.jpg ├── org_20201220_000601_9V037T.jpg ├── org_20210119_120536_adyKnd.jpg ├── org_20210119_122031_BNYKTp.jpg └── org_20210119_122628_AUuEu3.jpg ├── Setup.hs ├── Worklog.org ├── app └── Main.hs ├── buildAndRun.sh ├── com.ph-uhl.deadd.notification.service.in ├── deadd-notification-center.cabal ├── deadd-notification-center.service.in ├── docs ├── linux-notification-center.man └── linux-notification-center.org ├── notification.glade ├── notification_center.glade ├── notification_in_center.glade ├── notification_section.glade ├── pkg-bin └── PKGBUILD ├── pkg-git └── PKGBUILD ├── pkg └── PKGBUILD ├── sendShowupNotis.sh ├── snap └── snapcraft.yaml ├── src ├── Config.hs ├── Helpers.hs ├── NotificationCenter.hs ├── NotificationCenter │ ├── Button.hs │ ├── Glade.hs │ ├── Notification │ │ └── Glade.hs │ ├── NotificationInCenter.hs │ ├── Notifications.hs │ └── Notifications │ │ ├── AbstractNotification.hs │ │ ├── Action.hs │ │ ├── Data.hs │ │ ├── Notification │ │ └── Glade.hs │ │ └── NotificationPopup.hs └── TransparentWindow.hs ├── stack.yaml ├── stack.yaml.lock ├── style.css ├── test └── Spec.hs ├── translation ├── bn_BD.po ├── bn_BD │ └── LC_MESSAGES │ │ └── deadd-notification-center.mo ├── de.po ├── de │ └── LC_MESSAGES │ │ └── deadd-notification-center.mo ├── deadd-notification-center.pot ├── en.po ├── en │ └── LC_MESSAGES │ │ └── deadd-notification-center.mo ├── ru.po ├── ru │ └── LC_MESSAGES │ │ └── deadd-notification-center.mo ├── tr.po └── tr │ └── LC_MESSAGES │ └── deadd-notification-center.mo └── updateyourconfig2021.org /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [master] 7 | 8 | jobs: 9 | build: 10 | name: Build 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - uses: actions/cache@v3 17 | name: Cache ~/.stack 18 | with: 19 | path: ~/.stack 20 | key: stack-global-${{ hashFiles('stack.yaml') }} 21 | restore-keys: stack-global- 22 | 23 | - uses: actions/cache@v3 24 | name: Cache .stack-work 25 | with: 26 | path: .stack-work 27 | key: stack-work-${{ hashFiles('stack.yaml') }}-${{ hashFiles('package.yaml') }}-${{ hashFiles('**/*.hs') }} 28 | restore-keys: stack-work- 29 | 30 | - uses: haskell/actions/setup@v2 31 | name: Setup Haskell Stack 32 | with: 33 | stack-version: latest 34 | stack-no-global: true 35 | stack-setup-ghc: true 36 | enable-stack: true 37 | 38 | - name: Install non-hs dependencies 39 | run: sudo apt-get update && sudo apt-get install libgtk-3-dev gobject-introspection libgirepository1.0-dev libwebkit2gtk-4.0-dev libgtksourceview-3.0-dev 40 | 41 | - name: Install dependencies 42 | run: stack build --only-dependencies 43 | 44 | - name: Build 45 | run: stack build 46 | 47 | - name: Test 48 | run: stack test 49 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Releases 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | jobs: 9 | build: 10 | name: Release 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: write 14 | steps: 15 | - uses: actions/checkout@v3 16 | 17 | - uses: actions/cache@v3 18 | name: Cache ~/.stack 19 | with: 20 | path: ~/.stack 21 | key: stack-global-${{ hashFiles('stack.yaml') }} 22 | restore-keys: stack-global- 23 | 24 | - uses: actions/cache@v3 25 | name: Cache .stack-work 26 | with: 27 | path: .stack-work 28 | key: stack-work-${{ hashFiles('stack.yaml') }}-${{ hashFiles('package.yaml') }}-${{ hashFiles('**/*.hs') }} 29 | restore-keys: stack-work- 30 | 31 | - uses: haskell/actions/setup@v2 32 | name: Setup Haskell Stack 33 | with: 34 | stack-version: latest 35 | stack-no-global: true 36 | stack-setup-ghc: true 37 | enable-stack: true 38 | 39 | - name: Install non-hs dependencies 40 | run: sudo apt-get update && sudo apt-get install libgtk-3-dev gobject-introspection libgirepository1.0-dev libwebkit2gtk-4.0-dev libgtksourceview-3.0-dev 41 | 42 | - name: Install dependencies 43 | run: stack build --only-dependencies 44 | 45 | - name: Build 46 | run: stack install --local-bin-path .out 47 | 48 | - uses: ncipollo/release-action@v1 49 | with: 50 | artifacts: ".out/deadd-notification-center" 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #haskell compilation files 2 | *.hi 3 | *.ho 4 | *.o 5 | 6 | ~* 7 | 8 | #emacs backup files 9 | *~ 10 | *\# 11 | 12 | #tern files 13 | .tern-port 14 | 15 | .stack-work/ 16 | 17 | com.ph-uhl.deadd.notification.service 18 | deadd-notification-center.service 19 | 20 | pkg/ 21 | !pkg/PKGBUILD 22 | 23 | .out/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Philipp Uhl (c) 2017 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | * Neither the name of Author name here nor the names of other 17 | contributors may be used to endorse or promote products derived 18 | from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # deadd-notification-center - A notification center and notification daemon 2 | # See LICENSE file for copyright and license details. 3 | 4 | PREFIX ?= /usr 5 | MANPREFIX = ${PREFIX}/share/man 6 | PKG_CONFIG ?= pkg-config 7 | SYSTEMCTL ?= systemctl 8 | XDG_CONFIG_HOME ?= ${HOME}/.config 9 | XDG_CONFIG_SYSTEM ?= /etc/xdg 10 | 11 | 12 | ifeq (,${SYSTEMD}) 13 | # Check for systemctl to avoid discrepancies on systems, where 14 | # systemd is installed, but systemd.pc is in another package 15 | systemctl := $(shell command -v ${SYSTEMCTL} >/dev/null && echo systemctl) 16 | ifeq (systemctl,${systemctl}) 17 | SYSTEMD := 1 18 | else 19 | SYSTEMD := 0 20 | endif 21 | endif 22 | 23 | ifneq (0,${SYSTEMD}) 24 | SERVICEDIR_SYSTEMD ?= $(shell $(PKG_CONFIG) systemd --variable=systemduserunitdir) 25 | SERVICEDIR_SYSTEMD := ${SERVICEDIR_SYSTEMD} 26 | ifeq (,${SERVICEDIR_SYSTEMD}) 27 | $(error "Failed to query $(PKG_CONFIG) for package 'systemd'!") 28 | endif 29 | endif 30 | 31 | SERVICEDIR_DBUS ?= $(shell $(PKG_CONFIG) dbus-1 --variable=session_bus_services_dir) 32 | SERVICEDIR_DBUS := ${SERVICEDIR_DBUS} 33 | 34 | 35 | 36 | all: stack service 37 | 38 | 39 | stack: 40 | stack setup 41 | stack install --local-bin-path .out 42 | 43 | clean-stack: 44 | rm -f -r .stack-work 45 | rm -f -r .out 46 | rm -f com.ph-uhl.deadd.notification.service 47 | rm -f deadd.notification.systemd.service 48 | 49 | clean: clean-stack 50 | 51 | distclean: clean clean-config 52 | 53 | clean-config: 54 | rm -f ${XDG_CONFIG_HOME}/deadd/deadd.css 55 | rm -f ${XDG_CONFIG_HOME}/deadd/deadd.conf 56 | 57 | doc: 58 | stack haddock 59 | 60 | service: 61 | @sed "s|##PREFIX##|$(PREFIX)|" com.ph-uhl.deadd.notification.service.in > com.ph-uhl.deadd.notification.service 62 | @sed "s|##PREFIX##|$(PREFIX)|" deadd-notification-center.service.in > deadd-notification-center.service 63 | 64 | install-stack: 65 | mkdir -p ${DESTDIR}${PREFIX}/bin 66 | install -Dm755 .out/deadd-notification-center ${DESTDIR}${PREFIX}/bin 67 | mkdir -p ${DESTDIR}${MANPREFIX}/man1 68 | install -Dm644 docs/linux-notification-center.man ${DESTDIR}${MANPREFIX}/man1/deadd-notification-center.1 69 | install -Dm644 LICENSE ${DESTDIR}${PREFIX}/share/licenses/deadd-notification-center/LICENSE 70 | 71 | CSS_CONFIG_DIR = ${DESTDIR}${XDG_CONFIG_SYSTEM}/deadd 72 | CSS_CONFIG_FILE = ${CSS_CONFIG_DIR}/deadd.css 73 | install-config: 74 | mkdir -p ${CSS_CONFIG_DIR} 75 | ifneq ("$(wildcard $(CSS_CONFIG_FILE))","") 76 | install -Dm644 style.css ${CSS_CONFIG_FILE}_new 77 | $(warning Warning: $(CSS_CONFIG_FILE) exists. Instead of overwriting, created $(CSS_CONFIG_FILE)_new) 78 | else 79 | install -Dm644 style.css ${CSS_CONFIG_FILE} 80 | endif 81 | 82 | 83 | 84 | install-service: service 85 | mkdir -p ${DESTDIR}${SERVICEDIR_DBUS} 86 | install -Dm644 com.ph-uhl.deadd.notification.service ${DESTDIR}${SERVICEDIR_DBUS} 87 | ifneq (0,${SYSTEMD}) 88 | install-service: install-service-systemd 89 | install-service-systemd: 90 | mkdir -p ${DESTDIR}${SERVICEDIR_SYSTEMD} 91 | install -Dm644 deadd-notification-center.service ${DESTDIR}${SERVICEDIR_SYSTEMD} 92 | endif 93 | 94 | 95 | 96 | 97 | install-lang: 98 | mkdir -p ${DESTDIR}${PREFIX}/share/locale/bn_BD/LC_MESSAGES 99 | mkdir -p ${DESTDIR}${PREFIX}/share/locale/de/LC_MESSAGES 100 | mkdir -p ${DESTDIR}${PREFIX}/share/locale/en/LC_MESSAGES 101 | mkdir -p ${DESTDIR}${PREFIX}/share/locale/tr/LC_MESSAGES 102 | install -Dm644 translation/bn_BD/LC_MESSAGES/deadd-notification-center.mo ${DESTDIR}${PREFIX}/share/locale/bn_BD/LC_MESSAGES/deadd-notification-center.mo 103 | install -Dm644 translation/de/LC_MESSAGES/deadd-notification-center.mo ${DESTDIR}${PREFIX}/share/locale/de/LC_MESSAGES/deadd-notification-center.mo 104 | install -Dm644 translation/en/LC_MESSAGES/deadd-notification-center.mo ${DESTDIR}${PREFIX}/share/locale/en/LC_MESSAGES/deadd-notification-center.mo 105 | install -Dm644 translation/tr/LC_MESSAGES/deadd-notification-center.mo ${DESTDIR}${PREFIX}/share/locale/tr/LC_MESSAGES/deadd-notification-center.mo 106 | 107 | install: install-stack install-service install-lang install-config 108 | 109 | uninstall: 110 | rm -f ${DESTDIR}${PREFIX}/bin/deadd-notification-center 111 | rm -f ${DESTDIR}${MANPREFIX}/man1/deadd-notification-center.1 112 | rm -f ${DESTDIR}${SERVICEDIR_DBUS}/com.ph-uhl.deadd.notification.service 113 | rm -f ${DESTDIR}${PREFIX}/share/licenses/deadd-notification-center/LICENSE 114 | rm -f ${DESTDIR}${PREFIX}/share/locale/{bn_BD,de,en,tr}/LC_MESSAGES/deadd-notification-center.mo 115 | 116 | ifneq (0,${SYSTEMD}) 117 | uninstall: uninstall-service-systemd 118 | uninstall-service-systemd: 119 | rm -f ${DESTDIR}${SERVICEDIR_SYSTEMD}/deadd-notification-center.service 120 | endif 121 | 122 | 123 | .PHONY: all clean install uninstall 124 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | 2 | Discalimer: Currently no active development going on. 3 | 4 | [[https://github.com/phuhl/linux_notification_center/workflows/CI/badge.svg?branch=master]] 5 | 6 | * Linux Notification Center 7 | 8 | A haskell-written notification center for users that like a desktop with style... 9 | 10 | Take part in the discussion on our [[https://github.com/phuhl/linux_notification_center/discussions][discussion board]]! 11 | 12 | #+BEGIN_EXAMPLE 13 | 14 | 15 | 16 | ▙ █ █ ████████████ ███▙ █ ▄██████▄ 17 | ███▄ █ █ ████ █ ███▙ █ ████ ██ 18 | ▜█████▄ █ █ ████ █ ███▙ █ █ ████ ▜█ 19 | ▜██████▄ █ █ ████ █ ███▙ ▙ █ █ ████ 20 | █▄ ▜██████▄ █ █ ████████████ █ ███▙ ██▙ █ █ ████ 21 | █ ▜█▄ ▜█████▄█ █ ████ █ ███▙ ███▙ █ █ ████ 22 | █ ▜█▄ ▜████ █ ████ █ ███▙█ ████ █ ████ 23 | █ ▜█▄ ▜█ █ ████ █ ███ █ ██ █▙ █ ████ 24 | █ ▜█▄ █ ████ █ █ █ █▙ █ ████ 25 | █ ▜▄ █ ████████████ █ █ ▜████▛ ██▛ 26 | 27 | 28 | ▄▇▇▀▀▀▀█▌ ▄▄▄▄▄▀▀██▇█▄▄▀▀▀▀▀ ▄▄▄▄▄▄ ▄▄█▇██▀▀▄▄▄▄▄ ▐█▀▀▀▀▇▇▄ 29 | ▐██▌ ▀▀ ▄▄▄████████▄▄█▀ ▄█████▀▀▀███▄ ▀█▄▄████████▄▄▄ ▀▀ ▐██▌ 30 | ████▄ ▄▄▄██████████▀▀ ▀▀▀█▄ ▇████▀ ▀▀██▌ ▄█▀▀▀ ▀▀██████████▄▄▄ ▄████ 31 | ▀███████████████▀▀ ▐█▌ ██████ ▄ ▐███ ▐█▌ ▀▀███████████████▀ 32 | ▀▀▀▀██████▀ ▀▀ ▐██▐▇▇ ▀▀▀▀ ▀▀ ▀██████▀▀▀▀ 33 | ▄▄██████▀ ██ ██▌ ▄ ▀██████▄▄ 34 | ▄███████▀ █▀██ ▀█▄▄ ▄▄ ▀███████▄ 35 | ▄███████ ▀██▄▄█████████▀ ███████▄ 36 | ██████ ▀▀█████▀▀▀ ███████ 37 | ▐████▌ ▐████▌ 38 | ███▄▄▄▄▄▌ ▐▄▄▄▄▄███ 39 | ▀▀▀▀▀▀ ▀▀▀▀▀▀ 40 | 41 | 42 | ╒══════╕ 43 | NEW │ 2023 │ Update: 44 | ╘══════╛ 45 | 46 | 47 | For all users of the non-git packages: 48 | ¨¨¨¨¨ ¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨ 49 | 50 | Customize all CSS might [probably will] brake your current styling settings. 51 | (more information on this in the file updateyourconfig2021.org) 52 | 53 | 54 | New config style 55 | ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ 56 | 57 | █▀▄ █▀▄ █▀█ █ █ ▀█▀ █▀█ █▀▀ █▀▀ █ █ █▀█ █▀█ █▀▀ █▀▀ 58 | █▀▄ █▀▄ █▀█ █▀▄ █ █ █ █ █ █ █▀█ █▀█ █ █ █ █ █▀▀ 59 | ▀▀ ▀ ▀ ▀ ▀ ▀ ▀ ▀▀▀ ▀ ▀ ▀▀▀ ▀▀▀ ▀ ▀ ▀ ▀ ▀ ▀ ▀▀▀ ▀▀▀ 60 | 61 | The configuration file is now in YAML format. It's also massively 62 | cleaned up thanks to the input of MyriaCore! 63 | 64 | The new configuration allows for: 65 | 66 | Extended Scriptability 67 | ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ 68 | 69 | Now any notification can be modified by an external script. This 70 | allows for all the logic you could possibly do on a notification. 71 | 72 | Define special styles for special notifications! 73 | 74 | 75 | Many more fixes 76 | ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ 77 | 78 | Additional Features from 2021 79 | ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ 80 | 81 | New deadd.css configuration file 82 | Style all elements yourself 83 | 84 | Reload CSS styling on the fly 85 | 86 | Use CSS transitions when loading a new colortheme 87 | and smoothly change the mood 88 | 89 | 90 | 91 | As always, many thanks to our contributors: 92 | 93 | 94 | (¯`·.¸¸.·´ `·.¸¸.·´¯) 95 | ( \ / ) 96 | ( \ ) MyriaCore S-NA ahmubashshir mgil2 resolritter ( / ) 97 | ( ) ( woutervb avdv TaylanTatli rbowden91 kianmeng ) ( ) 98 | ( / ) opalmay balsoft trk9001 CobaltSpace lierdakil ( \ ) 99 | ( / \ ) 100 | (_.·´¯`·.¸ ¸.·´¯`·.¸_) 101 | 102 | 103 | Thank you very much! 104 | 105 | 106 | Finally, thank you to everyone who created issues, commented, and 107 | helped to further this project. 108 | 109 | ▝ 110 | ▝ 111 | ▖ ▐ 112 | ▖ ▐ 113 | ▌ Further news in releasenotes.org ▐ 114 | ▙▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▟ 115 | 116 | #+END_EXAMPLE 117 | 118 | 119 | ** Features 120 | 121 | The notification center receives notifications via DBUS (like any 122 | notification daemon) and shows them in the upper right corner of the 123 | screen. The notification (if not specified in the notification 124 | otherwise) will also be shown in the notification center even after 125 | the notification disappeared by itself. The notifications can be 126 | clicked to make them disappear. 127 | 128 | Notifications can be replaced by the use of the =replaces-id= feature 129 | of the notification specification. 130 | 131 | The notification center can (optionally) show user-specified buttons 132 | in the bottom that can be in two states (highlighted/not highlighted) 133 | and that can run customizable shell commands. 134 | 135 | 136 | ** Screenshots 137 | 138 | Some applications, notification: 139 | [[file:README.org.img/org_20200223_193450_1en7sh.jpg]] 140 | 141 | Notification Center opened: 142 | [[file:README.org.img/org_20200223_193345_VhlbOf.jpg]] 143 | 144 | Link, Markup, Progressbar, and Action support: 145 | [[file:README.org.img/org_20201220_000601_9V037T.jpg]] 146 | 147 | 148 | ** Usage 149 | 150 | To start it: 151 | #+BEGIN_SRC sh 152 | > deadd-notification-center 153 | #+END_SRC 154 | 155 | 156 | Toggle between hidden and shown state of the notification center: 157 | #+BEGIN_SRC sh 158 | kill -s USR1 $(pidof deadd-notification-center) 159 | #+END_SRC 160 | 161 | 162 | Set the state of a user defined button (in this example the first 163 | button, which has =id= 0): 164 | #+BEGIN_SRC sh 165 | # turn highlighting on 166 | notify-send.py a --hint boolean:deadd-notification-center:true \ 167 | int:id:0 boolean:state:true type:string:buttons 168 | 169 | # turn highlighting off 170 | notify-send.py a --hint boolean:deadd-notification-center:true \ 171 | int:id:0 boolean:state:false type:string:buttons 172 | #+END_SRC 173 | This snippet uses [[https://github.com/phuhl/notify-send.py][notify-send.py]], an improved version of libnotify 174 | (notify-send). 175 | 176 | Clear all notifications 177 | #+BEGIN_SRC sh 178 | # within the notification center 179 | notify-send.py a --hint boolean:deadd-notification-center:true \ 180 | string:type:clearInCenter 181 | 182 | # popups 183 | notify-send.py a --hint boolean:deadd-notification-center:true \ 184 | string:type:clearPopups 185 | #+END_SRC 186 | 187 | Pause/Unpause popup notifications 188 | #+BEGIN_SRC sh 189 | # pause popup notifications 190 | notify-send.py a --hint boolean:deadd-notification-center:true \ 191 | string:type:pausePopups 192 | 193 | # unpause popup notifications 194 | notify-send.py a --hint boolean:deadd-notification-center:true \ 195 | string:type:unpausePopups 196 | #+END_SRC 197 | 198 | 199 | Reload CSS Styling file 200 | #+BEGIN_SRC sh 201 | notify-send.py a --hint boolean:deadd-notification-center:true \ 202 | string:type:reloadStyle 203 | #+END_SRC 204 | 205 | 206 | Send notifications that only show up in the notification center but do 207 | not produce a popup: 208 | #+BEGIN_SRC sh 209 | notify-send.py "Does not pop up" -t 1 210 | #+END_SRC 211 | 212 | *** Supported hints and features 213 | 214 | Action buttons with gtk icons 215 | #+BEGIN_SRC sh 216 | notify-send.py "And buttons" "Do you like buttons?" \ 217 | --hint boolean:action-icons:true \ 218 | --action yes:face-cool no:face-sick 219 | #+END_SRC 220 | 221 | Notification images by gtk icon 222 | #+BEGIN_SRC sh 223 | notify-send.py "Icons are" "COOL" \ 224 | --hint string:image-path:face-cool 225 | #+END_SRC 226 | 227 | Notification images by file 228 | #+BEGIN_SRC sh 229 | notify-send.py "Images are" "COOL" \ 230 | --hint string:image-path:file://path/to/image/from/root.png 231 | #+END_SRC 232 | 233 | Notification with progress bar 234 | #+BEGIN_SRC sh 235 | notify-send.py "This notification has a progressbar" "33%" \ 236 | --hint int:has-percentage:33) 237 | #or 238 | notify-send.py "This notification has a progressbar" "33%" \ 239 | --hint int:value:33) 240 | #+END_SRC 241 | 242 | Notification with slider 243 | #+BEGIN_SRC sh 244 | notify-send.py "This notification has a slider" "33%" \ 245 | --hint int:has-percentage:33 246 | --action changeValue:abc) 247 | #+END_SRC 248 | 249 | *** Example: Brightness indicator 250 | 251 | This snippet can be used to produce a brightness-indicator. It requires the 252 | [[https://github.com/phuhl/notify-send.py][notify-send.py]] script. 253 | 254 | #+BEGIN_SRC sh 255 | #!/bin/bash 256 | 257 | if [ "$1" == "inc" ]; then 258 | xbacklight -inc 5 259 | fi 260 | 261 | if [ "$1" == "dec" ]; then 262 | xbacklight -lower 5 263 | fi 264 | 265 | BRIGHTNESS=$(xbacklight -get) 266 | NOTI_ID=$(notify-send.py "Bildschirmhelligkeit" "$BRIGHTNESS/100" \ 267 | --hint string:image-path:video-display boolean:transient:true \ 268 | int:has-percentage:$BRIGHTNESS \ 269 | --replaces-process "brightness-popup") 270 | #+END_SRC 271 | 272 | *** Example: Volume indicator 273 | 274 | This snippet can be used to produce a volume-indicator. It requires the 275 | [[https://github.com/phuhl/notify-send.py][notify-send.py]] script. 276 | 277 | #+BEGIN_SRC sh 278 | #!/bin/bash 279 | 280 | if [ "$1" == "inc" ]; then 281 | amixer -q sset Master 5%+ 282 | fi 283 | 284 | if [ "$1" == "dec" ]; then 285 | amixer -q sset Master 5%- 286 | fi 287 | 288 | if [ "$1" == "mute" ]; then 289 | amixer -q sset Master toggle 290 | fi 291 | 292 | 293 | AMIXER=$(amixer sget Master) 294 | VOLUME=$(echo $AMIXER | grep 'Right:' | awk -F'[][]' '{ print $2 }' | tr -d "%") 295 | MUTE=$(echo $AMIXER | grep -o '\[off\]' | tail -n 1) 296 | if [ "$VOLUME" -le 20 ]; then 297 | ICON=audio-volume-low 298 | else if [ "$VOLUME" -le 60 ]; then 299 | ICON=audio-volume-medium 300 | else 301 | ICON=audio-volume-high 302 | fi 303 | fi 304 | if [ "$MUTE" == "[off]" ]; then 305 | ICON=audio-volume-muted 306 | fi 307 | 308 | 309 | 310 | NOTI_ID=$(notify-send.py "Lautstärke" "$VOLUME/100" \ 311 | --hint string:image-path:$ICON boolean:transient:true \ 312 | int:has-percentage:$VOLUME \ 313 | --replaces-process "volume-popup") 314 | #+END_SRC 315 | 316 | 317 | ** Install 318 | 319 | Install from the AUR for Arch: [[https://aur.archlinux.org/packages/deadd-notification-center/][deadd-notification-center]]. 320 | 321 | *OR* 322 | 323 | If you want to spare yourself the hassle of the 324 | build time there is a binary package available: 325 | [[https://aur.archlinux.org/packages/deadd-notification-center-bin/][deadd-notification-center-bin]]. 326 | 327 | *OR* 328 | 329 | If you don't want to wait for me to publish the next stable release: Use the new AUR git-package. 330 | 331 | [[https://aur.archlinux.org/packages/deadd-notification-center-git/][deadd-notification-center-git]]. 332 | 333 | *OR* 334 | 335 | On Ubuntu, Debian, everything... Replace 1.7.2 with the current-most release from the 336 | [[https://github.com/phuhl/linux_notification_center/releases][release section]]. 337 | 338 | Manually install the dependencies (exact names might differ in your distribution): 339 | - gtk3 340 | - gobject-introspection-runtime 341 | 342 | #+BEGIN_SRC sh 343 | tar -xvzf linux_notification_center-1.7.2.tar.gz 344 | cd linux_notification_center-1.7.2 345 | wget https://github.com/phuhl/linux_notification_center/releases/download/1.7.2/deadd-notification-center 346 | mkdir -p .out 347 | mv deadd-notification-center .out 348 | sudo make install 349 | #+END_SRC 350 | 351 | *OR* 352 | 353 | Dependencies: 354 | - [[https://www.archlinux.org/packages/community/x86_64/stack/][stack]] 355 | - cairo 356 | - pango 357 | - gobject-introspection 358 | - gtk3 359 | 360 | #+BEGIN_SRC shell-script 361 | make 362 | sudo make install 363 | #+END_SRC 364 | 365 | ** Configuration 366 | 367 | NOTE: Some styling config has moved. More infos in this file: 368 | [[https://github.com/phuhl/linux_notification_center/blob/master/updateyourconfig2021.org][updateyourconfig2021.org]] 369 | 370 | No configuration is necessary, the notification center comes with 371 | sensible defaults™. 372 | 373 | All colors and sizes are customizable, as well as the default timeout 374 | for notifications and the optional buttons in the notification 375 | center. Below are possible configurable options shown. The 376 | configuration file must be located at =~/.config/deadd/deadd.yml= (or, 377 | if configured differently on your system: 378 | =${XDG_CONFIG_HOME}/deadd/deadd.yml=). 379 | 380 | Additionally, a =deadd.css= will be loaded from the same folder. It 381 | contains the styling of the notification center. You can load changes 382 | from the =deadd.css= file by using the command described in the section 383 | "Usage". 384 | 385 | #+BEGIN_SRC yaml 386 | ### Margins for notification-center/notifications 387 | margin-top: 0 388 | margin-right: 0 389 | 390 | ### Margins for notification-center 391 | margin-bottom: 0 392 | 393 | ### Width of the notification center/notifications in pixels. 394 | width: 500 395 | 396 | ### Command to run at startup. This can be used to setup 397 | ### button states. 398 | # startup-command: deadd-notification-center-startup 399 | 400 | ### Monitor on which the notification center/notifications will be 401 | ### printed. If "follow-mouse" is set true, this does nothing. 402 | monitor: 0 403 | 404 | ### If true, the notification center/notifications will open on the 405 | ### screen, on which the mouse is. Overrides the "monitor" setting. 406 | follow-mouse: false 407 | 408 | notification-center: 409 | ### Margin at the top/right/bottom of the notification center in 410 | ### pixels. This can be used to avoid overlap between the notification 411 | ### center and bars such as polybar or i3blocks. 412 | # margin-top: 0 413 | # margin-right: 0 414 | # margin-bottom: 0 415 | 416 | ### Width of the notification center in pixels. 417 | # width: 500 418 | 419 | ### Monitor on which the notification center will be printed. If 420 | ### "follow-mouse" is set true, this does nothing. 421 | # monitor: 0 422 | 423 | ### If true, the notification center will open on the screen, on which 424 | ### the mouse is. Overrides the "monitor" setting. 425 | # follow-mouse: false 426 | 427 | ### Notification center closes when the mouse leaves it 428 | hide-on-mouse-leave: true 429 | 430 | ### If newFirst is set to true, newest notifications appear on the top 431 | ### of the notification center. Else, notifications stack, from top to 432 | ### bottom. 433 | new-first: true 434 | 435 | ### If true, the transient field in notifications will be ignored, 436 | ### thus the notification will be persisted in the notification 437 | ### center anyways 438 | ignore-transient: false 439 | 440 | ### Custom buttons in notification center 441 | buttons: 442 | ### Numbers of buttons that can be drawn on a row of the notification 443 | ### center. 444 | # buttons-per-row: 5 445 | 446 | ### Height of buttons in the notification center (in pixels). 447 | # buttons-height: 60 448 | 449 | ### Horizontal and vertical margin between each button in the 450 | ### notification center (in pixels). 451 | # buttons-margin: 2 452 | 453 | ### Button actions and labels. For each button you must specify a 454 | ### label and a command. 455 | actions: 456 | # - label: VPN 457 | # command: "sudo vpnToggle" 458 | # - label: Bluetooth 459 | # command: bluetoothToggle 460 | # - label: Wifi 461 | # command: wifiToggle 462 | # - label: Screensaver 463 | # command: screensaverToggle 464 | # - label: Keyboard 465 | # command: keyboardToggle 466 | 467 | notification: 468 | ### If true, markup (, , , ) will be displayed properly 469 | use-markup: true 470 | 471 | ### If true, html entities (& for &, % for %, etc) will be 472 | ### parsed properly. This is useful for chromium-based apps, which 473 | ### tend to send these in notifications. 474 | parse-html-entities: true 475 | 476 | dbus: 477 | 478 | ### If noti-closed messages are enabled, the sending application 479 | ### will know that a notification was closed/timed out. This can 480 | ### be an issue for certain applications, that overwrite 481 | ### notifications on status updates (e.g. Spotify on each 482 | ### song). When one of these applications thinks, the notification 483 | ### has been closed/timed out, they will not overwrite existing 484 | ### notifications but send new ones. This can lead to redundant 485 | ### notifications in the notification center, as the close-message 486 | ### is send regardless of the notification being persisted. 487 | send-noti-closed: false 488 | 489 | app-icon: 490 | 491 | ### If set to true: If no icon is passed by the app_icon parameter 492 | ### and no application "desktop-entry"-hint is present, deadd will 493 | ### try to guess the icon from the application name (if present). 494 | guess-icon-from-name: true 495 | 496 | ### The display size of the application icons in the notification 497 | ### pop-ups and in the notification center 498 | icon-size: 20 499 | 500 | image: 501 | 502 | ### The maximal display size of images that are part of 503 | ### notifications for notification pop-ups and in the notification 504 | ### center 505 | size: 100 506 | 507 | ### The margin around the top, bottom, left, and right of 508 | ### notification images. 509 | margin-top: 15 510 | margin-bottom: 15 511 | margin-left: 15 512 | margin-right: 0 513 | 514 | ### Apply modifications to certain notifications: 515 | ### Each modification rule needs a "match" and either a "modify" or 516 | ### a "script" entry. 517 | modifications: 518 | ### Match: 519 | ### Matches the notifications against these rules. If all of the 520 | ### values (of one modification rule) match, the "modify"/"script" 521 | ### part is applied. 522 | # - match: 523 | ### Possible match criteria: 524 | # title: "Notification title" 525 | # body: "Notification body" 526 | # time: "12:44" 527 | # app-name: "App name" 528 | # urgency: "low" # "low", "normal" or "critical" 529 | 530 | # modify: 531 | ### Possible modifications 532 | # title: "abc" 533 | # body: "abc" 534 | # app-name: "abc" 535 | # app-icon: "file:///abc.png" 536 | ### The timeout has three special values: 537 | ### timeout: 0 -> don't time out at all 538 | ### timeout: -1 -> use default timeout 539 | ### timeout: 1 -> don't show as pop-up 540 | ### timeout: >1 -> milliseconds until timeout 541 | # timeout: 1 542 | # margin-right: 10 543 | # margin-top: 10 544 | # image: "file:///abc.png" 545 | # image-size: 10 546 | # transient: true 547 | # send-noti-closed: false 548 | ### Remove action buttons from notifications 549 | # remove-actions: true 550 | ### Set the action-icons hint to true, action labels will then 551 | ### be intergreted as GTK icon names 552 | # action-icons: true 553 | ### List of actions, where the even elements (0, 2, ...) are the 554 | ### action name and the odd elements are the label 555 | # actions: 556 | # - previous 557 | # - media-skip-backward 558 | # - play 559 | # - media-playback-start 560 | # - next 561 | # - media-skip-forward 562 | ### Action commands, where the keys (e.g. "play") is the action 563 | ### name and the value is a program call that should be executed 564 | ### on action. Prevents sending of the action to the application. 565 | # action-commands: 566 | # play: playerctl play-pause 567 | # previous: playerctl previous 568 | # next: playerctl next 569 | 570 | ### Add a class-name to the notification container, that can be 571 | ### used for specific styling of notifications using the 572 | ### deadd.css file 573 | # class-name: "abc" 574 | 575 | # - match: 576 | # app-name: "Chromium" 577 | 578 | ### Instead of modifying a notification directly, a script can be 579 | ### run, which will receive the notification as JSON on STDIN. It 580 | ### is expected to return JSON/YAML configuration that defines the 581 | ### modifications that should be applied. Minimum complete return 582 | ### value must be '{"modify": {}, "match": {}}'. Always leave the "match" 583 | ### object empty (technical reasons, i.e. I am lazy). 584 | # script: "linux-notification-center-parse-chromium" 585 | - match: 586 | app-name: "Spotify" 587 | modify: 588 | image-size: 80 589 | timeout: 1 590 | send-noti-closed: true 591 | class-name: "Spotify" 592 | action-icons: true 593 | actions: 594 | - previous 595 | - media-skip-backward 596 | - play 597 | - media-playback-start 598 | - next 599 | - media-skip-forward 600 | action-commands: 601 | play: playerctl play-pause 602 | previous: playerctl previous 603 | next: playerctl next 604 | 605 | # - match: 606 | # title: Bildschirmhelligkeit 607 | # modify: 608 | # image-size: 60 609 | popup: 610 | 611 | ### Default timeout used for notifications in milli-seconds. This can 612 | ### be overwritten with the "-t" option (or "--expire-time") of the 613 | ### notify-send command. 614 | default-timeout: 10000 615 | 616 | ### Margin above/right/between notifications (in pixels). This can 617 | ### be used to avoid overlap between notifications and a bar such as 618 | ### polybar or i3blocks. 619 | margin-top: 50 620 | margin-right: 50 621 | margin-between: 20 622 | 623 | ### Defines after how many lines of text the body will be truncated. 624 | ### Use 0 if you want to disable truncation. 625 | max-lines-in-body: 3 626 | 627 | ### Determines whether the GTK widget that displays the notification body 628 | ### in the notification popup will be hidden when empty. This is especially 629 | ### useful for transient notifications that display a progress bar. 630 | # hide-body-if-empty: false 631 | 632 | ### Monitor on which the notifications will be 633 | ### printed. If "follow-mouse" is set true, this does nothing. 634 | # monitor: 0 635 | 636 | ### If true, the notifications will open on the 637 | ### screen, on which the mouse is. Overrides the "monitor" setting. 638 | # follow-mouse: false 639 | 640 | click-behavior: 641 | 642 | ### The mouse button for dismissing a popup. Must be either "mouse1", 643 | ### "mouse2", "mouse3", "mouse4", or "mouse5" 644 | dismiss: mouse1 645 | 646 | ### The mouse button for opening a popup with the default action. 647 | ### Must be either "mouse1", "mouse2", "mouse3", "mouse4", or "mouse5" 648 | default-action: mouse3 649 | #+END_SRC 650 | 651 | *** CSS styling 652 | 653 | The default CSS style can be found in =/etc/xdg/deadd/deadd.css=. It is 654 | advised to copy this file to =${XDG_CONFIG_HOME}/deadd/deadd.css= (usually 655 | =.config/deadd/deadd.css=) if you want to make changes. 656 | 657 | In the file you can change CSS styles (GTK3-flavor). Should the 658 | installation not have created a =dead.css=, you can use the content of 659 | [[https://github.com/phuhl/linux_notification_center/blob/master/style.css][style.css]] as a foundation. 660 | 661 | The following class-names for labels are defined: 662 | - Notifications: 663 | - =label.deadd-noti-center.notification.appname= :: Appname 664 | - =label.deadd-noti-center.notification.body= :: Textbody 665 | - =label.deadd-noti-center.notification.title= :: Notification title 666 | - =image.deadd-noti-center.notification.image= :: Image of a notification 667 | - =image.deadd-noti-center.notification.icon= :: Appicon 668 | - =button.deadd-noti-center.notification.actionbutton= :: Action buttons 669 | - Notifications in the notification center: 670 | - =label.deadd-noti-center.in-center.appname= :: Appname 671 | - =label.deadd-noti-center.in-center.body= :: Textbody 672 | - =label.deadd-noti-center.in-center.title= :: Notification title 673 | - =label.deadd-noti-center.in-center.time= :: Notification time 674 | - =image.deadd-noti-center.in-center.image= :: Image of a notification 675 | - =image.deadd-noti-center.in-center.icon= :: Appicon 676 | - =button.deadd-noti-center.in-center.button-close= :: Close button on notification 677 | - =button.deadd-noti-center.in-center.actionbutton= :: Action buttons 678 | - Notification-center: 679 | - =label.deadd-noti-center.noti-center.time= :: The big time at the top 680 | - =label.deadd-noti-center.noti-center.date= :: The date text 681 | - =label.deadd-noti-center.noti-center.delete-all= :: "Delete all" Button 682 | - =button.deadd-noti-center.noti-center.userbutton= :: User buttons 683 | 684 | Additionally, you can specify custom class-names in the 685 | modifications-section of your =deadd.yml=. These class names will be 686 | defined on the notification container (all notification elements lie 687 | within) of pop-ups and in-center. 688 | 689 | **** Examples: 690 | 691 | *Remove appname and icon from notifications* 692 | 693 | #+BEGIN_SRC css 694 | image.deadd-noti-center.notification.icon, 695 | label.deadd-noti-center.notification.appname, 696 | image.deadd-noti-center.in-center.icon, 697 | label.deadd-noti-center.in-center.appname { 698 | opacity: 0 699 | } 700 | #+END_SRC 701 | 702 | 703 | *Change font* 704 | 705 | #+BEGIN_SRC css 706 | .deadd-noti-center { 707 | font-family: monospace; 708 | } 709 | #+END_SRC 710 | 711 | 712 | 713 | *Specify special background for one app* 714 | 715 | [[file:README.org.img/org_20210119_120536_adyKnd.jpg]] 716 | 717 | #+BEGIN_SRC css 718 | .notificationInCenter.Spotify { 719 | background: linear-gradient(130deg, rgba(0, 0, 0, 0.1), rgba(0, 255, 0, 0.3)); 720 | border-radius: 5px; 721 | } 722 | #+END_SRC 723 | 724 | This change requires a modification in your deadd.yml: 725 | 726 | #+BEGIN_SRC yaml 727 | notification: 728 | modifications: 729 | - match: 730 | app-name: "Spotify" 731 | modify: 732 | class-name: "Spotify" 733 | #+END_SRC 734 | 735 | *** Notification-based scripting 736 | 737 | You can modify notifications if they match certain criteria. 738 | 739 | _Matching:_ 740 | 741 | The criteria you can specify are equality for the following parameters: 742 | - title 743 | - body 744 | - app-name 745 | - time 746 | 747 | The matching parameters can be specified in the section 748 | =notification.modifications.match= of your =deadd.yml=. 749 | 750 | _Modifying:_ 751 | 752 | You can set the following parameters: 753 | 754 | - =title= 755 | - =body= 756 | - =app-name= 757 | - =timeout= (specified in milliseconds) 758 | - =margin-right= (overrides ~distanceRight~ from the configuration) 759 | - =margin-top= (overrides ~distanceTop~ from the configuration) 760 | - =icon= (overrides the app-icon, value must be either empty, a path to 761 | an image or a gtk-icon-name) 762 | - =image= (overrides the image of the notification, value must be either 763 | empty, a path to an image or a gtk-icon-name) 764 | - =image-size= 765 | - =transient= (value has to be =true= or =false=) 766 | - =send-noti-closed= (value has to be =true= or =false=, if set to true it 767 | will prevent that a DBUS =NotificationClosed= message will be send 768 | for this notification. Only applies if the configuration parameter 769 | =configSendNotiClosedDbusMessage= is set to =true=) 770 | - =class-name= (adds a CSS-class name to the container of the 771 | notification for styling) 772 | - =remove-actions= (value can be anything, if used, no action buttons 773 | will be displayed on the notifications) 774 | - =action-icons= (=true= or =false=, if set to =true= the action label will be 775 | interpreted as an gtk-icon-name and an icon is displayed) 776 | - =actions= (array where every even element is the action name, every 777 | odd element is the action label. Actions are rendered as buttons in 778 | a notification. As the action most likely won't be known by the 779 | notification sender, you probably want to use this with the 780 | =action-commands= modification) 781 | - =action-commands= (object where the keys are the action name and the 782 | value is a program call that should be executed when the action 783 | button has been clicked. Prevents sending of the action to the 784 | application.) 785 | 786 | The modification parameters can be specified in the section 787 | =notification.modifications.modify= of your =deadd.yml=. 788 | 789 | 790 | _Running Scripts:_ 791 | 792 | Instead of modifying a notification directly, a script can be run, 793 | which will receive the notification as JSON on STDIN. The script is 794 | expected to return JSON/YAML configuration that defines the 795 | modifications that should be applied. Minimum complete return value 796 | must be ={"modify": {}, "match": {}}=. Always leave the "match" object 797 | empty (technical reasons, i.e. I am lazy). 798 | 799 | *Example script* to turn a notification from WhatsApp in Chromium into 800 | something sensible: 801 | 802 | Before: 803 | 804 | [[file:README.org.img/org_20210119_122628_AUuEu3.jpg]] 805 | 806 | After: 807 | 808 | [[file:README.org.img/org_20210119_122031_BNYKTp.jpg]] 809 | 810 | Configuration: 811 | 812 | #+BEGIN_SRC yaml 813 | notification: 814 | modifications: 815 | - match: 816 | app-name: "Chromium" 817 | script: "linux-notification-center-parse-chromium" 818 | 819 | #+END_SRC 820 | 821 | 822 | Script executed on Chromium notifications: 823 | 824 | #+BEGIN_SRC sh 825 | #!/bin/bash 826 | 827 | # Read notification from STDIN 828 | noti="" 829 | while read line 830 | do 831 | noti=${noti}${line} 832 | done < "${1:-/dev/stdin}" 833 | 834 | # Use jq to parse JSON and get the body field of the notification 835 | body=$(echo $noti | jq '.body') 836 | if [[ "$body" == "\"web.whatsapp.com"* ]]; then 837 | # It's Whatsapp web, lets modify the notification 838 | isWhatsapp=1 839 | body=$(echo $body | cut -c 64-) 840 | img=$(echo $noti | jq '.image') 841 | if [[ "$img" == "\"NamedIcon \\\""* ]]; then 842 | filepath=$(echo $img | cut -c 14- | head -c -4) 843 | cp $filepath /tmp/whatsappimg.png 844 | fi 845 | fi 846 | 847 | if [[ "$isWhatsapp" == "1" ]]; then 848 | # Returning the modifications to dnc as JSON 849 | echo "{\"modify\": {\"app-icon\": \"whatsapp-desktop\", \"app-name\": \"WhatsApp\", \"image-size\": 50, \"image\": \"file:///tmp/whatsappimg.png\", \"remove-actions\": true, \"class-name\": \"WhatsApp\", \"body\":\"${body}}, \"match\": {}}" 850 | else 851 | echo '{"modify": {}, "match": {}}' 852 | fi 853 | #+END_SRC 854 | 855 | ** Contribute 856 | 857 | First of all: Contribution is obviously 100% optional. 858 | 859 | If you want to join the development chat, join our *matrix channel: 860 | #deadd-notification-center:beeper.com* or drop by on the discussion 861 | board: https://github.com/phuhl/linux_notification_center/discussions 862 | 863 | If you do not want to contribute with your time, you can buy me a 864 | beer. Someone mentioned, they would be willing to donate, so here is 865 | my PayPal link: [[https://paypal.me/phuhl]]. Should you consider to 866 | donate, please be aware that this does not buy you the right to demand 867 | anything. This is a hobby and will be. But if you just want to give me 868 | some motivation by showing me that you appreciate my work, feel 869 | free to do so :) 870 | 871 | ** See also 872 | 873 | Also take a look at my [[https://github.com/phuhl/notify-send.py][notify-send.py]] which imitates notify-send (libnotify) but also is able to replace notifications. 874 | 875 | -------------------------------------------------------------------------------- /README.org.img/more_notifications.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phuhl/linux_notification_center/6c3de4644b4c6cd027f2c0a9fb73f203327efd94/README.org.img/more_notifications.png -------------------------------------------------------------------------------- /README.org.img/org_20200223_193345_VhlbOf.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phuhl/linux_notification_center/6c3de4644b4c6cd027f2c0a9fb73f203327efd94/README.org.img/org_20200223_193345_VhlbOf.jpg -------------------------------------------------------------------------------- /README.org.img/org_20200223_193450_1en7sh.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phuhl/linux_notification_center/6c3de4644b4c6cd027f2c0a9fb73f203327efd94/README.org.img/org_20200223_193450_1en7sh.jpg -------------------------------------------------------------------------------- /README.org.img/org_20200223_200131_4WWV2Y.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phuhl/linux_notification_center/6c3de4644b4c6cd027f2c0a9fb73f203327efd94/README.org.img/org_20200223_200131_4WWV2Y.jpg -------------------------------------------------------------------------------- /README.org.img/org_20201220_000601_9V037T.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phuhl/linux_notification_center/6c3de4644b4c6cd027f2c0a9fb73f203327efd94/README.org.img/org_20201220_000601_9V037T.jpg -------------------------------------------------------------------------------- /README.org.img/org_20210119_120536_adyKnd.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phuhl/linux_notification_center/6c3de4644b4c6cd027f2c0a9fb73f203327efd94/README.org.img/org_20210119_120536_adyKnd.jpg -------------------------------------------------------------------------------- /README.org.img/org_20210119_122031_BNYKTp.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phuhl/linux_notification_center/6c3de4644b4c6cd027f2c0a9fb73f203327efd94/README.org.img/org_20210119_122031_BNYKTp.jpg -------------------------------------------------------------------------------- /README.org.img/org_20210119_122628_AUuEu3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phuhl/linux_notification_center/6c3de4644b4c6cd027f2c0a9fb73f203327efd94/README.org.img/org_20210119_122628_AUuEu3.jpg -------------------------------------------------------------------------------- /Setup.hs: -------------------------------------------------------------------------------- 1 | import Distribution.Simple 2 | main = defaultMain 3 | -------------------------------------------------------------------------------- /Worklog.org: -------------------------------------------------------------------------------- 1 | * Distribution 2 | 3 | ** Hackage 4 | 5 | [[https://svejcar.dev/posts/2020/02/29/uploading-package-to-hackage/][Uploading Package to Hackage]] 6 | 7 | #+BEGIN_SRC sh 8 | cabal check 9 | stack sdist 10 | 11 | # Upload as package candidate: https://hackage.haskell.org/packages/candidates/upload 12 | 13 | # Upload 14 | # Change version in path: 15 | cabal upload .stack-work/dist/x86_64-linux-tinfo6/Cabal-2.2.0.1/deadd-notification-center-1.7.3.tar.gz 16 | #+END_SRC 17 | 18 | 19 | * Issues 20 | ** DONE Sizes configurable (CLOSED) 21 | :LOGBOOK: 22 | CLOCK: [2018-11-22 Thu 20:48]--[2018-11-22 Thu 21:05] => 0:17 23 | :END: 24 | 25 | - [X] Window dimensions for all windows 26 | 27 | ** DONE Buttons with info-updates (CLOSED) 28 | :LOGBOOK: 29 | CLOCK: [2018-11-23 Fri 18:46]--[2018-11-23 Fri 20:24] => 1:38 30 | CLOCK: [2018-11-23 Fri 16:01]--[2018-11-23 Fri 18:01] => 2:00 31 | :END: 32 | 33 | - [X] Problem: i don't know if multiple hints per noti are possible... yes they are 34 | 35 | ** DONE Buttons at bottom (CLOSED) 36 | :LOGBOOK: 37 | CLOCK: [2018-11-23 Fri 14:27]--[2018-11-23 Fri 15:48] => 1:21 38 | CLOCK: [2018-11-23 Fri 00:33]--[2018-11-23 Fri 02:30] => 1:57 39 | CLOCK: [2018-11-22 Thu 23:33]--[2018-11-23 Fri 00:11] => 0:38 40 | :END: 41 | 42 | - In the configuration one could specify button-captions and 43 | shell-commands 44 | - The buttons could be generated dynamically 45 | 46 | 47 | - [X] Problem: all commands are executed simultaneously... solved 48 | - [X] Problem 2: to many buttons overlap to the right 49 | 50 | ** DONE More styling (#2, CLOSED) 51 | :LOGBOOK: 52 | CLOCK: [2019-01-19 Sat 23:02]--[2019-01-19 Sat 23:49] => 0:47 53 | :END: 54 | 55 | ** DONE Color configurable (CLOSED) 56 | 57 | - [X] done 58 | 59 | ** TODO Multiple Screen support (#1) 60 | :LOGBOOK: 61 | CLOCK: [2019-01-02 Wed 02:04]--[2019-01-02 Wed 03:14] => 1:10 62 | :END: 63 | ** DONE Aur does not build (#4) 64 | :LOGBOOK: 65 | CLOCK: [2019-01-24 Thu 14:31]--[2019-01-24 Thu 14:42] => 0:11 66 | CLOCK: [2019-01-24 Thu 13:04]--[2019-01-24 Thu 14:04] => 1:00 67 | :END: 68 | 69 | ** DONE Notifications stuck (#5, CLOSED) 70 | :LOGBOOK: 71 | CLOCK: [2019-02-11 Mon 22:54]--[2019-02-11 Mon 23:05] => 0:11 72 | CLOCK: [2019-02-11 Mon 22:29]--[2019-02-11 Mon 22:44] => 0:15 73 | CLOCK: [2019-02-11 Mon 21:13]--[2019-02-11 Mon 22:29] => 1:16 74 | :END: 75 | 76 | 1. fixed race condition 77 | 2. created new bug (replaceid does not work anymore) 78 | 3. Easy fix (a + 1 to much)... 79 | 80 | ** DONE Transient Notifications (#6, CLOSED) 81 | :LOGBOOK: 82 | CLOCK: [2019-03-01 Fri 19:27]--[2019-03-01 Fri 19:38] => 0:11 83 | :END: 84 | 85 | - =ignoreTransient= configuration added 86 | 87 | If you want to send transient notifications (notifications that should 88 | not be stored in the notification center, but only showed once) 89 | yourself, you can do so with notify-send: 90 | 91 | #+BEGIN_SRC sh 92 | notify-send --hint=int:transient:1 "My Caption" "My Body..." 93 | #+END_SRC 94 | 95 | ** DONE Sort notis by time with newest on top (CLOSED) 96 | :LOGBOOK: 97 | CLOCK: [2019-01-19 Sat 22:36]--[2019-01-19 Sat 22:54] => 0:18 98 | CLOCK: [2019-01-19 Sat 22:10]--[2019-01-19 Sat 22:34] => 0:24 99 | :END: 100 | 101 | ** DONE Notification based scripting (#3, #6, CLOSED) 102 | :LOGBOOK: 103 | CLOCK: [2019-03-05 Tue 14:59]--[2019-03-05 Tue 15:20] => 0:21 104 | CLOCK: [2019-03-01 Fri 22:59]--[2019-03-01 Fri 23:55] => 0:56 105 | CLOCK: [2019-03-01 Fri 19:39]--[2019-03-01 Fri 22:43] => 3:04 106 | :END: 107 | 108 | ** DONE Fixing build issue (CLOSED) 109 | :LOGBOOK: 110 | CLOCK: [2019-03-20 Wed 13:11]--[2019-03-20 Wed 13:14] => 0:03 111 | :END: 112 | 113 | ** DONE Handling of XML tags (CLOSED) 114 | :LOGBOOK: 115 | CLOCK: [2019-03-20 Wed 13:19]--[2019-03-20 Wed 14:09] => 0:50 116 | :END: 117 | 118 | ** DONE Signals (NotificationClosed) (CLOSED) 119 | :LOGBOOK: 120 | CLOCK: [2019-03-20 Wed 18:34]--[2019-03-20 Wed 19:03] => 0:29 121 | CLOCK: [2019-03-20 Wed 14:10]--[2019-03-20 Wed 15:47] => 1:37 122 | :END: 123 | 124 | - Implemented but disabled by default 125 | - WHY?: Because e.g. Spotify takes the =NotificationClosed= message 126 | as a reason to not replace old notifications but to send a new 127 | one. As the Notifications are stored in the center after being 128 | closed though, they would accumulate. 129 | - If you want the =NotificationClosed= message for some reason 130 | anyways, you can script the =NotificationClosed= message for 131 | certain notifications away by using the =noClosedMsg= option. For 132 | more information on this, consult [[Notification based scripting (#3, #6, CLOSED)]]. 133 | - To enable the =NotificationClosed= message, set 134 | =configSendNotiClosedDbusMessage= to =true= in the 135 | =notification-center= section of your config file. 136 | - Why is the default for =configSendNotiClosedDbusMessage= =false=? 137 | Because, for a new user it is not obvious, that e.g. Spotify does 138 | what it does how it does it and might be highly extremely annoyed 139 | by that behaviour without knowing, it could be prevented. The need 140 | for that =NotificationClosed= is not very apparent to me anyways. 141 | - What a wast of time to implement this stupid 142 | =NotificationClosed=, jeeeze. 143 | 144 | ** DONE Actions (CLOSED) 145 | :LOGBOOK: 146 | CLOCK: [2019-03-20 Wed 15:47]--[2019-03-20 Wed 17:41] => 1:54 147 | :END: 148 | ** DONE Image support 149 | ** DONE Bin-Package 150 | :LOGBOOK: 151 | CLOCK: [2019-03-25 Mon 12:32]--[2019-03-25 Mon 13:20] => 0:48 152 | :END: 153 | 154 | - No build time 155 | - No compilation issues 156 | ** DONE Translation of program 157 | :LOGBOOK: 158 | CLOCK: [2019-05-24 Fri 18:15]--[2019-05-24 Fri 19:15] => 1:00 159 | :END: 160 | 161 | -------------------------------------------------------------------------------- /app/Main.hs: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | import qualified NotificationCenter 4 | 5 | main :: IO () 6 | main = NotificationCenter.main 7 | -------------------------------------------------------------------------------- /buildAndRun.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | make 4 | if [ $? -eq 0 ]; then 5 | killall deadd-notification-center 6 | ./.out/deadd-notification-center & 7 | sleep 1 8 | notify-send "Build done successfully" 9 | else 10 | notify-send "Build failed" 11 | fi 12 | -------------------------------------------------------------------------------- /com.ph-uhl.deadd.notification.service.in: -------------------------------------------------------------------------------- 1 | [D-BUS Service] 2 | Name=org.freedesktop.Notifications 3 | Exec=##PREFIX##/bin/deadd-notification-center 4 | -------------------------------------------------------------------------------- /deadd-notification-center.cabal: -------------------------------------------------------------------------------- 1 | cabal-version: 2.4 2 | name: deadd-notification-center 3 | version: 1.7.3 4 | synopsis: A notification daemon and center 5 | description: A customizable notification center for linux. Notifications are received via DBUS (like any notification daemon) and shown in the upper right corner of the screen. A notification center can be shown, that displays the last notifications and optionally some user defined buttons. The notification center supports true transparency. 6 | homepage: https://github.com/phuhl/linux-notification-center#readme 7 | license: BSD-3-Clause 8 | license-file: LICENSE 9 | author: Philipp Uhl 10 | maintainer: git@ph-uhl.com 11 | copyright: 2017 Philipp Uhl 12 | category: Web 13 | build-type: Simple 14 | extra-source-files: README.org 15 | , notification_center.glade 16 | , notification.glade 17 | , notification_in_center.glade 18 | , style.css 19 | data-files: translation/bn_BD/LC_MESSAGES/*.mo 20 | , translation/de/LC_MESSAGES/*.mo 21 | , translation/en/LC_MESSAGES/*.mo 22 | , translation/tr/LC_MESSAGES/*.mo 23 | 24 | library 25 | hs-source-dirs: src 26 | exposed-modules: NotificationCenter 27 | , Config 28 | , NotificationCenter.NotificationInCenter 29 | , NotificationCenter.Button 30 | , NotificationCenter.Notification.Glade 31 | , NotificationCenter.Notifications 32 | , NotificationCenter.Notifications.Data 33 | , NotificationCenter.Notifications.Action 34 | , NotificationCenter.Notifications.AbstractNotification 35 | , NotificationCenter.Notifications.NotificationPopup 36 | , NotificationCenter.Notifications.Notification.Glade 37 | , NotificationCenter.Glade 38 | , TransparentWindow 39 | , Helpers 40 | other-modules: Paths_deadd_notification_center 41 | autogen-modules: Paths_deadd_notification_center 42 | build-depends: base >= 4.7 && < 5 43 | , regex-tdfa 44 | , transformers 45 | , filepath 46 | , haskell-gi 47 | , haskell-gi-base 48 | , haskell-gettext 49 | , gi-cairo 50 | , gi-pango >= 1.0.26 51 | , gi-glib >= 2.0.17 52 | , gi-gdk 53 | , gi-gdkpixbuf >= 2.0.26 54 | , gi-gtk >= 3.0.19 55 | , gi-gio 56 | , time 57 | , env-locale 58 | , gi-gobject 59 | , text 60 | , bytestring 61 | , ConfigFile 62 | , mtl 63 | , dbus >= 1.0.1 && < 2 64 | , containers 65 | , unix 66 | , stm >= 2.5.0.0 67 | , here 68 | , hdaemonize 69 | , directory 70 | , process 71 | , tagsoup >= 0.14.7 72 | , tuple 73 | , split 74 | , setlocale 75 | , lens 76 | , yaml 77 | , aeson 78 | default-language: Haskell2010 79 | 80 | executable deadd-notification-center 81 | hs-source-dirs: app 82 | main-is: Main.hs 83 | ghc-options: -threaded -rtsopts -with-rtsopts=-N 84 | build-depends: base 85 | , deadd-notification-center 86 | default-language: Haskell2010 87 | 88 | test-suite deadd-notification-center-test 89 | type: exitcode-stdio-1.0 90 | hs-source-dirs: test 91 | main-is: Spec.hs 92 | build-depends: base 93 | , deadd-notification-center 94 | ghc-options: -threaded -rtsopts -with-rtsopts=-N 95 | default-language: Haskell2010 96 | 97 | source-repository head 98 | type: git 99 | location: https://github.com/phuhl/linux-notification-center 100 | -------------------------------------------------------------------------------- /deadd-notification-center.service.in: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Deadd Notification Center 3 | 4 | [Service] 5 | Environment="DISPLAY=:0" 6 | Environment="DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus" 7 | PIDFile=/run/notification-center.pid 8 | ExecStart=##PREFIX##/bin/deadd-notification-center 9 | Restart=always 10 | RestartSec=10 11 | 12 | [Install] 13 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /docs/linux-notification-center.man: -------------------------------------------------------------------------------- 1 | .TH " " "1" 2 | .SH "NAME" 3 | .PP 4 | Linux notification center - notification daemon 5 | .SH "SYNOPSIS" 6 | .RS 7 | .nf 8 | \fClinux-notification-center 9 | \fP 10 | .fi 11 | .RE 12 | 13 | .SH "DESCRIPTION" 14 | .PP 15 | A textual description of the functioning of the command or function. 16 | .SH "EXAMPLES" 17 | .PP 18 | Some examples of common usage. 19 | .SH "SEE ALSO" 20 | .PP 21 | A list of related commands or functions. 22 | .SH "BUGS" 23 | .PP 24 | List known bugs. 25 | .SH "AUTHOR" 26 | .PP 27 | Specify your contact information. 28 | .SH "COPYRIGHT" 29 | .PP 30 | Specify your copyright information. 31 | -------------------------------------------------------------------------------- /docs/linux-notification-center.org: -------------------------------------------------------------------------------- 1 | #+TITLE: linux-notification-center 2 | #+MAN_CLASS_OPTIONS: :section-id 1 :date "Apr 14, 2017" 3 | 4 | * NAME 5 | Linux notification center - notification daemon 6 | * SYNOPSIS 7 | #+BEGIN_SRC shell-script 8 | linux-notification-center 9 | #+END_SRC 10 | 11 | * DESCRIPTION 12 | A textual description of the functioning of the command or function. 13 | * EXAMPLES 14 | Some examples of common usage. 15 | * SEE ALSO 16 | A list of related commands or functions. 17 | * BUGS 18 | List known bugs. 19 | * AUTHOR 20 | Specify your contact information. 21 | * COPYRIGHT 22 | Specify your copyright information. 23 | -------------------------------------------------------------------------------- /notification.glade: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 100 8 | 1 9 | 10 10 | 11 | 12 | 300 13 | False 14 | False 15 | True 16 | popup 17 | notification 18 | False 19 | notification 20 | 21 | 22 | True 23 | False 24 | True 25 | 26 | 27 | True 28 | False 29 | True 30 | 31 | 32 | -1 33 | 34 | 35 | 36 | 37 | True 38 | False 39 | 40 | 41 | True 42 | False 43 | start 44 | True 45 | 185 46 | 0 47 | 52 | 53 | 54 | False 55 | True 56 | 0 57 | 58 | 59 | 60 | 61 | True 62 | False 63 | True 64 | vertical 65 | bottom 66 | 67 | 68 | True 69 | False 70 | start 71 | start 72 | 10 73 | Titel 74 | True 75 | word-char 76 | 81 | 82 | 83 | False 84 | False 85 | 0 86 | 87 | 88 | 89 | 90 | True 91 | False 92 | start 93 | start 94 | True 95 | True 96 | char 97 | 102 | 103 | 104 | False 105 | False 106 | 1 107 | 108 | 109 | 110 | 111 | False 112 | 5 113 | 5 114 | 119 | 120 | 121 | False 122 | True 123 | 2 124 | 125 | 126 | 127 | 128 | True 129 | adjustment1 130 | False 131 | 1 132 | False 133 | 138 | 139 | 140 | False 141 | True 142 | 3 143 | 144 | 145 | 146 | 147 | True 148 | False 149 | center 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 164 | 165 | 166 | False 167 | True 168 | 4 169 | 170 | 171 | 172 | 173 | True 174 | False 175 | end 176 | 177 | 178 | 10 179 | True 180 | False 181 | end 182 | end 183 | 10 184 | 0 185 | 190 | 191 | 192 | False 193 | False 194 | 0 195 | 196 | 197 | 198 | 199 | True 200 | False 201 | end 202 | Appname 203 | 208 | 209 | 210 | False 211 | True 212 | 1 213 | 214 | 215 | 216 | 217 | False 218 | False 219 | 8 220 | 5 221 | 222 | 223 | 227 | 228 | 229 | True 230 | True 231 | end 232 | 1 233 | 234 | 235 | 240 | 241 | 242 | 243 | 244 | 248 | 249 | 250 | -------------------------------------------------------------------------------- /notification_center.glade: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 100 8 | 1 9 | 10 10 | 11 | 12 | False 13 | False 14 | popup 15 | notification-center 16 | False 17 | False 18 | north-east 19 | 20 | 21 | True 22 | False 23 | False 24 | 25 | 26 | True 27 | False 28 | 29 | 30 | -1 31 | 32 | 33 | 34 | 35 | True 36 | False 37 | 10 38 | 5 39 | 5 40 | False 41 | vertical 42 | 43 | 44 | True 45 | False 46 | 20 47 | vertical 48 | 49 | 50 | True 51 | False 52 | start 53 | 5 54 | 01:31 55 | 60 | 61 | 62 | False 63 | True 64 | 0 65 | 66 | 67 | 68 | 69 | True 70 | False 71 | start 72 | Sonntag, 09.04.2017 73 | 78 | 79 | 80 | False 81 | True 82 | 1 83 | 84 | 85 | 86 | 87 | False 88 | True 89 | 0 90 | 91 | 92 | 93 | 94 | True 95 | False 96 | False 97 | True 98 | vertical 99 | 100 | 101 | Alle Löschen 102 | True 103 | True 104 | end 105 | none 106 | 110 | 111 | 112 | False 113 | False 114 | 0 115 | 116 | 117 | 118 | 119 | True 120 | True 121 | False 122 | True 123 | box_notis_adj 124 | never 125 | 312 126 | 127 | 128 | True 129 | False 130 | False 131 | True 132 | none 133 | 134 | 135 | True 136 | False 137 | False 138 | True 139 | vertical 140 | 20 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | False 151 | True 152 | 1 153 | 154 | 155 | 156 | 157 | False 158 | True 159 | 1 160 | 161 | 162 | 163 | 164 | True 165 | False 166 | vertical 167 | 168 | 169 | 170 | 171 | 172 | False 173 | True 174 | end 175 | 2 176 | 177 | 178 | 183 | 184 | 185 | 186 | 187 | 190 | 191 | 192 | -------------------------------------------------------------------------------- /notification_in_center.glade: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 100 7 | 1 8 | 10 9 | 10 | 11 | True 12 | False 13 | 14 | 15 | True 16 | False 17 | start 18 | 10 19 | 0 20 | 25 | 26 | 27 | False 28 | True 29 | 0 30 | 31 | 32 | 33 | 34 | True 35 | False 36 | 10 37 | 5 38 | 5 39 | True 40 | vertical 41 | 42 | 43 | True 44 | False 45 | 46 | 47 | True 48 | False 49 | start 50 | False 51 | Sample notification 52 | True 53 | char 54 | 59 | 60 | 61 | True 62 | True 63 | 0 64 | 65 | 66 | 67 | 68 | 69 | 15 70 | 15 71 | True 72 | True 73 | True 74 | end 75 | start 76 | 0 77 | none 78 | 83 | 84 | 85 | False 86 | True 87 | end 88 | 1 89 | 90 | 91 | 92 | 93 | False 94 | True 95 | 0 96 | 97 | 98 | 99 | 100 | 1 101 | True 102 | False 103 | start 104 | start 105 | 10 106 | False 107 | True 108 | char 109 | 114 | 115 | 116 | False 117 | True 118 | 1 119 | 120 | 121 | 122 | 123 | False 124 | 10 125 | 5 126 | 5 127 | 132 | 133 | 134 | False 135 | True 136 | 2 137 | 138 | 139 | 140 | 141 | True 142 | 10 143 | adjustment1 144 | False 145 | 1 146 | False 147 | 152 | 153 | 154 | False 155 | True 156 | 3 157 | 158 | 159 | 160 | 161 | box_actions 162 | True 163 | False 164 | center 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | False 177 | True 178 | 4 179 | 180 | 181 | 182 | 183 | True 184 | False 185 | 3 186 | 5 187 | 188 | 189 | True 190 | False 191 | 23:49 192 | 197 | 198 | 199 | False 200 | True 201 | 0 202 | 203 | 204 | 205 | 206 | True 207 | False 208 | 10 209 | 10 210 | 215 | 216 | 217 | False 218 | True 219 | end 220 | 2 221 | 222 | 223 | 224 | 225 | True 226 | False 227 | a 228 | right 229 | 234 | 235 | 236 | False 237 | True 238 | end 239 | 3 240 | 241 | 242 | 243 | 244 | False 245 | True 246 | 5 247 | 248 | 249 | 250 | 251 | False 252 | True 253 | 1 254 | 255 | 256 | 259 | 260 | 261 | -------------------------------------------------------------------------------- /notification_section.glade: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | True 7 | False 8 | vertical 9 | 10 | 11 | True 12 | False 13 | 14 | 15 | True 16 | False 17 | start 18 | Sectiontitle 19 | 20 | 21 | 22 | 23 | 24 | True 25 | True 26 | 0 27 | 28 | 29 | 30 | 31 | x 32 | True 33 | True 34 | True 35 | 36 | 37 | False 38 | True 39 | 1 40 | 41 | 42 | 43 | 44 | False 45 | True 46 | 0 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /pkg-bin/PKGBUILD: -------------------------------------------------------------------------------- 1 | # Maintainer: Philipp Uhl 2 | 3 | pkgname=deadd-notification-center-bin 4 | pkgver=1.7.3 5 | pkgrel=1 6 | pkgdesc="Customizable notification-daemon with notification center" 7 | url="https://github.com/phuhl/linux_notification_center" 8 | license=("BSD") 9 | arch=('x86_64') 10 | depends=('gobject-introspection-runtime' 'gtk3') 11 | provides=('deadd-notification-center') 12 | conflicts=('deadd-notification-center' 'deadd-notification-center-git') 13 | source=("${pkgname}-${pkgver}.tar.gz::https://github.com/phuhl/linux_notification_center/archive/${pkgver}.tar.gz") 14 | 15 | prepare() { 16 | tar -zxvf "${pkgname}-${pkgver}.tar.gz" 17 | } 18 | 19 | build() { 20 | cd "linux_notification_center-${pkgver}" 21 | } 22 | 23 | package() { 24 | cd "linux_notification_center-${pkgver}" 25 | make service 26 | make DESTDIR="$pkgdir" install 27 | } 28 | 29 | -------------------------------------------------------------------------------- /pkg-git/PKGBUILD: -------------------------------------------------------------------------------- 1 | # Maintainer: Philipp Uhl 2 | 3 | _gitname="linux_notification_center" 4 | pkgname=deadd-notification-center-git 5 | pkgver=1.7.3 6 | pkgrel=1 7 | pkgdesc="Customizable notification-daemon with notification center" 8 | url="https://github.com/phuhl/linux_notification_center" 9 | license=("BSD") 10 | arch=('x86_64') 11 | depends=('gobject-introspection-runtime' 'gtk3') 12 | makedepends=('stack' 'cairo' 'pango' 'gobject-introspection' 'git') 13 | provides=('deadd-notification-center') 14 | conflicts=('deadd-notification-center-bin' 'deadd-notification-center') 15 | source=("git+${url}.git#branch=master") 16 | md5sums=("SKIP") 17 | 18 | pkgver() { 19 | cd "${srcdir}/${_gitname}" 20 | git describe --long --tags | sed 's/\([^-]*-g\)/r\1/;s/-/./g' 21 | } 22 | 23 | build() { 24 | cd "${srcdir}/${_gitname}" 25 | make 26 | } 27 | 28 | package() { 29 | cd "${srcdir}/${_gitname}" 30 | make DESTDIR="$pkgdir" install 31 | } 32 | -------------------------------------------------------------------------------- /pkg/PKGBUILD: -------------------------------------------------------------------------------- 1 | # Maintainer: Philipp Uhl 2 | 3 | pkgname=deadd-notification-center 4 | pkgver=1.7.3 5 | pkgrel=1 6 | pkgdesc="Customizable notification-daemon with notification center" 7 | url="https://github.com/phuhl/linux_notification_center" 8 | license=("BSD") 9 | arch=('x86_64') 10 | depends=('gobject-introspection-runtime' 'gtk3') 11 | makedepends=('stack' 'cairo' 'pango' 'gobject-introspection') 12 | provides=('deadd-notification-center') 13 | conflicts=('deadd-notification-center-bin' 'deadd-notification-center-git') 14 | source=("${pkgname}-${pkgver}.tar.gz::https://github.com/phuhl/linux_notification_center/archive/${pkgver}.tar.gz") 15 | 16 | prepare() { 17 | tar -zxvf "${pkgname}-${pkgver}.tar.gz" 18 | } 19 | 20 | build() { 21 | cd "linux_notification_center-${pkgver}" 22 | make 23 | } 24 | 25 | package() { 26 | cd "linux_notification_center-${pkgver}" 27 | make DESTDIR="$pkgdir" install 28 | } 29 | 30 | -------------------------------------------------------------------------------- /sendShowupNotis.sh: -------------------------------------------------------------------------------- 1 | notify-send.py "With link support ❤️" "Best side: zombo.com" -a Firefox 2 | 3 | notify-send.py "Now with markup" "The longest yeah boiiiiiiiii!!" -a notify-send 4 | 5 | notify-send.py "We can do fancy progress" -a Volume --hint int:has-percentage:40 6 | 7 | notify-send.py "And buttons" "Do you like buttons?" \ 8 | --hint boolean:action-icons:false \ 9 | --action Yes:Yes "Of course!":"Of course!" \ 10 | -a Buttonsender & 11 | 12 | -------------------------------------------------------------------------------- /snap/snapcraft.yaml: -------------------------------------------------------------------------------- 1 | name: deadd-notification-center 2 | confinement: strict 3 | base: core18 4 | summary: Linux Notification Center 5 | adopt-info: deadd-notification-center 6 | 7 | description: | 8 | A haskell-written notification center for users that like a desktop with style... 9 | 10 | slots: 11 | dbus-daemon: 12 | interface: dbus 13 | bus: session 14 | name: org.freedesktop.Notifications 15 | 16 | apps: 17 | deadd-notification-center: 18 | command: usr/bin/deadd-notification-center 19 | extensions: [gnome-3-28] 20 | slots: 21 | - dbus-daemon 22 | 23 | parts: 24 | deadd-notification-center: 25 | plugin: make 26 | source: https://github.com/phuhl/linux_notification_center.git 27 | build-packages: 28 | - libcairo2-dev 29 | - libpango1.0-dev 30 | - libgirepository1.0-dev 31 | - libgtk-3-dev 32 | - libxml2-dev 33 | stage-packages: 34 | - curl 35 | build-environment: 36 | - PATH: "/root/.local/bin/$PATH" 37 | override-build: | 38 | mkdir -p /root/parts/deadd-notification-center/install/usr/share/locale/en/LC_MESSAGES 39 | mkdir -p /root/parts/deadd-notification-center/install/usr/share/locale/de/LC_MESSAGES 40 | curl -SSL https://get.haskellstack.org | sh -s - -f 41 | which stack 42 | snapcraftctl build 43 | override-pull: | 44 | snapcraftctl pull 45 | version="$(git describe --always --tags | sed -e 's/-/+git/;y/-/./')" 46 | [ -n "$(echo $version | grep "+git")" ] && grade=devel || grade=stable 47 | snapcraftctl set-version "$version" 48 | snapcraftctl set-grade "$grade" 49 | echo "Version: ${version}" 50 | echo "Grade: ${grade}" 51 | -------------------------------------------------------------------------------- /src/Config.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | {-# LANGUAGE DeriveGeneric #-} 3 | 4 | module Config 5 | ( 6 | Config(..) 7 | , ButtonConfig(..) 8 | , ModificationRule(..) 9 | , getConfig 10 | )where 11 | 12 | import Data.List.Split (splitOn) 13 | import Helpers (orElse, split, removeOuterLetters, readConfig, replace) 14 | import Data.Maybe (fromMaybe) 15 | import qualified Data.Text as Text 16 | import Data.Text.Encoding (decodeUtf8, encodeUtf8) 17 | import Data.Int ( Int32, Int ) 18 | import qualified Data.Map as Map 19 | import qualified Data.Yaml as Y 20 | import Data.Yaml ((.=), (.!=), (.:?), FromJSON(..), (.:)) 21 | import Data.Aeson.Key as AesonKey 22 | 23 | import System.Process (readCreateProcess, shell) 24 | 25 | import NotificationCenter.Notifications.Data 26 | (notiSendClosedMsg, notiTransient, notiIcon, notiTime, notiAppName 27 | , notiBody, notiSummary, Notification(..), parseImageString) 28 | 29 | 30 | data ModificationRule = Modify 31 | { 32 | mMatch :: Map.Map String String 33 | , modifyTitle :: Maybe String 34 | , modifyBody :: Maybe String 35 | , modifyAppname :: Maybe String 36 | , modifyAppicon :: Maybe String 37 | , modifyTimeout :: Maybe Int32 38 | , modifyRight :: Maybe Int 39 | , modifyTop :: Maybe Int 40 | , modifyImage :: Maybe String 41 | , modifyImageSize :: Maybe Int 42 | , modifyTransient :: Maybe Bool 43 | , modifyNoClosedMsg :: Maybe Bool 44 | , modifyRemoveActions :: Maybe Bool 45 | , modifyActionIcons :: Maybe Bool 46 | , modifyActionCommands :: Maybe (Map.Map String String) 47 | , modifyActions :: Maybe [String] 48 | , modifyClassName :: Maybe String 49 | } | 50 | Script 51 | { 52 | mMatch :: Map.Map String String 53 | , mScript :: String 54 | } 55 | 56 | instance FromJSON ModificationRule where 57 | parseJSON (Y.Object o) = do 58 | mScript <- o .:? "script" 59 | case mScript of 60 | Nothing -> Modify 61 | <$> o .: "match" 62 | -- modifyTitle 63 | <*> o .: "modify" .:. "title" 64 | -- modifyBody 65 | <*> o .: "modify" .:. "body" 66 | -- modifyAppname 67 | <*> o .: "modify" .:. "app-name" 68 | -- modifyAppicon 69 | <*> o .: "modify" .:. "app-icon" 70 | -- modifyTimeout 71 | <*> o .: "modify" .:. "timeout" 72 | -- modifyRight 73 | <*> o .: "modify" .:. "margin-right" 74 | -- modifyTop 75 | <*> o .: "modify" .:. "margin-top" 76 | -- modifyImage 77 | <*> o .: "modify" .:. "image" 78 | -- modifyImageSize 79 | <*> o .: "modify" .:. "image-size" 80 | -- modifyTransient 81 | <*> o .: "modify" .:. "transient" 82 | -- modifyNoClosedMsg 83 | <*> o .: "modify" .:. "send-noti-closed" 84 | -- modifyRemoveActions 85 | <*> o .: "modify" .:. "remove-actions" 86 | -- modifyActionIcons 87 | <*> o .: "modify" .:. "action-icons" 88 | -- modifyActionIcons 89 | <*> o .: "modify" .:. "action-commands" 90 | -- modifyActions 91 | <*> o .: "modify" .:. "actions" 92 | -- modifyClassName 93 | <*> o .: "modify" .:. "class-name" 94 | Just (script)-> Script 95 | <$> o .: "match" 96 | -- mScript 97 | <*> return script 98 | 99 | 100 | data Config = Config 101 | { 102 | -- notification-center 103 | configBarHeight :: Int 104 | , configBottomBarHeight :: Int 105 | , configRightMargin :: Int 106 | , configWidth :: Int 107 | , configStartupCommand :: String 108 | , configNotiCenterMonitor :: Int 109 | , configNotiCenterFollowMouse :: Bool 110 | , configNotiCenterNewFirst :: Bool 111 | , configIgnoreTransient :: Bool 112 | , configMatchingRules :: [ModificationRule] 113 | , configActionIcons :: Bool 114 | , configNotiMarkup :: Bool 115 | , configNotiParseHtmlEntities :: Bool 116 | , configSendNotiClosedDbusMessage :: Bool 117 | , configGuessIconFromAppname :: Bool 118 | , configNotiCenterHideOnMouseLeave :: Bool 119 | 120 | -- notification-center-notification-popup 121 | , configNotiDefaultTimeout :: Int 122 | , configDistanceTop :: Int 123 | , configDistanceRight :: Int 124 | , configDistanceBetween :: Int 125 | , configWidthNoti :: Int 126 | , configNotiFollowMouse :: Bool 127 | , configNotiMonitor :: Int 128 | , configImgSize :: Int 129 | , configImgMarginTop :: Int 130 | , configImgMarginLeft :: Int 131 | , configImgMarginBottom :: Int 132 | , configImgMarginRight :: Int 133 | , configIconSize :: Int 134 | , configPopupMaxLinesInBody :: Int 135 | , configPopupEllipsizeBody :: Bool 136 | , configPopupDismissButton :: String 137 | , configPopupDefaultActionButton :: String 138 | , configPopupHideBodyIfEmpty :: Bool 139 | 140 | -- buttons 141 | , configButtonsPerRow :: Int 142 | , configButtonHeight :: Int 143 | , configButtonMargin :: Int 144 | , configButtons :: [ButtonConfig] 145 | } 146 | 147 | (.:.) :: FromJSON a => Y.Parser (Maybe Y.Object) -> Text.Text -> Y.Parser (Maybe a) 148 | (.:.) po name = do 149 | mO <- po 150 | case mO of 151 | Nothing -> return Nothing 152 | (Just x) -> x .:? AesonKey.fromString (Text.unpack name) 153 | 154 | (.!=>) :: FromJSON a => Y.Parser (Maybe a) -> Y.Parser (Maybe a) -> Y.Parser (Maybe a) 155 | (.!=>) a b = orElse <$> a <*> b 156 | 157 | firstLevel o firstKey alt = 158 | o .:? firstKey 159 | .!= alt 160 | 161 | secondLevel o firstKey secondKey alt = 162 | o .:? firstKey .:. secondKey 163 | .!= alt 164 | 165 | thirdLevel o firstKey secondKey thirdKey alt = 166 | o .:? firstKey .:. secondKey .:. thirdKey 167 | .!= alt 168 | 169 | fourthLevel o firstKey secondKey thirdKey fourthKey alt = 170 | o .:? firstKey .:. secondKey .:. thirdKey .:. fourthKey 171 | .!= alt 172 | 173 | inheritingSecondLevel o firstKey secondKey alt = 174 | o .:? firstKey .:. secondKey 175 | .!=> (o .:? AesonKey.fromString (Text.unpack secondKey)) 176 | .!= alt 177 | 178 | inheritingThirdLevel o firstKey secondKey thirdKey alt = 179 | o .:? firstKey .:. secondKey .:. thirdKey 180 | .!=> (o .:? AesonKey.fromString (Text.unpack secondKey) .:. thirdKey) 181 | .!=> (o .:? AesonKey.fromString (Text.unpack thirdKey)) 182 | .!= alt 183 | 184 | instance FromJSON Config where 185 | parseJSON (Y.Object o) = 186 | Config 187 | --configBarHeight 188 | <$> inheritingSecondLevel o "notification-center" "margin-top" 0 189 | -- configBottomBarHeight 190 | <*> inheritingSecondLevel o "notification-center" "margin-bottom" 0 191 | -- configRightMargin 192 | <*> inheritingSecondLevel o "notification-center" "margin-right" 0 193 | -- configWidth 194 | <*> inheritingSecondLevel o "notification-center" "width" 500 195 | -- configStartupCommand 196 | <*> firstLevel o "startup-command" "" 197 | -- configNotiCenterMonitor 198 | <*> inheritingSecondLevel o "notification-center" "monitor" 0 199 | -- configNotiCenterFollowMouse 200 | <*> inheritingSecondLevel o "notification-center" "follow-mouse" False 201 | -- configNotiCenterNewFirst 202 | <*> secondLevel o "notification-center" "new-first" True 203 | -- configIgnoreTransient 204 | <*> secondLevel o "notification-center" "ignore-transient" False 205 | -- configMatchingRules 206 | <*> secondLevel o "notification" "modifications" [] 207 | -- configActionIcons 208 | <*> secondLevel o "notification" "use-action-icons" True 209 | -- configNotiMarkup 210 | <*> secondLevel o "notification" "use-markup" True 211 | -- configNotiParseHtmlEntities 212 | <*> secondLevel o "notification" "parse-html-entities" True 213 | -- configSendNotiClosedDbusMessage 214 | <*> thirdLevel o "notification" "dbus" "send-noti-closed" False 215 | -- configGuessIconFromAppname 216 | <*> inheritingThirdLevel o "notification" "app-icon" "guess-icon-from-name" 217 | True 218 | -- configNotiCenterHideOnMouseLeave 219 | <*> secondLevel o "notification-center" "hide-on-mouse-leave" True 220 | -- configNotiDefaultTimeout 221 | <*> thirdLevel o "notification" "popup" "default-timeout" 1000 222 | -- configDistanceTop 223 | <*> inheritingThirdLevel o "notification" "popup" "margin-top" 50 224 | -- configDistanceRight 225 | <*> inheritingThirdLevel o "notification" "popup" "margin-right" 50 226 | -- configDistanceBetween 227 | <*> thirdLevel o "notification" "popup" "margin-between" 20 228 | -- configWidthNoti 229 | <*> inheritingThirdLevel o "notification" "popup" "width" 300 230 | -- configNotiFollowMouse 231 | <*> inheritingThirdLevel o "notification" "popup" "follow-mouse" False 232 | -- configNotiMonitor 233 | <*> inheritingThirdLevel o "notification" "popup" "monitor" 0 234 | -- configImgSize 235 | <*> thirdLevel o "notification" "image" "size" 100 236 | -- configImgMarginTop 237 | <*> thirdLevel o "notification" "image" "margin-top" 15 238 | -- configImgMarginLeft 239 | <*> thirdLevel o "notification" "image" "margin-left" 15 240 | -- configImgMarginBottom 241 | <*> thirdLevel o "notification" "image" "margin-bottom" 15 242 | -- configImgMarginRight 243 | <*> thirdLevel o "notification" "image" "margin-right" 0 244 | -- configIconSize 245 | <*> thirdLevel o "notification" "app-icon" "icon-size" 20 246 | -- configPopupMaxLinesInBody 247 | <*> inheritingThirdLevel o "notification" "popup" "max-lines-in-body" 3 248 | -- configPopupEllipsizeBody 249 | <*> ((/= (0 :: Int)) <$> 250 | inheritingThirdLevel o "notification" "popup" "max-lines-in-body" 3) 251 | -- configPopupDismissButton 252 | <*> fourthLevel o "notification" "popup" "click-behavior" "dismiss" "mouse1" 253 | -- configPopupDefaultActionButton 254 | <*> fourthLevel o "notification" "popup" "click-behavior" "default-action" "mouse3" 255 | -- configPopupHideBodyIfEmpty 256 | <*> thirdLevel o "notification" "popup" "hide-body-if-empty" False 257 | -- configButtonsPerRow 258 | <*> thirdLevel o "notification-center" "buttons" "buttons-per-row" 5 259 | -- configButtonHeight 260 | <*> thirdLevel o "notification-center" "buttons" "buttons-height" 60 261 | -- configButtonMargin 262 | <*> thirdLevel o "notification-center" "buttons" "buttons-margin" 2 263 | -- configLabels 264 | <*> thirdLevel o "notification-center" "buttons" "actions" [] 265 | parseJSON _ = fail "Expected Object for Config value" 266 | 267 | data ButtonConfig = Button 268 | { 269 | configButtonLabel :: String 270 | , configButtonCommand :: String 271 | } 272 | 273 | instance FromJSON ButtonConfig where 274 | parseJSON (Y.Object o) = Button 275 | <$> o .: "label" 276 | <*> o .: "command" 277 | 278 | 279 | getConfig :: Text.Text -> IO Config 280 | getConfig configYml = Y.decodeThrow $ encodeUtf8 configYml 281 | -------------------------------------------------------------------------------- /src/Helpers.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | 3 | module Helpers where 4 | 5 | import qualified Data.ConfigFile as CF 6 | import qualified Control.Monad.Except as Error 7 | import Control.Applicative ((<|>)) 8 | import Control.Monad 9 | import Data.Foldable 10 | import Data.Functor ( fmap, (<&>) ) 11 | import Data.Gettext 12 | import Data.Maybe ( fromMaybe, listToMaybe ) 13 | import Text.HTML.TagSoup (Tag(..), renderTags 14 | , canonicalizeTags, parseTags, isTagCloseName) 15 | 16 | import Text.Regex.TDFA 17 | import qualified Data.Text as Text 18 | import Data.Char ( chr ) 19 | import System.Directory 20 | import System.Environment ( getExecutablePath ) 21 | import System.Exit ( die ) 22 | import System.FilePath 23 | import System.Locale.SetLocale 24 | 25 | import Paths_deadd_notification_center 26 | 27 | i18nInit :: IO Catalog 28 | i18nInit = do 29 | mo <- getMoFile 30 | case mo of 31 | Just mo -> loadCatalog mo 32 | Nothing -> die "Unable to set up localization." 33 | 34 | getMoFile :: IO (Maybe FilePath) 35 | getMoFile = do 36 | currentLocale <- fromMaybe "en" <$> setLocale LC_ALL (Just "") 37 | 38 | -- Find location of folder holding translations installed by the Makefile. 39 | applicationDirectory <- takeDirectory . takeDirectory <$> getExecutablePath 40 | let localesDirectory = applicationDirectory "share" "locale" 41 | 42 | -- Create a list of paths to search for translations. 43 | possiblePaths <- generateCabalAndMakefilePaths localesDirectory currentLocale 44 | 45 | -- Verify a path exists for the currentLocale if not, warn and use English. 46 | -- If English does not exist, return Nothing 47 | verifiedPath <- findExistingPath possiblePaths 48 | case verifiedPath of 49 | Just p -> return $ Just p 50 | Nothing -> do 51 | putStrLn $ unlines 52 | [ "No existing translations for " ++ currentLocale ++ "." 53 | , "Consider contributing your language." 54 | , "https://github.com/phuhl/linux_notification_center/tree/master/translation" 55 | , "Trying English instead..." 56 | ] 57 | enPossiblePaths <- generateCabalAndMakefilePaths localesDirectory "en" 58 | enVerifiedPath <- findExistingPath enPossiblePaths 59 | case enVerifiedPath of 60 | Just p -> return $ Just p 61 | Nothing -> do 62 | putStrLn "Could not find English locale." 63 | return Nothing 64 | where 65 | -- Returns the first existing path to the mo file, if none exist, returns Nothing. 66 | findExistingPath :: [FilePath] -> IO (Maybe FilePath) 67 | findExistingPath p = filterM doesFileExist p <&> listToMaybe 68 | -- Generate a list of possible locale paths from Cabal and the Makefile. 69 | generateCabalAndMakefilePaths :: FilePath -> String -> IO [FilePath] 70 | generateCabalAndMakefilePaths localesDirectory locale = do 71 | let pathsFromMakefile = generatePossiblePaths localesDirectory locale 72 | pathsFromCabal <- mapM getDataFileName 73 | (generatePossiblePaths "translation" locale) 74 | return $ pathsFromCabal ++ pathsFromMakefile 75 | -- POSIX.1-2017, section 8.2 Internationalization Variables states the format 76 | -- is language[_territory][.codeset]. Since there are translations with 77 | -- territory specified, search for locales with reducing granularity. 78 | generatePossiblePaths :: FilePath -> String -> [FilePath] 79 | generatePossiblePaths localesDirectory locale = fmap 80 | ( "LC_MESSAGES" textDomain <> ".mo") 81 | [ localesDirectory locale 82 | , localesDirectory takeWhile (/= '.') locale 83 | , localesDirectory takeWhile (/= '_') locale 84 | ] 85 | where textDomain = "deadd-notification-center" 86 | 87 | readConfig :: CF.Get_C a => a -> CF.ConfigParser -> String -> String -> a 88 | readConfig defaultVal conf sec opt = fromEither defaultVal 89 | $ fromEither (Right defaultVal) $ Error.runExceptT $ CF.get conf sec opt 90 | 91 | readConfigFile :: String -> IO CF.ConfigParser 92 | readConfigFile path = do 93 | c <- Error.catchError (CF.readfile CF.emptyCP{CF.optionxform = id} path) 94 | (\e -> do 95 | putStrLn $ show e 96 | return $ return CF.emptyCP) 97 | let c1 = fromEither CF.emptyCP c 98 | return c1 99 | 100 | fth (_, _, _, a) = a 101 | 102 | fromEither :: a -> Either b a -> a 103 | fromEither a e = case e of 104 | Left _ -> a 105 | Right x -> x 106 | 107 | eitherToMaybe :: Either a b -> Maybe b 108 | eitherToMaybe (Right b) = Just b 109 | eitherToMaybe _ = Nothing 110 | 111 | orElse :: Maybe a -> Maybe a -> Maybe a 112 | x `orElse` y = case x of 113 | Just _ -> x 114 | Nothing -> y 115 | 116 | 117 | replace a b c = replace' c a b 118 | replace' :: Eq a => [a] -> [a] -> [a] -> [a] 119 | replace' [] _ _ = [] 120 | replace' s find repl = 121 | if take (length find) s == find 122 | then repl ++ (replace' (drop (length find) s) find repl) 123 | else [head s] ++ (replace' (tail s) find repl) 124 | 125 | -- split a string at ":" 126 | split :: String -> [String] 127 | split ('"':':':'"':ds) = "" : split ds 128 | split (a:[]) = [[a]] 129 | split (a:bs) = (a:(split bs !! 0)): (tail $ split bs) 130 | split [] = [] 131 | 132 | splitOn :: Char -> String -> [String] 133 | splitOn c s = case rest of 134 | [] -> [chunk] 135 | _:rest -> chunk : splitOn c rest 136 | where (chunk, rest) = break (==c) s 137 | 138 | 139 | trimFront :: String -> String 140 | trimFront (' ':ss) = trimFront ss 141 | trimFront ss = ss 142 | 143 | trimBack :: String -> String 144 | trimBack = reverse . trimFront . reverse 145 | 146 | trim :: String -> String 147 | trim = trimBack . trimFront 148 | 149 | isPrefix :: String -> String -> Bool 150 | isPrefix (a:pf) (b:s) = a == b && isPrefix pf s 151 | isPrefix [] _ = True 152 | isPrefix _ _ = False 153 | 154 | removeOuterLetters [] = [] 155 | removeOuterLetters [x] = [] 156 | removeOuterLetters (x:xs) = init xs 157 | 158 | splitEvery :: Int -> [a] -> [[a]] 159 | splitEvery _ [] = [] 160 | splitEvery n as = (take n as) : (splitEvery n $ tailAt n as) 161 | where 162 | tailAt 0 as = as 163 | tailAt n (a:as) = tailAt (n - 1) as 164 | tailAt _ [] = [] 165 | 166 | atMay :: [a] -> Int -> Maybe a 167 | atMay ls i = if length ls > i then 168 | Just $ ls !! i else Nothing 169 | 170 | 171 | removeAllTags :: Text.Text -> Text.Text 172 | removeAllTags = renderTags . (filterTags []) . canonicalizeTags . parseTags 173 | 174 | -- The following tags should be supported: 175 | -- ... Bold 176 | -- ... Italic 177 | -- ... Underline 178 | -- ... Hyperlink 179 | markupify :: Text.Text -> Text.Text 180 | markupify = renderTags . (filterTags ["b", "i", "u", "a"]) 181 | . canonicalizeTags . parseTags 182 | 183 | filterTags :: [Text.Text] -> [Tag Text.Text] -> [Tag Text.Text] 184 | filterTags supportedTags [] = [] 185 | filterTags supportedTags (tag : rest) = case tag of 186 | TagText _ -> keep 187 | TagOpen "img" _ -> process "img" skip 188 | TagOpen name _ -> 189 | let conversion = if isSupported name then enclose name else strip 190 | in process name conversion 191 | otherwise -> next 192 | where 193 | isSupported name = elem name supportedTags 194 | 195 | keep = tag : next 196 | next = filterTags supportedTags rest 197 | 198 | skip _ = [] 199 | strip = filterTags supportedTags 200 | enclose name i = tag : (filterTags supportedTags i) ++ [TagClose name] 201 | 202 | process name conversion = 203 | let 204 | (inner, endTagRest) = break (isTagCloseName name) rest 205 | in (conversion inner) ++ (filterTags supportedTags endTagRest) 206 | 207 | 208 | getImgTagAttrs :: Text.Text -> [(Text.Text, Text.Text)] 209 | getImgTagAttrs text = getImg $ canonicalizeTags $ parseTags text 210 | where 211 | getImg [] = [] 212 | getImg (tag : rest) = case tag of 213 | TagOpen "img" attr -> attr 214 | otherwise -> getImg rest 215 | 216 | 217 | 218 | -- | Parses HTML Entities in the given string and replaces them with 219 | -- their representative characters. Only operates on entities in 220 | -- the ASCII Range. See the following for details: 221 | -- 222 | -- 223 | -- 224 | parseHtmlEntities :: String -> String 225 | parseHtmlEntities = 226 | let parseAsciiEntities text = 227 | let (a, matched, c, ms) = 228 | (text =~ ("&#([0-9]{2,3});" :: String) 229 | :: (String, String, String, [String])) 230 | ascii = if length ms > 0 then (read $ head ms :: Int) else -1 231 | repl = if 32 <= ascii && ascii <= 126 232 | then [chr ascii] else matched 233 | in a ++ repl ++ (if length c > 0 then parseAsciiEntities c else "") 234 | parseNamedEntities text = 235 | let (a, matched, c, ms) = 236 | (text =~ ("&([A-Za-z0-9]+);" :: String) 237 | :: (String, String, String, [String])) 238 | name = if length ms > 0 then head ms else "" 239 | repl = case name of 240 | "quot" -> "\"" 241 | "apos" -> "'" 242 | "grave" -> "`" 243 | "amp" -> "&" 244 | "tilde" -> "~" 245 | "" -> matched 246 | _ -> matched 247 | in a ++ repl ++ (if length c > 0 then parseNamedEntities c else "") 248 | in parseAsciiEntities . parseNamedEntities 249 | -------------------------------------------------------------------------------- /src/NotificationCenter.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | {-# LANGUAGE OverloadedLabels #-} 3 | 4 | module NotificationCenter where 5 | 6 | import Config 7 | (getConfig, Config(..), ButtonConfig(..)) 8 | import NotificationCenter.NotificationInCenter 9 | (DisplayingNotificationInCenter(..), showNotification, updateNoti) 10 | import NotificationCenter.Notifications 11 | (NotifyState(..), startNotificationDaemon, hideAllNotis) 12 | import NotificationCenter.Notifications.Data (Notification(..)) 13 | import NotificationCenter.Glade (glade) 14 | import NotificationCenter.Button 15 | (Button(..), createButton, setButtonState) 16 | import TransparentWindow 17 | import Helpers 18 | 19 | import Prelude 20 | 21 | import System.Locale.SetLocale 22 | import System.IO.Unsafe 23 | import System.IO (readFile) 24 | import System.IO.Error (tryIOError) 25 | 26 | import Data.Int (Int32(..)) 27 | import Data.Tuple.Sequence (sequenceT) 28 | import Data.Maybe 29 | import Data.IORef 30 | import Data.Gettext 31 | import Data.List 32 | import Data.Time 33 | import Data.Time.LocalTime 34 | import qualified Data.Text as Text 35 | import qualified Data.Text.Lazy as LT 36 | import qualified Data.ByteString.Char8 as BS 37 | import qualified Data.Map as Map 38 | import Data.Complex 39 | import Data.Monoid ((<>)) 40 | 41 | import Control.Applicative 42 | import Control.Exception (finally) 43 | import Control.Concurrent (forkIO, threadDelay, ThreadId(..)) 44 | import Control.Concurrent.STM 45 | ( readTVarIO, modifyTVar', TVar(..), atomically, newTVarIO ) 46 | import Control.Monad 47 | 48 | import System.Process (spawnCommand, interruptProcessGroupOf, waitForProcess) 49 | import System.Locale.Current 50 | import System.Posix.Signals (sigUSR1) 51 | import System.Posix.Daemonize (serviced, daemonize) 52 | import System.Directory (doesFileExist, getXdgDirectory, XdgDirectory(..)) 53 | 54 | import DBus ( fromVariant ) 55 | 56 | import GI.Gtk 57 | (buttonSetLabel, widgetSetHalign, widgetSetHexpand, buttonNew, setWidgetMargin, buttonSetRelief, widgetSetSizeRequest, widgetShowAll, widgetShow, widgetHide, onWidgetDestroy 58 | , windowSetDefaultSize, setWindowTitle, boxPackStart, boxNew 59 | , setWindowWindowPosition, WindowPosition(..), windowMove 60 | , frameSetShadowType, aspectFrameNew 61 | , widgetGetAllocatedHeight, widgetGetAllocatedWidth, onWidgetDraw 62 | , adjustmentSetValue, adjustmentGetLower, adjustmentGetUpper, adjustmentGetPageSize 63 | , onWidgetLeaveNotifyEvent, onWidgetMotionNotifyEvent 64 | , widgetAddEvents, alignmentSetPadding, alignmentNew, rangeSetValue 65 | , scaleSetDigits, scaleSetValuePos, rangeGetValue 66 | , afterScaleButtonValueChanged, scaleNewWithRange, containerAdd 67 | , buttonBoxNew, mainQuit, onButtonActivate 68 | , toggleButtonGetActive, onToggleButtonToggled, buttonSetUseStock 69 | , toggleButtonNewWithLabel, onButtonClicked 70 | , buttonNewWithLabel, widgetQueueDraw, drawingAreaNew 71 | , windowNew, widgetDestroy, dialogRun, setAboutDialogComments 72 | , setAboutDialogAuthors, setAboutDialogVersion 73 | , setAboutDialogProgramName, aboutDialogNew, labelNew, get 74 | , afterWindowSetFocus, labelSetText 75 | , onWidgetFocusOutEvent, onWidgetKeyReleaseEvent, widgetGetParentWindow 76 | , onButtonClicked, windowGetScreen, boxNew, widgetSetValign) 77 | import qualified GI.Gtk as Gtk (containerAdd, Window(..), Box(..), Label(..), Button(..), Adjustment(..)) 78 | 79 | import qualified GI.Gtk as GI (init, main) 80 | import GI.GLib (sourceRemove, timeoutAdd, unixSignalAdd) 81 | import GI.GLib.Constants 82 | import GI.Gdk.Constants 83 | import GI.Gdk.Flags (EventMask(..)) 84 | import GI.Gtk.Enums 85 | (Orientation(..), WindowType(..), ShadowType(..) 86 | , PositionType(..), ReliefStyle(..), Align(..)) 87 | import Data.GI.Base.BasicConversions (gflagsToWord) 88 | import qualified GI.Gdk.Objects.Window 89 | 90 | 91 | data State = State 92 | { stMainWindow :: Gtk.Window 93 | , stNotiBox :: Gtk.Box 94 | , stNotiBoxAdj :: Gtk.Adjustment 95 | , stTimeLabel :: Gtk.Label 96 | , stDateLabel :: Gtk.Label 97 | , stDeleteAll :: Gtk.Button 98 | , stUserButtons :: [ Button ] 99 | , stNotiState :: TVar NotifyState 100 | , stDisplayingNotiList :: [ DisplayingNotificationInCenter ] 101 | , stNotisForMe :: [ Notification ] 102 | , stCenterShown :: Bool 103 | , stPopupsPaused :: Bool 104 | } 105 | 106 | 107 | setTime :: TVar State -> IO () 108 | setTime tState = do 109 | state <- readTVarIO tState 110 | now <- zonedTimeToLocalTime <$> getZonedTime 111 | zone <- System.Locale.Current.currentLocale 112 | let format = Text.pack . flip (formatTime zone) now 113 | labelSetText (stTimeLabel state) $ format "%H:%M" 114 | labelSetText (stDateLabel state) $ format "%A, %x" 115 | 116 | 117 | startSetTimeThread :: TVar State -> IO () 118 | startSetTimeThread tState = do 119 | runAfterDelay 1000 (startSetTimeThread' tState) 120 | return () 121 | 122 | startSetTimeThread' :: TVar State -> IO () 123 | startSetTimeThread' tState = do 124 | addSource (setTime tState) 125 | time <- fromIntegral <$> diffTimeToPicoseconds <$> utctDayTime <$> getCurrentTime 126 | let delay = (60 * 1000000) - ((ceiling (time / 1000000)) `mod` (1000000 * 60)) 127 | runAfterDelay delay (startSetTimeThread' tState) 128 | return () 129 | 130 | deleteInCenter tState = do 131 | displayList <- stDisplayingNotiList <$> readTVarIO tState 132 | mapM (removeNoti tState) displayList 133 | return () 134 | 135 | setWindowStyle tState = do 136 | state <- readTVarIO tState 137 | homeDir <- getXdgDirectory XdgConfig "" 138 | paths <- filterM doesFileExist 139 | $ [homeDir ++ "/deadd/deadd.css", "/etc/xdg/deadd/deadd.css"] 140 | >>= return 141 | if length paths > 0 then do 142 | style <- readFile =<< (filterM doesFileExist paths >>= return . head) 143 | screen <- windowGetScreen $ stMainWindow state 144 | setStyle screen $ BS.pack $ style 145 | else 146 | return () 147 | 148 | createNotiCenter :: TVar State -> Config -> Catalog -> IO () 149 | createNotiCenter tState config catalog = do 150 | (objs, _) <- createTransparentWindow (Text.pack glade) 151 | [ "main_window" 152 | , "label_time" 153 | , "label_date" 154 | , "box_notis" 155 | , "box_notis_adj" 156 | , "box_buttons" 157 | , "button_deleteAll" ] 158 | (Just "Notification area") 159 | 160 | mainWindow <- window objs "main_window" 161 | notiBox <- box objs "box_notis" 162 | buttonBox <- box objs "box_buttons" 163 | timeL <- label objs "label_time" 164 | timeD <- label objs "label_date" 165 | notiBoxAdj <- adjustment objs "box_notis_adj" 166 | deleteButton <- button objs "button_deleteAll" 167 | 168 | onButtonClicked deleteButton $ deleteInCenter tState 169 | buttonSetLabel deleteButton $ LT.toStrict $ gettext catalog "Delete all" 170 | 171 | let buttons = configButtons config 172 | margin = fromIntegral $ configButtonMargin config 173 | width = fromIntegral (((configWidth config) - 20) 174 | `div` (configButtonsPerRow config)) 175 | - margin * 2 176 | height = fromIntegral $ configButtonHeight config 177 | linesNeeded = fromIntegral $ ceiling 178 | $ ((fromIntegral $ length buttons) / (fromIntegral $ configButtonsPerRow config)) 179 | lines' <- sequence $ take linesNeeded $ repeat $ boxNew OrientationHorizontal 0 180 | buttons' <- sequence $ map 181 | (\(button) -> createButton config width height 182 | (configButtonCommand button) 183 | (configButtonLabel button)) 184 | buttons 185 | 186 | 187 | sequence $ map (\(box, buttons'') -> 188 | sequence $ Gtk.containerAdd box <$> buttons'') 189 | $ zip (reverse lines') (splitEvery (configButtonsPerRow config) 190 | $ (buttonButton <$> buttons')) 191 | sequence $ Gtk.containerAdd buttonBox <$> lines' 192 | sequence $ widgetShowAll <$> lines' 193 | 194 | atomically $ modifyTVar' tState $ 195 | \state -> state { stMainWindow = mainWindow 196 | , stNotiBox = notiBox 197 | , stNotiBoxAdj = notiBoxAdj 198 | , stTimeLabel = timeL 199 | , stDateLabel = timeD 200 | , stDeleteAll = deleteButton 201 | , stNotisForMe = [] 202 | , stUserButtons = buttons' } 203 | 204 | setWindowStyle tState 205 | 206 | startSetTimeThread tState 207 | 208 | 209 | when (configNotiCenterHideOnMouseLeave config) $ do 210 | onWidgetLeaveNotifyEvent mainWindow $ \(_) -> do 211 | hideNotiCenter tState 212 | return True 213 | return () 214 | 215 | setNotificationCenterPosition mainWindow config 216 | 217 | onWidgetDestroy mainWindow mainQuit 218 | 219 | return () 220 | 221 | setNotificationCenterPosition mainWindow config = do 222 | 223 | (screenW, screenY, screenH) <- if configNotiCenterFollowMouse config then 224 | getMouseActiveScreenPos mainWindow (fromIntegral $ configNotiMonitor config) 225 | else 226 | getScreenPos mainWindow (fromIntegral $ configNotiCenterMonitor config) 227 | 228 | windowSetDefaultSize mainWindow 229 | width -- w 230 | (screenH - barHeightTop - barHeightBottom) -- h 231 | windowMove mainWindow 232 | (screenW - width - marginRight) -- x 233 | (screenY + barHeightTop) -- y 234 | return () 235 | where 236 | barHeightTop = fromIntegral $ configBarHeight config 237 | barHeightBottom = fromIntegral $ configBottomBarHeight config 238 | marginRight = fromIntegral $ configRightMargin config 239 | width = fromIntegral $ configWidth config 240 | 241 | parseButtons noti tState = do 242 | state <- readTVarIO tState 243 | let buttons = stUserButtons state 244 | maybeId = (Map.lookup "id" noti >>= fromVariant) :: Maybe Int32 245 | id = checkId maybeId 246 | where 247 | checkId (Just id) 248 | | id < (fromIntegral $ length buttons) = Just id 249 | | otherwise = Nothing 250 | checkId Nothing = Nothing 251 | maybeState = Map.lookup "state" noti >>= fromVariant :: Maybe Bool 252 | maybeButton = (!!) buttons <$> (fromIntegral <$> id) 253 | fromMaybe (return ()) $ setButtonState <$> maybeButton <*> maybeState 254 | return () 255 | 256 | 257 | parseNotisForMe tState = do 258 | state <- readTVarIO tState 259 | -- do stuff 260 | let notisForMe = stNotisForMe state 261 | myNotiHints = notiHints <$> notisForMe 262 | atomically $ modifyTVar' tState 263 | (\state -> state { stNotisForMe = [] }) 264 | sequence $ map (\noti -> do 265 | let maybeType = Map.lookup "type" noti 266 | in case (maybeType >>= fromVariant) :: Maybe (String) of 267 | (Just "clearInCenter") -> do 268 | putStrLn "clearing in center" 269 | deleteInCenter tState 270 | (Just "clearPopups") -> do 271 | putStrLn "clearing popups" 272 | hideAllNotis $ stNotiState state 273 | (Just "buttons") -> parseButtons noti tState 274 | Nothing -> parseButtons noti tState 275 | (Just "pausePopups") -> do 276 | putStrLn "pausing popups" 277 | atomically $ modifyTVar' tState 278 | (\state -> state { stPopupsPaused = True }) 279 | (Just "unpausePopups") -> do 280 | putStrLn "unpausing popups" 281 | atomically $ modifyTVar' tState 282 | (\state -> state { stPopupsPaused = False }) 283 | (Just "reloadStyle") -> do 284 | putStrLn "Reloading Style" 285 | addSource $ setWindowStyle tState 286 | return () 287 | ) myNotiHints 288 | return () 289 | 290 | hideNotiCenter tState = do 291 | state <- readTVarIO tState 292 | mainWindow <- stMainWindow <$> readTVarIO tState 293 | widgetHide mainWindow 294 | atomically $ modifyTVar' tState 295 | (\state -> state { stCenterShown = False }) 296 | 297 | 298 | showNotiCenter tState notiState config = do 299 | state <- readTVarIO tState 300 | mainWindow <- stMainWindow <$> readTVarIO tState 301 | setNotificationCenterPosition mainWindow config 302 | newShown <- if stCenterShown state then 303 | do 304 | widgetHide mainWindow 305 | return False 306 | else 307 | do 308 | hideAllNotis $ stNotiState state 309 | widgetShow mainWindow 310 | return True 311 | atomically $ modifyTVar' tState 312 | (\state -> state {stCenterShown = newShown }) 313 | 314 | notiBoxAdj <- stNotiBoxAdj <$> readTVarIO tState 315 | notiBoxAdjVal <- if configNotiCenterNewFirst config then 316 | do 317 | adj <- adjustmentGetLower notiBoxAdj 318 | return adj 319 | else 320 | do 321 | adj <- adjustmentGetUpper notiBoxAdj 322 | page <- adjustmentGetPageSize notiBoxAdj 323 | return (adj - page) 324 | adjustmentSetValue notiBoxAdj notiBoxAdjVal 325 | 326 | return True 327 | 328 | updateNotisForMe :: TVar State -> IO() 329 | updateNotisForMe tState = do 330 | state <- readTVarIO tState 331 | notiState <- readTVarIO $ stNotiState state 332 | atomically $ modifyTVar' (stNotiState state) ( 333 | \state -> state { notiForMeList = [] }) 334 | atomically $ modifyTVar' tState ( 335 | \state -> state { stNotisForMe = notiForMeList notiState 336 | ++ stNotisForMe state}) 337 | 338 | addSource (parseNotisForMe tState) 339 | return () 340 | 341 | updateNotis :: Config -> TVar State -> IO() 342 | updateNotis config tState = do 343 | state <- readTVarIO tState 344 | notiState <- readTVarIO $ stNotiState state 345 | 346 | let newNotis = filter ( 347 | \n -> (find (\nd -> _dNotiId nd == notiId n ) 348 | (stDisplayingNotiList state)) 349 | == Nothing 350 | && ((configIgnoreTransient config) || (not $ notiTransient n))) 351 | $ notiStList notiState 352 | newNotis' <- mapM ( 353 | \n -> do 354 | let newNoti = DisplayingNotificationInCenter 355 | { _dNotiId = notiId n } 356 | showNotification config 357 | (stNotiBox state) newNoti 358 | (stNotiState state) $ removeNoti tState 359 | ) newNotis 360 | 361 | let delNotis = filter (\nd -> (find (\n -> _dNotiId nd == notiId n) 362 | $ notiStList notiState) == Nothing) 363 | $ stDisplayingNotiList state 364 | atomically $ modifyTVar' tState ( 365 | \state -> state { stDisplayingNotiList = 366 | newNotis' ++ stDisplayingNotiList state 367 | }) 368 | setDeleteAllState tState 369 | mapM (removeNoti tState) $ delNotis 370 | when (stCenterShown state || stPopupsPaused state) $ 371 | do 372 | hideAllNotis $ stNotiState state 373 | return () 374 | 375 | 376 | removeNoti :: TVar State -> DisplayingNotificationInCenter -> IO () 377 | removeNoti tState dNoti = do 378 | state <- readTVarIO tState 379 | atomically $ modifyTVar' tState $ \state -> 380 | state { stDisplayingNotiList = 381 | filter ((/=) dNoti) $ stDisplayingNotiList state} 382 | setDeleteAllState tState 383 | atomically $ modifyTVar' (stNotiState state) $ \state -> 384 | state { notiStList = filter 385 | (\n -> notiId n /= _dNotiId dNoti) $ 386 | notiStList state } 387 | addSource $ _dNotiDestroy dNoti 388 | return () 389 | 390 | setDeleteAllState tState = do 391 | addSource $ do 392 | state <- readTVarIO tState 393 | if (length $ stDisplayingNotiList state) > 1 then 394 | widgetShow $ stDeleteAll state 395 | else 396 | widgetHide $ stDeleteAll state 397 | return () 398 | 399 | getInitialState = do 400 | newTVarIO $ State 401 | { stDisplayingNotiList = [] 402 | , stCenterShown = False 403 | , stPopupsPaused = False} 404 | 405 | main' :: IO () 406 | main' = do 407 | GI.init Nothing 408 | 409 | homeDir <- getXdgDirectory XdgConfig "" 410 | configData <- fromEither (Text.pack "{}") <$> (tryIOError $ do 411 | Text.pack <$> readFile (homeDir ++ "/deadd/deadd.yml")) 412 | config <- getConfig configData 413 | 414 | catalog <- i18nInit 415 | 416 | istate <- getInitialState 417 | notiState <- startNotificationDaemon config 418 | (updateNotis config istate) (updateNotisForMe istate) 419 | 420 | atomically $ modifyTVar' istate $ 421 | \istate' -> istate' { stNotiState = notiState } 422 | createNotiCenter istate config catalog 423 | 424 | unixSignalAdd PRIORITY_HIGH (fromIntegral sigUSR1) 425 | (showNotiCenter istate notiState config) 426 | 427 | ph <- spawnCommand $ configStartupCommand config 428 | waitForProcess ph `finally` interruptProcessGroupOf ph 429 | 430 | GI.main 431 | 432 | main :: IO () 433 | main = do 434 | -- daemonize main' 435 | main' 436 | -------------------------------------------------------------------------------- /src/NotificationCenter/Button.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | 3 | module NotificationCenter.Button 4 | ( createButton 5 | , setButtonState 6 | , Button(..) 7 | ) where 8 | 9 | import Config (Config(..)) 10 | import TransparentWindow 11 | 12 | import Control.Exception (finally) 13 | 14 | import GI.Gtk 15 | (widgetSetHalign, widgetSetHexpand, buttonNew, setWidgetMargin 16 | , buttonSetRelief, widgetSetSizeRequest, widgetShowAll, widgetShow 17 | , widgetHide, onWidgetDestroy 18 | , windowSetDefaultSize, setWindowTitle, boxPackStart, boxNew 19 | , setWindowWindowPosition, WindowPosition(..), windowMove 20 | , frameSetShadowType, aspectFrameNew 21 | , widgetGetAllocatedHeight, widgetGetAllocatedWidth, onWidgetDraw 22 | , onWidgetLeaveNotifyEvent, onWidgetMotionNotifyEvent 23 | , widgetAddEvents, alignmentSetPadding, alignmentNew, rangeSetValue 24 | , scaleSetDigits, scaleSetValuePos, rangeGetValue 25 | , afterScaleButtonValueChanged, scaleNewWithRange, containerAdd 26 | , buttonBoxNew, mainQuit, onButtonActivate 27 | , toggleButtonGetActive, onToggleButtonToggled, buttonSetUseStock 28 | , toggleButtonNewWithLabel, onButtonClicked 29 | , buttonNewWithLabel, widgetQueueDraw, drawingAreaNew 30 | , windowNew, widgetDestroy, dialogRun, setAboutDialogComments 31 | , setAboutDialogAuthors, setAboutDialogVersion 32 | , setAboutDialogProgramName, aboutDialogNew, labelNew, get 33 | , afterWindowSetFocus, labelSetText 34 | , onWidgetFocusOutEvent, onWidgetKeyReleaseEvent, widgetGetParentWindow 35 | , onButtonClicked, windowGetScreen, boxNew, widgetSetValign) 36 | import GI.Gtk.Enums 37 | (Orientation(..), PositionType(..), ReliefStyle(..), Align(..)) 38 | 39 | import qualified GI.Gtk as Gtk (containerAdd, Box(..), Label(..), Button(..)) 40 | import qualified Data.Text as Text 41 | import System.Process (spawnCommand, interruptProcessGroupOf, waitForProcess) 42 | 43 | data Button = Button 44 | { buttonButton :: Gtk.Button 45 | -- ^ Button Element for displaying 46 | , buttonLabel :: Gtk.Label 47 | -- ^ Description text label 48 | , buttonCommand :: String 49 | -- ^ Shell command to execute 50 | } 51 | 52 | createButton :: Config -> Int -> Int -> String -> String -> IO Button 53 | createButton config width height command description = do 54 | button <- buttonNew 55 | label <- labelNew $ Just $ Text.pack description 56 | widgetSetSizeRequest button (fromIntegral width) (fromIntegral height) 57 | addClass button "userbutton" 58 | addClass button "deadd-noti-center" 59 | buttonSetRelief button ReliefStyleNone 60 | setWidgetMargin button $ fromIntegral $ configButtonMargin config 61 | widgetSetHalign label AlignStart 62 | widgetSetValign label AlignEnd 63 | addClass label "userbuttonlabel" 64 | addClass label "deadd-noti-center" 65 | 66 | let theButton = Button 67 | { buttonButton = button 68 | , buttonLabel = label 69 | , buttonCommand = command } 70 | onButtonClicked button $ do 71 | addSource $ setButtonState2 $ theButton 72 | ph <- spawnCommand command 73 | waitForProcess ph `finally` interruptProcessGroupOf ph 74 | return () 75 | Gtk.containerAdd button label 76 | return theButton 77 | 78 | setButtonState2 :: Button -> IO () 79 | setButtonState2 button = do 80 | addClass (buttonButton button) "buttonState2" 81 | addClass (buttonLabel button) "buttonState2" 82 | 83 | setButtonState :: Button -> Bool -> IO () 84 | setButtonState button state = do 85 | removeClass (buttonButton button) "buttonState2" 86 | removeClass (buttonLabel button) "buttonState2" 87 | if state then 88 | do 89 | addClass (buttonButton button) "buttonState1" 90 | addClass (buttonLabel button) "buttonState1" 91 | else 92 | do 93 | removeClass (buttonButton button) "buttonState1" 94 | removeClass (buttonLabel button) "buttonState1" 95 | -------------------------------------------------------------------------------- /src/NotificationCenter/Glade.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE QuasiQuotes, OverloadedStrings #-} 2 | 3 | module NotificationCenter.Glade where 4 | 5 | import Data.String.Here.Uninterpolated (hereFile) 6 | 7 | glade = 8 | [hereFile|notification_center.glade|] 9 | 10 | -------------------------------------------------------------------------------- /src/NotificationCenter/Notification/Glade.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE QuasiQuotes, OverloadedStrings #-} 2 | 3 | module NotificationCenter.Notification.Glade (glade) where 4 | 5 | import Data.String.Here.Uninterpolated (hereFile) 6 | 7 | glade = 8 | [hereFile|notification_in_center.glade|] 9 | -------------------------------------------------------------------------------- /src/NotificationCenter/NotificationInCenter.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE TemplateHaskell #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | {-# LANGUAGE OverloadedLabels #-} 4 | 5 | module NotificationCenter.NotificationInCenter 6 | ( showNotification 7 | , updateNoti 8 | , DisplayingNotificationInCenter(..) 9 | ) where 10 | 11 | import TransparentWindow 12 | import NotificationCenter.Notification.Glade (glade) 13 | import NotificationCenter.Notifications (NotifyState(..)) 14 | import Config (Config(..)) 15 | import NotificationCenter.Notifications.Data (Notification(..)) 16 | import NotificationCenter.Notifications.AbstractNotification 17 | (DisplayingNotificationContent(..), HasDisplayingNotificationContent(..) 18 | , createNotification, updateNotiContent, setUrgencyLevel) 19 | 20 | import Data.List 21 | import qualified Data.Text as Text 22 | 23 | import Control.Lens.TH (makeClassy) 24 | import Control.Lens (view, set) 25 | 26 | 27 | import Control.Monad 28 | import Control.Concurrent.STM 29 | ( readTVarIO, modifyTVar, TVar(..), atomically, newTVarIO ) 30 | 31 | import GI.Pango.Enums (EllipsizeMode(..)) 32 | import qualified GI.Gtk as Gtk 33 | (widgetShowAll, onButtonClicked, boxReorderChild, containerAdd 34 | , builderAddFromString, widgetDestroy, labelSetText 35 | , labelSetEllipsize, labelSetLines 36 | , builderNew, Button(..), Label(..), Box(..)) 37 | 38 | data DisplayingNotificationInCenter = DisplayingNotificationInCenter 39 | { _dpopupContent :: DisplayingNotificationContent 40 | , _dNotiId :: Int 41 | , _dNotiDestroy :: IO () 42 | , _dLabelTime :: Gtk.Label 43 | , _dButtonClose :: Gtk.Button 44 | } 45 | makeClassy ''DisplayingNotificationInCenter 46 | instance HasDisplayingNotificationContent DisplayingNotificationInCenter where 47 | displayingNotificationContent = dpopupContent 48 | 49 | 50 | instance Eq DisplayingNotificationInCenter where 51 | a == b = _dNotiId a == _dNotiId b 52 | 53 | 54 | showNotification :: Config -> Gtk.Box -> DisplayingNotificationInCenter 55 | -> TVar NotifyState 56 | -> (DisplayingNotificationInCenter -> IO ()) 57 | -> IO DisplayingNotificationInCenter 58 | showNotification config mainBox dNoti tNState closeNotification = do 59 | nState <- readTVarIO tNState 60 | let (Just noti) = find (\n -> notiId n == _dNotiId dNoti) 61 | $ notiStList nState 62 | 63 | builder <- Gtk.builderNew 64 | Gtk.builderAddFromString builder (Text.pack glade) (-1) 65 | objs <- getObjs builder 66 | [ "label_time" 67 | , "button_close" ] 68 | 69 | labelTime <- label objs "label_time" 70 | buttonClose <- button objs "button_close" 71 | 72 | dispNotiWithoutDestroy <- createNotification config builder noti 73 | $ dNoti { _dButtonClose = buttonClose 74 | , _dLabelTime = labelTime 75 | , _dpopupContent = DisplayingNotificationContent {} } 76 | 77 | let dispNoti = set dNotiDestroy 78 | (Gtk.widgetDestroy (view dContainer dispNoti)) dispNotiWithoutDestroy 79 | lblBody = (flip view) dispNoti $ dLabelBody 80 | 81 | setUrgencyLevel (notiUrgency noti) [view dContainer dispNoti] 82 | setUrgencyLevel (notiUrgency noti) 83 | $ (flip view) dispNoti 84 | <$> [dLabelTitel, dLabelBody, dLabelAppname, dLabelTime] 85 | 86 | Gtk.containerAdd mainBox (view dContainer dispNoti) 87 | updateNoti config mainBox dispNoti tNState 88 | 89 | Gtk.onButtonClicked buttonClose $ do 90 | closeNotification dispNoti 91 | 92 | Gtk.widgetShowAll (view dContainer dispNoti) 93 | return dispNoti 94 | 95 | 96 | updateNoti :: Config -> Gtk.Box 97 | -> DisplayingNotificationInCenter -> TVar NotifyState -> IO () 98 | updateNoti config mainBox dNoti tNState = do 99 | addSource $ do 100 | nState <- readTVarIO tNState 101 | let mNoti = find (\n -> notiId n == _dNotiId dNoti) 102 | $ notiStList nState 103 | case mNoti of 104 | (Just noti) -> do 105 | updateNotiContent config noti dNoti 106 | Gtk.labelSetText (_dLabelTime dNoti) $ notiTime noti 107 | when (configNotiCenterNewFirst config) 108 | (Gtk.boxReorderChild mainBox (view dContainer dNoti) 0) 109 | Nothing -> return () 110 | return () 111 | return () 112 | -------------------------------------------------------------------------------- /src/NotificationCenter/Notifications.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | 3 | module NotificationCenter.Notifications 4 | ( startNotificationDaemon 5 | , NotifyState(..) 6 | , hideAllNotis 7 | ) where 8 | 9 | import Helpers (trim, isPrefix, splitOn, atMay, eitherToMaybe 10 | , getImgTagAttrs, removeAllTags, parseHtmlEntities ) 11 | 12 | import NotificationCenter.Notifications.NotificationPopup 13 | ( showNotificationWindow 14 | , updateNoti 15 | , DisplayingNotificationPopup(..) 16 | ) 17 | import NotificationCenter.Notifications.Data 18 | (Urgency(..), Notification(..), Image(..), parseImageString) 19 | import TransparentWindow 20 | import Config (Config(..), ModificationRule(..)) 21 | import NotificationCenter.Notifications.Data 22 | 23 | import Control.Monad (when) 24 | import Control.Applicative ((<|>)) 25 | import Control.Concurrent (forkIO) 26 | import Control.Concurrent.STM 27 | (readTVarIO, stateTVar, modifyTVar, modifyTVar', TVar(..), 28 | atomically, newTVarIO) 29 | 30 | import DBus (Variant(..), Structure(..), fromVariant, signal, toVariant, variantType) 31 | import DBus.Internal.Message (Signal(..)) 32 | import DBus.Client 33 | ( connectSession, AutoMethod(..), autoMethod, requestName, export 34 | , defaultInterface, interfaceName, interfaceMethods 35 | , nameAllowReplacement, nameReplaceExisting, emit) 36 | import Data.Char (toLower) 37 | import Data.Foldable (asum) 38 | import Data.Text (unpack, Text, pack ) 39 | import Data.Text.Encoding (decodeUtf8, encodeUtf8) 40 | import Data.ByteString.Lazy (toStrict) 41 | 42 | import qualified Data.Text as Text 43 | import Data.Word ( Word, Word8, Word32 ) 44 | import Data.Int ( Int32 ) 45 | import Data.List 46 | import qualified Data.Map as Map 47 | import Data.Time 48 | import Data.Time.LocalTime 49 | import Data.Maybe (fromMaybe, isJust) 50 | 51 | import qualified Data.Yaml as Yaml 52 | import qualified Data.Aeson as Aeson 53 | 54 | import Control.Exception (finally) 55 | import System.Process (readCreateProcess, shell, spawnCommand, interruptProcessGroupOf, waitForProcess) 56 | import System.Locale.Current 57 | import System.Directory (getXdgDirectory, XdgDirectory(..)) 58 | import System.Environment (getEnv) 59 | import System.FilePath (splitSearchPath, ()) 60 | import System.IO (readFile) 61 | import System.IO.Error (tryIOError) 62 | import Data.GI.Base.GError (catchGErrorJust) 63 | 64 | import GI.Gio.Interfaces.AppInfo (appInfoGetIcon, appInfoGetAll, appInfoGetName) 65 | import GI.Gio.Interfaces.Icon (iconToString, Icon(..)) 66 | 67 | data NotifyState = NotifyState 68 | { notiStList :: [ Notification ] 69 | -- ^ List of all notis 70 | , notiDisplayingList :: [ DisplayingNotificationPopup ] 71 | -- ^ List of all notis getting displayed as popup 72 | , notiStNextId :: Int 73 | -- ^ Id for the next noti 74 | , notiStOnUpdate :: IO () 75 | -- ^ Update-function for the NotificationCenter 76 | , notiStOnUpdateForMe :: IO () 77 | -- ^ Update-function for the NotificationCenter for notifications 78 | -- for internal use 79 | , notiConfig :: Config 80 | -- ^ Configuration 81 | , notiForMeList :: [ Notification ] 82 | -- ^ Notifications that should not be displayed but are used as 83 | -- as hints for this notification daemon 84 | } 85 | 86 | 87 | getServerInformation :: IO (Text, Text, Text, Text) 88 | getServerInformation = 89 | return ("haskell-notification-daemon", 90 | "abc", 91 | "0.0.1", 92 | "1.2") 93 | 94 | getCapabilities :: Config -> IO [Text] 95 | getCapabilities config = return ( [ "body" 96 | , "hints" 97 | , "actions" 98 | , "persistence" 99 | , "icon-static" 100 | , "action-icons" ] 101 | ++ if (configNotiMarkup config) then 102 | [ "body-markup" 103 | , "body-hyperlinks"] else []) 104 | 105 | emitNotificationClosed :: Bool -> (Signal -> IO ()) -> Int -> CloseType -> IO () 106 | emitNotificationClosed doSend onClose id ctype = 107 | if doSend then 108 | onClose $ (signal "/org/freedesktop/Notifications" 109 | "org.freedesktop.Notifications" 110 | "NotificationClosed") 111 | { signalBody = [ toVariant (fromIntegral id :: Word32) 112 | , toVariant (case ctype of 113 | Timeout -> 1 114 | User -> 2 115 | CloseByCall -> 3 116 | _ -> 4 :: Word32)] } 117 | else return () 118 | 119 | emitAction :: (Signal -> IO ()) -> Int -> [(String, String)] -> String -> Maybe String -> IO () 120 | emitAction onAction id actionCommands key mParam = do 121 | let mCommand = lookup key actionCommands 122 | if isJust mCommand then do 123 | ph <- spawnCommand $ fromMaybe "" mCommand 124 | waitForProcess ph `finally` interruptProcessGroupOf ph 125 | return () 126 | else 127 | onAction $ (signal "/org/freedesktop/Notifications" 128 | "org.freedesktop.Notifications" 129 | "ActionInvoked") 130 | { signalBody = [ toVariant (fromIntegral id :: Word32) 131 | , toVariant key] 132 | ++ if mParam == Nothing then 133 | [] 134 | else 135 | [toVariant $ fromMaybe "" mParam] 136 | } 137 | 138 | 139 | parseActionIcons hints = 140 | fromMaybe False $ (fromVariant =<< Map.lookup "action-icons" hints :: Maybe Bool) 141 | 142 | parseUrgency hints = 143 | let urgency = fromVariant =<< Map.lookup "urgency" hints :: Maybe Word8 144 | in case urgency of 145 | (Just 0) -> Low 146 | Nothing -> Normal 147 | (Just 1) -> Normal 148 | (Just 2) -> High 149 | 150 | 151 | parseTransient :: Map.Map Text Variant -> Bool 152 | parseTransient hints = 153 | let transient = Map.lookup "transient" hints 154 | in case transient of 155 | Nothing -> False 156 | (Just b) -> True 157 | 158 | getDesktopFile :: String -> IO (Maybe String) 159 | getDesktopFile name = do 160 | xdgDesktopFiles <- getXdgDirectory XdgData "applications" 161 | xdgDataDirsDesktopFiles <- 162 | getEnv "XDG_DATA_DIRS" >>= return . splitSearchPath >>= return . fmap 163 | ( "applications") 164 | let paths = concat 165 | [ [ xdgDesktopFiles 166 | , "/usr/local/share/applications" 167 | , "/usr/share/applications" 168 | ] 169 | , xdgDataDirsDesktopFiles 170 | ] 171 | mapM getIt paths >>= return . asum 172 | where 173 | getIt path = 174 | eitherToMaybe <$> (tryIOError $ readFile $ path name ++ ".desktop") 175 | 176 | getAppIcon :: String -> IO Image 177 | getAppIcon name = do 178 | apps <- appInfoGetAll 179 | names <- sequence $ appInfoGetName <$> apps 180 | let appInfos = filter (\(_, name') -> name' == (pack name)) 181 | $ zip apps names 182 | if length appInfos > 0 then do 183 | mIcon <- appInfoGetIcon (fst $ appInfos !! 0) 184 | case mIcon of 185 | (Just icon) -> imgFromMaybe <$> ((<$>) unpack) <$> iconToString icon 186 | Nothing -> return NoImage 187 | else 188 | return NoImage 189 | where imgFromMaybe mI= case mI of 190 | Nothing -> NoImage 191 | (Just w) -> if isPrefix "/" w then 192 | ImagePath w else NamedIcon w 193 | 194 | parseIcon :: Config -> Map.Map Text Variant -> Text -> Text -> IO Image 195 | parseIcon config hints icon appName = 196 | if (Text.length icon) > 0 then do 197 | return $ parseImageString icon 198 | else do 199 | let mFileName = fromVariant =<< Map.lookup "desktop-entry" hints 200 | in case mFileName of 201 | (Just fileName) -> do 202 | getAppIcon fileName 203 | 204 | Nothing -> if configGuessIconFromAppname config then 205 | getAppIcon $ unpack appName 206 | else 207 | return NoImage 208 | 209 | parseImg :: Map.Map Text Variant -> Text -> Image 210 | parseImg hints text = 211 | fromMaybe NoImage 212 | $ fromBody <|> fromImageData <|> fromImagePath <|> fromIcon 213 | where 214 | fromBody = ImagePath <$> unpack <$> lookup (pack "src") (getImgTagAttrs text) 215 | fromIcon = RawImg <$> (fromVariant =<< Map.lookup "icon_data" hints) 216 | fromImageData = RawImg <$> (fromVariant =<< Map.lookup "image-data" hints) 217 | fromImagePath = parseImageString <$> (fromVariant =<< Map.lookup "image-path" hints) 218 | 219 | getTime = do 220 | now <- zonedTimeToLocalTime <$> getZonedTime 221 | zone <- System.Locale.Current.currentLocale 222 | let format = pack . flip (formatTime zone) now 223 | return $ format "%H:%M" 224 | 225 | htmlEntitiesStrip :: Config -> Text -> Text 226 | htmlEntitiesStrip config text = 227 | if configNotiParseHtmlEntities config 228 | then Text.pack $ parseHtmlEntities $ unpack text 229 | else text 230 | 231 | xmlStrip :: Config -> Text -> Text 232 | xmlStrip config text = do 233 | if configNotiMarkup config then 234 | text 235 | else removeAllTags text 236 | 237 | 238 | notify :: Config 239 | -> TVar NotifyState 240 | -> (Signal -> IO ()) 241 | -> Text -- ^ Application name 242 | -> Word32 -- ^ Replaces id 243 | -> Text -- ^ App icon 244 | -> Text -- ^ Summary 245 | -> Text -- ^ Body 246 | -> [Text] -- ^ Actions 247 | -> Map.Map Text Variant -- ^ Hints 248 | -> Int32 -- ^ Expires timeout (milliseconds) 249 | -> IO Word32 250 | notify config tState emit 251 | appName replaceId icon summary body actions hints timeout = do 252 | state <- readTVarIO tState 253 | time <- getTime 254 | icon <- parseIcon config hints icon appName 255 | let newNotiWithoutId = Notification 256 | { notiAppName = appName 257 | , notiRepId = replaceId 258 | 259 | -- in order to not create a race condition this can not be 260 | -- done, instead it is handled lower done 261 | , notiId = 0 262 | , notiIcon = icon 263 | , notiImg = parseImg hints body 264 | , notiImgSize = configImgSize config 265 | , notiSummary = htmlEntitiesStrip config summary 266 | , notiBody = htmlEntitiesStrip config $ xmlStrip config body 267 | , notiActions = actions 268 | , notiActionIcons = parseActionIcons hints 269 | , notiActionCommands = [] 270 | , notiHints = hints 271 | , notiUrgency = parseUrgency hints 272 | , notiTimeout = timeout 273 | , notiTime = time 274 | , notiTransient = parseTransient hints 275 | , notiSendClosedMsg = (configSendNotiClosedDbusMessage config) 276 | , notiTop = Nothing 277 | , notiRight = Nothing 278 | , notiPercentage = fromIntegral 279 | <$> ( fromVariant =<< Map.lookup "has-percentage" hints 280 | <|> Map.lookup "value" hints :: Maybe Int32 ) 281 | , notiClassName = ""} 282 | 283 | if Map.member (pack "deadd-notification-center") 284 | $ notiHints newNotiWithoutId 285 | then 286 | -- Noti is for messaging the noti-center 287 | do 288 | atomically $ modifyTVar' tState $ \state -> 289 | state { notiForMeList = newNotiWithoutId:(notiForMeList state) } 290 | notiStOnUpdateForMe state 291 | return $ fromIntegral 0 292 | else 293 | -- Noti has to be displayed 294 | do 295 | -- Apply modifications and run scripts for noti 296 | newNotiWoIdModified <- modifyNoti config newNotiWithoutId 297 | 298 | let newNoti = newNotiWoIdModified 299 | 300 | let notisToBeReplaced = filter (\n -> _dNotiId n == 301 | fromIntegral (notiRepId newNoti)) 302 | $ notiDisplayingList state 303 | newId <- atomically $ stateTVar tState 304 | $ \state -> ( 305 | notiStNextId state, state 306 | { notiStNextId = notiStNextId state + 1 307 | , notiStList = 308 | updatedNotiList (notiStList state) 309 | (newNoti 310 | { notiId = notiStNextId state 311 | , notiOnClosed = emitNotificationClosed (notiSendClosedMsg newNoti) 312 | emit (notiStNextId state) 313 | , notiOnAction = emitAction 314 | emit (notiStNextId state) }) 315 | (fromIntegral (notiRepId newNoti)) 316 | }) 317 | let newNotiWithId = newNoti 318 | { notiId = newId 319 | , notiOnClosed = emitNotificationClosed (notiSendClosedMsg newNoti) 320 | emit newId 321 | , notiOnAction = emitAction emit newId } 322 | if length notisToBeReplaced == 0 then 323 | insertNewNoti newNotiWithId tState 324 | else 325 | replaceNoti newNotiWithId tState 326 | return $ fromIntegral $ notiId newNotiWithId 327 | where 328 | updatedNotiList :: [Notification] -> Notification 329 | -> Int -> [Notification] 330 | updatedNotiList oldNotis newNoti repId = 331 | let notis' = map (\n -> if notiId n == repId then newNoti 332 | else n) oldNotis 333 | in if (find ((==) newNoti) notis') /= Nothing then notis' 334 | else (newNoti:notis') 335 | 336 | 337 | modifyNoti :: Config -> Notification -> IO Notification 338 | modifyNoti config noti = 339 | let modificationRules = configMatchingRules config 340 | in foldr (\rule ioNoti -> modifies rule =<< ioNoti) (return noti) 341 | $ filter (\rule -> rule `matches` noti) modificationRules 342 | where matches rule noti = 343 | Map.foldrWithKey (\k v matches -> matches && ((v == lookupFun k noti))) 344 | True (mMatch rule) 345 | lookupFun name noti = Text.unpack $ fromMaybe (Text.pack "") 346 | (let notiUrgencyStr = (\n -> case (notiUrgency n) of 347 | Low -> "low" 348 | Normal -> "normal" 349 | High -> "critical") 350 | in ((lookup name 351 | [ ("title", notiSummary) 352 | , ("body", notiBody) 353 | , ("app-name", notiAppName) 354 | , ("urgency", notiUrgencyStr) 355 | , ("time", notiTime) 356 | ]) <*> (Just noti))) 357 | replace ('\\':cs) = "\\\\" ++ replace cs 358 | replace (c:cs) = c : replace cs 359 | replace ([]) = [] 360 | modifies (Script m s) noti = do 361 | putStrLn $ show noti 362 | putStrLn $ Text.unpack $ notiBody noti 363 | putStrLn $ unpack $ decodeUtf8 $ toStrict $ Aeson.encode noti 364 | returnText <- readCreateProcess (shell s) 365 | $ replace (unpack $ decodeUtf8 $ toStrict $ Aeson.encode noti) ++ "\n" 366 | newModifier <- Yaml.decodeThrow $ encodeUtf8 $ pack returnText 367 | modifies newModifier noti 368 | modifies modify noti = do 369 | let newnoti = noti 370 | { notiSummary = fromMaybe (notiSummary noti) 371 | $ pack <$> modifyTitle modify 372 | , notiBody = fromMaybe (notiBody noti) 373 | $ pack <$> modifyBody modify 374 | , notiAppName = fromMaybe (notiAppName noti) 375 | $ pack <$> modifyAppname modify 376 | , notiIcon = fromMaybe (notiIcon noti) 377 | $ parseImageString <$> pack <$> modifyAppicon modify 378 | , notiTimeout = fromMaybe (notiTimeout noti) 379 | $ modifyTimeout modify 380 | , notiRight = fromMaybe (notiRight noti) 381 | $ Just <$> modifyRight modify 382 | , notiTop = fromMaybe (notiTop noti) 383 | $ Just <$> modifyTop modify 384 | , notiImg = fromMaybe (notiImg noti) 385 | $ parseImageString <$> pack <$> modifyImage modify 386 | , notiImgSize = fromMaybe (notiImgSize noti) 387 | $ modifyImageSize modify 388 | , notiTransient = fromMaybe (notiTransient noti) 389 | $ modifyTransient modify 390 | , notiSendClosedMsg = fromMaybe (notiSendClosedMsg noti) 391 | $ modifyNoClosedMsg modify 392 | , notiActions = (pack <$> (fromMaybe [] $ modifyActions modify)) 393 | ++ (maybe (notiActions noti) 394 | (\_ -> []) $ modifyRemoveActions modify) 395 | , notiActionIcons = fromMaybe (notiActionIcons noti) 396 | $ modifyActionIcons modify 397 | , notiActionCommands = fromMaybe (notiActionCommands noti) 398 | $ Map.assocs <$> modifyActionCommands modify 399 | , notiClassName = fromMaybe (notiClassName noti) 400 | $ pack <$> modifyClassName modify } 401 | return newnoti 402 | 403 | replaceNoti:: Notification -> TVar NotifyState -> IO () 404 | replaceNoti newNoti tState = do 405 | addSource $ do 406 | atomically $ modifyTVar tState $ \state -> 407 | state { notiDisplayingList = map 408 | (\n -> if _dNotiId n /= repId then n 409 | else n { _dNotiId = notiId newNoti } ) 410 | $ notiDisplayingList state} 411 | state <- readTVarIO tState 412 | let notis = filter (\n -> _dNotiId n == notiId newNoti) 413 | $ notiDisplayingList state 414 | mapM (updateNoti (notiConfig state) 415 | (removeNotiFromDistList tState $ notiId newNoti) 416 | newNoti) notis 417 | notiStOnUpdate state 418 | return () 419 | where repId = fromIntegral (notiRepId newNoti) 420 | 421 | insertNewNoti :: Notification -> TVar NotifyState -> IO () 422 | insertNewNoti newNoti tState = do 423 | addSource $ do 424 | state <- readTVarIO tState 425 | -- new noti-window 426 | when ((fromIntegral $ notiTimeout newNoti) /= 1) $ do 427 | dnoti <- showNotificationWindow 428 | (notiConfig state) 429 | newNoti 430 | (notiDisplayingList state) 431 | (removeNotiFromDistList tState $ notiId newNoti) 432 | atomically $ modifyTVar' tState $ \state -> 433 | state { notiDisplayingList = dnoti : notiDisplayingList state } 434 | return () 435 | -- trigger update in noti-center 436 | notiStOnUpdate state 437 | return () 438 | 439 | removeNotiFromDistList' :: TVar NotifyState -> Word32 -> IO () 440 | removeNotiFromDistList' tState id = 441 | removeNotiFromDistList tState $ fromIntegral id 442 | 443 | removeNotiFromDistList :: TVar NotifyState -> Int -> IO () 444 | removeNotiFromDistList tState id = do 445 | state <- readTVarIO tState 446 | atomically $ modifyTVar' tState $ \state -> 447 | state { notiDisplayingList = 448 | filter (\n -> (_dNotiId n) /= id) 449 | (notiDisplayingList state)} 450 | addSource $ do 451 | let mDn = find (\n -> _dNotiId n == id) $ notiDisplayingList state 452 | maybe (return ()) _dNotiDestroy mDn 453 | return () 454 | 455 | hideAllNotis tState = do 456 | state <- readTVarIO tState 457 | mapM (removeNotiFromDistList tState . _dNotiId) 458 | $ notiDisplayingList state 459 | return () 460 | 461 | closeNotification tState id = do 462 | state <- readTVarIO tState 463 | let notis = filter (\n -> (notiId n) /= fromIntegral id) (notiStList state) 464 | sequence $ (\noti -> addSource $ notiOnClosed noti $ CloseByCall ) <$> notis 465 | removeNotiFromDistList' tState id 466 | 467 | 468 | notificationDaemon :: (AutoMethod f1, AutoMethod f2) => 469 | Config -> ((Signal -> IO ()) -> f1) -> f2 -> IO () 470 | notificationDaemon config onNote onCloseNote = do 471 | putStrLn "notificationDaemon started" 472 | client <- connectSession 473 | _ <- requestName client "org.freedesktop.Notifications" 474 | [nameAllowReplacement, nameReplaceExisting] 475 | export client "/org/freedesktop/Notifications" defaultInterface 476 | { interfaceName = "org.freedesktop.Notifications" 477 | , interfaceMethods = 478 | [ autoMethod "GetServerInformation" getServerInformation 479 | , autoMethod "GetCapabilities" (getCapabilities config) 480 | , autoMethod "CloseNotification" onCloseNote 481 | , autoMethod "Notify" (onNote (emit client)) 482 | ] 483 | } 484 | 485 | startNotificationDaemon :: Config -> IO () -> IO () -> IO (TVar NotifyState) 486 | startNotificationDaemon config onUpdate onUpdateForMe = do 487 | istate <- newTVarIO $ NotifyState [] [] 1 onUpdate onUpdateForMe config [] 488 | forkIO (notificationDaemon config (notify config istate) 489 | (closeNotification istate)) 490 | return istate 491 | -------------------------------------------------------------------------------- /src/NotificationCenter/Notifications/AbstractNotification.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE MonoLocalBinds #-} 2 | {-# LANGUAGE TemplateHaskell #-} 3 | {-# LANGUAGE OverloadedStrings #-} 4 | {-# LANGUAGE OverloadedLabels #-} 5 | 6 | module NotificationCenter.Notifications.AbstractNotification 7 | ( createNotification 8 | , setUrgencyLevel 9 | , updateNotiContent 10 | , HasDisplayingNotificationContent(..) 11 | , DisplayingNotificationContent(..) 12 | ) where 13 | 14 | import Helpers (atMay, markupify) 15 | import Config (Config(..)) 16 | import NotificationCenter.Notifications.Data 17 | (Urgency(..), CloseType(..), Notification(..), Image(..), rawImgToPixBuf) 18 | import NotificationCenter.Notifications.Action 19 | (Action(..), createAction) 20 | import TransparentWindow (scale, label, image, box, getObjs, addClass, progressbar) 21 | 22 | import Data.Text as Text 23 | import Data.Int ( Int32 ) 24 | import Data.Maybe (fromMaybe) 25 | 26 | import Control.Lens.TH (makeClassy) 27 | import Control.Lens (view, set) 28 | import Control.Monad (when) 29 | 30 | import GI.Gtk (rangeGetValue, onRangeValueChanged, rangeSetValue, widgetShowAll, widgetHide, windowMove, widgetDestroy 31 | , widgetSetValign, widgetSetMarginStart, widgetSetMarginEnd 32 | , widgetSetMarginTop, widgetSetMarginBottom, labelSetText 33 | , labelSetMarkup, widgetSetSizeRequest, labelSetXalign 34 | , widgetGetPreferredHeightForWidth, onWidgetButtonPressEvent 35 | , imageSetFromPixbuf, imageSetFromIconName, setWidgetWidthRequest 36 | , setImagePixelSize, widgetSetMarginStart, widgetSetMarginEnd 37 | , progressBarSetFraction, widgetSetVisible 38 | , catchGErrorJustDomain, GErrorMessage(..)) 39 | import GI.GLib (FileError(..)) 40 | import GI.GdkPixbuf (pixbufScaleSimple, pixbufGetHeight, pixbufGetWidth 41 | , Pixbuf(..), pixbufNewFromFileAtScale 42 | , InterpType(..), PixbufError(..)) 43 | import qualified GI.Gtk as Gtk 44 | (Scale, ProgressBar, IsWidget, Box(..), Label(..), Button(..), Window(..), Image(..) 45 | , Builder(..), containerAdd, containerRemove, containerGetChildren) 46 | import GI.Gtk.Enums (Align(..)) 47 | 48 | data DisplayingNotificationContent = DisplayingNotificationContent 49 | { _dLabelTitel :: Gtk.Label 50 | , _dLabelBody :: Gtk.Label 51 | , _dLabelAppname :: Gtk.Label 52 | , _dImgAppIcon :: Gtk.Image 53 | , _dImgImage :: Gtk.Image 54 | , _dContainer :: Gtk.Box 55 | , _dActions :: Gtk.Box 56 | , _dProgressbar :: Gtk.ProgressBar 57 | , _dScale :: Gtk.Scale 58 | } 59 | makeClassy ''DisplayingNotificationContent 60 | 61 | 62 | 63 | createNotification :: HasDisplayingNotificationContent dn => 64 | Config -> Gtk.Builder -> Notification -> dn -> IO dn 65 | createNotification config builder noti dispNoti = do 66 | 67 | objs <- getObjs builder [ "label_titel" 68 | , "label_body" 69 | , "label_appname" 70 | , "img_icon" 71 | , "img_img" 72 | , "box_container" 73 | , "box_actions" 74 | , "progressbar" 75 | , "scale"] 76 | 77 | labelTitel <- label objs "label_titel" 78 | labelBody <- label objs "label_body" 79 | labelAppname <- label objs "label_appname" 80 | container <- box objs "box_container" 81 | actions <- box objs "box_actions" 82 | imgAppIcon <- image objs "img_icon" 83 | imgImage <- image objs "img_img" 84 | progressBar <- progressbar objs "progressbar" 85 | scale <- scale objs "scale" 86 | 87 | -- set margins from config 88 | widgetSetMarginTop imgImage 89 | (fromIntegral $ configImgMarginTop config) 90 | widgetSetMarginBottom imgImage 91 | (fromIntegral $ configImgMarginBottom config) 92 | widgetSetMarginStart imgImage 93 | (fromIntegral $ configImgMarginLeft config) 94 | widgetSetMarginEnd imgImage 95 | (fromIntegral $ configImgMarginRight config) 96 | 97 | onWidgetButtonPressEvent container $ \(_) -> do 98 | notiOnAction noti (notiActionCommands noti) "default" Nothing 99 | return False 100 | 101 | 102 | return 103 | $ set dLabelTitel labelTitel 104 | $ set dLabelBody labelBody 105 | $ set dLabelAppname labelAppname 106 | $ set dImgAppIcon imgAppIcon 107 | $ set dImgImage imgImage 108 | $ set dContainer container 109 | $ set dActions actions 110 | $ set dProgressbar progressBar 111 | $ set dScale scale 112 | dispNoti 113 | 114 | setUrgencyLevel :: Gtk.IsWidget widget => Urgency -> [widget] -> IO () 115 | setUrgencyLevel urgency elems = do 116 | case urgency of 117 | High -> do 118 | sequence $ (flip addClass) "critical" <$> elems 119 | Low -> do 120 | sequence $ (flip addClass) "low" <$> elems 121 | Normal -> do 122 | sequence $ (flip addClass) "normal" <$> elems 123 | return () 124 | 125 | 126 | updateNotiContent :: HasDisplayingNotificationContent dn 127 | => Config -> Notification -> dn -> IO () 128 | updateNotiContent config noti dNoti = do 129 | labelSetText (view dLabelTitel dNoti) $ notiSummary noti 130 | 131 | if configNotiMarkup config 132 | then labelSetMarkup (view dLabelBody dNoti) . markupify $ notiBody noti 133 | else labelSetText (view dLabelBody dNoti) $ notiBody noti 134 | 135 | when (configPopupHideBodyIfEmpty config && notiBody noti == "") $ do 136 | widgetSetVisible (view dLabelBody dNoti) False 137 | 138 | if notiAppName noti /= "" then 139 | labelSetText (view dLabelAppname dNoti) $ notiAppName noti 140 | else 141 | widgetSetVisible (view dLabelAppname dNoti) False 142 | labelSetXalign (view dLabelTitel dNoti) 0 143 | labelSetXalign (view dLabelBody dNoti) 0 144 | 145 | if notiIcon noti /= NoImage then 146 | setImage (notiIcon noti) (fromIntegral $ configIconSize config) 147 | $ view dImgAppIcon dNoti 148 | else 149 | widgetSetVisible (view dImgAppIcon dNoti) False 150 | 151 | setImage (notiImg noti) (fromIntegral $ notiImgSize noti) 152 | $ view dImgImage dNoti 153 | 154 | let takeTwo (a:b:cs) = (a,b):(takeTwo cs) 155 | takeTwo _ = [] 156 | actionButtons <- sequence 157 | $ (\(a, b) -> createAction 158 | config 159 | (notiActionCommands noti) 160 | (notiActionIcons noti) 161 | (notiOnAction noti) 162 | 20 163 | 20 164 | a 165 | b) 166 | <$> (Prelude.filter (\(a, b) -> a /= "default" 167 | && (notiPercentage noti == Nothing 168 | || a /= "changeValue")) 169 | $ takeTwo (unpack <$> notiActions noti)) 170 | currentButtons <- Gtk.containerGetChildren (view dActions dNoti) 171 | sequence $ Gtk.containerRemove (view dActions dNoti) <$> currentButtons 172 | sequence $ Gtk.containerAdd (view dActions dNoti) <$> actionButton <$> actionButtons 173 | 174 | addClass (view dContainer dNoti) (notiClassName noti) 175 | 176 | widgetShowAll (view dActions dNoti) 177 | 178 | 179 | if (notiPercentage noti /= Nothing) then do 180 | if (onChangeAction == Nothing) then do 181 | progressBarSetFraction (view dProgressbar dNoti) 182 | ((fromMaybe 0 $ notiPercentage noti) / 100.0) 183 | widgetSetVisible (view dProgressbar dNoti) True 184 | widgetSetVisible (view dScale dNoti) False 185 | return () 186 | else do 187 | rangeSetValue (view dScale dNoti) 188 | (fromMaybe 0 $ notiPercentage noti) 189 | onRangeValueChanged (view dScale dNoti) $ do 190 | value <- rangeGetValue (view dScale dNoti) 191 | (notiOnAction noti) (notiActionCommands noti) "changeValue" $ Just $ show value 192 | return () 193 | widgetSetVisible (view dScale dNoti) True 194 | widgetSetVisible (view dProgressbar dNoti) False 195 | return () 196 | else do 197 | widgetSetVisible (view dProgressbar dNoti) False 198 | widgetSetVisible (view dScale dNoti) False 199 | return () 200 | where onChangeAction = atMay 201 | (Prelude.filter (\(a, b) -> a == "changeValue") 202 | $ takeTwo (unpack <$> notiActions noti)) 0 203 | takeTwo (a:b:cs) = (a,b):(takeTwo cs) 204 | takeTwo _ = [] 205 | 206 | 207 | 208 | setImage :: Image -> Int32 -> Gtk.Image -> IO () 209 | setImage image imageSize widget = do 210 | case image of 211 | NoImage -> do 212 | widgetSetMarginStart widget 0 213 | widgetSetMarginEnd widget 0 214 | widgetSetVisible widget False 215 | (ImagePath path) -> do 216 | pb <- catchGErrorJustDomain 217 | (catchGErrorJustDomain 218 | (pixbufNewFromFileAtScale path imageSize imageSize True) 219 | ((\err message -> return Nothing) 220 | :: PixbufError -> GErrorMessage -> IO (Maybe Pixbuf))) 221 | ((\err message -> return Nothing) 222 | :: FileError -> GErrorMessage -> IO (Maybe Pixbuf)) 223 | case pb of 224 | (Just pb') -> imageSetFromPixbuf widget (Just pb') 225 | Nothing -> return () 226 | (NamedIcon name) -> do 227 | imageSetFromIconName widget 228 | (Just $ pack name) imageSize 229 | setImagePixelSize widget imageSize 230 | (RawImg a) -> do 231 | pb <- rawImgToPixBuf $ RawImg a 232 | pb' <- scalePixbuf imageSize imageSize pb 233 | imageSetFromPixbuf widget pb' 234 | 235 | 236 | scalePixbuf :: Int32 -> Int32 -> Pixbuf -> IO (Maybe Pixbuf) 237 | scalePixbuf w h pb = do 238 | oldW <- fromIntegral <$> pixbufGetWidth pb :: IO Double 239 | oldH <- fromIntegral <$> pixbufGetHeight pb :: IO Double 240 | let targetW = fromIntegral w :: Double 241 | targetH = fromIntegral h :: Double 242 | if (oldW < targetW || oldH < targetH) then 243 | return (Just pb) 244 | else do 245 | let newW = fromIntegral $ floor $ if oldW > oldH then 246 | targetW else (oldW * (targetH / oldH)) 247 | newH = fromIntegral $ floor $ if oldW > oldH then 248 | (oldH * (targetW / oldW)) else targetH 249 | pixbufScaleSimple pb newW newH InterpTypeBilinear 250 | -------------------------------------------------------------------------------- /src/NotificationCenter/Notifications/Action.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | 3 | module NotificationCenter.Notifications.Action 4 | ( createAction 5 | , Action(..) 6 | ) where 7 | 8 | import Config (Config(..)) 9 | import TransparentWindow 10 | 11 | import GI.Gtk 12 | (widgetSetHalign, widgetSetHexpand, buttonNew, setWidgetMargin 13 | , buttonSetRelief, widgetSetSizeRequest, widgetShowAll, widgetShow 14 | , widgetHide, onWidgetDestroy 15 | , windowSetDefaultSize, setWindowTitle, boxPackStart, boxNew 16 | , setWindowWindowPosition, WindowPosition(..), windowMove 17 | , frameSetShadowType, aspectFrameNew 18 | , widgetGetAllocatedHeight, widgetGetAllocatedWidth, onWidgetDraw 19 | , onWidgetLeaveNotifyEvent, onWidgetMotionNotifyEvent 20 | , widgetAddEvents, alignmentSetPadding, alignmentNew, rangeSetValue 21 | , scaleSetDigits, scaleSetValuePos, rangeGetValue 22 | , afterScaleButtonValueChanged, scaleNewWithRange, containerAdd 23 | , buttonBoxNew, mainQuit, onButtonActivate 24 | , toggleButtonGetActive, onToggleButtonToggled, buttonSetUseStock 25 | , toggleButtonNewWithLabel, onButtonClicked 26 | , buttonNewWithLabel, widgetQueueDraw, drawingAreaNew 27 | , windowNew, widgetDestroy, dialogRun, setAboutDialogComments 28 | , setAboutDialogAuthors, setAboutDialogVersion 29 | , setAboutDialogProgramName, aboutDialogNew, labelNew, get 30 | , afterWindowSetFocus, labelSetText 31 | , onWidgetFocusOutEvent, onWidgetKeyReleaseEvent, widgetGetParentWindow 32 | , onButtonClicked, windowGetScreen, boxNew, widgetSetValign 33 | , imageNewFromIconName) 34 | import GI.Gtk.Enums 35 | (Orientation(..), PositionType(..), ReliefStyle(..), Align(..), IconSize(..)) 36 | 37 | import qualified GI.Gtk as Gtk (containerAdd, Box(..), Label(..), Button(..)) 38 | import qualified Data.Text as Text 39 | 40 | data Action = Action 41 | { actionButton :: Gtk.Button 42 | -- ^ Button Element for displaying 43 | , actionCommand :: String 44 | -- ^ Shell command to execute 45 | } 46 | 47 | createAction :: Config -> [(String, String)] -> Bool 48 | -> ([(String, String)] -> String -> Maybe String 49 | -> IO ()) -> Int -> Int -> String -> String -> IO Action 50 | createAction config actionCommands useIcons onAction width height command description = do 51 | button <- buttonNew 52 | widgetSetSizeRequest button (fromIntegral width) (fromIntegral height) 53 | addClass button "actionbutton" 54 | addClass button "deadd-noti-center" 55 | buttonSetRelief button ReliefStyleNone 56 | setWidgetMargin button $ fromIntegral $ configButtonMargin config 57 | -- widgetSetHalign label AlignStart 58 | -- widgetSetValign label AlignEnd 59 | 60 | let theButton = Action 61 | { actionButton = button 62 | , actionCommand = command } 63 | onButtonClicked button $ do 64 | onAction actionCommands command Nothing 65 | return () 66 | if useIcons && configActionIcons config then do 67 | img <-imageNewFromIconName (Just $ Text.pack description) (fromIntegral $ fromEnum IconSizeButton) 68 | Gtk.containerAdd button img 69 | else do 70 | label <- labelNew $ Just $ Text.pack description 71 | addClass label "actionbuttonlabel" 72 | addClass label "deadd-noti-center" 73 | Gtk.containerAdd button label 74 | return theButton 75 | -------------------------------------------------------------------------------- /src/NotificationCenter/Notifications/Data.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | 3 | module NotificationCenter.Notifications.Data 4 | ( Urgency(..) 5 | , CloseType (..) 6 | , Notification(..) 7 | , Image(..) 8 | , parseImageString 9 | , rawImgToPixBuf 10 | ) where 11 | 12 | import qualified Data.Text as Text 13 | import Data.Word ( Word32, Word8 ) 14 | import Data.Int ( Int32, Int ) 15 | import qualified Data.ByteString as BS 16 | import Foreign.Marshal.Array (newArray) 17 | import Foreign.C.Types (CUChar(..)) 18 | import Foreign.Ptr (Ptr) 19 | import qualified Data.Map as Map ( Map ) 20 | import Data.List ( sortOn ) 21 | import DBus ( Variant (..), Signal ) 22 | 23 | import qualified Data.Yaml as Y 24 | import Data.Yaml as Y ((.=)) 25 | 26 | import GI.GdkPixbuf (Pixbuf(..), pixbufNewFromData, Colorspace(..)) 27 | 28 | data Urgency = Normal | Low | High deriving Eq 29 | data CloseType = Timeout | User | CloseByCall | Other deriving Eq 30 | 31 | instance Eq Notification where 32 | a == b = notiId a == notiId b 33 | 34 | data Notification = Notification 35 | { notiAppName :: Text.Text -- ^ Application name 36 | , notiRepId :: Word32 -- ^ Replaces id 37 | , notiId :: Int -- ^ Id 38 | , notiIcon :: Image -- ^ App icon 39 | , notiImg :: Image -- ^ Image 40 | , notiImgSize :: Int -- ^ Image size 41 | , notiSummary :: Text.Text -- ^ Summary 42 | , notiBody :: Text.Text -- ^ Body 43 | , notiActions :: [Text.Text] -- ^ Actions 44 | , notiActionCommands :: [(String, String)] -- ^ Actions 45 | , notiActionIcons :: Bool -- ^ Use icons for action-buttons 46 | , notiHints :: Map.Map Text.Text Variant -- ^ Hints 47 | , notiUrgency :: Urgency 48 | , notiTimeout :: Int32 -- ^ Expires timeout (milliseconds) 49 | , notiTime :: Text.Text 50 | , notiTransient :: Bool 51 | , notiSendClosedMsg :: Bool -- ^ If notiOnClosed should be ignored 52 | , notiOnClosed :: CloseType -> IO () 53 | , notiTop :: Maybe Int 54 | , notiRight :: Maybe Int 55 | -- ^ Should be called when the notification is closed, either by 56 | -- timeout or by user 57 | , notiOnAction :: [(String, String)] -> String -> Maybe String -> IO () 58 | -- ^ Should be called when an action is used 59 | , notiPercentage :: Maybe Double 60 | -- ^ The percentage that should be shown in a percentage bar 61 | , notiClassName :: Text.Text 62 | -- ^ Class name to be attached to the window of the notification 63 | } 64 | 65 | instance Y.ToJSON Notification where 66 | toJSON n = Y.object [ 67 | "appname" .= notiAppName n 68 | , "repId" .= notiRepId n 69 | , "id" .= notiId n 70 | , "icon" .= ((show $ notiIcon n) :: String) 71 | , "image" .= ((show $ notiImg n) :: String) 72 | , "imageSize" .= notiImgSize n 73 | , "title" .= notiSummary n 74 | , "body" .= notiBody n 75 | , "actions" .= notiActions n 76 | , "actionIcons" .= notiActionIcons n 77 | -- , "notiHints" .= notiHints n 78 | -- , "notiUrgency" .= ((show $ notiUrgency n) :: String) 79 | , "timeout" .= notiTimeout n 80 | , "time" .= notiTime n 81 | , "transient" .= notiTransient n 82 | , "sendClosedMsg" .= notiSendClosedMsg n 83 | , "top" .= notiTop n 84 | , "right" .= notiRight n 85 | , "percentage" .= notiPercentage n ] 86 | 87 | instance Show Notification where 88 | show n = foldl (++) "" 89 | [ "Notification { \n" 90 | , " notiAppName = " ++ (Text.unpack $ notiAppName n) ++ ", \n" 91 | , " notiRepId = " ++ (show $ notiRepId n) ++ ", \n" 92 | , " notiId = " ++ (show $ notiId n) ++ ", \n" 93 | , " notiIcon = " ++ (show $ notiIcon n) ++ ", \n" 94 | , " notiImg = " ++ (show $ notiImg n) ++ ", \n" 95 | , " notiImgSize = " ++ (show $ notiImgSize n) ++ ", \n" 96 | , " notiSummary = " ++ (Text.unpack $ notiSummary n) ++ ", \n" 97 | , " notiBody = " ++ (Text.unpack $ notiBody n) ++ ", \n" 98 | , " notiActions = " ++ (show $ Text.unpack <$> notiActions n) ++ ", \n" 99 | , " notiActionIcons = " ++ (show $ notiActionIcons n) ++ ", \n" 100 | , " notiHints = " ++ (show $ notiHints n) ++ ", \n" 101 | , " notiTimeout = " ++ (show $ notiTimeout n) ++ ", \n" 102 | , " notiTime = " ++ (Text.unpack $ notiTime n) ++ ", \n" 103 | , " notiTransient = " ++ (show $ notiTransient n) ++ ", \n" 104 | , " notiSendClosedMsg = " ++ (show $ notiSendClosedMsg n) ++ "\n" 105 | , " notiTop = " ++ (show $ notiTop n) ++ "\n" 106 | , " notiRight = " ++ (show $ notiRight n) ++ "\n" 107 | , " notiPercentage = " ++ (show $ notiPercentage n) ++ "\n" 108 | , " }\n" ] 109 | 110 | data Image = RawImg 111 | ( Int32 -- width 112 | , Int32 -- height 113 | , Int32 -- rowstride 114 | , Bool -- alpha 115 | , Int32 -- bits per sample 116 | , Int32 -- channels 117 | , BS.ByteString -- image data 118 | ) 119 | | ImagePath String 120 | | NamedIcon String 121 | | NoImage deriving (Show, Eq) 122 | 123 | parseImageString :: Text.Text -> Image 124 | parseImageString a = if (Text.isPrefixOf "file://" a) then 125 | ImagePath $ Text.unpack$ Text.drop 6 a 126 | else 127 | if (Text.length a > 0) then 128 | NamedIcon $ Text.unpack a 129 | else 130 | NoImage 131 | 132 | 133 | rawImgToPixBuf :: Image -> IO Pixbuf 134 | rawImgToPixBuf (RawImg (width, height, rowstride, alpha, bits, channels, datas)) 135 | = do datas' <- newArray $ BS.unpack datas 136 | pixbufNewFromData datas' 137 | ColorspaceRgb alpha bits width height rowstride Nothing 138 | -------------------------------------------------------------------------------- /src/NotificationCenter/Notifications/Notification/Glade.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE QuasiQuotes, OverloadedStrings #-} 2 | 3 | module NotificationCenter.Notifications.Notification.Glade where 4 | 5 | import Data.String.Here.Uninterpolated (hereFile) 6 | 7 | glade = 8 | [hereFile|notification.glade|] 9 | -------------------------------------------------------------------------------- /src/NotificationCenter/Notifications/NotificationPopup.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE TemplateHaskell #-} 2 | {-# LANGUAGE MultiWayIf #-} 3 | {-# LANGUAGE OverloadedStrings #-} 4 | {-# LANGUAGE OverloadedLabels #-} 5 | 6 | module NotificationCenter.Notifications.NotificationPopup 7 | ( showNotificationWindow 8 | , updateNoti 9 | , DisplayingNotificationPopup(..) 10 | ) where 11 | 12 | import TransparentWindow (getMouseActiveScreenPos 13 | , addSource 14 | , runAfterDelay 15 | , getScreenPos 16 | , label 17 | , window 18 | , addClass 19 | , createTransparentWindow) 20 | 21 | import Config (Config(..)) 22 | import NotificationCenter.Notifications.AbstractNotification 23 | (DisplayingNotificationContent(..), HasDisplayingNotificationContent(..) 24 | , createNotification, updateNotiContent, setUrgencyLevel) 25 | import NotificationCenter.Notifications.Data (CloseType(..), Notification(..)) 26 | import NotificationCenter.Notifications.Notification.Glade (glade) 27 | 28 | import Control.Lens.TH (makeClassy) 29 | import Control.Lens (view, set) 30 | 31 | import Data.Text as Text hiding (elem) 32 | import Data.Word ( Word32 ) 33 | import Data.Int ( Int32 ) 34 | import qualified Data.Map as Map ( Map ) 35 | import Data.List ( sortOn, filter ) 36 | import Data.Maybe ( fromMaybe, isJust ) 37 | 38 | import Control.Monad 39 | import DBus ( Variant (..) ) 40 | 41 | import GI.Gdk (getEventButtonButton) 42 | import GI.Gtk (widgetShow, widgetGetPreferredHeightForWidth, widgetSetSizeRequest 43 | , widgetShowAll, onWidgetButtonPressEvent, windowMove 44 | , setWidgetWidthRequest, widgetDestroy , labelGetText 45 | , labelSetText, labelGetLayout) 46 | import GI.Pango.Enums (EllipsizeMode(..)) 47 | import GI.Pango.Objects.Layout (layoutGetLinesReadonly, layoutGetLineCount) 48 | import GI.Pango.Structs.LayoutLine (getLayoutLineLength, getLayoutLineStartIndex) 49 | import qualified GI.Gtk as Gtk (Window(..), Label(..)) 50 | 51 | 52 | instance Eq DisplayingNotificationPopup where 53 | a == b = _dNotiId a == _dNotiId b 54 | 55 | 56 | data DisplayingNotificationPopup = DisplayingNotificationPopup 57 | { _dpopupContent :: DisplayingNotificationContent 58 | , _dNotiGetHeight :: IO Int32 59 | , _dNotiTop :: Int32 60 | , _dNotiId :: Int 61 | , _dNotiDestroy :: IO () 62 | , _dMainWindow :: Gtk.Window 63 | , _dLabelBG :: Gtk.Label 64 | , _dHasCustomPosition :: Bool 65 | } 66 | makeClassy ''DisplayingNotificationPopup 67 | instance HasDisplayingNotificationContent DisplayingNotificationPopup where 68 | displayingNotificationContent = dpopupContent 69 | 70 | 71 | showNotificationWindow :: Config -> Notification 72 | -> [DisplayingNotificationPopup] -> (IO ()) -> IO DisplayingNotificationPopup 73 | showNotificationWindow config noti dispNotis onClose = do 74 | 75 | let distanceTopFromConfig = configDistanceTop config 76 | distanceTop = fromIntegral $ fromMaybe distanceTopFromConfig (notiTop noti) 77 | distanceBetween = configDistanceBetween config 78 | distanceRight = fromMaybe (configDistanceRight config) (notiRight noti) 79 | hasCustomPosition = (isJust $ notiTop noti) || (isJust $ notiRight noti) 80 | 81 | (objs, builder) <- createTransparentWindow (Text.pack glade) 82 | [ "main_window" 83 | , "label_background" ] 84 | Nothing 85 | 86 | mainWindow <- window objs "main_window" 87 | labelBG <- label objs "label_background" 88 | 89 | dispNotiWithoutHeight <- createNotification config builder noti 90 | $ DisplayingNotificationPopup 91 | { _dMainWindow = mainWindow 92 | , _dLabelBG = labelBG 93 | , _dNotiId = notiId noti 94 | , _dNotiDestroy = widgetDestroy mainWindow 95 | , _dHasCustomPosition = hasCustomPosition 96 | , _dpopupContent = DisplayingNotificationContent {} } 97 | 98 | let dispNoti = set dNotiGetHeight 99 | (getHeight (view dContainer dispNotiWithoutHeight) config) 100 | dispNotiWithoutHeight 101 | lblBody = (flip view) dispNoti $ dLabelBody 102 | 103 | setWidgetWidthRequest mainWindow $ fromIntegral $ configWidthNoti config 104 | 105 | setUrgencyLevel (notiUrgency noti) [mainWindow] 106 | setUrgencyLevel (notiUrgency noti) 107 | $ (flip view) dispNoti <$> [dLabelTitel, dLabelBody, dLabelAppname] 108 | 109 | height <- updateNoti' config onClose noti dispNoti 110 | 111 | -- Ellipsization of Body 112 | numLines <- fromIntegral <$> (layoutGetLineCount =<< labelGetLayout lblBody) 113 | let maxLines = (configPopupMaxLinesInBody config) 114 | ellipsizeBody = configPopupEllipsizeBody config 115 | height <- 116 | if numLines > maxLines && ellipsizeBody then do 117 | lines <- layoutGetLinesReadonly =<< labelGetLayout lblBody 118 | let lastLine = lines !! (maxLines - 1) 119 | len <- fromIntegral <$> getLayoutLineLength lastLine 120 | startOffset <- fromIntegral <$> getLayoutLineStartIndex lastLine 121 | bodyText <- labelGetText lblBody 122 | let lenOfTruncatedBody = len - 4 + startOffset + (maxLines - 1) 123 | let truncatedBody = Text.take lenOfTruncatedBody $ bodyText 124 | ellipsizedBody = Text.append truncatedBody "..." 125 | labelSetText lblBody ellipsizedBody 126 | -- re-request height to reflect ellipsized body 127 | height' <- getHeight (view dContainer dispNoti) config 128 | widgetSetSizeRequest (_dLabelBG dispNoti) (-1) height' 129 | return height' 130 | else 131 | return height 132 | 133 | (screenW, screenY, screenH) <- if configNotiFollowMouse config then 134 | getMouseActiveScreenPos mainWindow 135 | (fromIntegral $ configNotiMonitor config) 136 | else 137 | getScreenPos mainWindow 138 | (fromIntegral $ configNotiMonitor config) 139 | 140 | hBefores <- sortOn fst <$> mapM 141 | (\n -> (,) (_dNotiTop n) <$> (_dNotiGetHeight n)) (Data.List.filter (not . _dHasCustomPosition) dispNotis) 142 | let hBefore = if hasCustomPosition then 143 | distanceTop 144 | else 145 | findBefore hBefores (distanceTop + screenY) 146 | height (fromIntegral distanceBetween) 147 | 148 | windowMove mainWindow 149 | (screenW - fromIntegral 150 | (configWidthNoti config + distanceRight)) 151 | hBefore 152 | 153 | onWidgetButtonPressEvent mainWindow $ \eventButton -> do 154 | mouseButton <- (\n -> "mouse" ++ n) . show <$> getEventButtonButton eventButton 155 | let validMouseButtons = ["mouse1", "mouse2", "mouse3", "mouse4", "mouse5"] 156 | validInput = mouseButton `elem` validMouseButtons 157 | validDismiss = configPopupDismissButton config `elem` validMouseButtons 158 | validDefaultAction = configPopupDefaultActionButton config `elem` validMouseButtons 159 | valid = validInput && validDismiss && validDefaultAction 160 | dismiss = configPopupDismissButton config == mouseButton 161 | defaultAction = configPopupDefaultActionButton config == mouseButton 162 | if | valid && dismiss -> do 163 | notiOnClosed noti $ User 164 | onClose 165 | | valid && defaultAction -> do 166 | notiOnAction noti (notiActionCommands noti) "default" Nothing 167 | notiOnClosed noti $ User 168 | onClose 169 | | not validDismiss -> do 170 | putStrLn $ "Warning: Unknown mouse button '" ++ (show $ configPopupDismissButton config) ++ "'." 171 | notiOnClosed noti $ User 172 | onClose 173 | | not validDefaultAction -> do 174 | putStrLn $ "Warning: Unknown mouse button '" ++ (show $ configPopupDefaultActionButton config) ++ "'." 175 | notiOnClosed noti $ User 176 | onClose 177 | | otherwise -> do 178 | putStrLn $ "Warning: Popup received unknown mouse input '" ++ (show mouseButton) ++ "'." 179 | notiOnClosed noti $ User 180 | onClose 181 | return False 182 | 183 | widgetShow mainWindow 184 | 185 | return $ dispNoti { _dNotiTop = hBefore } 186 | 187 | updateNoti' :: Config -> (IO ()) -> Notification -> DisplayingNotificationPopup -> IO Int32 188 | updateNoti' config onClose noti dNoti = do 189 | updateNotiContent config noti dNoti 190 | addClass (view dMainWindow dNoti) (notiClassName noti) 191 | 192 | height <- getHeight (view dContainer dNoti) config 193 | widgetSetSizeRequest (_dLabelBG dNoti) (-1) height 194 | let notiDefaultTimeout = configNotiDefaultTimeout config 195 | startTimeoutThread notiDefaultTimeout 196 | (fromIntegral $ notiTimeout noti) ( 197 | do addSource $ notiOnClosed noti $ Timeout 198 | onClose) 199 | return height 200 | 201 | updateNoti config onClose noti dNoti = do 202 | addSource $ do 203 | updateNoti' config onClose noti dNoti 204 | return () 205 | return () 206 | 207 | getHeight widget config = do 208 | (a, b) <- widgetGetPreferredHeightForWidth widget 209 | $ fromIntegral $ configWidthNoti config 210 | return a 211 | 212 | startTimeoutThread notiDefaultTimeout timeout onClose = do 213 | when (timeout /= 0) $ do 214 | let timeout' = if timeout > 0 then timeout 215 | else notiDefaultTimeout 216 | runAfterDelay (1000 * timeout') $ do 217 | addSource $ onClose 218 | return () 219 | return () 220 | return () 221 | 222 | 223 | findBefore ((s, l):bs) p height distanceBetween = 224 | if ((p + height) <= (s - distanceBetween)) then 225 | p 226 | else 227 | findBefore bs (s + l + distanceBetween) height distanceBetween 228 | findBefore [] p _ _ = p 229 | -------------------------------------------------------------------------------- /src/TransparentWindow.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | {-# LANGUAGE OverloadedLabels #-} 3 | 4 | module TransparentWindow 5 | ( createTransparentWindow 6 | -- * Lookups 7 | , ObjDict(..) 8 | , gObjLookup 9 | , window 10 | , drawingArea 11 | , label 12 | , box 13 | , button 14 | , image 15 | , progressbar 16 | , adjustment 17 | , scale 18 | , getObjs 19 | , getScreenPos 20 | , getMouseActiveScreenPos 21 | -- * General 22 | , getScreenProportions 23 | , runAfterDelay 24 | , addSource 25 | , setStyle 26 | , addClass 27 | , removeClass 28 | -- * Colors 29 | ) where 30 | 31 | import Data.Int ( Int32 ) 32 | import Data.Word ( Word32 ) 33 | import Data.Maybe 34 | import Data.List (elem) 35 | import qualified Data.Text as Text 36 | import qualified Data.ByteString as BS 37 | import Control.Monad 38 | import Control.Monad.Trans.Reader (ReaderT(..)) 39 | import Control.Monad.IO.Class (MonadIO(..)) 40 | import Control.Concurrent (forkIO, threadDelay, ThreadId(..), killThread) 41 | 42 | import GI.Gtk 43 | (styleContextRemoveClass, widgetShowAll, widgetHide 44 | , onWidgetDestroy, windowSetDefaultSize 45 | , setWindowTitle, boxPackStart, boxNew, setWindowWindowPosition 46 | , WindowPosition(..), windowMove 47 | , frameSetShadowType, aspectFrameNew 48 | , widgetGetAllocatedHeight, widgetGetAllocatedWidth, onWidgetDraw 49 | , onWidgetLeaveNotifyEvent, onWidgetMotionNotifyEvent 50 | , widgetAddEvents, alignmentSetPadding, alignmentNew, rangeSetValue 51 | , scaleSetDigits, scaleSetValuePos, rangeGetValue 52 | , afterScaleButtonValueChanged, scaleNewWithRange, containerAdd 53 | , buttonBoxNew, mainQuit, onButtonActivate 54 | , toggleButtonGetActive, onToggleButtonToggled, buttonSetUseStock 55 | , toggleButtonNewWithLabel, onButtonClicked 56 | , buttonNewWithLabel, widgetQueueDraw, drawingAreaNew 57 | , windowNew, widgetDestroy, dialogRun, setAboutDialogComments 58 | , setAboutDialogAuthors, setAboutDialogVersion 59 | , setAboutDialogProgramName, aboutDialogNew, labelNew, get 60 | , afterWindowSetFocus, labelSetText 61 | , onWidgetFocusOutEvent, onWidgetKeyReleaseEvent, widgetGetParentWindow 62 | , onWidgetRealize, styleContextAddProviderForScreen 63 | , cssProviderLoadFromData, cssProviderNew, styleContextAddClass 64 | , widgetGetStyleContext, CssProvider(..)) 65 | import qualified GI.Gtk as Gtk 66 | (ProgressBar(..), Scale(..), DrawingArea(..), unsafeCastTo, Window(..), IsWidget(..) 67 | , builderGetObject, builderAddFromString 68 | , builderNew, Builder(..), Label(..), Box(..), Button(..), Image(..), Adjustment(..)) 69 | import GI.Gtk.Constants 70 | import GI.Gdk (getRectangleHeight, getRectangleWidth, getRectangleY 71 | , getRectangleX, Monitor, monitorGetGeometry, displayGetMonitor 72 | , screenGetDisplay, screenGetHeight, screenGetWidth, Screen (..) 73 | , displayGetPointer, displayGetDefault, displayGetDefaultSeat 74 | , deviceGetPosition, seatGetPointer, displayGetMonitorAtPoint) 75 | 76 | import GI.GObject.Objects (IsObject(..), Object(..)) 77 | 78 | import GI.GLib (idleSourceNew, sourceSetCallback, sourceAttach 79 | , sourceUnref, idleAdd, ) 80 | import GI.GLib.Constants 81 | import GI.Cairo () 82 | import Foreign.Ptr (castPtr) 83 | import qualified GHC.Int (Int32(..)) 84 | 85 | 86 | type ObjDict = [(Text.Text, GI.GObject.Objects.Object)] 87 | 88 | gObjLookup :: (GI.GObject.Objects.Object -> IO a) 89 | -> ObjDict -> Text.Text -> IO a 90 | gObjLookup f dict name = f $ fromJust $ lookup name dict 91 | 92 | --window :: [(Text.Text, GI.GObject.Objects.Object)] -> Text.Text -> IO Gtk.Window 93 | window = gObjLookup (Gtk.unsafeCastTo Gtk.Window) 94 | drawingArea = gObjLookup (Gtk.unsafeCastTo Gtk.DrawingArea) 95 | label = gObjLookup (Gtk.unsafeCastTo Gtk.Label) 96 | adjustment = gObjLookup (Gtk.unsafeCastTo Gtk.Adjustment) 97 | box = gObjLookup (Gtk.unsafeCastTo Gtk.Box) 98 | button = gObjLookup (Gtk.unsafeCastTo Gtk.Button) 99 | image = gObjLookup (Gtk.unsafeCastTo Gtk.Image) 100 | progressbar = gObjLookup (Gtk.unsafeCastTo Gtk.ProgressBar) 101 | scale= gObjLookup (Gtk.unsafeCastTo Gtk.Scale) 102 | 103 | 104 | getObjs :: Gtk.Builder -> [Text.Text] -> IO ObjDict 105 | getObjs builder dict = do 106 | mapM getObj dict 107 | where getObj name = do 108 | (Just obj) <- Gtk.builderGetObject builder name 109 | return (name, obj) 110 | 111 | getScreenProportions :: Gtk.Window -> IO (GHC.Int.Int32, GHC.Int.Int32) 112 | getScreenProportions window = do 113 | screen <- window `get` #screen 114 | h <- screenGetHeight screen 115 | w <- screenGetWidth screen 116 | return (h, w) 117 | 118 | createTransparentWindow :: Text.Text -- ^ Content of glade-file 119 | -> [Text.Text] -- ^ List of widgets that should be returned, listed by name 120 | -> Maybe Text.Text -- ^ Optional, title of the window 121 | -> IO (ObjDict, Gtk.Builder) 122 | createTransparentWindow glade objsToGet title = do 123 | builder <- Gtk.builderNew 124 | Gtk.builderAddFromString builder glade (-1) 125 | let objsToGet' = objsToGet ++ 126 | (filter (\name -> not $ elem name objsToGet) ["main_window"]) 127 | objs <- getObjs builder objsToGet' 128 | 129 | mainWindow <- window objs "main_window" 130 | 131 | screen <- mainWindow `get` #screen 132 | visual <- #getRgbaVisual screen 133 | #setVisual mainWindow visual 134 | 135 | when (title /= Nothing) $ let (Just title') = title in 136 | setWindowTitle mainWindow title' 137 | 138 | return (objs, builder) 139 | 140 | runAfterDelay :: Int -> IO () -> IO ThreadId 141 | runAfterDelay t f = forkIO (threadDelay t >> f) 142 | 143 | 144 | addSource :: IO () -> IO Word32 145 | addSource f = do 146 | idleAdd PRIORITY_DEFAULT $ do 147 | f 148 | return False 149 | 150 | 151 | setStyle :: Screen -> BS.ByteString -> IO () 152 | setStyle screen style = do 153 | provider <- cssProviderNew 154 | cssProviderLoadFromData provider style 155 | styleContextAddProviderForScreen screen provider 156 | $ fromIntegral STYLE_PROVIDER_PRIORITY_USER 157 | return () 158 | 159 | addClass :: Gtk.IsWidget a => a -> Text.Text -> IO () 160 | addClass w clazz = do 161 | context <- widgetGetStyleContext w 162 | styleContextAddClass context clazz 163 | 164 | removeClass :: Gtk.IsWidget a => a -> Text.Text -> IO () 165 | removeClass w clazz = do 166 | context <- widgetGetStyleContext w 167 | styleContextRemoveClass context clazz 168 | 169 | getScreenPos :: Gtk.Window -> GHC.Int.Int32 170 | -> IO (GHC.Int.Int32, GHC.Int.Int32, GHC.Int.Int32) 171 | getScreenPos window number = do 172 | screen <- window `get` #screen 173 | display <- screenGetDisplay screen 174 | monitor <- fromMaybe (error "Unknown screen") 175 | <$> displayGetMonitor display number 176 | getMonitorProps monitor 177 | 178 | getMouseActiveScreenPos :: Gtk.Window -> GHC.Int.Int32 179 | -> IO (GHC.Int.Int32, GHC.Int.Int32, GHC.Int.Int32) 180 | getMouseActiveScreenPos window number = do 181 | screen <- window `get` #screen 182 | display <- screenGetDisplay screen 183 | mPointerPos <- getPointerPos 184 | monitor <- case mPointerPos of 185 | Just (x, y) -> displayGetMonitorAtPoint display x y 186 | Nothing -> fromMaybe (error "Unknown screen") 187 | <$> displayGetMonitor display number 188 | getMonitorProps monitor 189 | 190 | getMonitorProps :: Monitor -> IO (GHC.Int.Int32, GHC.Int.Int32, GHC.Int.Int32) 191 | getMonitorProps monitor = do 192 | monitorGeometry <- monitorGetGeometry monitor 193 | monitorX <- getRectangleX monitorGeometry 194 | monitorY <- getRectangleY monitorGeometry 195 | monitorWidth <- getRectangleWidth monitorGeometry 196 | monitorHeight <- getRectangleHeight monitorGeometry 197 | return (monitorX + monitorWidth, monitorY, monitorHeight) 198 | 199 | 200 | getPointerPos :: IO (Maybe (Int32, Int32)) 201 | getPointerPos = do 202 | mDisplay <- displayGetDefault 203 | mSeat <- sequence $ displayGetDefaultSeat <$> mDisplay 204 | case mSeat of 205 | Just (seat) -> do 206 | mPointer <- seatGetPointer seat 207 | case mPointer of 208 | Just (pointer) -> do 209 | (screen, x, y) <- deviceGetPosition pointer 210 | return $ Just (x, y) 211 | Nothing -> return Nothing 212 | Nothing -> return Nothing 213 | -------------------------------------------------------------------------------- /stack.yaml: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by 'stack init' 2 | # 3 | # Some commonly used options have been documented as comments in this file. 4 | # For advanced use and comprehensive documentation of the format, please see: 5 | # http://docs.haskellstack.org/en/stable/yaml_configuration/ 6 | 7 | # Resolver to choose a 'specific' stackage snapshot or a compiler version. 8 | # A snapshot resolver dictates the compiler version and the set of packages 9 | # to be used for project dependencies. For example: 10 | # 11 | # resolver: lts-3.5 12 | # resolver: nightly-2015-09-21 13 | # resolver: ghc-7.10.2 14 | # resolver: ghcjs-0.1.0_ghc-7.10.2 15 | # resolver: 16 | # name: custom-snapshot 17 | # location: "./custom-snapshot.yaml" 18 | resolver: lts-22.28 19 | 20 | # User packages to be built. 21 | # Various formats can be used as shown in the example below. 22 | # 23 | # packages: 24 | # - some-directory 25 | # - https://example.com/foo/bar/baz-0.0.2.tar.gz 26 | # - location: 27 | # git: https://github.com/commercialhaskell/stack.git 28 | # commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a 29 | # - location: https://github.com/commercialhaskell/stack/commit/e7b331f14bcffb8367cd58fbfc8b40ec7642100a 30 | # extra-dep: true 31 | # subdirs: 32 | # - auto-update 33 | # - wai 34 | # 35 | # A package marked 'extra-dep: true' will only be built if demanded by a 36 | # non-dependency (i.e. a user package), and its test suites and benchmarks 37 | # will not be run. This is useful for tweaking upstream packages. 38 | packages: 39 | - "." 40 | # Dependency packages to be pulled from upstream that are not in the resolver 41 | # (e.g., acme-missiles-0.3) 42 | extra-deps: 43 | - env-locale-1.0.0.1 44 | - haskell-gettext-0.1.2.0 45 | # ConfigFile-1.2.0 46 | # 47 | # https://github.com/jgoerzen/configfile holds version 1.1.4 that has not been 48 | # updated in 10 years and does not build with GHC after 9.4. 49 | - git: https://github.com/rvl/configfile 50 | commit: '83ee30b43f74d2b6781269072cf5ed0f0e00012f' 51 | 52 | # Override default flag values for local packages and extra-deps 53 | flags: {} 54 | 55 | # Extra package databases containing global packages 56 | extra-package-dbs: [] 57 | 58 | # Control whether we use the GHC we find on the path 59 | # system-ghc: true 60 | # 61 | # Require a specific version of stack, using version ranges 62 | # require-stack-version: -any # Default 63 | # require-stack-version: ">=1.4" 64 | # 65 | # Override the architecture used by stack, especially useful on Windows 66 | # arch: i386 67 | # arch: x86_64 68 | # 69 | # Extra directories used by stack for building 70 | # extra-include-dirs: [/path/to/dir] 71 | # extra-lib-dirs: [/path/to/dir] 72 | # 73 | # Allow a newer minor version of GHC than the snapshot specifies 74 | # compiler-check: newer-minor 75 | 76 | nix: 77 | packages: 78 | - gobject-introspection 79 | - gtk3 80 | - libxml2 81 | - pango 82 | - pkgconfig 83 | - zlib 84 | 85 | pvp-bounds: both 86 | -------------------------------------------------------------------------------- /stack.yaml.lock: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by Stack. 2 | # You should not edit this file by hand. 3 | # For more information, please see the documentation at: 4 | # https://docs.haskellstack.org/en/stable/lock_files 5 | 6 | packages: 7 | - completed: 8 | hackage: env-locale-1.0.0.1@sha256:aedab9b7bbed9f523f9821771b4c67d64f34a00be2be60de1d25b1a97278d475,894 9 | pantry-tree: 10 | sha256: 091cde93ceb962d18f21ec926436dc03bab61137db9eae9841f6c75725e09e5f 11 | size: 271 12 | original: 13 | hackage: env-locale-1.0.0.1 14 | - completed: 15 | hackage: haskell-gettext-0.1.2.0@sha256:e16fc0521aa6cd53febb7916b178c43afcc7ea7c93cb70818f4ded6cc3cdad98,2930 16 | pantry-tree: 17 | sha256: a9a2f74cc9c969be0a5e84fd39190358d9de451d97bc995d3379bcb0b627067a 18 | size: 579 19 | original: 20 | hackage: haskell-gettext-0.1.2.0 21 | - completed: 22 | commit: 83ee30b43f74d2b6781269072cf5ed0f0e00012f 23 | git: https://github.com/rvl/configfile 24 | name: ConfigFile 25 | pantry-tree: 26 | sha256: aed82f71ef30a2132aca35d12dc77fdfbae67616b94dd8ce93f822be256545bb 27 | size: 1525 28 | version: 1.2.0 29 | original: 30 | commit: 83ee30b43f74d2b6781269072cf5ed0f0e00012f 31 | git: https://github.com/rvl/configfile 32 | snapshots: 33 | - completed: 34 | sha256: 87da71cb0ae9ee1ea1bf51a8eb9812f39f779be76abc0a3c926defd8afda05d1 35 | size: 719139 36 | url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/22/28.yaml 37 | original: lts-22.28 38 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | 2 | /* Notification center */ 3 | 4 | .blurredBG, #main_window, .blurredBG.low, .blurredBG.normal { 5 | background: rgba(255, 255, 255, 0.5); 6 | } 7 | 8 | .noti-center.time { 9 | font-size: 32px; 10 | } 11 | 12 | /* Notifications */ 13 | 14 | .notification.content { 15 | margin-left: 15px; 16 | margin-right: 15px; 17 | } 18 | 19 | .title { 20 | font-weight: bold; 21 | font-size: 16px; 22 | } 23 | 24 | .appname { 25 | font-size: 12px; 26 | } 27 | 28 | .time { 29 | font-size: 12px; 30 | } 31 | 32 | .blurredBG.notification { 33 | background: rgba(255, 255, 255, 0.4); 34 | } 35 | 36 | .blurredBG.notification.critical { 37 | background: rgba(255, 0, 0, 0.5); 38 | } 39 | 40 | .notificationInCenter.critical { 41 | background: rgba(155, 0, 20, 0.5); 42 | } 43 | 44 | /* Labels */ 45 | 46 | label { 47 | color: #322; 48 | } 49 | 50 | label.notification { 51 | color: #322; 52 | } 53 | 54 | label.critical { 55 | color: #000; 56 | } 57 | .notificationInCenter label.critical { 58 | color: #000; 59 | } 60 | 61 | 62 | /* Buttons */ 63 | 64 | button { 65 | background: transparent; 66 | color: #322; 67 | border-radius: 3px; 68 | border-width: 0px; 69 | background-position: 0px 0px; 70 | text-shadow: none; 71 | } 72 | 73 | button:hover { 74 | border-radius: 3px; 75 | background: rgba(0, 20, 20, 0.2); 76 | border-width: 0px; 77 | border-top: transparent; 78 | border-color: #f00; 79 | color: #fee; 80 | } 81 | 82 | 83 | /* Custom Buttons */ 84 | 85 | .userbutton { 86 | background: rgba(20,0,0, 0.15); 87 | } 88 | 89 | .userbuttonlabel { 90 | color: #222; 91 | font-size: 12px; 92 | } 93 | 94 | .userbutton:hover { 95 | background: rgba(20, 0, 0, 0.2); 96 | } 97 | 98 | .userbuttonlabel:hover { 99 | color: #111; 100 | } 101 | 102 | button.buttonState1 { 103 | background: rgba(20,0,0,0.5); 104 | } 105 | 106 | .userbuttonlabel.buttonState1 { 107 | color: #fff; 108 | } 109 | 110 | button.buttonState1:hover { 111 | background: rgba(20,0,0, 0.4); 112 | } 113 | 114 | .userbuttonlabel.buttonState1:hover { 115 | color: #111; 116 | } 117 | 118 | button.buttonState2 { 119 | background: rgba(255,255,255,0.3); 120 | } 121 | 122 | .userbuttonlabel.buttonState2 { 123 | color: #111; 124 | } 125 | 126 | button.buttonState2:hover { 127 | background: rgba(20,0,0, 0.3); 128 | } 129 | 130 | .userbuttonlabel.buttonState2:hover { 131 | color: #000; 132 | } 133 | 134 | 135 | /* Images */ 136 | 137 | image.deadd-noti-center.notification.image { 138 | margin-left: 20px; 139 | } 140 | -------------------------------------------------------------------------------- /test/Spec.hs: -------------------------------------------------------------------------------- 1 | main :: IO () 2 | main = putStrLn "Test suite not yet implemented" 3 | -------------------------------------------------------------------------------- /translation/bn_BD.po: -------------------------------------------------------------------------------- 1 | # Translation file 2 | msgid "" 3 | msgstr "" 4 | "Project-Id-Version: deadd-notification-center\n" 5 | "Report-Msgid-Bugs-To: https://github.com/phuhl/linux_notification_center\n" 6 | "POT-Creation-Date: 2019-05-24 06:05-0800\n" 7 | "PO-Revision-Date: 2020-12-24 07:54+0600\n" 8 | "Language-Team: \n" 9 | "MIME-Version: 1.0\n" 10 | "Content-Type: text/plain; charset=UTF-8\n" 11 | "Content-Transfer-Encoding: 8bit\n" 12 | "X-Generator: Poedit 2.4.1\n" 13 | "Last-Translator: \n" 14 | "Plural-Forms: nplurals=2; plural=(n==0 || n==1);\n" 15 | "Language: bn_BD\n" 16 | 17 | #: Main.hs:0 18 | msgid "Delete all" 19 | msgstr "সব ডিলিট করুন" 20 | -------------------------------------------------------------------------------- /translation/bn_BD/LC_MESSAGES/deadd-notification-center.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phuhl/linux_notification_center/6c3de4644b4c6cd027f2c0a9fb73f203327efd94/translation/bn_BD/LC_MESSAGES/deadd-notification-center.mo -------------------------------------------------------------------------------- /translation/de.po: -------------------------------------------------------------------------------- 1 | # Translation file 2 | msgid "" 3 | msgstr "" 4 | "Project-Id-Version: deadd-notification-center\n" 5 | "Report-Msgid-Bugs-To: https://github.com/phuhl/linux_notification_center\n" 6 | "POT-Creation-Date: 2019-05-24 06:05-0800\n" 7 | "PO-Revision-Date: 2019-05-24 18:46+0200\n" 8 | "Last-Translator: \n" 9 | "Language-Team: German\n" 10 | "MIME-Version: 1.0\n" 11 | "Content-Type: text/plain; charset=UTF-8\n" 12 | "Content-Transfer-Encoding: 8bit\n" 13 | "Language: de\n" 14 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 15 | 16 | #: Main.hs:0 17 | msgid "Delete all" 18 | msgstr "Alle Löschen" 19 | -------------------------------------------------------------------------------- /translation/de/LC_MESSAGES/deadd-notification-center.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phuhl/linux_notification_center/6c3de4644b4c6cd027f2c0a9fb73f203327efd94/translation/de/LC_MESSAGES/deadd-notification-center.mo -------------------------------------------------------------------------------- /translation/deadd-notification-center.pot: -------------------------------------------------------------------------------- 1 | # Translation file 2 | 3 | msgid "" 4 | msgstr "" 5 | 6 | "Project-Id-Version: deadd-notification-center\n" 7 | "Report-Msgid-Bugs-To: https://github.com/phuhl/linux_notification_center\n" 8 | "POT-Creation-Date: 2019-05-24 06:05-0800\n" 9 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 10 | "Last-Translator: \n" 11 | "Language-Team: \n" 12 | "MIME-Version: 1.0\n" 13 | "Content-Type: text/plain; charset=UTF-8\n" 14 | "Content-Transfer-Encoding: 8bit\n" 15 | 16 | #: Main.hs:0 17 | msgid "Delete all" 18 | msgstr "" 19 | -------------------------------------------------------------------------------- /translation/en.po: -------------------------------------------------------------------------------- 1 | # Translation file 2 | msgid "" 3 | msgstr "" 4 | "Project-Id-Version: deadd-notification-center\n" 5 | "Report-Msgid-Bugs-To: https://github.com/phuhl/linux_notification_center\n" 6 | "POT-Creation-Date: 2019-05-24 06:05-0800\n" 7 | "PO-Revision-Date: 2019-05-24 18:47+0200\n" 8 | "Last-Translator: \n" 9 | "Language-Team: English\n" 10 | "MIME-Version: 1.0\n" 11 | "Content-Type: text/plain; charset=UTF-8\n" 12 | "Content-Transfer-Encoding: 8bit\n" 13 | "Language: en\n" 14 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 15 | 16 | #: Main.hs:0 17 | msgid "Delete all" 18 | msgstr "Delete all" 19 | -------------------------------------------------------------------------------- /translation/en/LC_MESSAGES/deadd-notification-center.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phuhl/linux_notification_center/6c3de4644b4c6cd027f2c0a9fb73f203327efd94/translation/en/LC_MESSAGES/deadd-notification-center.mo -------------------------------------------------------------------------------- /translation/ru.po: -------------------------------------------------------------------------------- 1 | # Translation file 2 | msgid "" 3 | msgstr "" 4 | "Project-Id-Version: deadd-notification-center\n" 5 | "Report-Msgid-Bugs-To: https://github.com/phuhl/linux_notification_center\n" 6 | "POT-Creation-Date: 2019-05-24 06:05-0800\n" 7 | "PO-Revision-Date: 2023-06-29 15:00+0000\n" 8 | "Last-Translator: <3jl0y_pycckui@riseup.net>\n" 9 | "Language-Team: Russian\n" 10 | "MIME-Version: 1.0\n" 11 | "Content-Type: text/plain; charset=UTF-8\n" 12 | "Content-Transfer-Encoding: 8bit\n" 13 | "Language: ru\n" 14 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 15 | 16 | #: Main.hs:0 17 | msgid "Delete all" 18 | msgstr "Удалить все" 19 | -------------------------------------------------------------------------------- /translation/ru/LC_MESSAGES/deadd-notification-center.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phuhl/linux_notification_center/6c3de4644b4c6cd027f2c0a9fb73f203327efd94/translation/ru/LC_MESSAGES/deadd-notification-center.mo -------------------------------------------------------------------------------- /translation/tr.po: -------------------------------------------------------------------------------- 1 | # Translation file 2 | msgid "" 3 | msgstr "" 4 | "Project-Id-Version: deadd-notification-center\n" 5 | "Report-Msgid-Bugs-To: https://github.com/phuhl/linux_notification_center\n" 6 | "POT-Creation-Date: 2019-05-24 06:05-0800\n" 7 | "PO-Revision-Date: 2021-01-21 19:54+0300\n" 8 | "Language-Team: \n" 9 | "MIME-Version: 1.0\n" 10 | "Content-Type: text/plain; charset=UTF-8\n" 11 | "Content-Transfer-Encoding: 8bit\n" 12 | "X-Generator: Poedit 2.4.1\n" 13 | "Last-Translator: \n" 14 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 15 | "Language: tr\n" 16 | 17 | #: Main.hs:0 18 | msgid "Delete all" 19 | msgstr "Tümünü sil" 20 | -------------------------------------------------------------------------------- /translation/tr/LC_MESSAGES/deadd-notification-center.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phuhl/linux_notification_center/6c3de4644b4c6cd027f2c0a9fb73f203327efd94/translation/tr/LC_MESSAGES/deadd-notification-center.mo -------------------------------------------------------------------------------- /updateyourconfig2021.org: -------------------------------------------------------------------------------- 1 | The new CSS configuration file makes some configuration options from 2 | the =deadd.conf= obsolete. These are no longer in use: 3 | 4 | - timeTextSize 5 | - titleTextSize 6 | - appNameTextSize 7 | - timeTextSize 8 | 9 | From [colors] section: 10 | - background 11 | - notiBackground 12 | - notiColor 13 | - critical 14 | - criticalColor 15 | - criticalInCenter 16 | - criticalInCenterColor 17 | - labelColor 18 | - buttonColor 19 | - buttonHover 20 | - buttonHoverColor 21 | - buttonBackground 22 | - buttonColor 23 | - buttonBackground 24 | - buttonHover 25 | - buttonHoverColor 26 | - buttonTextSize 27 | - buttonState1 28 | - buttonState1Color 29 | - buttonState1Hover 30 | - buttonState1HoverColor 31 | - buttonState2 32 | - buttonState2Color 33 | - buttonState2Hover 34 | - buttonState2HoverColor 35 | 36 | 37 | All these colors can now be set directly in the =deadd.css= file which 38 | resides in the =${XDG_CONFIG_HOME}/deadd/= folder (usually 39 | =.config/deadd/=). 40 | 41 | If the installer did not create a =deadd.css= file, create it yourself. You can use [[https://github.com/phuhl/linux_notification_center/blob/master/style.css][style.css]] as a foundation. 42 | 43 | Changes to =gtk.css= are now longer required. 44 | --------------------------------------------------------------------------------