{-# 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