├── Setup.hs
├── test
└── Spec.hs
├── app
└── Main.hs
├── 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
├── com.ph-uhl.deadd.notification.service.in
├── translation
├── de
│ └── LC_MESSAGES
│ │ └── deadd-notification-center.mo
├── en
│ └── LC_MESSAGES
│ │ └── deadd-notification-center.mo
├── ru
│ └── LC_MESSAGES
│ │ └── deadd-notification-center.mo
├── tr
│ └── LC_MESSAGES
│ │ └── deadd-notification-center.mo
├── bn_BD
│ └── LC_MESSAGES
│ │ └── deadd-notification-center.mo
├── deadd-notification-center.pot
├── de.po
├── en.po
├── ru.po
├── tr.po
└── bn_BD.po
├── src
├── NotificationCenter
│ ├── Glade.hs
│ ├── Notification
│ │ └── Glade.hs
│ ├── Notifications
│ │ ├── Notification
│ │ │ └── Glade.hs
│ │ ├── Action.hs
│ │ ├── Data.hs
│ │ ├── NotificationPopup.hs
│ │ └── AbstractNotification.hs
│ ├── Button.hs
│ ├── NotificationInCenter.hs
│ └── Notifications.hs
├── TransparentWindow.hs
├── Helpers.hs
├── Config.hs
└── NotificationCenter.hs
├── .gitignore
├── deadd-notification-center.service.in
├── sendShowupNotis.sh
├── docs
├── linux-notification-center.man
└── linux-notification-center.org
├── pkg-bin
└── PKGBUILD
├── pkg
└── PKGBUILD
├── pkg-git
└── PKGBUILD
├── updateyourconfig2021.org
├── .github
└── workflows
│ ├── main.yml
│ └── release.yml
├── stack.yaml.lock
├── LICENSE
├── snap
└── snapcraft.yaml
├── notification_section.glade
├── style.css
├── stack.yaml
├── deadd-notification-center.cabal
├── Makefile
├── Worklog.org
├── notification_center.glade
├── notification_in_center.glade
├── notification.glade
└── README.org
/Setup.hs:
--------------------------------------------------------------------------------
1 | import Distribution.Simple
2 | main = defaultMain
3 |
--------------------------------------------------------------------------------
/test/Spec.hs:
--------------------------------------------------------------------------------
1 | main :: IO ()
2 | main = putStrLn "Test suite not yet implemented"
3 |
--------------------------------------------------------------------------------
/app/Main.hs:
--------------------------------------------------------------------------------
1 | module Main where
2 |
3 | import qualified NotificationCenter
4 |
5 | main :: IO ()
6 | main = NotificationCenter.main
7 |
--------------------------------------------------------------------------------
/README.org.img/more_notifications.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phuhl/linux_notification_center/HEAD/README.org.img/more_notifications.png
--------------------------------------------------------------------------------
/com.ph-uhl.deadd.notification.service.in:
--------------------------------------------------------------------------------
1 | [D-BUS Service]
2 | Name=org.freedesktop.Notifications
3 | Exec=##PREFIX##/bin/deadd-notification-center
4 |
--------------------------------------------------------------------------------
/README.org.img/org_20200223_193345_VhlbOf.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phuhl/linux_notification_center/HEAD/README.org.img/org_20200223_193345_VhlbOf.jpg
--------------------------------------------------------------------------------
/README.org.img/org_20200223_193450_1en7sh.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phuhl/linux_notification_center/HEAD/README.org.img/org_20200223_193450_1en7sh.jpg
--------------------------------------------------------------------------------
/README.org.img/org_20200223_200131_4WWV2Y.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phuhl/linux_notification_center/HEAD/README.org.img/org_20200223_200131_4WWV2Y.jpg
--------------------------------------------------------------------------------
/README.org.img/org_20201220_000601_9V037T.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phuhl/linux_notification_center/HEAD/README.org.img/org_20201220_000601_9V037T.jpg
--------------------------------------------------------------------------------
/README.org.img/org_20210119_120536_adyKnd.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phuhl/linux_notification_center/HEAD/README.org.img/org_20210119_120536_adyKnd.jpg
--------------------------------------------------------------------------------
/README.org.img/org_20210119_122031_BNYKTp.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phuhl/linux_notification_center/HEAD/README.org.img/org_20210119_122031_BNYKTp.jpg
--------------------------------------------------------------------------------
/README.org.img/org_20210119_122628_AUuEu3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phuhl/linux_notification_center/HEAD/README.org.img/org_20210119_122628_AUuEu3.jpg
--------------------------------------------------------------------------------
/translation/de/LC_MESSAGES/deadd-notification-center.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phuhl/linux_notification_center/HEAD/translation/de/LC_MESSAGES/deadd-notification-center.mo
--------------------------------------------------------------------------------
/translation/en/LC_MESSAGES/deadd-notification-center.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phuhl/linux_notification_center/HEAD/translation/en/LC_MESSAGES/deadd-notification-center.mo
--------------------------------------------------------------------------------
/translation/ru/LC_MESSAGES/deadd-notification-center.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phuhl/linux_notification_center/HEAD/translation/ru/LC_MESSAGES/deadd-notification-center.mo
--------------------------------------------------------------------------------
/translation/tr/LC_MESSAGES/deadd-notification-center.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phuhl/linux_notification_center/HEAD/translation/tr/LC_MESSAGES/deadd-notification-center.mo
--------------------------------------------------------------------------------
/translation/bn_BD/LC_MESSAGES/deadd-notification-center.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phuhl/linux_notification_center/HEAD/translation/bn_BD/LC_MESSAGES/deadd-notification-center.mo
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/.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/
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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.
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/notification_section.glade:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
53 |
54 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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 |
--------------------------------------------------------------------------------
/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.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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------