{-# Language OverloadedStrings, BangPatterns #-}
module Client.Image.Message
( MessageRendererParams(..)
, RenderMode(..)
, IdentifierColorMode(..)
, defaultRenderParams
, msgImage
, metadataImg
, ignoreImage
, quietIdentifier
, coloredUserInfo
, coloredIdentifier
, cleanText
, cleanChar
, nickPad
, timeImage
, drawWindowLine
, parseIrcTextWithNicks
) where
import Client.Configuration (PaddingMode(..))
import Client.Image.LineWrap
import Client.Image.MircFormatting
import Client.Image.PackedImage
import Client.Image.Palette
import Client.Message
import Client.State.DCC (isSend)
import Client.State.Window
import Client.UserHost
import Control.Lens
import Data.Char
import Data.Hashable (hash)
import Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as HashMap
import Data.HashSet (HashSet)
import qualified Data.HashSet as HashSet
import Data.List
import Data.Text (Text)
import qualified Data.Text as Text
import Data.Time
import qualified Data.Vector as Vector
import Graphics.Vty.Attributes
import Irc.Codes
import Irc.Identifier
import Irc.Message
import Irc.RawIrcMsg
import Irc.UserInfo
import Text.Read
data MessageRendererParams = MessageRendererParams
{ rendStatusMsg :: [Char]
, rendUserSigils :: [Char]
, rendNicks :: HashSet Identifier
, rendMyNicks :: HashSet Identifier
, rendPalette :: Palette
, rendAccounts :: HashMap Identifier UserAndHost
}
defaultRenderParams :: MessageRendererParams
defaultRenderParams = MessageRendererParams
{ rendStatusMsg = ""
, rendUserSigils = ""
, rendNicks = HashSet.empty
, rendMyNicks = HashSet.empty
, rendPalette = defaultPalette
, rendAccounts = HashMap.empty
}
msgImage ::
ZonedTime ->
MessageRendererParams ->
MessageBody ->
(Image', Image', Image')
msgImage when params body = (prefix, image, full)
where
si = statusMsgImage (rendStatusMsg params)
prefix = si <> prefixImage params body
image = bodyImage NormalRender params body
full =
mconcat
[ datetimeImage (rendPalette params) when
, statusMsgImage (rendStatusMsg params)
, bodyImage DetailedRender params body
]
cleanChar :: Char -> Char
cleanChar x
| x < '\x20' = chr (0x2400 + ord x)
| x == '\DEL' = '\x2421'
| isControl x = '\xfffd'
| otherwise = x
cleanText :: Text -> Text
cleanText = Text.map cleanChar
errorPrefix ::
MessageRendererParams ->
Image'
errorPrefix params =
text' (view palError (rendPalette params)) "error" <>
char defAttr ':'
normalPrefix :: MessageRendererParams -> Image'
normalPrefix params =
text' (view palLabel (rendPalette params)) "client" <>
char defAttr ':'
statusMsgImage :: [Char] -> Image'
statusMsgImage modes
| null modes = mempty
| otherwise = "(" <> string statusMsgColor modes <> ") "
where
statusMsgColor = withForeColor defAttr red
prefixImage ::
MessageRendererParams ->
MessageBody -> Image'
prefixImage params body =
case body of
IrcBody irc -> ircLinePrefix params irc
ErrorBody{} -> errorPrefix params
NormalBody{} -> normalPrefix params
bodyImage ::
RenderMode ->
MessageRendererParams ->
MessageBody -> Image'
bodyImage rm params body =
case body of
IrcBody irc | NormalRender <- rm -> ircLineImage params irc
| DetailedRender <- rm -> fullIrcLineImage params irc
ErrorBody txt -> parseIrcText txt
NormalBody txt -> parseIrcText txt
timeImage :: Palette -> TimeOfDay -> Image'
timeImage palette
= string (view palTime palette)
. formatTime defaultTimeLocale "%R "
datetimeImage :: Palette -> ZonedTime -> Image'
datetimeImage palette
= string (view palTime palette)
. formatTime defaultTimeLocale "%m-%d %T "
data RenderMode
= NormalRender
| DetailedRender
nickPad ::
PaddingMode ->
Image' ->
Image'
nickPad mode img =
case mode of
LeftPadding w | w > iw -> mkpad (w-iw) <> img
RightPadding w | w > iw -> img <> mkpad (w-iw)
_ -> img
where
iw = imageWidth img
mkpad n = string defAttr (replicate n ' ')
ircLinePrefix ::
MessageRendererParams ->
IrcMsg -> Image'
ircLinePrefix !rp body =
let pal = rendPalette rp
sigils = rendUserSigils rp
myNicks = rendMyNicks rp
rm = NormalRender
who n = string (view palSigil pal) sigils <> ui
where
baseUI = coloredUserInfo pal rm myNicks n
ui = case rendAccounts rp ^? ix (userNick n) . uhAccount of
Just acct
| Text.null acct -> "~" <> baseUI
| mkId acct /= userNick n -> baseUI <> "(" <> text' defAttr (cleanText acct) <> ")"
_ -> baseUI
in
case body of
Join {} -> mempty
Part {} -> mempty
Quit {} -> mempty
Ping {} -> mempty
Pong {} -> mempty
Nick {} -> mempty
Topic src _ _ ->
who src <> " changed the topic:"
Kick kicker _channel kickee _reason ->
who kicker <>
" kicked " <>
coloredIdentifier pal NormalIdentifier myNicks kickee <>
":"
Notice src _ _ ->
who src <>
string (withForeColor defAttr red) ":"
Privmsg src _ _ -> who src <> ":"
Wallops src _ ->
string (withForeColor defAttr red) "WALL " <> who src <> ":"
Ctcp src _dst "ACTION" _txt ->
string (withForeColor defAttr blue) "* " <> who src
Ctcp src _dst "DCC" txt | isSend txt ->
who src <> " offers a DCC transfer"
Ctcp {} -> mempty
CtcpNotice src _dst _cmd _txt ->
string (withForeColor defAttr red) "! " <> who src
Error {} -> string (view palError pal) "ERROR" <> ":"
Reply code _ -> replyCodePrefix code
UnknownMsg irc ->
case view msgPrefix irc of
Just ui -> who ui
Nothing -> string (view palError pal) "?"
Cap cmd ->
text' (withForeColor defAttr magenta) (renderCapCmd cmd) <> ":"
Mode nick _ _ -> who nick <> " set mode:"
Authenticate{} -> "AUTHENTICATE"
BatchStart{} -> mempty
BatchEnd{} -> mempty
Account user _ -> who user <> " account:"
Chghost ui _ _ -> who ui <> " chghost:"
ircLineImage ::
MessageRendererParams ->
IrcMsg -> Image'
ircLineImage !rp body =
let pal = rendPalette rp
myNicks = rendMyNicks rp
nicks = rendNicks rp
in
case body of
Join {} -> mempty
Part {} -> mempty
Quit {} -> mempty
Ping {} -> mempty
Pong {} -> mempty
BatchStart {} -> mempty
BatchEnd {} -> mempty
Nick {} -> mempty
Authenticate{} -> "***"
Error txt -> parseIrcText txt
Topic _ _ txt -> parseIrcTextWithNicks pal myNicks nicks False txt
Kick _ _ _ txt -> parseIrcTextWithNicks pal myNicks nicks False txt
Notice _ _ txt -> parseIrcTextWithNicks pal myNicks nicks False txt
Privmsg _ _ txt -> parseIrcTextWithNicks pal myNicks nicks False txt
Wallops _ txt -> parseIrcTextWithNicks pal myNicks nicks False txt
Ctcp _ _ "ACTION" txt -> parseIrcTextWithNicks pal myNicks nicks False txt
Ctcp {} -> mempty
CtcpNotice _ _ cmd txt -> parseIrcText cmd <> " " <>
parseIrcTextWithNicks pal myNicks nicks False txt
Reply code params -> renderReplyCode pal NormalRender code params
UnknownMsg irc ->
text' defAttr (view msgCommand irc) <>
char defAttr ' ' <>
separatedParams (view msgParams irc)
Cap cmd -> text' defAttr (cleanText (capCmdText cmd))
Mode _ _ params -> ircWords params
Account _ acct -> if Text.null acct then "*" else text' defAttr (cleanText acct)
Chghost _ user host -> text' defAttr (cleanText user) <> " " <> text' defAttr (cleanText host)
fullIrcLineImage ::
MessageRendererParams ->
IrcMsg -> Image'
fullIrcLineImage !rp body =
let quietAttr = view palMeta pal
pal = rendPalette rp
sigils = rendUserSigils rp
myNicks = rendMyNicks rp
nicks = rendNicks rp
rm = DetailedRender
plainWho = coloredUserInfo pal rm myNicks
who n =
string (view palSigil pal) sigils <>
plainWho n <>
case rendAccounts rp ^? ix (userNick n) . uhAccount of
Just acct
| not (Text.null acct) -> text' quietAttr ("(" <> cleanText acct <> ")")
_ -> ""
in
case body of
Nick old new ->
string quietAttr "nick " <>
who old <>
" is now known as " <>
coloredIdentifier pal NormalIdentifier myNicks new
Join nick _chan acct ->
string quietAttr "join " <>
plainWho nick <>
if Text.null acct
then mempty
else text' quietAttr ("(" <> cleanText acct <> ")")
Part nick _chan mbreason ->
string quietAttr "part " <>
who nick <>
foldMap (\reason -> string quietAttr " (" <>
parseIrcText reason <>
string quietAttr ")") mbreason
Quit nick mbreason ->
string quietAttr "quit " <>
who nick <>
foldMap (\reason -> string quietAttr " (" <>
parseIrcText reason <>
string quietAttr ")") mbreason
Kick kicker _channel kickee reason ->
string quietAttr "kick " <>
who kicker <>
" kicked " <>
coloredIdentifier pal NormalIdentifier myNicks kickee <>
": " <>
parseIrcText reason
Topic src _dst txt ->
string quietAttr "tpic " <>
who src <>
" changed the topic: " <>
parseIrcText txt
Notice src _dst txt ->
string quietAttr "note " <>
who src <>
string (withForeColor defAttr red) ": " <>
parseIrcTextWithNicks pal myNicks nicks False txt
Privmsg src _dst txt ->
string quietAttr "chat " <>
who src <> ": " <>
parseIrcTextWithNicks pal myNicks nicks False txt
Wallops src txt ->
string quietAttr "wall " <>
who src <> ": " <>
parseIrcTextWithNicks pal myNicks nicks False txt
Ctcp src _dst "ACTION" txt ->
string quietAttr "actp " <>
string (withForeColor defAttr blue) "* " <>
who src <> " " <>
parseIrcTextWithNicks pal myNicks nicks False txt
Ctcp src _dst cmd txt ->
string quietAttr "ctcp " <>
string (withForeColor defAttr blue) "! " <>
who src <> " " <>
parseIrcText cmd <>
if Text.null txt then mempty else separatorImage <> parseIrcText txt
CtcpNotice src _dst cmd txt ->
string quietAttr "ctcp " <>
string (withForeColor defAttr red) "! " <>
who src <> " " <>
parseIrcText cmd <>
if Text.null txt then mempty else separatorImage <> parseIrcText txt
Ping params ->
"PING " <> separatedParams params
Pong params ->
"PONG " <> separatedParams params
Error reason ->
string (view palError pal) "ERROR " <>
parseIrcText reason
Reply code params ->
renderReplyCode pal DetailedRender code params
UnknownMsg irc ->
foldMap (\ui -> coloredUserInfo pal rm myNicks ui <> char defAttr ' ')
(view msgPrefix irc) <>
text' defAttr (view msgCommand irc) <>
char defAttr ' ' <>
separatedParams (view msgParams irc)
Cap cmd ->
text' (withForeColor defAttr magenta) (renderCapCmd cmd) <>
text' defAttr ": " <>
text' defAttr (cleanText (capCmdText cmd))
Mode nick _chan params ->
string quietAttr "mode " <>
who nick <> " set mode: " <>
ircWords params
Authenticate{} -> "AUTHENTICATE ***"
BatchStart{} -> "BATCH +"
BatchEnd{} -> "BATCH -"
Account src acct ->
string quietAttr "acct " <>
who src <> ": " <>
if Text.null acct then "*" else text' defAttr (cleanText acct)
Chghost user newuser newhost ->
string quietAttr "chng " <>
who user <> ": " <>
text' defAttr (cleanText newuser) <> " " <> text' defAttr (cleanText newhost)
renderCapCmd :: CapCmd -> Text
renderCapCmd cmd =
case cmd of
CapLs {} -> "caps-available"
CapList {} -> "caps-active"
CapAck {} -> "caps-acknowledged"
CapNak {} -> "caps-rejected"
CapNew {} -> "caps-offered"
CapDel {} -> "caps-withdrawn"
separatorImage :: Image'
separatorImage = char (withForeColor defAttr blue) '·'
separatedParams :: [Text] -> Image'
separatedParams = mconcat . intersperse separatorImage . map parseIrcText
ircWords :: [Text] -> Image'
ircWords = mconcat . intersperse (char defAttr ' ') . map parseIrcText
replyCodePrefix :: ReplyCode -> Image'
replyCodePrefix code =
text' attr (replyCodeText info) <>
char defAttr ':'
where
info = replyCodeInfo code
color = case replyCodeType info of
ClientServerReply -> magenta
CommandReply -> green
ErrorReply -> red
UnknownReply -> yellow
attr = withForeColor defAttr color
renderReplyCode :: Palette -> RenderMode -> ReplyCode -> [Text] -> Image'
renderReplyCode pal rm code@(ReplyCode w) params =
case rm of
DetailedRender -> string attr (shows w " ") <> rawParamsImage
NormalRender ->
case code of
RPL_WHOISUSER -> whoisUserParamsImage
RPL_WHOWASUSER -> whoisUserParamsImage
RPL_WHOISACTUALLY-> param_3_4_Image
RPL_WHOISIDLE -> whoisIdleParamsImage
RPL_WHOISCHANNELS-> param_3_3_Image
RPL_WHOISACCOUNT -> param_3_4_Image
RPL_WHOISSERVER -> whoisServerParamsImage
RPL_WHOISSECURE -> param_3_3_Image
RPL_WHOISOPERATOR-> param_3_3_Image
RPL_WHOISCERTFP -> param_3_3_Image
RPL_WHOISSPECIAL -> param_3_3_Image
RPL_WHOISHOST -> param_3_3_Image
RPL_ENDOFWHOIS -> ""
RPL_ENDOFWHOWAS -> ""
RPL_TOPIC -> param_3_3_Image
RPL_TOPICWHOTIME -> topicWhoTimeParamsImage
RPL_CHANNEL_URL -> param_3_3_Image
RPL_CREATIONTIME -> creationTimeParamsImage
RPL_INVITING -> params_2_3_Image
RPL_TESTLINE -> testlineParamsImage
_ -> rawParamsImage
where
label t = text' (view palLabel pal) t <> ": "
rawParamsImage = separatedParams params'
params' = case rm of
DetailedRender -> params
NormalRender -> drop 1 params
info = replyCodeInfo code
color = case replyCodeType info of
ClientServerReply -> magenta
CommandReply -> green
ErrorReply -> red
UnknownReply -> yellow
attr = withForeColor defAttr color
params_2_3_Image =
case params of
[_, p, _] -> parseIrcText' False p
_ -> rawParamsImage
param_3_3_Image =
case params of
[_, _, txt] -> parseIrcText' False txt
_ -> rawParamsImage
param_3_4_Image =
case params of
[_, _, p, _] -> parseIrcText' False p
_ -> rawParamsImage
topicWhoTimeParamsImage =
case params of
[_, _, who, time] ->
label "set by" <>
text' defAttr who <>
label " at" <>
string defAttr (prettyUnixTime (Text.unpack time))
_ -> rawParamsImage
creationTimeParamsImage =
case params of
[_, _, time, _] -> string defAttr (prettyUnixTime (Text.unpack time))
_ -> rawParamsImage
whoisUserParamsImage =
case params of
[_, nick, user, host, _, real] ->
text' (withStyle defAttr bold) nick <>
text' (view palLabel pal) "!" <>
parseIrcText' False user <>
text' (view palLabel pal) "@" <>
parseIrcText' False host <>
label " gecos" <>
parseIrcText' False real
_ -> rawParamsImage
whoisIdleParamsImage =
case params of
[_, _, idle, signon, _txt] ->
string defAttr (prettyTime 1 (Text.unpack idle)) <>
label " sign-on" <>
string defAttr (prettyUnixTime (Text.unpack signon))
_ -> rawParamsImage
whoisServerParamsImage =
case params of
[_, _, host, txt] ->
parseIrcText' False host <>
label " note" <>
parseIrcText' False txt
_ -> rawParamsImage
testlineParamsImage =
case params of
[_, name, mins, mask, msg] ->
text' defAttr name <>
label " duration" <>
string defAttr (prettyTime 60 (Text.unpack mins)) <>
label " mask" <>
text' defAttr mask <>
label " reason" <>
text' defAttr msg
_ -> rawParamsImage
prettyUnixTime :: String -> String
prettyUnixTime str =
case parseTimeM False defaultTimeLocale "%s" str of
Nothing -> str
Just t -> formatTime defaultTimeLocale "%A %B %e, %Y %H:%M:%S %Z" (t :: UTCTime)
prettyTime :: Int -> String -> String
prettyTime scale str =
case readMaybe str of
Nothing -> str
Just 0 -> "0s"
Just n -> intercalate " "
$ map (\(u,i) -> show i ++ [u])
$ filter (\x -> snd x /= 0)
$ zip "dhms" [d,h,m,s :: Int]
where
n0 = n * scale
(n1,s) = quotRem n0 60
(n2,m) = quotRem n1 60
(d ,h) = quotRem n2 24
data IdentifierColorMode
= PrivmsgIdentifier
| NormalIdentifier
coloredIdentifier ::
Palette ->
IdentifierColorMode ->
HashSet Identifier ->
Identifier ->
Image'
coloredIdentifier palette icm myNicks ident =
text' color (idText ident)
where
color
| ident `HashSet.member` myNicks =
case icm of
PrivmsgIdentifier -> view palSelfHighlight palette
NormalIdentifier -> view palSelf palette
| otherwise = v Vector.! i
v = view palNicks palette
i = hash ident `mod` Vector.length v
coloredUserInfo ::
Palette ->
RenderMode ->
HashSet Identifier ->
UserInfo ->
Image'
coloredUserInfo palette NormalRender myNicks ui =
coloredIdentifier palette NormalIdentifier myNicks (userNick ui)
coloredUserInfo palette DetailedRender myNicks !ui =
mconcat
[ coloredIdentifier palette NormalIdentifier myNicks (userNick ui)
, aux '!' (userName ui)
, aux '@' (userHost ui)
]
where
quietAttr = view palMeta palette
aux x xs
| Text.null xs = mempty
| otherwise = char quietAttr x <> text' quietAttr xs
quietIdentifier :: Palette -> Identifier -> Image'
quietIdentifier palette ident =
text' (view palMeta palette) (idText ident)
parseIrcTextWithNicks ::
Palette ->
HashSet Identifier ->
HashSet Identifier ->
Bool ->
Text ->
Image'
parseIrcTextWithNicks palette myNick nicks explicit txt
| Text.any isControl txt = parseIrcText' explicit txt
| otherwise = highlightNicks palette myNick nicks txt
highlightNicks ::
Palette ->
HashSet Identifier ->
HashSet Identifier ->
Text -> Image'
highlightNicks palette myNicks nicks txt = foldMap highlight1 txtParts
where
txtParts = nickSplit txt
allNicks = HashSet.union myNicks nicks
highlight1 part
| HashSet.member partId allNicks = coloredIdentifier palette PrivmsgIdentifier myNicks partId
| otherwise = text' defAttr part
where
partId = mkId part
metadataImg :: IrcSummary -> Maybe (Image', Identifier, Maybe Identifier)
metadataImg msg =
case msg of
QuitSummary who -> Just (char (withForeColor defAttr red ) 'x', who, Nothing)
PartSummary who -> Just (char (withForeColor defAttr red ) '-', who, Nothing)
JoinSummary who -> Just (char (withForeColor defAttr green ) '+', who, Nothing)
CtcpSummary who -> Just (char (withForeColor defAttr white ) 'C', who, Nothing)
NickSummary old new -> Just (char (withForeColor defAttr yellow) '>', old, Just new)
ChngSummary who -> Just (char (withForeColor defAttr blue ) '*', who, Nothing)
AcctSummary who -> Just (char (withForeColor defAttr blue ) '*', who, Nothing)
_ -> Nothing
ignoreImage :: Image'
ignoreImage = char (withForeColor defAttr yellow) 'I'
drawWindowLine ::
Palette ->
Int ->
PaddingMode ->
WindowLine ->
[Image']
drawWindowLine palette w padAmt wl = wrap (drawPrefix wl) (view wlImage wl)
where
drawTime = timeImage palette . unpackTimeOfDay
padNick = nickPad padAmt
wrap pfx body = reverse (lineWrapPrefix w pfx body)
drawPrefix = views wlTimestamp drawTime <>
views wlPrefix padNick