urbit/pkg/king/lib/Vere/Term.hs

323 lines
10 KiB
Haskell
Raw Normal View History

{-# OPTIONS_GHC -Wwarn #-}
module Vere.Term (initializeTerminal, term, VereTerminal) where
import UrbitPrelude
import Arvo hiding (Term)
import Vere.Pier.Types
import Data.Char
import Foreign.Marshal.Alloc
import Foreign.Ptr
2019-08-29 21:12:50 +03:00
import Foreign.Storable
import System.Posix.IO
import System.Posix.Terminal
import System.Console.Terminfo.Base
2019-08-29 21:12:50 +03:00
import Data.ByteString.Internal
-- Types -----------------------------------------------------------------------
data TermDrv = TermDrv
{ tdPreviousConfiguration :: TerminalAttributes
, tdReader :: Async ()
}
-- Output to the attached terminal is either a series of vere blits, or it is an
-- injected printf line from the interpreter.
data VereOutput = VereBlitOutput [Blit]
| VerePrintOutput String
data VereTerminal = VereTerminal
{ vtWidth :: Word
, vtHeight :: Word
--
, vtWriteQueue :: TQueue VereOutput
, vtWriter :: Async ()
}
data LineState = LineState String Int
-- A list of terminal flags that we disable
disabledFlags = [
-- lflag
2019-08-30 00:54:34 +03:00
StartStopOutput, KeyboardInterrupts, EnableEcho, EchoLF, ProcessInput, ExtendedFunctions,
-- iflag
MapCRtoLF, CheckParity, StripHighBit,
-- cflag, todo: Terminal library missing CSIZE?
EnableParity,
-- oflag
ProcessOutput
]
-- A record used in reading data from stdInput.
data ReadData = ReadData
{ rBuf :: Ptr Word8
, rEscape :: Bool
, rBracket :: Bool
}
-- Utils -----------------------------------------------------------------------
-- TODO: We lie about terminal size for now and just pass 80x24 because getting
-- it is a call to ioctl() which is in IO.
initialBlew w h = EvBlip $ BlipEvTerm $ TermEvBlew (UD 1, ()) w h
initialHail = EvBlip $ BlipEvTerm $ TermEvHail (UD 1, ()) ()
-- What we need is an equivalent to _term_io_suck_char(). That's a manual, hand
-- rolled parser to deal with the escape state.
-- Version one of this is punting on the ops_u.dem flag: whether we're running
-- in daemon mode.
--------------------------------------------------------------------------------
runMaybeTermOutput :: Terminal -> (Terminal -> Maybe TermOutput) -> IO ()
runMaybeTermOutput t getter = case (getter t) of
Nothing -> pure ()
Just x -> runTermOutput t x
--------------------------------------------------------------------------------
initializeTerminal :: Acquire VereTerminal
initializeTerminal = mkAcquire start stop
where
start :: IO VereTerminal
start = do
t <- setupTermFromEnv
let vtWidth = 80
let vtHeight = 24
vtWriteQueue <- newTQueueIO
vtWriter <- asyncBound (writeTerminal t vtWriteQueue)
pure VereTerminal{..}
stop :: VereTerminal -> IO ()
stop (VereTerminal{..}) = cancel vtWriter
getCap term cap =
getCapability term (tiGetOutput1 cap) :: Maybe TermOutput
vtClearScreen t = getCap t "clear"
vtClearToBegin t = getCap t "el"
vtSoundBell t = getCap t "bel"
vtParmLeft t = getCap t "cub1"
vtParmRight t = getCap t "cuf1"
-- Writes data to the terminal. Both the terminal reading, normal logging,
-- and effect handling can all emit bytes which go to the terminal.
writeTerminal :: Terminal -> TQueue VereOutput -> IO ()
writeTerminal t q = loop (LineState "" 0)
where
loop s = do
x <- atomically $ readTQueue q
case x of
VereBlitOutput blits -> do
newS <- foldM (writeBlit t) s blits
loop newS
VerePrintOutput p -> do
runTermOutput t $ termText "\r"
runMaybeTermOutput t vtClearToBegin
runTermOutput t $ termText p
newS <- termRefreshLine t s
loop newS
-- Writes an individual blit to the screen
writeBlit :: Terminal -> LineState -> Blit -> IO LineState
writeBlit t ls = \case
Bel () -> do
runMaybeTermOutput t vtSoundBell
pure ls
Clr () -> do
runMaybeTermOutput t vtClearScreen
termRefreshLine t ls
(Hop w) -> do
termShowCursor t ls (fromIntegral w)
(Lin c) -> do
ls2 <- termShowClear t ls
termShowLine t ls2 (pack c)
(Mor ()) -> do
termShowMore t ls
(Sag path noun) -> pure ls
(Sav path atom) -> pure ls
(Url url) -> pure ls
-- Moves the cursor to the requested position
termShowCursor :: Terminal -> LineState -> Int -> IO LineState
termShowCursor t (LineState line pos) newPos = do
if newPos < pos then do
replicateM_ (pos - newPos) (runMaybeTermOutput t vtParmLeft)
pure (LineState line newPos)
else if newPos > pos then do
replicateM_ (newPos - pos) (runMaybeTermOutput t vtParmRight)
pure (LineState line newPos)
else
pure (LineState line pos)
-- Displays and sets the current line
termShowLine :: Terminal -> LineState -> String -> IO LineState
termShowLine t ls newStr = do
-- TODO: Really think about how term.c munged cus_w. Amidoinitrit?
runTermOutput t $ termText newStr
pure (LineState newStr (length newStr))
termShowClear :: Terminal -> LineState -> IO LineState
termShowClear t ls = do
runTermOutput t $ termText "\r"
runMaybeTermOutput t vtClearToBegin
pure (LineState "" 0)
-- New Current Line
termShowMore :: Terminal -> LineState -> IO LineState
termShowMore t ls = do
runTermOutput t $ termText "\r\n"
pure (LineState "" 0)
-- Redraw the current LineState, moving cursor to the end.
termRefreshLine :: Terminal -> LineState -> IO LineState
termRefreshLine t ls@(LineState line pos) = do
runMaybeTermOutput t vtClearToBegin
newLs <- termShowLine t ls line
termShowCursor t newLs pos
term :: VereTerminal -> KingId -> QueueEv -> ([Ev], Acquire (EffCb e TermEf))
term VereTerminal{..} king enqueueEv =
(initialEvents, runTerm)
where
initialEvents = [(initialBlew vtWidth vtHeight), initialHail]
runTerm :: Acquire (EffCb e TermEf)
runTerm = do
tim <- mkAcquire start stop
pure (io . handleEffect vtWriteQueue tim)
start :: IO TermDrv
start = do
putStrLn "term start"
tdPreviousConfiguration <- getTerminalAttributes stdInput
-- Create a new configuration where we put the terminal in raw mode and
-- disable a bunch of preprocessing.
let newTermSettings =
2019-08-30 00:54:34 +03:00
flip withTime 0 .
flip withMinInput 1 $
foldl' withoutMode tdPreviousConfiguration disabledFlags
setTerminalAttributes stdInput newTermSettings Immediately
tdReader <- asyncBound (readTerminal (bell vtWriteQueue))
pure TermDrv{..}
bell :: TQueue VereOutput -> IO ()
bell q = do
atomically $ writeTQueue q $ VereBlitOutput [Bel ()]
stop :: TermDrv -> IO ()
stop (TermDrv{..}) = do
-- cancel our threads
cancel tdReader
-- take the terminal out of raw mode
setTerminalAttributes stdInput tdPreviousConfiguration Immediately
-- Reads data from stdInput and emit the proper effect
--
-- This entire path is a divergence from how term.c does things,
-- probably. First, the vtime is 0, not 1 in term.c. So (IIUC), we'll
-- always have a latency of 1/10 of a second.
--
-- A better way to do this would be to get some sort of epoll on stdInput,
-- since that's kinda closer to what libuv does?
readTerminal :: (IO ()) -> IO ()
readTerminal bell = allocaBytes 1 $ \ buf -> loop (ReadData buf False False)
where
loop rd@ReadData{..} = do
-- The problem with using fdRead raw is that it will text encode things
-- like \ESC instead of 27. That makes it broken for our purposes.
2019-08-30 00:54:34 +03:00
--
t <- try (fdReadBuf stdInput rBuf 1)
case t of
2019-08-30 00:54:34 +03:00
Left (e :: IOException) -> do
-- Ignore EAGAINs when doing reads
loop rd
Right 0 -> loop rd
Right _ -> do
w <- peek rBuf
2019-08-30 00:54:34 +03:00
-- print ("{" ++ (show w) ++ "}")
let c = w2c w
if rEscape then
if rBracket then do
case c of
'A' -> sendBelt $ Aro U
'B' -> sendBelt $ Aro D
'C' -> sendBelt $ Aro R
'D' -> sendBelt $ Aro L
2019-08-30 00:54:34 +03:00
_ -> bell
loop rd { rEscape = False, rBracket = False}
2019-08-30 00:54:34 +03:00
else if isAsciiLower c then do
sendBelt $ Met $ Cord $ pack [c]
loop rd { rEscape = False }
else if c == '.' then do
sendBelt $ Met $ Cord "dot"
loop rd { rEscape = False }
else if w == 8 || w == 127 then do
sendBelt $ Met $ Cord "bac"
loop rd { rEscape = False }
else if c == '[' || c == '0' then do
loop rd { rBracket = True }
else do
bell
loop rd { rEscape = False }
-- if not escape
else if False then
-- TODO: Put the unicode accumulation logic here.
2019-08-30 00:54:34 +03:00
loop rd
else if w >= 32 && w < 127 then do
sendBelt $ Txt $ Tour $ [c]
2019-08-30 00:54:34 +03:00
loop rd
else if w == 0 then do
bell
2019-08-30 00:54:34 +03:00
loop rd
else if w == 8 || w == 127 then do
sendBelt $ Bac ()
loop rd
else if w == 13 then do
sendBelt $ Ret ()
loop rd
2019-08-30 00:54:34 +03:00
else if w == 3 then do
-- ETX (^C)
putStrLn "{ctrl-c}"
loop rd
else if w == 4 then do
-- EOT (^D)
putStrLn "{ctrl-d}"
loop rd
else if w <= 26 then do
sendBelt $ Ctl $ Cord $ pack [w2c (w + 97 - 1)]
loop rd
else if w == 27 then do
loop rd { rEscape = True }
2019-08-30 00:54:34 +03:00
else do
-- start the utf8 accumulation buffer
2019-08-30 00:54:34 +03:00
loop rd
sendBelt :: Belt -> IO ()
sendBelt b = do
let blip = EvBlip $ BlipEvTerm $ TermEvBelt (UD 1, ()) $ b
2019-08-29 21:12:50 +03:00
atomically $ enqueueEv $ blip
handleEffect :: TQueue VereOutput -> TermDrv -> TermEf -> IO ()
handleEffect writeQueue TermDrv{..} = \case
TermEfBlit _ blits -> atomically $ writeTQueue writeQueue (VereBlitOutput blits)
TermEfInit _ _ -> pure ()
TermEfLogo path _ -> pure ()
TermEfMass _ _ -> pure ()