-- Author:     Andy Stewart <lazycat.manatee@gmail.com>
-- Maintainer: Andy Stewart <lazycat.manatee@gmail.com>
-- 
-- Copyright (C) 2010 Andy Stewart, all rights reserved.
-- 
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- any later version.
-- 
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-- GNU General Public License for more details.
-- 
-- You should have received a copy of the GNU General Public License
-- along with this program.  If not, see <http://www.gnu.org/licenses/>.

{-# LANGUAGE ExistentialQuantification, RankNTypes, DeriveDataTypeable, ScopedTypeVariables #-}
{-# LANGUAGE OverloadedStrings #-}
module Manatee.Extension.IrcClient.IrcView where

import Control.Applicative
import Control.Concurrent.STM
import Control.Monad
import Data.Map (Map)
import Data.ByteString (ByteString)
import Data.Text.Lazy (Text)
import Data.Typeable
import Graphics.UI.Gtk hiding (Statusbar, statusbarNew, get, Language)
import Graphics.UI.Gtk.Gdk.SerializedEvent
import Graphics.UI.Gtk.SourceView
import Language.Translate.Google
import Manatee.Core.DBus
import Manatee.Core.PageView
import Manatee.Core.Types
import Manatee.Extension.IrcClient.DBus
import Manatee.Extension.IrcClient.HighlightNick
import Manatee.Extension.IrcClient.IrcBuffer
import Manatee.Extension.IrcClient.Types
import Manatee.Toolkit.General.Basic
import Manatee.Toolkit.General.ByteString
import Manatee.Toolkit.General.Map
import Manatee.Toolkit.General.Maybe
import Manatee.Toolkit.General.Process
import Manatee.Toolkit.General.STM
import Manatee.Toolkit.General.String
import Manatee.Toolkit.Gtk.Concurrent
import Manatee.Toolkit.Gtk.Gtk
import Manatee.Toolkit.Gtk.Multiline
import Manatee.Toolkit.Gtk.ScrolledWindow
import Network.FastIRC.Messages
import Text.Morse

import qualified Data.ByteString.Char8 as B
import qualified Data.Map as M
import qualified Data.Set as S
import qualified Data.ByteString.UTF8 as UTF8

data IrcView =
    IrcView {ircViewPlugId            :: TVar PagePlugId
            ,ircViewScrolledWindow    :: ScrolledWindow
            ,ircViewView              :: SourceView
            ,ircViewBuffer            :: IrcBuffer
            ,ircViewBroadcastChannel  :: ViewChannel IrcBufferSignal
            }
    deriving Typeable

instance PageBuffer IrcBuffer where
    pageBufferGetName           = return . ircBufferChannel
    pageBufferSetName _ _       = return ()
    pageBufferClient            = ircBufferClient
    pageBufferCreateView a pId  = PageViewWrap <$> ircViewNew a pId
    pageBufferMode              = ircBufferMode

instance PageView IrcView where
    pageViewBuffer              = PageBufferWrap . ircViewBuffer
    pageViewPlugId              = ircViewPlugId
    pageViewFocus               = widgetGrabFocus . ircViewView
    pageViewCut                 = ircViewCut
    pageViewCopy                = ircViewCopy
    pageViewPaste               = ircViewPaste
    pageViewScrolledWindow      = ircViewScrolledWindow
    pageViewHandleKeyAction     = ircViewHandleKeyAction
    pageViewScrollToTop         = ircViewScrollToTop
    pageViewScrollToBottom      = ircViewScrollToBottom
    pageViewScrollVerticalPage  = ircViewScrollVerticalPage
    pageViewScrollVerticalStep  = ircViewScrollVerticalStep

-- | Create new irc view.
ircViewNew :: IrcBuffer -> PagePlugId -> IO IrcView
ircViewNew sb plugId = do
  -- Create plug id.
  pId <- newTVarIO plugId

  -- Create UI frame.
  scrolledWindow <- scrolledWindowNew_

  -- Create source view.
  ircView <- sourceViewNewWithBuffer (ircBufferBuffer sb)
  scrolledWindow `containerAdd` ircView
  forM_ [StateNormal, StateActive, StatePrelight, StateSelected, StateInsensitive] 
    $ \state -> widgetModifyBg ircView state (nickColorToColor backgroundColor)

  -- Broadcast channel.
  channel <- createViewChannel (ircBufferBroadcastChannel sb) ircView

  let sv = IrcView pId scrolledWindow ircView sb channel

  -- Customize setup.
  sourceViewSetHighlightCurrentLine ircView True      -- highlight current line.
  sourceViewSetInsertSpacesInsteadOfTabs ircView True -- use space instead tabs
  sourceViewSetShowLineNumbers ircView showLineNumber -- show line number
  textViewSetCursorVisible ircView True               -- make cursor visible
  textViewSetWrapMode ircView WrapWord                -- auto wrap text

  -- Set fixed-width font, otherwise wrap mode can't work.
  fontDescr <- fontDescriptionFromString "Monospace"
  widgetModifyFont ircView (Just fontDescr)

  -- Update time stamp at right.
  gutter <- sourceViewGetGutter ircView timeStampPosition
  cell   <- cellRendererTextNew
  sourceGutterInsert gutter cell 0

  -- Set gutter data.
  sourceGutterSetCellDataFunc gutter cell $ \ c l _ -> do
         -- Display time stamp.
         timeStampMap <- readTVarIO $ ircBufferTimeStamp sb
         let (timeStamp, timeColor) = 
                 case findMinMatch timeStampMap (\ line _ -> line == l) of
                   Just x  -> snd x
                   Nothing -> (defaultTimeStamp, Color 0 0 0)
         set (castToCellRendererText c) [cellText := timeStamp]
         set (castToCellRendererText c) [cellTextForegroundColor := timeColor]

  -- Set gutter size.
  sourceGutterSetCellSizeFunc gutter cell $ \ c -> 
      -- -1 mean cell renderer will adjust width with chars dynamically.
      set (castToCellRendererText c) [cellTextWidthChars := (-1)]

  -- Read channel.
  ircViewListenChannel sv

  return sv

-- | Listen broadcast channel for draw view synchronous.
ircViewListenChannel :: IrcView -> IO ()
ircViewListenChannel view = 
  listenViewChannel (ircViewBroadcastChannel view) $ \ signal -> 
      case signal of
        SwitchTranslateLanguage -> do
          -- Update language status
          lang <- readTVarIO $ ircBufferTranslateLanguage $ ircViewBuffer view
          pageViewUpdateInfoStatus view "Translate" ("Translate to (" ++ show lang ++ ")")
        BufferChanged -> do
          -- Scroll screen if prompt mark at left of insert mark.
          -- Otherwise don't screen, because user screen download to read old messages.
          let buffer = ircViewBuffer view
              textBuffer = ircBufferBuffer buffer
              ircView = ircViewView view
          readTVarIO (ircBufferScrollMark buffer)
              >?>= \ mark -> do
                  iter   <- textBufferGetIterAtMark textBuffer mark
                  (y, _) <- textViewGetLineYrange ircView iter
                  (Rectangle _ vy _ vh) <- textViewGetVisibleRect ircView
                  when (y >= vy + vh) $ 
                       textViewScrollMarkOnscreen ircView mark

-- | Swtich translate language.
ircViewSwitchTranslateLanguage :: IrcView -> IO ()
ircViewSwitchTranslateLanguage IrcView {ircViewBuffer           = buffer
                                       ,ircViewBroadcastChannel = channel} = do
  modifyTVarIO (ircBufferTranslateLanguage buffer) 
      $ \lang -> if lang == targetLanguage
                    then sourceLanguage
                    else targetLanguage
  writeTChanIO (viewChannel channel) SwitchTranslateLanguage

-- | Handle key action.
ircViewHandleKeyAction :: IrcView -> Text -> SerializedEvent -> IO ()
ircViewHandleKeyAction view keystoke sEvent = 
  case M.lookup keystoke ircViewKeymap of
    -- Execute action when found in keymap.
    Just action -> action view
    -- Otherwise propagate event.
    Nothing -> widgetPropagateEvent (ircViewView view) sEvent 

-- | Begin.
ircViewScrollToTop :: IrcView -> IO ()
ircViewScrollToTop a = do
  textViewBegin (ircViewView a) (ircViewScrolledWindow a)
  ircViewApplySelectionMark a

-- | End.
ircViewScrollToBottom :: IrcView -> IO ()
ircViewScrollToBottom a = do
  textViewEnd (ircViewView a) (ircViewScrolledWindow a)
  ircViewApplySelectionMark a

-- | Scroll page vertically.
ircViewScrollVerticalPage :: Bool -> IrcView -> IO ()
ircViewScrollVerticalPage isDown a = do
  let sw = ircViewScrolledWindow a
      tv = ircViewView a
  pageInc <- (<=<) adjustmentGetPageIncrement scrolledWindowGetVAdjustment sw
  textViewScrollVertical tv sw (if isDown then pageInc else (- pageInc))
  ircViewApplySelectionMark a

-- | Scroll step vertically.
ircViewScrollVerticalStep :: Bool -> IrcView -> IO ()
ircViewScrollVerticalStep isDown a = do
  let sw = ircViewScrolledWindow a
      tv = ircViewView a
  ti <- textViewGetTextIter tv
  (_, lineHeight) <- textViewGetLineYrange tv ti
  let stepInc = i2d lineHeight
  textViewScrollVertical tv sw (if isDown then stepInc else (- stepInc))
  ircViewApplySelectionMark a

-- | Send morse code.
ircViewSendMorse :: IrcView -> IO ()
ircViewSendMorse view@(IrcView {ircViewBuffer = buffer
                               ,ircViewView   = vView}) = do
  -- Scroll mark on screen visible area.
  let textBuffer = ircBufferBuffer buffer
      promptMark = ircBufferPromptMark buffer
  textViewScrollMarkOnscreen vView promptMark

  -- Get input string.
  startIter <- textBufferGetIterAtMark textBuffer promptMark
  endIter   <- textBufferGetEndIter textBuffer
  text      <- textBufferGetText textBuffer startIter endIter True

  -- Clean input.
  ircViewCleanInput view 

  if isBlankString text
     -- Avoid send blank string, server will ignore blank message.
     then pageViewUpdateOutputStatus view "Ignored blank ..." Nothing
     -- Otherwise send message to server.
     else ircViewSend view (UTF8.fromString $ encodeMorse text)

-- | Send irc message.
ircViewSendMessage :: Bool -> IrcView -> IO ()
ircViewSendMessage isTranslate
                   view@(IrcView {ircViewBuffer = buffer
                                 ,ircViewView   = vView}) = do
  -- Scroll mark on screen visible area.
  let promptMark = ircBufferPromptMark buffer
  textViewScrollMarkOnscreen vView promptMark

  -- Get input string.
  text <- ircViewGetInput view

  -- Clean input.
  ircViewCleanInput view

  if isBlankByteString text
     -- Avoid send blank string, server will ignore blank message.
     then pageViewUpdateOutputStatus view "Ignored blank ..." Nothing
     -- Otherwise send message to server.
     else 
       if isTranslate 
          -- Try get translation.
          then 
            forkGuiIO_ (do
                         pageViewUpdateOutputStatus view "Translation ..." Nothing
                         lang <- readTVarIO $ ircBufferTranslateLanguage buffer
                         translate text Nothing lang)
                       $ \result -> 
                           case result of
                             -- Send original message if translate failed.
                             Left _ -> do
                               pageViewUpdateOutputStatus view "Get translation failed, send original message." Nothing
                               ircViewSend view text
                             -- Otherwise send translation.
                             Right translation -> do
                                 pageViewUpdateOutputStatus view "Translation ... completed." Nothing
                                 ircViewSend view translation
          -- Send original message.
          else ircViewSend view text

-- | Select all.
ircViewSelectAll :: IrcView -> IO ()
ircViewSelectAll = textViewSelectAll . ircViewView

-- | Wrap delete action.
ircViewWrapDeleteAction :: IrcView -> IO Bool -> IO ()
ircViewWrapDeleteAction view action =
  unlessM action $ 
    pageViewUpdateOutputStatus view "Can't delete uneditable area." Nothing

-- | Delete lines.
ircViewDelLines :: IrcView -> IO ()
ircViewDelLines view =
  ircViewWrapDeleteAction view (textViewDelLines (ircViewView view))

-- | Delete.
ircViewDelete :: IrcView -> IO ()
ircViewDelete view = 
  textViewDelete (ircViewView view) True True >> return ()

-- | Delete forward char.
ircViewDeleteForwardChar :: IrcView -> IO ()
ircViewDeleteForwardChar view = 
  ircViewWrapDeleteAction view (textViewDeleteForwardChar (ircViewView view) True)

-- | Backward char.
ircViewDeleteBackwardChar :: IrcView -> IO ()
ircViewDeleteBackwardChar view = 
  ircViewWrapDeleteAction view (textViewDeleteBackwardChar (ircViewView view) True)

-- | Forward word.
ircViewDeleteForwardWord :: IrcView -> IO ()
ircViewDeleteForwardWord view = 
  ircViewWrapDeleteAction view (textViewDeleteForwardWord (ircViewView view) True)

-- | Backward word.
ircViewDeleteBackwardWord :: IrcView -> IO ()
ircViewDeleteBackwardWord view = 
  ircViewWrapDeleteAction view (textViewDeleteBackwardWord (ircViewView view) True)

-- | Delete to line end.
ircViewDeleteToLineEnd :: IrcView -> IO ()
ircViewDeleteToLineEnd view = 
  ircViewWrapDeleteAction view (textViewDeleteToLineEnd (ircViewView view) True)

-- | Delete to line start.
ircViewDeleteToLineStart :: IrcView -> IO ()
ircViewDeleteToLineStart view = 
  ircViewWrapDeleteAction view (textViewDeleteToLineStart (ircViewView view) True)

-- | Cut.
ircViewCut :: IrcView -> IO Bool
ircViewCut view = do
  textViewCut $ ircViewView view
  return True

-- | Copy.
ircViewCopy :: IrcView -> IO Bool
ircViewCopy view = do
  textViewCopy $ ircViewView view
  return True

-- | Paste.
ircViewPaste :: IrcView -> IO Bool
ircViewPaste view = do
  textViewPaste $ ircViewView view
  return True

-- | Forward line.
ircViewForwardLine :: IrcView -> IO ()
ircViewForwardLine a = do
    textViewForwardLine (ircViewView a) (ircViewScrolledWindow a)
    ircViewApplySelectionMark a

-- | Backward line.
ircViewBackwardLine :: IrcView -> IO ()
ircViewBackwardLine a = do
    textViewBackwardLine (ircViewView a) (ircViewScrolledWindow a)
    ircViewApplySelectionMark a

-- | Forward char.
ircViewForwardChar :: IrcView -> IO ()
ircViewForwardChar a = do
    textViewForwardChar (ircViewView a) (ircViewScrolledWindow a)
    ircViewApplySelectionMark a

-- | Backward char.
ircViewBackwardChar :: IrcView -> IO ()
ircViewBackwardChar a = do
    textViewBackwardChar (ircViewView a) (ircViewScrolledWindow a)
    ircViewApplySelectionMark a

-- | Forward word.
ircViewForwardWord :: IrcView -> IO ()
ircViewForwardWord a = do
    textViewForwardWord (ircViewView a) (ircViewScrolledWindow a)
    ircViewApplySelectionMark a

-- | Backward word.
ircViewBackwardWord :: IrcView -> IO ()
ircViewBackwardWord a = do
    textViewBackwardWord (ircViewView a) (ircViewScrolledWindow a)
    ircViewApplySelectionMark a

-- | Smart home.
ircViewSmartHome :: IrcView -> IO ()
ircViewSmartHome a = do
    textViewSmartHome $ ircViewView a
    ircViewApplySelectionMark a

-- | Smart end.
ircViewSmartEnd :: IrcView -> IO ()
ircViewSmartEnd a = do
    textViewSmartEnd $ ircViewView a
    ircViewApplySelectionMark a

-- | Set selection mark.
ircViewToggleSelectionMark :: IrcView -> IO ()
ircViewToggleSelectionMark = textViewToggleSelectionMark . ircViewView 

-- | Exchange selection mark.
ircViewExchangeSelectionMark :: IrcView -> IO ()
ircViewExchangeSelectionMark = textViewExchangeSelectionMark . ircViewView 

-- | Show selection mark.
ircViewApplySelectionMark :: IrcView -> IO ()
ircViewApplySelectionMark = textViewApplySelectionMark . ircViewView

-- | Newline.
ircViewNewline :: IrcView -> IO ()  
ircViewNewline = textViewNewLine . ircViewView

-- | Get text buffer.
ircViewGetTextBuffer :: IrcView -> IO TextBuffer 
ircViewGetTextBuffer = textViewGetBuffer . ircViewView

-- | Get source buffer.
ircViewGetSourceBuffer :: IrcView -> IO SourceBuffer
ircViewGetSourceBuffer sb = 
    castToSourceBuffer <$> ircViewGetTextBuffer sb

-- | Move to prompt position.
ircViewMoveToPrompt :: IrcView -> IO ()
ircViewMoveToPrompt IrcView {ircViewBuffer = buffer
                            ,ircViewView   = view} = 
  textViewPlaceCursorWithMark view (ircBufferPromptMark buffer)

-- | Clean input.
ircViewCleanInput :: IrcView -> IO ()
ircViewCleanInput IrcView {ircViewBuffer = buffer} = do
  -- Get iter range.
  let textBuffer = ircBufferBuffer buffer
      promptMark = ircBufferPromptMark buffer
  promptIter <- textBufferGetIterAtMark textBuffer promptMark 
  endIter    <- textBufferGetEndIter textBuffer

  -- Clean input.
  textBufferDelete textBuffer promptIter endIter

-- | Get input message.
ircViewGetInput :: IrcView -> IO ByteString
ircViewGetInput IrcView {ircViewBuffer = buffer} = do
  -- Get iter range.
  let textBuffer = ircBufferBuffer buffer
      promptMark = ircBufferPromptMark buffer
  promptIter <- textBufferGetIterAtMark textBuffer promptMark 
  endIter    <- textBufferGetEndIter textBuffer

  -- Return input message.
  textBufferGetByteString textBuffer promptIter endIter True

-- | Send message.
ircViewSend :: IrcView -> ByteString -> IO ()
ircViewSend view@(IrcView {ircViewBuffer = buffer}) 
            message = do
    nick <- readTVarIO $ ircBufferNick buffer
    -- Push message to buffer.
    ircBufferReceivePrivate buffer (B.pack nick) message
    -- Send message to server.
    let msg = showCommand $ PrivMsgCmd (S.singleton (B.pack $ ircBufferChannel buffer)) message
    mkIrcDaemonSignal (pageViewClient view) SendMessage (SendMessageArgs (ircBufferServer buffer) msg)

-- | Open url around pointer.
ircViewOpenUrl :: IrcView -> IO ()
ircViewOpenUrl view@(IrcView {ircViewBuffer = buffer}) = 
  textBufferGetTagText (ircBufferBuffer buffer) (ircBufferUrlColorTag buffer)
     >?>= \ url ->        
         mkDaemonSignal (pageViewClient view) NewTab (NewTabArgs "PageBrowser" url)

-- | Translate message around pointer.
ircViewTranslateMessage :: IrcView -> IO ()
ircViewTranslateMessage view@(IrcView {ircViewBuffer = 
                                                 IrcBuffer {ircBufferBuffer             = buffer
                                                           ,ircBufferMessageTag         = messageTag
                                                           ,ircBufferTranslateLanguage  = language}}) = do
  -- Use end line iter to match messageTag content (skip message head).
  iter <- textBufferGetCurrentLineEndIter_ buffer
  textIterBackwardChar iter
  textBufferGetTagByteStringWithIter buffer iter messageTag
     >?>= \ text -> 
         forkGuiIO_ (do
                      pageViewUpdateOutputStatus view "Fetch translation ..." Nothing
                      -- Translate reverse. :)
                      lang <- readTVarIO language
                      translate text Nothing (if lang == targetLanguage 
                                              then sourceLanguage
                                              else targetLanguage))
                     $ \result -> 
                         case result of
                           Left _ -> 
                               pageViewUpdateOutputStatus view "Fetch translation ... failed." Nothing
                           Right translation -> do
                               pageViewUpdateOutputStatus view "Fetch translation ... completed." Nothing
                               -- Get rectangle around cursor.
                               point <- ircViewGetTooltipPoint view
                               -- Show translation with tooltip.
                               mkDaemonSignal (pageViewClient view)
                                              ShowTooltip 
                                              (ShowTooltipArgs (UTF8.toString translation) 
                                                               (Just point) 10000
                                                               Nothing (Just (Color 0 65535 0)) 
                                                               True Nothing)

-- | Translate morse code.
ircViewTranslateMorse :: IrcView -> IO ()
ircViewTranslateMorse view@(IrcView {ircViewBuffer = 
                                                 IrcBuffer {ircBufferBuffer     = buffer
                                                           ,ircBufferMessageTag = messageTag}}) = do
  -- Use end line iter to match messageTag content (skip message head).
  iter <- textBufferGetCurrentLineEndIter_ buffer 
  textIterBackwardChar iter
  textBufferGetTagTextWithIter buffer iter messageTag
       >?>= \ text -> do
           -- Get rectangle around cursor.
           point <- ircViewGetTooltipPoint view
           mkDaemonSignal (pageViewClient view)
                          ShowTooltip
                          (ShowTooltipArgs (decodeMorse text)
                                           (Just point) 10000
                                           Nothing (Just (Color 65535 65535 0)) 
                                           True Nothing)
                          
-- | Read message around pointer.
ircViewReadMessage :: IrcView -> IO ()
ircViewReadMessage view@(IrcView {ircViewBuffer = 
                                      IrcBuffer {ircBufferBuffer     = buffer
                                                ,ircBufferMessageTag = messageTag}}) = do
  -- Use end line iter to match messageTag content (skip message head).
  iter <- textBufferGetCurrentLineEndIter_ buffer 
  textIterBackwardChar iter
  textBufferGetTagTextWithIter buffer iter messageTag
       >?>= \ text -> ircViewReadText view text

-- | Read irc message.
ircViewReadText :: IrcView -> String -> IO ()
ircViewReadText view text = 
  execute "festival" ("echo " ++ show text ++ " | %s --tts") True
          >?>= \ failedReason ->
              pageViewUpdateOutputStatus view failedReason Nothing

-- | Get tooltip coordinate.
ircViewGetTooltipPoint :: IrcView -> IO Point
ircViewGetTooltipPoint IrcView {ircViewView = textView} = do
  textIter <- textViewGetTextIter textView
  (Rectangle x y width height) <- textViewGetIterLocation textView textIter
  (wx, wy) <- textViewBufferToWindowCoords textView TextWindowWidget (x, y)
  return (wx + width, wy + height)


-- | Keymap.
ircViewKeymap :: Map Text (IrcView -> IO ())
ircViewKeymap = 
    M.fromList [("Return",   ircViewSendMessage False)
               ,("M-m",      ircViewSendMessage False)
               ,("C-m",      ircViewSendMessage True)
               ,("C-n",      ircViewSwitchTranslateLanguage)
               ,("C-N",      ircViewSendMorse)
               ,("M-M",      ircViewMoveToPrompt)
               ,("M-N",      ircViewCleanInput)
               ,("M-a",      ircViewSelectAll)
               ,("M-d",      ircViewDelLines)
               ,("M-D",      ircViewDelete)
               ,("M-,",      ircViewDeleteBackwardChar)
               ,("M-.",      ircViewDeleteForwardChar)
               ,("M-<",      ircViewDeleteBackwardWord)
               ,("M->",      ircViewDeleteForwardWord)
               ,("M-C-,",    ircViewDeleteToLineStart)
               ,("M-C-.",    ircViewDeleteToLineEnd)
               ,("M-j",      ircViewForwardLine)
               ,("M-k",      ircViewBackwardLine)
               ,("M-l",      ircViewForwardChar)
               ,("M-h",      ircViewBackwardChar)
               ,("Down",     ircViewForwardLine)
               ,("Up",       ircViewBackwardLine)
               ,("Right",    ircViewForwardChar)
               ,("Left",     ircViewBackwardChar)
               ,("S-Return", ircViewNewline)
               ,("M-L",      ircViewForwardWord)
               ,("M-H",      ircViewBackwardWord)
               ,("M-P-h",    ircViewSmartHome)
               ,("M-P-l",    ircViewSmartEnd)
               ,("C-c",      ircViewToggleSelectionMark)
               ,("C-C",      ircViewExchangeSelectionMark)
               ,("C-o",      ircViewOpenUrl)
               ,("C-j",      ircViewTranslateMessage)
               ,("C-J",      ircViewTranslateMorse)
               ,("C-k",      ircViewReadMessage)
               ]