{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} module Simplex.Chat.Terminal.Notification (Notification (..), initializeNotifications) where import Data.List (isInfixOf) import Data.Maybe (isJust) import qualified Data.Text as T import Simplex.Messaging.Util (catchAll_) import System.Directory (createDirectoryIfMissing, doesFileExist, findExecutable, getAppUserDataDirectory) import System.FilePath (combine) import System.Info (os) import System.Process (callProcess) data Notification = Notification {Notification -> Text title :: T.Text, Notification -> Text text :: T.Text} initializeNotifications :: IO (Notification -> IO ()) initializeNotifications :: IO (Notification -> IO ()) initializeNotifications = (Notification -> IO ()) -> Notification -> IO () forall a. (a -> IO ()) -> a -> IO () hideException ((Notification -> IO ()) -> Notification -> IO ()) -> IO (Notification -> IO ()) -> IO (Notification -> IO ()) forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b <$> case String os of String "darwin" -> (Notification -> IO ()) -> IO (Notification -> IO ()) forall a. a -> IO a forall (f :: * -> *) a. Applicative f => a -> f a pure Notification -> IO () macNotify String "mingw32" -> IO (Notification -> IO ()) initWinNotify String "linux" -> String -> IO Bool doesFileExist String "/proc/sys/kernel/osrelease" IO Bool -> (Bool -> IO (Notification -> IO ())) -> IO (Notification -> IO ()) forall a b. IO a -> (a -> IO b) -> IO b forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b >>= \case Bool False -> IO (Notification -> IO ()) initLinuxNotify Bool True -> do String v <- String -> IO String readFile String "/proc/sys/kernel/osrelease" if String "Microsoft" String -> String -> Bool forall a. Eq a => [a] -> [a] -> Bool `isInfixOf` String v Bool -> Bool -> Bool || String "WSL" String -> String -> Bool forall a. Eq a => [a] -> [a] -> Bool `isInfixOf` String v then IO (Notification -> IO ()) initWslNotify else IO (Notification -> IO ()) initLinuxNotify String _ -> (Notification -> IO ()) -> IO (Notification -> IO ()) forall a. a -> IO a forall (f :: * -> *) a. Applicative f => a -> f a pure Notification -> IO () noNotifications noNotifications :: Notification -> IO () noNotifications :: Notification -> IO () noNotifications Notification _ = () -> IO () forall a. a -> IO a forall (f :: * -> *) a. Applicative f => a -> f a pure () hideException :: (a -> IO ()) -> (a -> IO ()) hideException :: forall a. (a -> IO ()) -> a -> IO () hideException a -> IO () f a a = a -> IO () f a a IO () -> IO () -> IO () forall a. IO a -> IO a -> IO a `catchAll_` () -> IO () forall a. a -> IO a forall (f :: * -> *) a. Applicative f => a -> f a pure () initLinuxNotify :: IO (Notification -> IO ()) initLinuxNotify :: IO (Notification -> IO ()) initLinuxNotify = do Bool found <- Maybe String -> Bool forall a. Maybe a -> Bool isJust (Maybe String -> Bool) -> IO (Maybe String) -> IO Bool forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b <$> String -> IO (Maybe String) findExecutable String "notify-send" (Notification -> IO ()) -> IO (Notification -> IO ()) forall a. a -> IO a forall (f :: * -> *) a. Applicative f => a -> f a pure ((Notification -> IO ()) -> IO (Notification -> IO ())) -> (Notification -> IO ()) -> IO (Notification -> IO ()) forall a b. (a -> b) -> a -> b $ if Bool found then Notification -> IO () linuxNotify else Notification -> IO () noNotifications linuxNotify :: Notification -> IO () linuxNotify :: Notification -> IO () linuxNotify Notification {Text $sel:title:Notification :: Notification -> Text title :: Text title, Text $sel:text:Notification :: Notification -> Text text :: Text text} = String -> [String] -> IO () callProcess String "notify-send" [Text -> String T.unpack Text title, Text -> String T.unpack Text text] macNotify :: Notification -> IO () macNotify :: Notification -> IO () macNotify Notification {Text $sel:title:Notification :: Notification -> Text title :: Text title, Text $sel:text:Notification :: Notification -> Text text :: Text text} = String -> [String] -> IO () callProcess String "osascript" [String "-e", String "display notification \"" String -> String -> String forall a. Semigroup a => a -> a -> a <> Text -> String macEscape Text text String -> String -> String forall a. Semigroup a => a -> a -> a <> String "\" with title \"" String -> String -> String forall a. Semigroup a => a -> a -> a <> Text -> String macEscape Text title String -> String -> String forall a. Semigroup a => a -> a -> a <> String "\""] macEscape :: T.Text -> String macEscape :: Text -> String macEscape = (Char -> String) -> String -> String forall (t :: * -> *) a b. Foldable t => (a -> [b]) -> t a -> [b] concatMap Char -> String esc (String -> String) -> (Text -> String) -> Text -> String forall b c a. (b -> c) -> (a -> b) -> a -> c . Text -> String T.unpack where esc :: Char -> String esc Char '\\' = String "\\\\" esc Char '"' = String "\\\"" esc Char c = [Char c] initWslNotify :: IO (Notification -> IO ()) initWslNotify :: IO (Notification -> IO ()) initWslNotify = String -> Notification -> IO () wslNotify (String -> Notification -> IO ()) -> IO String -> IO (Notification -> IO ()) forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b <$> IO String savePowershellScript wslNotify :: FilePath -> Notification -> IO () wslNotify :: String -> Notification -> IO () wslNotify String path Notification {Text $sel:title:Notification :: Notification -> Text title :: Text title, Text $sel:text:Notification :: Notification -> Text text :: Text text} = String -> [String] -> IO () callProcess String "powershell.exe" [String "-File", String path, Text -> String T.unpack Text title, Text -> String T.unpack Text text] initWinNotify :: IO (Notification -> IO ()) initWinNotify :: IO (Notification -> IO ()) initWinNotify = String -> Notification -> IO () winNotify (String -> Notification -> IO ()) -> IO String -> IO (Notification -> IO ()) forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b <$> IO String savePowershellScript winNotify :: FilePath -> Notification -> IO () winNotify :: String -> Notification -> IO () winNotify String path Notification {Text $sel:title:Notification :: Notification -> Text title :: Text title, Text $sel:text:Notification :: Notification -> Text text :: Text text} = String -> [String] -> IO () callProcess String "powershell.exe" [String "-File", String path, Text -> String T.unpack Text title, Text -> String T.unpack Text text] savePowershellScript :: IO FilePath savePowershellScript :: IO String savePowershellScript = do String appDir <- String -> IO String getAppUserDataDirectory String "simplex" Bool -> String -> IO () createDirectoryIfMissing Bool False String appDir let psScript :: String psScript = String -> String -> String combine String appDir String "win-toast-notify.ps1" String -> String -> IO () writeFile String psScript String "[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null\n\ \$Template = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02)\n\ \$RawXml = [xml] $Template.GetXml()\n\ \($RawXml.toast.visual.binding.text|where {$_.id -eq \"1\"}).AppendChild($RawXml.CreateTextNode($args[0])) > $null\n\ \($RawXml.toast.visual.binding.text|where {$_.id -eq \"2\"}).AppendChild($RawXml.CreateTextNode($args[1])) > $null\n\ \$SerializedXml = New-Object Windows.Data.Xml.Dom.XmlDocument\n\ \$SerializedXml.LoadXml($RawXml.OuterXml)\n\ \$Toast = [Windows.UI.Notifications.ToastNotification]::new($SerializedXml)\n\ \$Toast.Tag = \"simplex-chat\"\n\ \$Toast.Group = \"simplex-chat\"\n\ \$Toast.ExpirationTime = [DateTimeOffset]::Now.AddMinutes(1)\n\ \$Notifier = [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier(\"PowerShell\")\n\ \$Notifier.Show($Toast);\n" String -> IO String forall a. a -> IO a forall (m :: * -> *) a. Monad m => a -> m a return String psScript