{- ---------------------------------------------------------------------------- Copyright (c) 2021, Daan Leijen This is free software; you can redistribute it and/or modify it under the terms of the MIT License. A copy of the license can be found in the "LICENSE" file at the root of this distribution. ---------------------------------------------------------------------------- -} {-| Description : Binding to the Isocline library, a portable alternative to GNU Readline Copyright : (c) 2021, Daan Leijen License : MIT Maintainer : daan@effp.org Stability : Experimental ![logo](https://raw.githubusercontent.com/daanx/isocline/main/doc/isocline-inline.svg) A Haskell wrapper around the [Isocline C library](https://github.com/daanx/isocline#readme) which can provide an alternative to GNU Readline. (The Isocline library is included whole and there are no runtime dependencies). Isocline works across Unix, Windows, and macOS, and relies on a minimal subset of ANSI escape sequences. It has a good multi-line editing mode (use shift/ctrl-enter) which is nice for inputting small functions etc. Other features include support for colors, history, completion, unicode, undo/redo, incremental history search, inline hints, brace matching, syntax highlighting, rich text using bbcode formatting, etc. Minimal example with history: @ import System.Console.Isocline main :: IO () main = do putStrLn \"Welcome\" `setHistory` \"history.txt\" 200 input \<- `readline` \"myprompt\" -- full prompt becomes \"myprompt> \" putFmtLn (\"[gray]You wrote:[\/gray]\\n\" ++ input) @ Or using custom completions with an interactive loop: @ import System.Console.Isocline import Data.Char( toLower ) main :: IO () main = do `styleDef' "ic-prompt" "ansi-maroon" `setHistory` "history.txt" 200 `enableAutoTab` `True` interaction interaction :: IO () interaction = do s <- `readlineEx` \"hαskell\" (Just completer) Nothing putStrLn (\"You wrote:\\n\" ++ s) if (s == \"\" || s == \"exit\") then return () else interaction completer :: `CompletionEnv` -> String -> IO () completer cenv input = do `completeFileName` cenv input Nothing [\".\",\"\/usr\/local\"] [\".hs\"] -- use [] for any extension `completeWord` cenv input wcompleter wcompleter :: String -> [`Completion`] wcompleter input = `completionsFor` (map toLower input) [\"print\",\"println\",\"prints\",\"printsln\",\"prompt\"] @ See a larger [example](https://github.com/daanx/isocline/blob/main/test/Example.hs) with syntax highlighting and more extenstive custom completion in the [Github repository](https://github.com/daanx/isocline). Enjoy, -- Daan -} module System.Console.Isocline( -- * Readline readline, readlineEx, -- * History setHistory, historyClear, historyRemoveLast, historyAdd, -- * Completion CompletionEnv, completeFileName, completeWord, completeQuotedWord, completeQuotedWordEx, Completion(..), completion, isPrefix, completionsFor, wordCompleter, -- * Syntax Highlighting highlightFmt, -- * Rich text Style, Fmt, style, plain, pre, putFmt, putFmtLn, styleDef, styleOpen, styleClose, withStyle, -- * Configuration setPromptMarker, enableAutoTab, enableColor, enableBeep, enableMultiline, enableHistoryDuplicates, enableCompletionPreview, enableMultilineIndent, enableHighlight, enableInlineHelp, enableHint, setHintDelay, enableBraceMatching, enableBraceInsertion, setMatchingBraces, setInsertionBraces, -- * Advanced setDefaultCompleter, addCompletion, addCompletionPrim, addCompletions, completeWordPrim, completeQuotedWordPrim, completeQuotedWordPrimEx, readlineMaybe, readlineExMaybe, readlinePrim, readlinePrimMaybe, getPromptMarker, getContinuationPromptMarker, stopCompleting, hasCompletions, asyncStop, -- * Low-level highlighting HighlightEnv, setDefaultHighlighter, setDefaultFmtHighlighter, -- * Low-level Terminal termInit, termDone, withTerm, termFlush, termWrite, termWriteLn, termColor, termBgColor, termColorAnsi, termBgColorAnsi, termUnderline, termReverse, termReset ) where import Data.List( intersperse, isPrefixOf ) import Control.Monad( when, foldM ) import Control.Exception( bracket ) import Foreign.C.String( CString, peekCString, peekCStringLen, withCString, castCharToCChar ) import Foreign.Ptr import Foreign.C.Types -- the following are used for utf8 encoding. import qualified Data.ByteString as B ( useAsCString, packCString ) import qualified Data.Text as T ( pack, unpack ) import Data.Text.Encoding as TE ( decodeUtf8With, encodeUtf8) import Data.Text.Encoding.Error ( lenientDecode ) ---------------------------------------------------------------------------- -- C Types ---------------------------------------------------------------------------- data IcCompletionEnv -- | Abstract list of current completions. newtype CompletionEnv = CompletionEnv (Ptr IcCompletionEnv) type CCompleterFun = Ptr IcCompletionEnv -> CString -> IO () type CompleterFun = CompletionEnv -> String -> IO () data IcHighlightEnv -- | Abstract highlight environment newtype HighlightEnv = HighlightEnv (Ptr IcHighlightEnv) type CHighlightFun = Ptr IcHighlightEnv -> CString -> Ptr () -> IO () type HighlightFun = HighlightEnv -> String -> IO () ---------------------------------------------------------------------------- -- Basic readline ---------------------------------------------------------------------------- foreign import ccall ic_free :: (Ptr a) -> IO () foreign import ccall ic_malloc :: CSize -> IO (Ptr a) foreign import ccall ic_strdup :: CString -> IO CString foreign import ccall ic_readline :: CString -> IO CString foreign import ccall ic_readline_ex :: CString -> FunPtr CCompleterFun -> (Ptr ()) -> FunPtr CHighlightFun -> (Ptr ()) -> IO CString foreign import ccall ic_async_stop :: IO CCBool unmaybe :: IO (Maybe String) -> IO String unmaybe action = do mb <- action case mb of Nothing -> return "" Just s -> return s -- | @readline prompt@: Read (multi-line) input from the user with rich editing abilities. -- Takes the prompt text as an argument. The full prompt is the combination -- of the given prompt and the promp marker (@\"> \"@ by default) . -- See also 'readlineEx', 'readlineMaybe', 'enableMultiline', and 'setPromptMarker'. readline :: String -> IO String readline prompt = unmaybe $ readlineMaybe prompt -- | As 'readline' but returns 'Nothing' on end-of-file or other errors (ctrl-C/ctrl-D). readlineMaybe:: String -> IO (Maybe String) readlineMaybe prompt = withUTF8String prompt $ \cprompt -> do cres <- ic_readline cprompt res <- peekUTF8StringMaybe cres ic_free cres return res -- | @readlineEx prompt mbCompleter mbHighlighter@: as 'readline' but -- uses the given @mbCompleter@ function to complete words on @tab@ (instead of the default completer). -- and the given @mbHighlighter@ function to highlight the input (instead of the default highlighter). -- See also 'readline' and 'readlineExMaybe'. readlineEx :: String -> Maybe (CompletionEnv -> String -> IO ()) -> Maybe (String -> Fmt) -> IO String readlineEx prompt completer highlighter = unmaybe $ readlineExMaybe prompt completer highlighter -- | As 'readlineEx' but returns 'Nothing' on end-of-file or other errors (ctrl-C/ctrl-D). -- See also 'readlineMaybe'. readlineExMaybe :: String -> Maybe (CompletionEnv -> String -> IO ()) -> Maybe (String -> Fmt) -> IO (Maybe String) readlineExMaybe prompt completer mbhighlighter = readlinePrimMaybe prompt completer (case mbhighlighter of Nothing -> Nothing Just hl -> Just (highlightFmt hl)) -- | @readlinePrim prompt mbCompleter mbHighlighter@: as 'readline' but -- uses the given @mbCompleter@ function to complete words on @tab@ (instead of the default completer). -- and the given @mbHighlighter@ function to highlight the input (instead of the default highlighter). -- See also 'readlineEx' and 'readlinePrimMaybe'. readlinePrim :: String -> Maybe (CompletionEnv -> String -> IO ()) -> Maybe (HighlightEnv -> String -> IO ()) -> IO String readlinePrim prompt completer highlighter = unmaybe $ readlinePrimMaybe prompt completer highlighter -- | As 'readlinePrim' but returns 'Nothing' on end-of-file or other errors (ctrl-C/ctrl-D). -- See also 'readlineMaybe'. readlinePrimMaybe :: String -> Maybe (CompletionEnv -> String -> IO ()) -> Maybe (HighlightEnv -> String -> IO ()) -> IO (Maybe String) readlinePrimMaybe prompt completer highlighter = withUTF8String prompt $ \cprompt -> do ccompleter <- makeCCompleter completer chighlighter <- makeCHighlighter highlighter cres <- ic_readline_ex cprompt ccompleter nullPtr chighlighter nullPtr res <- peekUTF8StringMaybe cres ic_free cres when (ccompleter /= nullFunPtr) $ freeHaskellFunPtr ccompleter when (chighlighter /= nullFunPtr) $ freeHaskellFunPtr chighlighter return res -- | Thread safe call to asynchronously send a stop event to a 'readline' -- which behaves as if the user pressed @ctrl-C@, -- which will return with 'Nothing' (or @\"\"@). -- Returns 'True' if the event was successfully delivered. asyncStop :: IO Bool asyncStop = uncbool $ ic_async_stop ---------------------------------------------------------------------------- -- History ---------------------------------------------------------------------------- foreign import ccall ic_set_history :: CString -> CInt -> IO () foreign import ccall ic_history_remove_last :: IO () foreign import ccall ic_history_clear :: IO () foreign import ccall ic_history_add :: CString -> IO () -- | @setHistory filename maxEntries@: -- Enable history that is persisted to the given file path with a given maximum number of entries. -- Use -1 for the default entries (200). -- See also 'enableHistoryDuplicates'. setHistory :: FilePath -> Int -> IO () setHistory fname maxEntries = withUTF8String0 fname $ \cfname -> do ic_set_history cfname (toEnum maxEntries) -- | Isocline automatically adds input of more than 1 character to the history. -- This command removes the last entry. historyRemoveLast :: IO () historyRemoveLast = ic_history_remove_last -- | Clear the history. historyClear :: IO () historyClear = ic_history_clear -- | @historyAdd entry@: add @entry@ to the history. historyAdd :: String -> IO () historyAdd entry = withUTF8String0 entry $ \centry -> do ic_history_add centry ---------------------------------------------------------------------------- -- Completion ---------------------------------------------------------------------------- -- use our own CBool for compatibility with an older base type CCBool = CInt type CCharClassFun = CString -> CLong -> IO CCBool type CharClassFun = Char -> Bool foreign import ccall ic_set_default_completer :: FunPtr CCompleterFun -> IO () foreign import ccall "wrapper" ic_make_completer :: CCompleterFun -> IO (FunPtr CCompleterFun) foreign import ccall "wrapper" ic_make_charclassfun :: CCharClassFun -> IO (FunPtr CCharClassFun) foreign import ccall ic_add_completion_ex :: Ptr IcCompletionEnv -> CString -> CString -> CString -> IO CCBool foreign import ccall ic_add_completion_prim :: Ptr IcCompletionEnv -> CString -> CString -> CString -> CInt -> CInt -> IO CCBool foreign import ccall ic_complete_filename :: Ptr IcCompletionEnv -> CString -> CChar -> CString -> CString -> IO () foreign import ccall ic_complete_word :: Ptr IcCompletionEnv -> CString -> FunPtr CCompleterFun -> FunPtr CCharClassFun -> IO () foreign import ccall ic_complete_qword :: Ptr IcCompletionEnv -> CString -> FunPtr CCompleterFun -> FunPtr CCharClassFun -> IO () foreign import ccall ic_complete_qword_ex :: Ptr IcCompletionEnv -> CString -> FunPtr CCompleterFun -> FunPtr CCharClassFun -> CChar -> CString -> IO () foreign import ccall ic_has_completions :: Ptr IcCompletionEnv -> IO CCBool foreign import ccall ic_stop_completing :: Ptr IcCompletionEnv -> IO CCBool -- | A completion entry data Completion = Completion { replacement :: String, -- ^ actual replacement display :: String, -- ^ display of the completion in the completion menu help :: String -- ^ help message } -- | Create a completion with just a replacement completion :: String -> Completion completion replacement = Completion replacement "" "" -- | @completionFull replacement display help@: Create a completion with a separate display and help string. completionFull :: String -> String -> String -> Completion completionFull replacement display help = Completion replacement display help -- | Is the given input a prefix of the completion replacement? isPrefix :: String -> Completion -> Bool isPrefix input compl = isPrefixOf input (replacement compl) -- | @completionsFor input replacements@: Filter those @replacements@ that -- start with the given @input@, and return them as completions. completionsFor :: String -> [String] -> [Completion] completionsFor input rs = map completion (filter (isPrefixOf input) rs) -- | Convenience: creates a completer function directly from a list -- of candidate completion strings. Uses `completionsFor` to filter the -- input and `completeWord` to find the word boundary. -- For example: @'readlineEx' \"myprompt\" (Just ('wordCompleter' completer)) Nothing@. wordCompleter :: [String] -> (CompletionEnv -> String -> IO ()) wordCompleter completions = (\cenv input -> completeWord cenv input Nothing (\input -> completionsFor input completions)) -- | @setDefaultCompleter completer@: Set a new tab-completion function @completer@ -- that is called by Isocline automatically. -- The callback is called with a 'CompletionEnv' context and the current user -- input up to the cursor. -- By default the 'completeFileName' completer is used. -- This overwrites any previously set completer. setDefaultCompleter :: (CompletionEnv -> String -> IO ()) -> IO () setDefaultCompleter completer = do ccompleter <- makeCCompleter (Just completer) ic_set_default_completer ccompleter withCCompleter :: Maybe CompleterFun -> (FunPtr CCompleterFun -> IO a) -> IO a withCCompleter completer action = bracket (makeCCompleter completer) (\cfun -> when (nullFunPtr /= cfun) (freeHaskellFunPtr cfun)) action makeCCompleter :: Maybe CompleterFun -> IO (FunPtr CCompleterFun) makeCCompleter Nothing = return nullFunPtr makeCCompleter (Just completer) = ic_make_completer wrapper where wrapper :: Ptr IcCompletionEnv -> CString -> IO () wrapper rpcomp cprefx = do prefx <- peekUTF8String0 cprefx completer (CompletionEnv rpcomp) prefx -- | @addCompletion compl completion@: Inside a completer callback, add a new completion. -- If 'addCompletion' returns 'True' keep adding completions, -- but if it returns 'False' an effort should be made to return from the completer -- callback without adding more completions. addCompletion :: CompletionEnv -> Completion -> IO Bool addCompletion (CompletionEnv rpc) (Completion replacement display help) = withUTF8String replacement $ \crepl -> withUTF8String0 display $ \cdisplay -> withUTF8String0 help $ \chelp -> do cbool <- ic_add_completion_ex rpc crepl cdisplay chelp return (fromEnum cbool /= 0) -- | @addCompletionPrim compl completion deleteBefore deleteAfter@: -- Primitive add completion, use with care and call only directly inside a completer callback. -- If 'addCompletion' returns 'True' keep adding completions, -- but if it returns 'False' an effort should be made to return from the completer -- callback without adding more completions. addCompletionPrim :: CompletionEnv -> Completion -> Int -> Int -> IO Bool addCompletionPrim (CompletionEnv rpc) (Completion replacement display help) deleteBefore deleteAfter = withUTF8String replacement $ \crepl -> withUTF8String0 display $ \cdisplay -> withUTF8String0 help $ \chelp -> do cbool <- ic_add_completion_prim rpc crepl cdisplay chelp (toEnum deleteBefore) (toEnum deleteAfter) return (fromEnum cbool /= 0) -- | @addCompletions compl completions@: add multiple completions at once. -- If 'addCompletions' returns 'True' keep adding completions, -- but if it returns 'False' an effort should be made to return from the completer -- callback without adding more completions. addCompletions :: CompletionEnv -> [Completion] -> IO Bool addCompletions compl [] = return True addCompletions compl (c:cs) = do continue <- addCompletion compl c if (continue) then addCompletions compl cs else return False -- | @completeFileName compls input dirSep roots extensions@: -- Complete filenames with the given @input@, a possible directory separator @dirSep@, -- a list of root folders @roots@ to search from -- (by default @["."]@), and a list of extensions to match (use @[]@ to match any extension). -- The directory separator is used when completing directory names. -- For example, using g @\'/\'@ as a directory separator, we get: -- -- > /ho --> /home/ -- > /home/.ba --> /home/.bashrc -- completeFileName :: CompletionEnv -> String -> Maybe Char -> [FilePath] -> [String] -> IO () completeFileName (CompletionEnv rpc) prefx dirSep roots extensions = withUTF8String prefx $ \cprefx -> withUTF8String0 (concat (intersperse ";" roots)) $ \croots -> withUTF8String0 (concat (intersperse ";" extensions)) $ \cextensions -> do let cdirSep = case dirSep of Nothing -> toEnum 0 Just c -> castCharToCChar c ic_complete_filename rpc cprefx cdirSep croots cextensions -- | @completeWord compl input isWordChar completer@: -- Complete a /word/ (or /token/) and calls the user @completer@ function with just the current word -- (instead of the whole input) -- Takes the 'CompletionEnv' environment @compl@, the current @input@, an possible -- @isWordChar@ function, and a user defined -- @completer@ function that is called with adjusted input which -- is limited to the /word/ just before the cursor. -- Pass 'Nothing' to @isWordChar@ for the default @not . separator@ -- where @separator = \c -> c `elem` \" \\t\\r\\n,.;:/\\\\(){}[]\"@. completeWord :: CompletionEnv -> String -> Maybe (Char -> Bool) -> (String -> [Completion]) -> IO () completeWord cenv input isWordChar completer = completeWordPrim cenv input isWordChar cenvCompleter where cenvCompleter cenv input = do addCompletions cenv (completer input) return () -- | @completeQuotedWord compl input isWordChar completer@: -- Complete a /word/ taking care of automatically quoting and escaping characters. -- Takes the 'CompletionEnv' environment @compl@, the current @input@, and a user defined -- @completer@ function that is called with adjusted input which is unquoted, unescaped, -- and limited to the /word/ just before the cursor. -- For example, with a @hello world@ completion, we get: -- -- > hel --> hello\ world -- > hello\ w --> hello\ world -- > hello w --> # no completion, the word is just 'w'> -- > "hel --> "hello world" -- > "hello w --> "hello world" -- -- The call @('completeWord' compl prefx isWordChar fun)@ is a short hand for -- @('completeQuotedWord' compl prefx isWordChar \'\\\\\' \"\'\\\"\" fun)@. -- Pass 'Nothing' to @isWordChar@ for the default @not . separator@ -- where @separator = \c -> c `elem` \" \\t\\r\\n,.;:/\\\\(){}[]\"@. completeQuotedWord :: CompletionEnv -> String -> Maybe (Char -> Bool) -> (String -> [Completion]) -> IO () completeQuotedWord cenv input isWordChar completer = completeWordPrim cenv input isWordChar cenvCompleter where cenvCompleter cenv input = do addCompletions cenv (completer input) return () -- | @completeQuotedWordEx compl input isWordChar escapeChar quoteChars completer@: -- Complete a /word/ taking care of automatically quoting and escaping characters. -- Takes the 'CompletionEnv' environment @compl@, the current @input@, and a user defined -- @completer@ function that is called with adjusted input which is unquoted, unescaped, -- and limited to the /word/ just before the cursor. -- Unlike 'completeQuotedWord', this function can specify -- the /escape/ character and the /quote/ characters. -- See also 'completeWord'. completeQuotedWordEx :: CompletionEnv -> String -> Maybe (Char -> Bool) -> Maybe Char -> String -> (String -> [Completion]) -> IO () completeQuotedWordEx cenv input isWordChar escapeChar quoteChars completer = completeQuotedWordPrimEx cenv input isWordChar escapeChar quoteChars cenvCompleter where cenvCompleter cenv input = do addCompletions cenv (completer input) return () -- | @completeWord compl input isWordChar completer@: -- Complete a /word/,/token/ and calls the user @completer@ function with just the current word -- (instead of the whole input) -- Takes the 'CompletionEnv' environment @compl@, the current @input@, an possible -- @isWordChar@ function, and a user defined -- @completer@ function that is called with adjusted input which -- is limited to the /word/ just before the cursor. -- Pass 'Nothing' to @isWordChar@ for the default @not . separator@ -- where @separator = \c -> c `elem` \" \\t\\r\\n,.;:/\\\\(){}[]\"@. completeWordPrim :: CompletionEnv -> String -> Maybe (Char -> Bool) -> (CompletionEnv -> String -> IO ()) -> IO () completeWordPrim (CompletionEnv rpc) prefx isWordChar completer = withUTF8String prefx $ \cprefx -> withCharClassFun isWordChar $ \cisWordChar -> withCCompleter (Just completer) $ \ccompleter -> do ic_complete_word rpc cprefx ccompleter cisWordChar -- | @completeWordPrim compl input isWordChar completer@: -- Complete a /word/ taking care of automatically quoting and escaping characters. -- Takes the 'CompletionEnv' environment @compl@, the current @input@, and a user defined -- @completer@ function that is called with adjusted input which is unquoted, unescaped, -- and limited to the /word/ just before the cursor. -- For example, with a @hello world@ completion, we get: -- -- > hel --> hello\ world -- > hello\ w --> hello\ world -- > hello w --> # no completion, the word is just 'w'> -- > "hel --> "hello world" -- > "hello w --> "hello world" -- -- The call @('completeWordPrim' compl prefx isWordChar fun)@ is a short hand for -- @('completeQuotedWordPrim' compl prefx isWordChar \'\\\\\' \"\'\\\"\" fun)@. -- Pass 'Nothing' to @isWordChar@ for the default @not . separator@ -- where @separator = \c -> c `elem` \" \\t\\r\\n,.;:/\\\\(){}[]\"@. completeQuotedWordPrim :: CompletionEnv -> String -> Maybe (Char -> Bool) -> (CompletionEnv -> String -> IO ()) -> IO () completeQuotedWordPrim (CompletionEnv rpc) prefx isWordChar completer = withUTF8String prefx $ \cprefx -> withCharClassFun isWordChar $ \cisWordChar -> withCCompleter (Just completer) $ \ccompleter -> do ic_complete_qword rpc cprefx ccompleter cisWordChar -- | @completeQuotedWordPrim compl input isWordChar escapeChar quoteChars completer@: -- Complete a /word/ taking care of automatically quoting and escaping characters. -- Takes the 'CompletionEnv' environment @compl@, the current @input@, and a user defined -- @completer@ function that is called with adjusted input which is unquoted, unescaped, -- and limited to the /word/ just before the cursor. -- Unlike 'completeWord', this function takes an explicit function to determine /word/ characters, -- the /escape/ character, and a string of /quote/ characters. -- See also 'completeWord'. completeQuotedWordPrimEx :: CompletionEnv -> String -> Maybe (Char -> Bool) -> Maybe Char -> String -> (CompletionEnv -> String -> IO ()) -> IO () completeQuotedWordPrimEx (CompletionEnv rpc) prefx isWordChar escapeChar quoteChars completer = withUTF8String prefx $ \cprefx -> withUTF8String0 quoteChars $ \cquoteChars -> withCharClassFun isWordChar $ \cisWordChar -> withCCompleter (Just completer) $ \ccompleter -> do let cescapeChar = case escapeChar of Nothing -> toEnum 0 Just c -> castCharToCChar c ic_complete_qword_ex rpc cprefx ccompleter cisWordChar cescapeChar cquoteChars withCharClassFun :: Maybe (Char -> Bool) -> (FunPtr CCharClassFun -> IO a) -> IO a withCharClassFun isInClass action = bracket (makeCharClassFun isInClass) (\cfun -> when (nullFunPtr /= cfun) (freeHaskellFunPtr cfun)) action makeCharClassFun :: Maybe (Char -> Bool) -> IO (FunPtr CCharClassFun) makeCharClassFun Nothing = return nullFunPtr makeCharClassFun (Just isInClass) = let charClassFun :: CString -> CLong -> IO CCBool charClassFun cstr clen = let len = (fromIntegral clen :: Int) in if (len <= 0) then return (cbool False) else do s <- peekCStringLen (cstr,len) return (if null s then (cbool False) else cbool (isInClass (head s))) in do ic_make_charclassfun charClassFun -- | If this returns 'True' an effort should be made to stop completing and return from the callback. stopCompleting :: CompletionEnv -> IO Bool stopCompleting (CompletionEnv rpc) = uncbool $ ic_stop_completing rpc -- | Have any completions be generated so far? hasCompletions :: CompletionEnv -> IO Bool hasCompletions (CompletionEnv rpc) = uncbool $ ic_has_completions rpc ---------------------------------------------------------------------------- -- Syntax highlighting ---------------------------------------------------------------------------- foreign import ccall ic_set_default_highlighter :: FunPtr CHighlightFun -> Ptr () -> IO () foreign import ccall "wrapper" ic_make_highlight_fun:: CHighlightFun -> IO (FunPtr CHighlightFun) foreign import ccall ic_highlight :: Ptr IcHighlightEnv -> CLong -> CLong -> CString -> IO () foreign import ccall ic_highlight_formatted :: Ptr IcHighlightEnv -> CString -> CString -> IO () -- | Set a syntax highlighter. -- There can only be one highlight function, setting it again disables the previous one. setDefaultHighlighter :: (HighlightEnv -> String -> IO ()) -> IO () setDefaultHighlighter highlighter = do chighlighter <- makeCHighlighter (Just highlighter) ic_set_default_highlighter chighlighter nullPtr makeCHighlighter :: Maybe (HighlightEnv -> String -> IO ()) -> IO (FunPtr CHighlightFun) makeCHighlighter Nothing = return nullFunPtr makeCHighlighter (Just highlighter) = ic_make_highlight_fun wrapper where wrapper :: Ptr IcHighlightEnv -> CString -> Ptr () -> IO () wrapper henv cinput carg = do input <- peekUTF8String0 cinput highlighter (HighlightEnv henv) input -- | @highlight henv pos len style@: Set the style of @len@ characters -- starting at position @pos@ in the input highlight :: HighlightEnv -> Int -> Int -> String -> IO () highlight (HighlightEnv henv) pos len style = withUTF8String0 style $ \cstyle -> do ic_highlight henv (clong (-pos)) (clong (-len)) cstyle -- | A style for formatted strings ('Fmt'). -- For example, a style can be @"red"@ or @"b #7B3050"@. -- See the full list of valid [properties](https://github.com/daanx/isocline#bbcode-format) type Style = String -- | A string with [bbcode](https://github.com/daanx/isocline#bbcode-format) formatting. -- For example @"[red]this is red[\/]"@.n type Fmt = String -- | Use an rich text formatted highlighter from inside a highlighter callback. highlightFmt :: (String -> Fmt) -> (HighlightEnv -> String -> IO ()) highlightFmt highlight (HighlightEnv henv) input = withUTF8String0 input $ \cinput -> withUTF8String0 (highlight input) $ \cfmt -> do ic_highlight_formatted henv cinput cfmt -- | Style a string, e.g. @style "b red" "bold and red"@ (which is equivalent to @"[b red]bold and red[\/]"@). -- See the repo for a full description of all [styles](https://github.com/daanx/isocline#bbcode-format). style :: Style -> Fmt -> Fmt style st s = if null st then s else ("[" ++ st ++ "]" ++ s ++ "[/]") -- | Escape a string so no tags are interpreted as formatting. plain :: String -> Fmt plain s = if (any (\c -> (c == '[' || c == ']')) s) then "[!pre]" ++ s ++ "[/pre]" else s -- | Style a string that is printed as is without interpreting markup inside it (using `plain`). pre :: Style -> String -> Fmt pre st s = style st (plain s) -- | Set a syntax highlighter that uses a pure function that returns a bbcode -- formatted string (using 'style', 'plain' etc). See 'highlightFmt' for more information. -- There can only be one highlight function, setting it again disables the previous one. setDefaultFmtHighlighter :: (String -> Fmt) -> IO () setDefaultFmtHighlighter highlight = setDefaultHighlighter (highlightFmt highlight) ---------------------------------------------------------------------------- -- Print rich text ---------------------------------------------------------------------------- foreign import ccall ic_print :: CString -> IO () foreign import ccall ic_println :: CString -> IO () foreign import ccall ic_style_def :: CString -> CString -> IO () foreign import ccall ic_style_open :: CString -> IO () foreign import ccall ic_style_close :: IO () -- | Output rich formatted text containing [bbcode](https://github.com/daanx/isocline#bbcode-format). -- For example: @putFmt \"[b]bold [red]and red[\/][\/]\"@ -- All unclosed tags are automatically closed (but see also 'styleOpen'). -- See the repo for more information about [formatted output](https://github.com/daanx/isocline#formatted-output). putFmt :: Fmt -> IO () putFmt s = withUTF8String0 s $ \cs -> do ic_print cs -- | Output rich formatted text containing bbcode's ending with a newline. putFmtLn :: Fmt -> IO () putFmtLn s = withUTF8String0 s $ \cs -> do ic_println cs -- | Define (or redefine) a style. -- For example @styleDef "warning" "crimon underline"@, -- and then use it as @'putFmtLn' "[warning]this is a warning[/]"@. -- This can be very useful for theming your application with semantic styles. -- See also [formatted output](https://github.com/daanx/isocline#formatted-output) styleDef :: String -> Style -> IO () styleDef name style = withUTF8String0 name $ \cname -> withUTF8String0 style $ \cstyle -> do ic_style_def cname cstyle -- | Open a style that is active for all 'putFmt' and 'putFmtLn' until it is closed again (`styleClose`). styleOpen :: Style -> IO () styleOpen style = withUTF8String0 style $ \cstyle -> do ic_style_open cstyle -- | Close a previously opened style. styleClose :: IO () styleClose = ic_style_close -- | Use a style over an action. withStyle :: Style -> IO a -> IO a withStyle style action = bracket (styleOpen style) (\() -> styleClose) (\() -> action) ---------------------------------------------------------------------------- -- Terminal ---------------------------------------------------------------------------- foreign import ccall ic_term_init :: IO () foreign import ccall ic_term_done :: IO () foreign import ccall ic_term_flush :: IO () foreign import ccall ic_term_write :: CString -> IO () foreign import ccall ic_term_writeln :: CString -> IO () foreign import ccall ic_term_underline :: CCBool -> IO () foreign import ccall ic_term_reverse :: CCBool -> IO () foreign import ccall ic_term_color_ansi :: CCBool -> CInt -> IO () foreign import ccall ic_term_color_rgb :: CCBool -> CInt -> IO () foreign import ccall ic_term_style :: CString -> IO () foreign import ccall ic_term_reset :: IO () -- | Initialize the terminal for the @term@ functions. -- Does nothing on most platforms but on windows enables UTF8 output -- and potentially enables virtual terminal processing. -- See also 'withTerm'. termInit :: IO () termInit = ic_term_init -- | Done using @term@ functions. -- See also 'withTerm'. termDone :: IO () termDone = ic_term_done -- | Use the @term@ functions (brackets 'termInit' and 'termDone'). withTerm :: IO a -> IO a withTerm action = bracket termInit (\() -> termDone) (\() -> action) -- | Flush terminal output. Happens automatically on newline (@'\\n'@) characters as well. termFlush :: IO () termFlush = ic_term_flush -- | Write output to the terminal where ANSI CSI sequences are -- handled portably across platforms (including Windows). termWrite :: String -> IO () termWrite s = withUTF8String0 s $ \cs -> ic_term_write cs -- | Write output with a ending newline to the terminal where -- ANSI CSI sequences are handled portably across platforms (including Windows). termWriteLn :: String -> IO () termWriteLn s = withUTF8String0 s $ \cs -> ic_term_writeln cs -- | Set the terminal text color as a hexadecimal number @0x@rrggbb. -- The color is auto adjusted for terminals with less colors. termColor :: Int -> IO () termColor color = ic_term_color_rgb (cbool True) (toEnum color) -- | Set the terminal text background color. The color is auto adjusted for terminals with less colors. termBgColor :: Int -> IO () termBgColor color = ic_term_color_rgb (cbool False) (toEnum color) -- | Set the terminal text color as an ANSI palette color (between @0@ and @255@). Use 256 for the default. -- The color is auto adjusted for terminals with less colors. termColorAnsi :: Int -> IO () termColorAnsi color = ic_term_color_ansi (cbool True) (toEnum color) -- | Set the terminal text background color as an ANSI palette color (between @0@ and @255@). Use 256 for the default. -- The color is auto adjusted for terminals with less colors. termBgColorAnsi :: Int -> IO () termBgColorAnsi color = ic_term_color_ansi (cbool False) (toEnum color) -- | Set the terminal attributes from a style termStyle :: Style -> IO () termStyle style = withUTF8String0 style $ \cstyle -> do ic_term_style cstyle -- | Set the terminal text underline mode. termUnderline :: Bool -> IO () termUnderline enable = ic_term_underline (cbool enable) -- | Set the terminal text reverse video mode. termReverse :: Bool -> IO () termReverse enable = ic_term_reverse (cbool enable) -- | Reset the terminal text mode to defaults termReset :: IO () termReset = ic_term_reset ---------------------------------------------------------------------------- -- Configuration ---------------------------------------------------------------------------- foreign import ccall ic_set_prompt_marker :: CString -> CString -> IO () foreign import ccall ic_get_prompt_marker :: IO CString foreign import ccall ic_get_continuation_prompt_marker :: IO CString foreign import ccall ic_enable_multiline :: CCBool -> IO CCBool foreign import ccall ic_enable_beep :: CCBool -> IO CCBool foreign import ccall ic_enable_color :: CCBool -> IO CCBool foreign import ccall ic_enable_auto_tab :: CCBool -> IO CCBool foreign import ccall ic_enable_inline_help:: CCBool -> IO CCBool foreign import ccall ic_enable_hint :: CCBool -> IO CCBool foreign import ccall ic_set_hint_delay :: CLong -> IO CLong foreign import ccall ic_enable_highlight :: CCBool -> IO CCBool foreign import ccall ic_enable_history_duplicates :: CCBool -> IO CCBool foreign import ccall ic_enable_completion_preview :: CCBool -> IO CCBool foreign import ccall ic_enable_multiline_indent :: CCBool -> IO CCBool foreign import ccall ic_enable_brace_matching :: CCBool -> IO CCBool foreign import ccall ic_enable_brace_insertion :: CCBool -> IO CCBool foreign import ccall ic_set_matching_braces :: CString -> IO () foreign import ccall ic_set_insertion_braces :: CString -> IO () cbool :: Bool -> CCBool cbool True = toEnum 1 cbool False = toEnum 0 uncbool :: IO CCBool -> IO Bool uncbool action = do i <- action return (i /= toEnum 0) clong :: Int -> CLong clong l = toEnum l -- | @setPromptMarker marker multiline_marker@: Set the prompt @marker@ (by default @\"> \"@). -- and a possible different continuation prompt marker @multiline_marker@ for multiline -- input (defaults to @marker@). setPromptMarker :: String -> String -> IO () setPromptMarker marker multiline_marker = withUTF8String0 marker $ \cmarker -> withUTF8String0 multiline_marker $ \cmultiline_marker -> do ic_set_prompt_marker cmarker cmultiline_marker -- | Get the current prompt marker. getPromptMarker :: IO String getPromptMarker = do cstr <- ic_get_prompt_marker if (nullPtr == cstr) then return "" else do cstr2 <- ic_strdup cstr peekUTF8String0 cstr2 -- | Get the current prompt continuation marker for multi-line input. getContinuationPromptMarker :: IO String getContinuationPromptMarker = do cstr <- ic_get_continuation_prompt_marker if (nullPtr == cstr) then return "" else do cstr2 <- ic_strdup cstr peekUTF8String0 cstr2 -- | Disable or enable multi-line input (enabled by default). -- Returns the previous value. enableMultiline :: Bool -> IO Bool enableMultiline enable = do uncbool $ ic_enable_multiline (cbool enable) -- | Disable or enable sound (enabled by default). -- | A beep is used when tab cannot find any completion for example. -- Returns the previous value. enableBeep :: Bool -> IO Bool enableBeep enable = do uncbool $ ic_enable_beep (cbool enable) -- | Disable or enable color output (enabled by default). -- Returns the previous value. enableColor :: Bool -> IO Bool enableColor enable = do uncbool $ ic_enable_color (cbool enable) -- | Disable or enable duplicate entries in the history (duplicate entries are not allowed by default). -- Returns the previous value. enableHistoryDuplicates :: Bool -> IO Bool enableHistoryDuplicates enable = do uncbool $ ic_enable_history_duplicates (cbool enable) -- | Disable or enable automatic tab completion after a completion -- to expand as far as possible if the completions are unique. (disabled by default). -- Returns the previous value. enableAutoTab :: Bool -> IO Bool enableAutoTab enable = do uncbool $ ic_enable_auto_tab (cbool enable) -- | Disable or enable short inline help message (for history search etc.) (enabled by default). -- Pressing F1 always shows full help regardless of this setting. -- Returns the previous value. enableInlineHelp :: Bool -> IO Bool enableInlineHelp enable = do uncbool $ ic_enable_inline_help (cbool enable) -- | Disable or enable preview of a completion selection (enabled by default) -- Returns the previous value. enableCompletionPreview :: Bool -> IO Bool enableCompletionPreview enable = do uncbool $ ic_enable_completion_preview (cbool enable) -- | Disable or enable brace matching (enabled by default) -- Returns the previous value. enableBraceMatching :: Bool -> IO Bool enableBraceMatching enable = do uncbool $ ic_enable_brace_matching (cbool enable) -- | Disable or enable automatic close brace insertion (enabled by default) -- Returns the previous value. enableBraceInsertion :: Bool -> IO Bool enableBraceInsertion enable = do uncbool $ ic_enable_brace_insertion (cbool enable) -- | Set pairs of matching braces, by default @\"(){}[]\"@. setMatchingBraces :: String -> IO () setMatchingBraces bracePairs = withUTF8String0 bracePairs $ \cbracePairs -> do ic_set_matching_braces cbracePairs -- | Set pairs of auto insertion braces, by default @\"(){}[]\\\"\\\"\'\'\"@. setInsertionBraces :: String -> IO () setInsertionBraces bracePairs = withUTF8String0 bracePairs $ \cbracePairs -> do ic_set_insertion_braces cbracePairs -- | Disable or enable automatic indentation to line up the -- multiline prompt marker with the initial prompt marker (enabled by default). -- Returns the previous value. -- See also 'setPromptMarker'. enableMultilineIndent :: Bool -> IO Bool enableMultilineIndent enable = do uncbool $ ic_enable_multiline_indent (cbool enable) -- | Disable or enable automatic inline hinting (enabled by default) -- Returns the previous value. enableHint :: Bool -> IO Bool enableHint enable = do uncbool $ ic_enable_hint (cbool enable) -- | Disable or enable syntax highlighting (enabled by default). -- Returns the previous value. enableHighlight :: Bool -> IO Bool enableHighlight enable = do uncbool $ ic_enable_highlight (cbool enable) -- | Set the delay in milliseconds before a hint is displayed (500ms by default) -- See also 'enableHint' setHintDelay :: Int -> IO Int setHintDelay ms = do cl <- ic_set_hint_delay (toEnum ms) return (fromEnum cl) ---------------------------------------------------------------------------- -- UTF8 Strings ---------------------------------------------------------------------------- withUTF8String0 :: String -> (CString -> IO a) -> IO a withUTF8String0 s action = if (null s) then action nullPtr else withUTF8String s action peekUTF8String0 :: CString -> IO String peekUTF8String0 cstr = if (nullPtr == cstr) then return "" else peekUTF8String cstr peekUTF8StringMaybe :: CString -> IO (Maybe String) peekUTF8StringMaybe cstr = if (nullPtr == cstr) then return Nothing else do s <- peekUTF8String cstr return (Just s) peekUTF8String :: CString -> IO String peekUTF8String cstr = do bstr <- B.packCString cstr return (T.unpack (TE.decodeUtf8With lenientDecode bstr)) withUTF8String :: String -> (CString -> IO a) -> IO a withUTF8String str action = do let bstr = TE.encodeUtf8 (T.pack str) B.useAsCString bstr action