From 84485c4463e089722e700ec99c7f4b14ed8bca45 Mon Sep 17 00:00:00 2001 From: phuhl Date: Sun, 6 Aug 2023 20:24:53 +0200 Subject: [PATCH] Adds modification for actions --- README.org | 498 ++++++++++-------- src/Config.hs | 3 + src/NotificationCenter/Notifications.hs | 26 +- .../Notifications/AbstractNotification.hs | 14 +- .../Notifications/Action.hs | 8 +- src/NotificationCenter/Notifications/Data.hs | 3 +- .../Notifications/NotificationPopup.hs | 2 +- 7 files changed, 311 insertions(+), 243 deletions(-) diff --git a/README.org b/README.org index 4dcd044..47fb60e 100644 --- a/README.org +++ b/README.org @@ -380,236 +380,269 @@ from the =deadd.css= file by using the command described in the section "Usage". #+BEGIN_SRC yaml -### Margins for notification-center/notifications -margin-top: 0 -margin-right: 0 - -### Margins for notification-center -margin-bottom: 0 - -### Width of the notification center/notifications in pixels. -width: 500 - -### Command to run at startup. This can be used to setup -### button states. -# startup-command: deadd-notification-center-startup - -### Monitor on which the notification center/notifications will be -### printed. If "follow-mouse" is set true, this does nothing. -monitor: 0 - -### If true, the notification center/notifications will open on the -### screen, on which the mouse is. Overrides the "monitor" setting. -follow-mouse: false - -notification-center: - ### Margin at the top/right/bottom of the notification center in - ### pixels. This can be used to avoid overlap between the notification - ### center and bars such as polybar or i3blocks. - # margin-top: 0 - # margin-right: 0 - # margin-bottom: 0 - - ### Width of the notification center in pixels. - # width: 500 - - ### Monitor on which the notification center will be printed. If - ### "follow-mouse" is set true, this does nothing. - # monitor: 0 - - ### If true, the notification center will open on the screen, on which - ### the mouse is. Overrides the "monitor" setting. - # follow-mouse: false - - ### Notification center closes when the mouse leaves it - hide-on-mouse-leave: true - - ### If newFirst is set to true, newest notifications appear on the top - ### of the notification center. Else, notifications stack, from top to - ### bottom. - new-first: true - - ### If true, the transient field in notifications will be ignored, - ### thus the notification will be persisted in the notification - ### center anyways - ignore-transient: false - - ### Custom buttons in notification center - buttons: - ### Numbers of buttons that can be drawn on a row of the notification - ### center. - # buttons-per-row: 5 - - ### Height of buttons in the notification center (in pixels). - # buttons-height: 60 - - ### Horizontal and vertical margin between each button in the - ### notification center (in pixels). - # buttons-margin: 2 - - ### Button actions and labels. For each button you must specify a - ### label and a command. - actions: - # - label: VPN - # command: "sudo vpnToggle" - # - label: Bluetooth - # command: bluetoothToggle - # - label: Wifi - # command: wifiToggle - # - label: Screensaver - # command: screensaverToggle - # - label: Keyboard - # command: keyboardToggle + ### Margins for notification-center/notifications + margin-top: 0 + margin-right: 0 -notification: - ### If true, markup (, , , ) will be displayed properly - use-markup: true - - ### If true, html entities (& for &, % for %, etc) will be - ### parsed properly. This is useful for chromium-based apps, which - ### tend to send these in notifications. - parse-html-entities: true - - dbus: - - ### If noti-closed messages are enabled, the sending application - ### will know that a notification was closed/timed out. This can - ### be an issue for certain applications, that overwrite - ### notifications on status updates (e.g. Spotify on each - ### song). When one of these applications thinks, the notification - ### has been closed/timed out, they will not overwrite existing - ### notifications but send new ones. This can lead to redundant - ### notifications in the notification center, as the close-message - ### is send regardless of the notification being persisted. - send-noti-closed: false - - app-icon: - - ### If set to true: If no icon is passed by the app_icon parameter - ### and no application "desktop-entry"-hint is present, deadd will - ### try to guess the icon from the application name (if present). - guess-icon-from-name: true - - ### The display size of the application icons in the notification - ### pop-ups and in the notification center - icon-size: 20 - - image: - - ### The maximal display size of images that are part of - ### notifications for notification pop-ups and in the notification - ### center - size: 100 - - ### The margin around the top, bottom, left, and right of - ### notification images. - margin-top: 15 - margin-bottom: 15 - margin-left: 15 - margin-right: 0 - - ### Apply modifications to certain notifications: - ### Each modification rule needs a "match" and either a "modify" or - ### a "script" entry. - modifications: - ### Match: - ### Matches the notifications against these rules. If all of the - ### values (of one modification rule) match, the "modify"/"script" - ### part is applied. - # - match: - ### Possible match criteria: - # title: "Notification title" - # body: "Notification body" - # time: "12:44" - # app-name: "App name" - # urgency: "low" # "low", "normal" or "critical" - - # modify: - ### Possible modifications - # title: "abc" - # body: "abc" - # app-name: "abc" - # app-icon: "file:///abc.png" - ### The timeout has three special values: - ### timeout: 0 -> don't time out at all - ### timeout: -1 -> use default timeout - ### timeout: 1 -> don't show as pop-up - ### timeout: >1 -> milliseconds until timeout - # timeout: 1 - # margin-right: 10 - # margin-top: 10 - # image: "file:///abc.png" - # image-size: 10 - # transient: true - # send-noti-closed: false - ### Remove action buttons from notifications - # remove-actions: true - ### Add a class-name to the notification container, that can be - ### used for specific styling of notifications using the - ### deadd.css file - # class-name: "abc" - - # - match: - # app-name: "Chromium" - - ### Instead of modifying a notification directly, a script can be - ### run, which will receive the notification as JSON on STDIN. It - ### is expected to return JSON/YAML configuration that defines the - ### modifications that should be applied. Minimum complete return - ### value must be '{"modify": {}, "match": {}}'. Always leave the "match" - ### object empty (technical reasons, i.e. I am lazy). - # script: "linux-notification-center-parse-chromium" - - match: - app-name: "Spotify" - modify: - image-size: 80 - timeout: 1 - send-noti-closed: true - class-name: "Spotify" - # - match: - # title: Bildschirmhelligkeit - # modify: - # image-size: 60 - popup: - - ### Default timeout used for notifications in milli-seconds. This can - ### be overwritten with the "-t" option (or "--expire-time") of the - ### notify-send command. - default-timeout: 10000 - - ### Margin above/right/between notifications (in pixels). This can - ### be used to avoid overlap between notifications and a bar such as - ### polybar or i3blocks. - margin-top: 50 - margin-right: 50 - margin-between: 20 - - ### Defines after how many lines of text the body will be truncated. - ### Use 0 if you want to disable truncation. - max-lines-in-body: 3 - - ### Determines whether the GTK widget that displays the notification body - ### in the notification popup will be hidden when empty. This is especially - ### useful for transient notifications that display a progress bar. - # hide-body-if-empty: false - - ### Monitor on which the notifications will be - ### printed. If "follow-mouse" is set true, this does nothing. + ### Margins for notification-center + margin-bottom: 0 + + ### Width of the notification center/notifications in pixels. + width: 500 + + ### Command to run at startup. This can be used to setup + ### button states. + # startup-command: deadd-notification-center-startup + + ### Monitor on which the notification center/notifications will be + ### printed. If "follow-mouse" is set true, this does nothing. + monitor: 0 + + ### If true, the notification center/notifications will open on the + ### screen, on which the mouse is. Overrides the "monitor" setting. + follow-mouse: false + + notification-center: + ### Margin at the top/right/bottom of the notification center in + ### pixels. This can be used to avoid overlap between the notification + ### center and bars such as polybar or i3blocks. + # margin-top: 0 + # margin-right: 0 + # margin-bottom: 0 + + ### Width of the notification center in pixels. + # width: 500 + + ### Monitor on which the notification center will be printed. If + ### "follow-mouse" is set true, this does nothing. # monitor: 0 - ### If true, the notifications will open on the - ### screen, on which the mouse is. Overrides the "monitor" setting. + ### If true, the notification center will open on the screen, on which + ### the mouse is. Overrides the "monitor" setting. # follow-mouse: false - click-behavior: - - ### The mouse button for dismissing a popup. Must be either "mouse1", - ### "mouse2", "mouse3", "mouse4", or "mouse5" - dismiss: mouse1 - - ### The mouse button for opening a popup with the default action. - ### Must be either "mouse1", "mouse2", "mouse3", "mouse4", or "mouse5" - default-action: mouse3 + ### Notification center closes when the mouse leaves it + hide-on-mouse-leave: true + + ### If newFirst is set to true, newest notifications appear on the top + ### of the notification center. Else, notifications stack, from top to + ### bottom. + new-first: true + + ### If true, the transient field in notifications will be ignored, + ### thus the notification will be persisted in the notification + ### center anyways + ignore-transient: false + + ### Custom buttons in notification center + buttons: + ### Numbers of buttons that can be drawn on a row of the notification + ### center. + # buttons-per-row: 5 + + ### Height of buttons in the notification center (in pixels). + # buttons-height: 60 + + ### Horizontal and vertical margin between each button in the + ### notification center (in pixels). + # buttons-margin: 2 + + ### Button actions and labels. For each button you must specify a + ### label and a command. + actions: + # - label: VPN + # command: "sudo vpnToggle" + # - label: Bluetooth + # command: bluetoothToggle + # - label: Wifi + # command: wifiToggle + # - label: Screensaver + # command: screensaverToggle + # - label: Keyboard + # command: keyboardToggle + + notification: + ### If true, markup (, , , ) will be displayed properly + use-markup: true + + ### If true, html entities (& for &, % for %, etc) will be + ### parsed properly. This is useful for chromium-based apps, which + ### tend to send these in notifications. + parse-html-entities: true + + dbus: + + ### If noti-closed messages are enabled, the sending application + ### will know that a notification was closed/timed out. This can + ### be an issue for certain applications, that overwrite + ### notifications on status updates (e.g. Spotify on each + ### song). When one of these applications thinks, the notification + ### has been closed/timed out, they will not overwrite existing + ### notifications but send new ones. This can lead to redundant + ### notifications in the notification center, as the close-message + ### is send regardless of the notification being persisted. + send-noti-closed: false + + app-icon: + + ### If set to true: If no icon is passed by the app_icon parameter + ### and no application "desktop-entry"-hint is present, deadd will + ### try to guess the icon from the application name (if present). + guess-icon-from-name: true + + ### The display size of the application icons in the notification + ### pop-ups and in the notification center + icon-size: 20 + + image: + + ### The maximal display size of images that are part of + ### notifications for notification pop-ups and in the notification + ### center + size: 100 + + ### The margin around the top, bottom, left, and right of + ### notification images. + margin-top: 15 + margin-bottom: 15 + margin-left: 15 + margin-right: 0 + + ### Apply modifications to certain notifications: + ### Each modification rule needs a "match" and either a "modify" or + ### a "script" entry. + modifications: + ### Match: + ### Matches the notifications against these rules. If all of the + ### values (of one modification rule) match, the "modify"/"script" + ### part is applied. + # - match: + ### Possible match criteria: + # title: "Notification title" + # body: "Notification body" + # time: "12:44" + # app-name: "App name" + # urgency: "low" # "low", "normal" or "critical" + + # modify: + ### Possible modifications + # title: "abc" + # body: "abc" + # app-name: "abc" + # app-icon: "file:///abc.png" + ### The timeout has three special values: + ### timeout: 0 -> don't time out at all + ### timeout: -1 -> use default timeout + ### timeout: 1 -> don't show as pop-up + ### timeout: >1 -> milliseconds until timeout + # timeout: 1 + # margin-right: 10 + # margin-top: 10 + # image: "file:///abc.png" + # image-size: 10 + # transient: true + # send-noti-closed: false + ### Remove action buttons from notifications + # remove-actions: true + ### Set the action-icons hint to true, action labels will then + ### be intergreted as GTK icon names + # action-icons: true + ### List of actions, where the even elements (0, 2, ...) are the + ### action name and the odd elements are the label + # actions: + # - previous + # - media-skip-backward + # - play + # - media-playback-start + # - next + # - media-skip-forward + ### Action commands, where the keys (e.g. "play") is the action + ### name and the value is a program call that should be executed + ### on action. Prevents sending of the action to the application. + # action-commands: + # play: playerctl play-pause + # previous: playerctl previous + # next: playerctl next + + ### Add a class-name to the notification container, that can be + ### used for specific styling of notifications using the + ### deadd.css file + # class-name: "abc" + + # - match: + # app-name: "Chromium" + + ### Instead of modifying a notification directly, a script can be + ### run, which will receive the notification as JSON on STDIN. It + ### is expected to return JSON/YAML configuration that defines the + ### modifications that should be applied. Minimum complete return + ### value must be '{"modify": {}, "match": {}}'. Always leave the "match" + ### object empty (technical reasons, i.e. I am lazy). + # script: "linux-notification-center-parse-chromium" + - match: + app-name: "Spotify" + modify: + image-size: 80 + timeout: 1 + send-noti-closed: true + class-name: "Spotify" + action-icons: true + actions: + - previous + - media-skip-backward + - play + - media-playback-start + - next + - media-skip-forward + action-commands: + play: playerctl play-pause + previous: playerctl previous + next: playerctl next + + # - match: + # title: Bildschirmhelligkeit + # modify: + # image-size: 60 + popup: + + ### Default timeout used for notifications in milli-seconds. This can + ### be overwritten with the "-t" option (or "--expire-time") of the + ### notify-send command. + default-timeout: 10000 + + ### Margin above/right/between notifications (in pixels). This can + ### be used to avoid overlap between notifications and a bar such as + ### polybar or i3blocks. + margin-top: 50 + margin-right: 50 + margin-between: 20 + + ### Defines after how many lines of text the body will be truncated. + ### Use 0 if you want to disable truncation. + max-lines-in-body: 3 + + ### Determines whether the GTK widget that displays the notification body + ### in the notification popup will be hidden when empty. This is especially + ### useful for transient notifications that display a progress bar. + # hide-body-if-empty: false + + ### Monitor on which the notifications will be + ### printed. If "follow-mouse" is set true, this does nothing. + # monitor: 0 + + ### If true, the notifications will open on the + ### screen, on which the mouse is. Overrides the "monitor" setting. + # follow-mouse: false + + click-behavior: + + ### The mouse button for dismissing a popup. Must be either "mouse1", + ### "mouse2", "mouse3", "mouse4", or "mouse5" + dismiss: mouse1 + + ### The mouse button for opening a popup with the default action. + ### Must be either "mouse1", "mouse2", "mouse3", "mouse4", or "mouse5" + default-action: mouse3 #+END_SRC *** CSS styling @@ -731,10 +764,21 @@ You can set the following parameters: will prevent that a DBUS =NotificationClosed= message will be send for this notification. Only applies if the configuration parameter =configSendNotiClosedDbusMessage= is set to =true=) -- =remove-actions= (value can be anything, if used, no action buttons - will be displayed on the notifications) - =class-name= (adds a CSS-class name to the container of the notification for styling) +- =remove-actions= (value can be anything, if used, no action buttons + will be displayed on the notifications) +- =action-icons= (=true= or =false=, if set to =true= the action label will be + interpreted as an gtk-icon-name and an icon is displayed) +- =actions= (array where every even element is the action name, every + odd element is the action label. Actions are rendered as buttons in + a notification. As the action most likely won't be known by the + notification sender, you probably want to use this with the + =action-commands= modification) +- =action-commands= (object where the keys are the action name and the + value is a program call that should be executed when the action + button has been clicked. Prevents sending of the action to the + application.) The modification parameters can be specified in the section =notification.modifications.modify= of your =deadd.yml=. diff --git a/src/Config.hs b/src/Config.hs index 83e2a90..63f7982 100644 --- a/src/Config.hs +++ b/src/Config.hs @@ -43,6 +43,7 @@ data ModificationRule = Modify , modifyNoClosedMsg :: Maybe Bool , modifyRemoveActions :: Maybe Bool , modifyActionIcons :: Maybe Bool + , modifyActionCommands :: Maybe (Map.Map String String) , modifyActions :: Maybe [String] , modifyClassName :: Maybe String } | @@ -84,6 +85,8 @@ instance FromJSON ModificationRule where <*> o .: "modify" .:. "remove-actions" -- modifyActionIcons <*> o .: "modify" .:. "action-icons" + -- modifyActionIcons + <*> o .: "modify" .:. "action-commands" -- modifyActions <*> o .: "modify" .:. "actions" -- modifyClassName diff --git a/src/NotificationCenter/Notifications.hs b/src/NotificationCenter/Notifications.hs index 85c4d88..bfaf207 100644 --- a/src/NotificationCenter/Notifications.hs +++ b/src/NotificationCenter/Notifications.hs @@ -46,12 +46,13 @@ import Data.List import qualified Data.Map as Map import Data.Time import Data.Time.LocalTime -import Data.Maybe (fromMaybe) +import Data.Maybe (fromMaybe, isJust) import qualified Data.Yaml as Yaml import qualified Data.Aeson as Aeson -import System.Process (readCreateProcess, shell) +import Control.Exception (finally) +import System.Process (readCreateProcess, shell, spawnCommand, interruptProcessGroupOf, waitForProcess) import System.Locale.Current import System.Directory (getXdgDirectory, XdgDirectory(..)) import System.Environment (getEnv) @@ -115,11 +116,17 @@ emitNotificationClosed doSend onClose id ctype = _ -> 4 :: Word32)] } else return () -emitAction :: (Signal -> IO ()) -> Int -> String -> Maybe String -> IO () -emitAction onAction id key mParam = do - onAction $ (signal "/org/freedesktop/Notifications" - "org.freedesktop.Notifications" - "ActionInvoked") +emitAction :: (Signal -> IO ()) -> Int -> [(String, String)] -> String -> Maybe String -> IO () +emitAction onAction id actionCommands key mParam = do + let mCommand = lookup key actionCommands + if isJust mCommand then do + ph <- spawnCommand $ fromMaybe "" mCommand + waitForProcess ph `finally` interruptProcessGroupOf ph + return () + else + onAction $ (signal "/org/freedesktop/Notifications" + "org.freedesktop.Notifications" + "ActionInvoked") { signalBody = [ toVariant (fromIntegral id :: Word32) , toVariant key] ++ if mParam == Nothing then @@ -259,6 +266,7 @@ notify config tState emit , notiBody = htmlEntitiesStrip config $ xmlStrip config body , notiActions = actions , notiActionIcons = parseActionIcons hints + , notiActionCommands = [] , notiHints = hints , notiUrgency = parseUrgency hints , notiTimeout = timeout @@ -302,7 +310,7 @@ notify config tState emit { notiId = notiStNextId state , notiOnClosed = emitNotificationClosed (notiSendClosedMsg newNoti) emit (notiStNextId state) - , notiOnAction = emitAction + , notiOnAction = emitAction emit (notiStNextId state) }) (fromIntegral (notiRepId newNoti)) }) @@ -386,6 +394,8 @@ modifyNoti config noti = (\_ -> []) $ modifyRemoveActions modify) , notiActionIcons = fromMaybe (notiActionIcons noti) $ modifyActionIcons modify + , notiActionCommands = fromMaybe (notiActionCommands noti) + $ Map.assocs <$> modifyActionCommands modify , notiClassName = fromMaybe (notiClassName noti) $ pack <$> modifyClassName modify } return newnoti diff --git a/src/NotificationCenter/Notifications/AbstractNotification.hs b/src/NotificationCenter/Notifications/AbstractNotification.hs index f1df039..29aab3b 100644 --- a/src/NotificationCenter/Notifications/AbstractNotification.hs +++ b/src/NotificationCenter/Notifications/AbstractNotification.hs @@ -95,7 +95,7 @@ createNotification config builder noti dispNoti = do (fromIntegral $ configImgMarginRight config) onWidgetButtonPressEvent container $ \(_) -> do - notiOnAction noti "default" Nothing + notiOnAction noti (notiActionCommands noti) "default" Nothing return False @@ -154,7 +154,15 @@ updateNotiContent config noti dNoti = do let takeTwo (a:b:cs) = (a,b):(takeTwo cs) takeTwo _ = [] actionButtons <- sequence - $ (\(a, b) -> createAction config (notiActionIcons noti) (notiOnAction noti) 20 20 a b) + $ (\(a, b) -> createAction + config + (notiActionCommands noti) + (notiActionIcons noti) + (notiOnAction noti) + 20 + 20 + a + b) <$> (Prelude.filter (\(a, b) -> a /= "default" && (notiPercentage noti == Nothing || a /= "changeValue")) @@ -180,7 +188,7 @@ updateNotiContent config noti dNoti = do (fromMaybe 0 $ notiPercentage noti) onRangeValueChanged (view dScale dNoti) $ do value <- rangeGetValue (view dScale dNoti) - (notiOnAction noti) "changeValue" $ Just $ show value + (notiOnAction noti) (notiActionCommands noti) "changeValue" $ Just $ show value return () widgetSetVisible (view dScale dNoti) True widgetSetVisible (view dProgressbar dNoti) False diff --git a/src/NotificationCenter/Notifications/Action.hs b/src/NotificationCenter/Notifications/Action.hs index 835d6e9..849590e 100644 --- a/src/NotificationCenter/Notifications/Action.hs +++ b/src/NotificationCenter/Notifications/Action.hs @@ -44,8 +44,10 @@ data Action = Action -- ^ Shell command to execute } -createAction :: Config -> Bool -> (String -> Maybe String -> IO ()) -> Int -> Int -> String -> String -> IO Action -createAction config useIcons onAction width height command description = do +createAction :: Config -> [(String, String)] -> Bool + -> ([(String, String)] -> String -> Maybe String + -> IO ()) -> Int -> Int -> String -> String -> IO Action +createAction config actionCommands useIcons onAction width height command description = do button <- buttonNew widgetSetSizeRequest button (fromIntegral width) (fromIntegral height) addClass button "actionbutton" @@ -59,7 +61,7 @@ createAction config useIcons onAction width height command description = do { actionButton = button , actionCommand = command } onButtonClicked button $ do - onAction command Nothing + onAction actionCommands command Nothing return () if useIcons && configActionIcons config then do img <-imageNewFromIconName (Just $ Text.pack description) (fromIntegral $ fromEnum IconSizeButton) diff --git a/src/NotificationCenter/Notifications/Data.hs b/src/NotificationCenter/Notifications/Data.hs index c1ee156..d630222 100644 --- a/src/NotificationCenter/Notifications/Data.hs +++ b/src/NotificationCenter/Notifications/Data.hs @@ -41,6 +41,7 @@ data Notification = Notification , notiSummary :: Text.Text -- ^ Summary , notiBody :: Text.Text -- ^ Body , notiActions :: [Text.Text] -- ^ Actions + , notiActionCommands :: [(String, String)] -- ^ Actions , notiActionIcons :: Bool -- ^ Use icons for action-buttons , notiHints :: Map.Map Text.Text Variant -- ^ Hints , notiUrgency :: Urgency @@ -53,7 +54,7 @@ data Notification = Notification , notiRight :: Maybe Int -- ^ Should be called when the notification is closed, either by -- timeout or by user - , notiOnAction :: String -> Maybe String -> IO () + , notiOnAction :: [(String, String)] -> String -> Maybe String -> IO () -- ^ Should be called when an action is used , notiPercentage :: Maybe Double -- ^ The percentage that should be shown in a percentage bar diff --git a/src/NotificationCenter/Notifications/NotificationPopup.hs b/src/NotificationCenter/Notifications/NotificationPopup.hs index b760a13..4f513e6 100644 --- a/src/NotificationCenter/Notifications/NotificationPopup.hs +++ b/src/NotificationCenter/Notifications/NotificationPopup.hs @@ -163,7 +163,7 @@ showNotificationWindow config noti dispNotis onClose = do notiOnClosed noti $ User onClose | valid && defaultAction -> do - notiOnAction noti "default" Nothing + notiOnAction noti (notiActionCommands noti) "default" Nothing notiOnClosed noti $ User onClose | not validDismiss -> do