packages feed

isocline (empty) → 1.0.0

raw patch · 37 files changed

+12497/−0 lines, 37 filesdep +basedep +bytestringdep +isocline

Dependencies added: base, bytestring, isocline, text

Files

+ LICENSE view
@@ -0,0 +1,21 @@+MIT License++Copyright (c) 2021 Daan Leijen++Permission is hereby granted, free of charge, to any person obtaining a copy+of this software and associated documentation files (the "Software"), to deal+in the Software without restriction, including without limitation the rights+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+copies of the Software, and to permit persons to whom the Software is+furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in all+copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE+SOFTWARE.
+ haskell/System/Console/Isocline.hs view
@@ -0,0 +1,1046 @@+{- ----------------------------------------------------------------------------
+  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
+       
+ haskell/readme.md view
@@ -0,0 +1,28 @@+# Haskell Binding for Isocline
+
+You can build and run the [example] program as:
+```
+$ ghc -ihaskell test/Example.hs src/isocline.c
+$ ./test/Example
+```
+
+## Building with Stack
+
+```
+$ stack build
+$ stack exec example
+```
+
+## Building with Cabal
+
+```
+$ cabal build
+$ cabal run example
+```
+
+You can install the `isocline` package locally as:
+```
+$ cabal install
+```
+
+[example]: https://github.com/daanx/isocline/blob/main/test/Example.hs
+ include/isocline.h view
@@ -0,0 +1,627 @@+/* ----------------------------------------------------------------------------+  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.+-----------------------------------------------------------------------------*/+#pragma once+#ifndef IC_ISOCLINE_H+#define IC_ISOCLINE_H++#ifdef __cplusplus+extern "C" {+#endif++#include <stddef.h>    // size_t+#include <stdbool.h>   // bool+#include <stdint.h>    // uint32_t+#include <stdarg.h>    // term_vprintf+++/*! \mainpage+Isocline C API reference.++Isocline is a pure C library that can be used as an alternative to the GNU readline library.++See the [Github repository](https://github.com/daanx/isocline#readme) +for general information and building the library.++Contents:+- \ref readline+- \ref bbcode+- \ref history+- \ref completion+- \ref highlight+- \ref options+- \ref helper+- \ref completex+- \ref term+- \ref async+- \ref alloc+*/++/// \defgroup readline Readline+/// The basic readline interface.+/// \{++/// Isocline version: 102 = 1.0.2.+#define IC_VERSION   (100)    /// 1.0.0+++/// Read input from the user using rich editing abilities.+/// @param prompt_text   The prompt text, can be NULL for the default (""). +///   The displayed prompt becomes `prompt_text` followed by the `prompt_marker` ("> "). +/// @returns the heap allocated input on succes, which should be `free`d by the caller.  +///   Returns NULL on error, or if the user typed ctrl+d or ctrl+c.+///+/// If the standard input (`stdin`) has no editing capability +/// (like a dumb terminal (e.g. `TERM`=`dumb`), running in a debuggen, a pipe or redirected file, etc.)+/// the input is read directly from the input stream up to the +/// next line without editing capability.+/// See also \a ic_set_prompt_marker(), \a ic_style_def()+///+/// @see ic_set_prompt_marker(), ic_style_def()+char* ic_readline(const char* prompt_text);   ++/// \}+++//--------------------------------------------------------------+/// \defgroup bbcode Formatted Text+/// Formatted text using [bbcode markup](https://github.com/daanx/isocline#bbcode-format).+/// \{++/// Print to the terminal while respection bbcode markup. +/// Any unclosed tags are closed automatically at the end of the print.+/// For example:+/// ```+/// ic_print("[b]bold, [i]bold and italic[/i], [red]red and bold[/][/b] default.");+/// ic_print("[b]bold[/], [i b]bold and italic[/], [yellow on blue]yellow on blue background");+/// ic_style_add("em","i color=#888800");+/// ic_print("[em]emphasis");+/// ```+/// Properties that can be assigned are:+/// * `color=` _clr_, `bgcolor=` _clr_: where _clr_ is either a hex value `#`RRGGBB or `#`RGB, a+///    standard HTML color name, or an ANSI palette name, like `ansi-maroon`, `ansi-default`, etc.+/// * `bold`,`italic`,`reverse`,`underline`: can be `on` or `off`. +/// * everything else is a style; all HTML and ANSI color names are also a style (so we can just use `red`+///   instead of `color=red`, or `on red` instead of `bgcolor=red`), and there are+///   the `b`, `i`, `u`, and `r` styles for bold, italic, underline, and reverse.+/// +/// See [here](https://github.com/daanx/isocline#bbcode-format) for a description of the full bbcode format.+void ic_print( const char* s );++/// Print with bbcode markup ending with a newline.+/// @see ic_print()+void ic_println( const char* s );++/// Print formatted with bbcode markup.+/// @see ic_print()+void ic_printf(const char* fmt, ...);++/// Print formatted with bbcode markup.+/// @see ic_print+void ic_vprintf(const char* fmt, va_list args);++/// Define or redefine a style.+/// @param style_name The name of the style. +/// @param fmt        The `fmt` string is the content of a tag and can contain+///   other styles. This is very useful to theme the output of a program+///   by assigning standard styles like `em` or `warning` etc.+void ic_style_def( const char* style_name, const char* fmt );++/// Start a global style that is only reset when calling a matching ic_style_close().+void ic_style_open( const char* fmt );++/// End a global style.+void ic_style_close(void);++/// \}+++//--------------------------------------------------------------+// History+//--------------------------------------------------------------+/// \defgroup history History+/// Readline input history.+/// \{++/// Enable history. +/// Use a \a NULL filename to not persist the history. Use -1 for max_entries to get the default (200).+void ic_set_history(const char* fname, long max_entries );++/// Remove the last entry in the history. +/// The last returned input from ic_readline() is automatically added to the history; this function removes it.+void ic_history_remove_last(void);++/// Clear the history.+void ic_history_clear(void);++/// Add an entry to the history+void ic_history_add( const char* entry );++/// \}++//--------------------------------------------------------------+// Basic Completion+//--------------------------------------------------------------++/// \defgroup completion Completion+/// Basic word completion.+/// \{++/// A completion environment+struct ic_completion_env_s;++/// A completion environment+typedef struct ic_completion_env_s ic_completion_env_t;++/// A completion callback that is called by isocline when tab is pressed.+/// It is passed a completion environment (containing the current input and the current cursor position), +/// the current input up-to the cursor (`prefix`)+/// and the user given argument when the callback was set.+/// When using completion transformers, like `ic_complete_quoted_word` the `prefix` contains the+/// the word to be completed without escape characters or quotes.+typedef void (ic_completer_fun_t)(ic_completion_env_t* cenv, const char* prefix );++/// Set the default completion handler.+/// @param completer  The completion function+/// @param arg        Argument passed to the \a completer.+/// There can only be one default completion function, setting it again disables the previous one.+/// The initial completer use `ic_complete_filename`.+void ic_set_default_completer( ic_completer_fun_t* completer, void* arg);+++/// In a completion callback (usually from ic_complete_word()), use this function to add a completion.+/// (the completion string is copied by isocline and do not need to be preserved or allocated).+///+/// Returns `true` if the callback should continue trying to find more possible completions.+/// If `false` is returned, the callback should try to return and not add more completions (for improved latency).+bool ic_add_completion(ic_completion_env_t* cenv, const char* completion);++/// In a completion callback (usually from ic_complete_word()), use this function to add a completion.+/// The `display` is used to display the completion in the completion menu, and `help` is+/// displayed for hints for example. Both can be `NULL` for the default.+/// (all are copied by isocline and do not need to be preserved or allocated).+///+/// Returns `true` if the callback should continue trying to find more possible completions.+/// If `false` is returned, the callback should try to return and not add more completions (for improved latency).+bool ic_add_completion_ex( ic_completion_env_t* cenv, const char* completion, const char* display, const char* help );++/// In a completion callback (usually from ic_complete_word()), use this function to add completions.+/// The `completions` array should be terminated with a NULL element, and all elements+/// are added as completions if they start with `prefix`.+///+/// Returns `true` if the callback should continue trying to find more possible completions.+/// If `false` is returned, the callback should try to return and not add more completions (for improved latency).+bool ic_add_completions(ic_completion_env_t* cenv, const char* prefix, const char** completions);++/// Complete a filename.+/// Complete a filename given a semi-colon separated list of root directories `roots` and +/// semi-colon separated list of possible extensions (excluding directories). +/// If `roots` is NULL, the current directory is the root ("."). +/// If `extensions` is NULL, any extension will match.+/// Each root directory should _not_ end with a directory separator.+/// If a directory is completed, the `dir_separator` is added at the end if it is not `0`.+/// Usually the `dir_separator` is `/` but it can be set to `\\` on Windows systems.+/// For example:+/// ```+/// /ho         --> /home/+/// /home/.ba   --> /home/.bashrc+/// ```+/// (This already uses ic_complete_quoted_word() so do not call it from inside a word handler).+void ic_complete_filename( ic_completion_env_t* cenv, const char* prefix, char dir_separator, const char* roots, const char* extensions );++++/// Function that returns whether a (utf8) character (of length `len`) is in a certain character class+/// @see ic_char_is_separator() etc.+typedef bool (ic_is_char_class_fun_t)(const char* s, long len);+++/// Complete a _word_ (i.e. _token_). +/// Calls the user provided function `fun` to complete on the+/// current _word_. Almost all user provided completers should use this function. +/// If `is_word_char` is NULL, the default `&ic_char_is_nonseparator` is used. +/// The `prefix` passed to `fun` is modified to only contain the current word, and +/// any results from `ic_add_completion` are automatically adjusted to replace that part.+/// For example, on the input "hello w", a the user `fun` only gets `w` and can just complete+/// with "world" resulting in "hello world" without needing to consider `delete_before` etc.+/// @see ic_complete_qword() for completing quoted and escaped tokens.+void ic_complete_word(ic_completion_env_t* cenv, const char* prefix, ic_completer_fun_t* fun, ic_is_char_class_fun_t* is_word_char);+++/// Complete a quoted _word_. +/// Calls the user provided function `fun` to complete while taking+/// care of quotes and escape characters. Almost all user provided completers should use+/// this function. The `prefix` passed to `fun` is modified to be unquoted and unescaped, and +/// any results from `ic_add_completion` are automatically quoted and escaped again.+/// For example, completing `hello world`, the `fun` always just completes `hel` or `hello w` to `hello world`, +/// but depending on user input, it will complete as:+/// ```+/// hel        -->  hello\ world+/// hello\ w   -->  hello\ world+/// hello w    -->                   # no completion, the word is just 'w'>+/// "hel       -->  "hello world" +/// "hello w   -->  "hello world"+/// ```+/// with proper quotes and escapes.+/// If `is_word_char` is NULL, the default `&ic_char_is_nonseparator` is used. +/// @see ic_complete_quoted_word() to customize the word boundary, quotes etc.+void ic_complete_qword( ic_completion_env_t* cenv, const char* prefix, ic_completer_fun_t* fun, ic_is_char_class_fun_t* is_word_char );++++/// Complete a _word_. +/// Calls the user provided function `fun` to complete while taking+/// care of quotes and escape characters. Almost all user provided completers should use this function. +/// The `is_word_char` is a set of characters that are part of a "word". Use NULL for the default (`&ic_char_is_nonseparator`).+/// The `escape_char` is the escaping character, usually `\` but use 0 to not have escape characters.+/// The `quote_chars` define the quotes, use NULL for the default `"\'\""` quotes.+/// @see ic_complete_word() which uses the default values for `non_word_chars`, `quote_chars` and `\` for escape characters.+void ic_complete_qword_ex( ic_completion_env_t* cenv, const char* prefix, ic_completer_fun_t fun, +                                ic_is_char_class_fun_t* is_word_char, char escape_char, const char* quote_chars );++/// \}++//--------------------------------------------------------------+/// \defgroup highlight Syntax Highlighting+/// Basic syntax highlighting.+/// \{++/// A syntax highlight environment+struct ic_highlight_env_s;+typedef struct ic_highlight_env_s ic_highlight_env_t;++/// A syntax highlighter callback that is called by readline to syntax highlight user input.+typedef void (ic_highlight_fun_t)(ic_highlight_env_t* henv, const char* input, void* arg);++/// Set a syntax highlighter.+/// There can only be one highlight function, setting it again disables the previous one.+void ic_set_default_highlighter(ic_highlight_fun_t* highlighter, void* arg);++/// Set the style of characters starting at position `pos`.+void ic_highlight(ic_highlight_env_t* henv, long pos, long count, const char* style );++/// Experimental: Convenience callback for a function that highlights `s` using bbcode's.+/// The returned string should be allocated and is free'd by the caller.+typedef char* (ic_highlight_format_fun_t)(const char* s, void* arg);++/// Experimental: Convenience function for highlighting with bbcodes.+/// Can be called in a `ic_highlight_fun_t` callback to colorize the `input` using the +/// the provided `formatted` input that is the styled `input` with bbcodes. The +/// content of `formatted` without bbcode tags should match `input` exactly.+void ic_highlight_formatted(ic_highlight_env_t* henv, const char* input, const char* formatted);++/// \}++//--------------------------------------------------------------+// Readline with a specific completer and highlighter+//--------------------------------------------------------------++/// \defgroup readline+/// \{++/// Read input from the user using rich editing abilities, +/// using a particular completion function and highlighter for this call only.+/// both can be NULL in which case the defaults are used.+/// @see ic_readline(), ic_set_prompt_marker(), ic_set_default_completer(), ic_set_default_highlighter().+char* ic_readline_ex(const char* prompt_text, ic_completer_fun_t* completer, void* completer_arg,+                                              ic_highlight_fun_t* highlighter, void* highlighter_arg);++/// \}+++//--------------------------------------------------------------+// Options+//--------------------------------------------------------------++/// \defgroup options Options+/// \{++/// Set a prompt marker and a potential marker for extra lines with multiline input. +/// Pass \a NULL for the `prompt_marker` for the default marker (`"> "`).+/// Pass \a NULL for continuation prompt marker to make it equal to the `prompt_marker`.+void ic_set_prompt_marker( const char* prompt_marker, const char* continuation_prompt_marker );++/// Get the current prompt marker.+const char* ic_get_prompt_marker(void);++/// Get the current continuation prompt marker.+const char* ic_get_continuation_prompt_marker(void);++/// Disable or enable multi-line input (enabled by default).+/// Returns the previous setting.+bool ic_enable_multiline( bool enable );++/// Disable or enable sound (enabled by default).+/// A beep is used when tab cannot find any completion for example.+/// Returns the previous setting.+bool ic_enable_beep( bool enable );++/// Disable or enable color output (enabled by default).+/// Returns the previous setting.+bool ic_enable_color( bool enable );++/// Disable or enable duplicate entries in the history (disabled by default).+/// Returns the previous setting.+bool ic_enable_history_duplicates( bool 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 setting.+bool ic_enable_auto_tab( bool enable );++/// Disable or enable preview of a completion selection (enabled by default)+/// Returns the previous setting.+bool ic_enable_completion_preview( bool enable );++/// Disable or enable automatic identation of continuation lines in multiline+/// input so it aligns with the initial prompt.+/// Returns the previous setting.+bool ic_enable_multiline_indent(bool enable);++/// Disable or enable display of short help messages for history search etc.+/// (full help is always dispayed when pressing F1 regardless of this setting)+/// @returns the previous setting.+bool ic_enable_inline_help(bool enable);++/// Disable or enable hinting (enabled by default)+/// Shows a hint inline when there is a single possible completion.+/// @returns the previous setting.+bool ic_enable_hint(bool enable);++/// Set millisecond delay before a hint is displayed. Can be zero. (500ms by default).+long ic_set_hint_delay(long delay_ms);++/// Disable or enable syntax highlighting (enabled by default).+/// This applies regardless whether a syntax highlighter callback was set (`ic_set_highlighter`)+/// Returns the previous setting.+bool ic_enable_highlight(bool enable);+++/// Set millisecond delay for reading escape sequences in order to distinguish+/// a lone ESC from the start of a escape sequence. The defaults are 100ms and 10ms, +/// but it may be increased if working with very slow terminals.+void ic_set_tty_esc_delay(long initial_delay_ms, long followup_delay_ms);++/// Enable highlighting of matching braces (and error highlight unmatched braces).`+bool ic_enable_brace_matching(bool enable);++/// Set matching brace pairs.+/// Pass \a NULL for the default `"()[]{}"`.+void ic_set_matching_braces(const char* brace_pairs);++/// Enable automatic brace insertion (enabled by default).+bool ic_enable_brace_insertion(bool enable);++/// Set matching brace pairs for automatic insertion.+/// Pass \a NULL for the default `()[]{}\"\"''`+void ic_set_insertion_braces(const char* brace_pairs);++/// \}+++//--------------------------------------------------------------+// Advanced Completion+//--------------------------------------------------------------++/// \defgroup completex Advanced Completion+/// \{++/// Get the raw current input (and cursor position if `cursor` != NULL) for the completion.+/// Usually completer functions should look at their `prefix` though as transformers+/// like `ic_complete_word` may modify the prefix (for example, unescape it).+const char* ic_completion_input( ic_completion_env_t* cenv, long* cursor );++/// Get the completion argument passed to `ic_set_completer`.+void* ic_completion_arg( ic_completion_env_t* cenv );++/// Do we have already some completions?+bool ic_has_completions( ic_completion_env_t* cenv );++/// Do we already have enough completions and should we return if possible? (for improved latency)+bool ic_stop_completing(ic_completion_env_t* cenv);+++/// Primitive completion, cannot be used with most transformers (like `ic_complete_word` and `ic_complete_qword`).+/// When completed, `delete_before` _bytes_ are deleted before the cursor position,+/// `delete_after` _bytes_ are deleted after the cursor, and finally `completion` is inserted.+/// The `display` is used to display the completion in the completion menu, and `help` is displayed+/// with hinting. Both `display` and `help` can be NULL.+/// (all are copied by isocline and do not need to be preserved or allocated).+///+/// Returns `true` if the callback should continue trying to find more possible completions.+/// If `false` is returned, the callback should try to return and not add more completions (for improved latency).+bool ic_add_completion_prim( ic_completion_env_t* cenv, const char* completion, +                              const char* display, const char* help, +                               long delete_before, long delete_after);++/// \}++//--------------------------------------------------------------+/// \defgroup helper Character Classes.+/// Convenience functions for character classes, highlighting and completion.+/// \{++/// Convenience: return the position of a previous code point in a UTF-8 string `s` from postion `pos`.+/// Returns `-1` if `pos <= 0` or `pos > strlen(s)` (or other errors).+long ic_prev_char( const char* s, long pos );++/// Convenience: return the position of the next code point in a UTF-8 string `s` from postion `pos`.+/// Returns `-1` if `pos < 0` or `pos >= strlen(s)` (or other errors).+long ic_next_char( const char* s, long pos );++/// Convenience: does a string `s` starts with a given `prefix` ?+bool ic_starts_with( const char* s, const char* prefix );++/// Convenience: does a string `s` starts with a given `prefix` ignoring (ascii) case?+bool ic_istarts_with( const char* s, const char* prefix );+++/// Convenience: character class for whitespace `[ \t\r\n]`.+bool ic_char_is_white(const char* s, long len);++/// Convenience: character class for non-whitespace `[^ \t\r\n]`.+bool ic_char_is_nonwhite(const char* s, long len);++/// Convenience: character class for separators.+/// (``[ \t\r\n,.;:/\\(){}\[\]]``.)+/// This is used for word boundaries in isocline.+bool ic_char_is_separator(const char* s, long len);++/// Convenience: character class for non-separators.+bool ic_char_is_nonseparator(const char* s, long len);++/// Convenience: character class for letters (`[A-Za-z]` and any unicode > 0x80).+bool ic_char_is_letter(const char* s, long len);++/// Convenience: character class for digits (`[0-9]`).+bool ic_char_is_digit(const char* s, long len);++/// Convenience: character class for hexadecimal digits (`[A-Fa-f0-9]`).+bool ic_char_is_hexdigit(const char* s, long len);++/// Convenience: character class for identifier letters (`[A-Za-z0-9_-]` and any unicode > 0x80).+bool ic_char_is_idletter(const char* s, long len);++/// Convenience: character class for filename letters (_not in_ " \t\r\n`@$><=;|&\{\}\(\)\[\]]").+bool ic_char_is_filename_letter(const char* s, long len);+++/// Convenience: If this is a token start, return the length. Otherwise return 0.+long ic_is_token(const char* s, long pos, ic_is_char_class_fun_t* is_token_char);++/// Convenience: Does this match the specified token? +/// Ensures not to match prefixes or suffixes, and returns the length of the match (in bytes).+/// E.g. `ic_match_token("function",0,&ic_char_is_letter,"fun")` returns 0.+/// while `ic_match_token("fun x",0,&ic_char_is_letter,"fun"})` returns 3.+long ic_match_token(const char* s, long pos, ic_is_char_class_fun_t* is_token_char, const char* token);+++/// Convenience: Do any of the specified tokens match? +/// Ensures not to match prefixes or suffixes, and returns the length of the match (in bytes).+/// E.g. `ic_match_any_token("function",0,&ic_char_is_letter,{"fun","func",NULL})` returns 0.+/// while `ic_match_any_token("func x",0,&ic_char_is_letter,{"fun","func",NULL})` returns 4.+long ic_match_any_token(const char* s, long pos, ic_is_char_class_fun_t* is_token_char, const char** tokens);++/// \}++//--------------------------------------------------------------+/// \defgroup term Terminal+///+/// Experimental: Low level terminal output.+/// Ensures basic ANSI SGR escape sequences are processed +/// in a portable way (e.g. on Windows)+/// \{++/// Initialize for terminal output.+/// Call this before using the terminal write functions (`ic_term_write`)+/// Does nothing on most platforms but on Windows it sets the console to UTF8 output and possible +/// enables virtual terminal processing.+void ic_term_init(void);++/// Call this when done with the terminal functions.+void ic_term_done(void);++/// Flush the terminal output. +/// (happens automatically on newline characters ('\n') as well).+void ic_term_flush(void);++/// Write a string to the console (and process CSI escape sequences).+void ic_term_write(const char* s);++/// Write a string to the console and end with a newline +/// (and process CSI escape sequences).+void ic_term_writeln(const char* s);++/// Write a formatted string to the console.+/// (and process CSI escape sequences)+void ic_term_writef(const char* fmt, ...);++/// Write a formatted string to the console.+void ic_term_vwritef(const char* fmt, va_list args);++/// Set text attributes from a style.+void ic_term_style( const char* style );++/// Set text attribute to bold.+void ic_term_bold(bool enable);++/// Set text attribute to underline.+void ic_term_underline(bool enable);++/// Set text attribute to italic.+void ic_term_italic(bool enable);++/// Set text attribute to reverse video.+void ic_term_reverse(bool enable);++/// Set text attribute to ansi color palette index between 0 and 255 (or 256 for the ANSI "default" color).+/// (auto matched to smaller palette if not supported)+void ic_term_color_ansi(bool foreground, int color);++/// Set text attribute to 24-bit RGB color (between `0x000000` and `0xFFFFFF`).+/// (auto matched to smaller palette if not supported)+void ic_term_color_rgb(bool foreground, uint32_t color );++/// Reset the text attributes.+void ic_term_reset( void );++/// Get the palette used by the terminal:+/// This is usually initialized from the COLORTERM environment variable. The +/// possible values of COLORTERM for each palette are given in parenthesis.+///+/// - 1: monochrome (`monochrome`)+/// - 3: old ANSI terminal with 8 colors, using bold for bright (`8color`/`3bit`)+/// - 4: regular ANSI terminal with 16 colors.     (`16color`/`4bit`)+/// - 8: terminal with ANSI 256 color palette.     (`256color`/`8bit`)+/// - 24: true-color terminal with full RGB colors. (`truecolor`/`24bit`/`direct`)+int ic_term_get_color_bits( void );++/// \}++//--------------------------------------------------------------+/// \defgroup async ASync+/// Async support+/// \{++/// Thread-safe way to asynchronously unblock a readline.+/// Behaves as if the user pressed the `ctrl-C` character+/// (resulting in returning NULL from `ic_readline`).+/// Returns `true` if the event was successfully delivered.+/// (This may not be supported on all platforms, but it is+/// functional on Linux, macOS and Windows).+bool ic_async_stop(void);++/// \}++//--------------------------------------------------------------+/// \defgroup alloc Custom Allocation+/// Register allocation functions for custom allocators+/// \{++typedef void* (ic_malloc_fun_t)( size_t size );+typedef void* (ic_realloc_fun_t)( void* p, size_t newsize );+typedef void  (ic_free_fun_t)( void* p );++/// Initialize with custom allocation functions.+/// This must be called as the first function in a program!+void ic_init_custom_alloc( ic_malloc_fun_t* _malloc, ic_realloc_fun_t* _realloc, ic_free_fun_t* _free );++/// Free a potentially custom alloc'd pointer (in particular, the result returned from `ic_readline`)+void ic_free( void* p );++/// Allocate using the current memory allocator.+void* ic_malloc(size_t sz);++/// Duplicate a string using the current memory allocator.+const char* ic_strdup( const char* s );++/// \}++#ifdef __cplusplus+}+#endif++#endif /// IC_ISOCLINE_H
+ isocline.cabal view
@@ -0,0 +1,99 @@+cabal-version: 1.12++-- This file has been generated from package.yaml by hpack version 0.34.4.+--+-- see: https://github.com/sol/hpack++name:           isocline+version:        1.0.0+synopsis:       A portable alternative to GNU Readline+description:    ![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).+                Please see the [readme](https://github.com/daanx/isocline/haskell#readme) on GitHub for more information.+category:       console+homepage:       https://github.com/daanx/isocline#readme+bug-reports:    https://github.com/daanx/isocline/issues+author:         Daan Leijen+maintainer:     daan@effp.org+license:        MIT+license-file:   LICENSE+build-type:     Simple+extra-source-files:+    haskell/readme.md+    src/attr.c+    src/bbcode.c+    src/bbcode_colors.c+    src/common.c+    src/completers.c+    src/completions.c+    src/editline.c+    src/editline_completion.c+    src/editline_help.c+    src/editline_history.c+    src/highlight.c+    src/history.c+    src/isocline.c+    src/stringbuf.c+    src/term.c+    src/term_color.c+    src/tty.c+    src/tty_esc.c+    src/undo.c+    src/wcwidth.c+    src/attr.h+    src/bbcode.h+    src/common.h+    src/completions.h+    src/env.h+    src/highlight.h+    src/history.h+    src/stringbuf.h+    src/term.h+    src/tty.h+    src/undo.h+    include/isocline.h++source-repository head+  type: git+  location: https://github.com/daanx/isocline++library+  exposed-modules:+      System.Console.Isocline+  other-modules:+      Paths_isocline+  hs-source-dirs:+      haskell+  c-sources:+      src/isocline.c+  build-depends:+      base >=3.0 && <5+    , bytestring+    , text+  default-language: Haskell2010++executable example+  main-is: Example.hs+  other-modules:+      Paths_isocline+  hs-source-dirs:+      test+  build-depends:+      base >=3.0 && <5+    , bytestring+    , isocline+    , text+  default-language: Haskell2010++test-suite test-example+  type: exitcode-stdio-1.0+  main-is: Example.hs+  other-modules:+      Paths_isocline+  hs-source-dirs:+      test+  build-depends:+      base >=3.0 && <5+    , bytestring+    , isocline+    , text+  default-language: Haskell2010
+ src/attr.c view
@@ -0,0 +1,294 @@+/* ----------------------------------------------------------------------------+  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.+-----------------------------------------------------------------------------*/+#include <string.h>++#include "common.h"+#include "stringbuf.h" // str_next_ofs+#include "attr.h"+#include "term.h"      // color_from_ansi256++//-------------------------------------------------------------+// Attributes+//-------------------------------------------------------------++ic_private attr_t attr_none(void) {+  attr_t attr;+  attr.value = 0;+  return attr;+}++ic_private attr_t attr_default(void) {+  attr_t attr = attr_none();+  attr.x.color = IC_ANSI_DEFAULT;+  attr.x.bgcolor = IC_ANSI_DEFAULT;+  attr.x.bold = IC_OFF;+  attr.x.underline = IC_OFF; +  attr.x.reverse = IC_OFF;+  attr.x.italic = IC_OFF; +  return attr;+}++ic_private bool attr_is_none(attr_t attr) {+  return (attr.value == 0);+}++ic_private bool attr_is_eq(attr_t attr1, attr_t attr2) {+  return (attr1.value == attr2.value);+}++ic_private attr_t attr_from_color( ic_color_t color ) {+  attr_t attr = attr_none();+  attr.x.color = color;+  return attr;+}+++ic_private attr_t attr_update_with( attr_t oldattr, attr_t newattr ) {+  attr_t attr = oldattr;+  if (newattr.x.color != IC_COLOR_NONE)   { attr.x.color = newattr.x.color; }+  if (newattr.x.bgcolor != IC_COLOR_NONE) { attr.x.bgcolor = newattr.x.bgcolor; }+  if (newattr.x.bold != IC_NONE)          { attr.x.bold = newattr.x.bold; }+  if (newattr.x.italic != IC_NONE)        { attr.x.italic = newattr.x.italic; }+  if (newattr.x.reverse != IC_NONE)       { attr.x.reverse = newattr.x.reverse; }+  if (newattr.x.underline != IC_NONE)     { attr.x.underline = newattr.x.underline; }+  return attr;+}++static bool sgr_is_digit(char c) {+  return (c >= '0' && c <= '9');+}++static bool sgr_is_sep( char c ) {+  return (c==';' || c==':');+}++static bool sgr_next_par(const char* s, ssize_t* pi, ssize_t* par) {+  const ssize_t i = *pi;+  ssize_t n = 0;+  while( sgr_is_digit(s[i+n])) { +    n++; +  }+  if (n==0) { +    *par = 0;+    return true;+  }+  else {+    *pi = i+n;+    return ic_atoz(s+i, par);    +  }+}++static bool sgr_next_par3(const char* s, ssize_t* pi, ssize_t* p1, ssize_t* p2, ssize_t* p3) {+  bool ok = false;+  ssize_t i = *pi;+  if (sgr_next_par(s,&i,p1) && sgr_is_sep(s[i])) {+    i++;+    if (sgr_next_par(s,&i,p2) && sgr_is_sep(s[i])) {+      i++;+      if (sgr_next_par(s,&i,p3)) {+        ok = true;+      };+    }+  }+  *pi = i;+  return ok;+}++ic_private attr_t attr_from_sgr( const char* s, ssize_t len) {+  attr_t attr = attr_none();+  for( ssize_t i = 0; i < len && s[i] != 0; i++) {+    ssize_t cmd = 0;+    if (!sgr_next_par(s,&i,&cmd)) continue;+    switch(cmd) {+      case  0: attr = attr_default(); break;+      case  1: attr.x.bold = IC_ON; break;+      case  3: attr.x.italic = IC_ON; break;+      case  4: attr.x.underline = IC_ON; break;+      case  7: attr.x.reverse = IC_ON; break;+      case 22: attr.x.bold = IC_OFF; break;+      case 23: attr.x.italic = IC_OFF; break;+      case 24: attr.x.underline = IC_OFF; break;+      case 27: attr.x.reverse = IC_OFF; break;+      case 39: attr.x.color = IC_ANSI_DEFAULT; break;+      case 49: attr.x.bgcolor = IC_ANSI_DEFAULT; break;+      default: {+        if (cmd >= 30 && cmd <= 37) {+          attr.x.color = IC_ANSI_BLACK + (unsigned)(cmd - 30);+        }+        else if (cmd >= 40 && cmd <= 47) {+          attr.x.bgcolor = IC_ANSI_BLACK + (unsigned)(cmd - 40);+        }+        else if (cmd >= 90 && cmd <= 97) {+          attr.x.color = IC_ANSI_DARKGRAY + (unsigned)(cmd - 90);+        }+        else if (cmd >= 100 && cmd <= 107) {+          attr.x.bgcolor = IC_ANSI_DARKGRAY + (unsigned)(cmd - 100);+        }+        else if ((cmd == 38 || cmd == 48) && sgr_is_sep(s[i])) {+          // non-associative SGR :-(          +          ssize_t par = 0;+          i++;+          if (sgr_next_par(s, &i, &par)) {+            if (par==5 && sgr_is_sep(s[i])) {+              // ansi 256 index+              i++;+              if (sgr_next_par(s, &i, &par) && par >= 0 && par <= 0xFF) {+                ic_color_t color = color_from_ansi256(par);+                if (cmd==38) { attr.x.color = color; }+                        else { attr.x.bgcolor = color; }+              }+            }+            else if (par == 2 && sgr_is_sep(s[i])) {+              // rgb value+              i++;+              ssize_t r,g,b;+              if (sgr_next_par3(s, &i, &r,&g,&b)) {+                ic_color_t color = ic_rgbx(r,g,b);+                if (cmd==38) { attr.x.color = color; }+                        else { attr.x.bgcolor = color; }+              }+            }+          }+        }+        else {+          debug_msg("attr: unknow ANSI SGR code: %zd\n", cmd );+        }+      }+    }+  }+  return attr;+}++ic_private attr_t attr_from_esc_sgr( const char* s, ssize_t len) {+  if (len <= 2 || s[0] != '\x1B' || s[1] != '[' || s[len-1] != 'm') return attr_none();+  return attr_from_sgr(s+2, len-2);+}+++//-------------------------------------------------------------+// Attribute buffer+//-------------------------------------------------------------+struct attrbuf_s {+  attr_t*  attrs;+  ssize_t  capacity;+  ssize_t  count;+  alloc_t* mem;+};++static bool attrbuf_ensure_capacity( attrbuf_t* ab, ssize_t needed ) {+  if (needed <= ab->capacity) return true;+  ssize_t newcap = (ab->capacity <= 0 ? 240 : (ab->capacity > 1000 ? ab->capacity + 1000 : 2*ab->capacity));+  if (needed > newcap) { newcap = needed; }+  attr_t* newattrs = mem_realloc_tp( ab->mem, attr_t, ab->attrs, newcap );+  if (newattrs == NULL) return false;+  ab->attrs = newattrs;+  ab->capacity = newcap;+  assert(needed <= ab->capacity);+  return true;+}++static bool attrbuf_ensure_extra( attrbuf_t* ab, ssize_t extra ) {+  const ssize_t needed = ab->count + extra;+  return attrbuf_ensure_capacity( ab, needed );+}+++ic_private attrbuf_t* attrbuf_new( alloc_t* mem ) {+  attrbuf_t* ab = mem_zalloc_tp(mem,attrbuf_t);+  if (ab == NULL) return NULL;+  ab->mem = mem;+  attrbuf_ensure_extra(ab,1);+  return ab;+}++ic_private void attrbuf_free( attrbuf_t* ab ) {+  if (ab==NULL) return;+  mem_free(ab->mem, ab->attrs);+  mem_free(ab->mem, ab);+}++ic_private void attrbuf_clear(attrbuf_t* ab) {+  if (ab == NULL) return;+  ab->count = 0;+}++ic_private ssize_t attrbuf_len( attrbuf_t* ab ) {+  return (ab==NULL ? 0 : ab->count);+}++ic_private const attr_t* attrbuf_attrs( attrbuf_t* ab, ssize_t expected_len ) {+  assert(expected_len <= ab->count );+  // expand if needed+  if (ab->count < expected_len) {    +    if (!attrbuf_ensure_capacity(ab,expected_len)) return NULL;+    for(ssize_t i = ab->count; i < expected_len; i++) {+      ab->attrs[i] = attr_none();  +    }+    ab->count = expected_len;+  }+  return ab->attrs;+}++++static void attrbuf_update_set_at( attrbuf_t* ab, ssize_t pos, ssize_t count, attr_t attr, bool update ) {+  const ssize_t end = pos + count;+  if (!attrbuf_ensure_capacity(ab, end)) return;+  ssize_t i;+  // initialize if end is beyond the count (todo: avoid duplicate init and set if update==false?)+  if (ab->count < end) {+    for(i = ab->count; i < end; i++) {+      ab->attrs[i] = attr_none();  +    }+    ab->count = end;+  }+  // fill pos to end with attr +  for(i = pos; i < end; i++) {+    ab->attrs[i] = (update ? attr_update_with(ab->attrs[i],attr) : attr);    +  }  +}++ic_private void attrbuf_set_at( attrbuf_t* ab, ssize_t pos, ssize_t count, attr_t attr ) {+  attrbuf_update_set_at(ab, pos, count, attr, false);+}++ic_private void attrbuf_update_at( attrbuf_t* ab, ssize_t pos, ssize_t count, attr_t attr ) {+  attrbuf_update_set_at(ab, pos, count, attr, true);  +}++ic_private void attrbuf_insert_at( attrbuf_t* ab, ssize_t pos, ssize_t count, attr_t attr ) {+  if (pos < 0 || pos > ab->count || count <= 0) return;+  if (!attrbuf_ensure_extra(ab,count)) return;  +  ic_memmove( ab->attrs + pos + count, ab->attrs + pos, (ab->count - pos)*ssizeof(attr_t) );+  ab->count += count;+  attrbuf_set_at( ab, pos, count, attr );+}+++// note: must allow ab == NULL!+ic_private ssize_t attrbuf_append_n( stringbuf_t* sb, attrbuf_t* ab, const char* s, ssize_t len, attr_t attr ) {+  if (s == NULL || len == 0) return sbuf_len(sb);+  if (ab != NULL) {+    if (!attrbuf_ensure_extra(ab,len)) return sbuf_len(sb);+    attrbuf_set_at(ab, ab->count, len, attr);+  }+  return sbuf_append_n(sb,s,len);+}++ic_private attr_t attrbuf_attr_at( attrbuf_t* ab, ssize_t pos ) {+  if (ab==NULL || pos < 0 || pos > ab->count) return attr_none();+  return ab->attrs[pos];+}++ic_private void attrbuf_delete_at( attrbuf_t* ab, ssize_t pos, ssize_t count ) {+  if (ab==NULL || pos < 0 || pos > ab->count) return;+  if (pos + count > ab->count) { count = ab->count - pos; }+  if (count == 0) return;+  assert(pos + count <= ab->count);+  ic_memmove( ab->attrs + pos, ab->attrs + pos + count, ab->count - (pos + count) );+  ab->count -= count;+}
+ src/attr.h view
@@ -0,0 +1,70 @@+/* ----------------------------------------------------------------------------+  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.+-----------------------------------------------------------------------------*/+#pragma once+#ifndef IC_ATTR_H+#define IC_ATTR_H++#include "common.h"+#include "stringbuf.h"++//-------------------------------------------------------------+// text attributes+//-------------------------------------------------------------++#define IC_ON   (1)+#define IC_OFF  (-1)+#define IC_NONE (0)++// try to fit in 64 bits +// note: order is important for some compilers+// note: each color can actually be 25 bits+typedef union attr_s {+  struct {+    unsigned int  color:28;+    signed int    bold:2;+    signed int    reverse:2;+    unsigned int  bgcolor:28;+    signed int    underline:2;+    signed int    italic:2;+  } x;+  uint64_t        value;+} attr_t;++ic_private attr_t attr_none(void);+ic_private attr_t attr_default(void);+ic_private attr_t attr_from_color( ic_color_t color );++ic_private bool attr_is_none(attr_t attr);+ic_private bool attr_is_eq(attr_t attr1, attr_t attr2);++ic_private attr_t attr_update_with( attr_t attr, attr_t newattr );++ic_private attr_t attr_from_sgr( const char* s, ssize_t len);+ic_private attr_t attr_from_esc_sgr( const char* s, ssize_t len);++//-------------------------------------------------------------+// attribute buffer used for rich rendering+//-------------------------------------------------------------++struct attrbuf_s;+typedef struct attrbuf_s attrbuf_t;++ic_private attrbuf_t*     attrbuf_new( alloc_t* mem );+ic_private void           attrbuf_free( attrbuf_t* ab );  // ab can be NULL+ic_private void           attrbuf_clear( attrbuf_t* ab ); // ab can be NULL+ic_private ssize_t        attrbuf_len( attrbuf_t* ab);    // ab can be NULL+ic_private const attr_t*  attrbuf_attrs( attrbuf_t* ab, ssize_t expected_len );+ic_private ssize_t        attrbuf_append_n( stringbuf_t* sb, attrbuf_t* ab, const char* s, ssize_t len, attr_t attr );++ic_private void           attrbuf_set_at( attrbuf_t* ab, ssize_t pos, ssize_t count, attr_t attr );+ic_private void           attrbuf_update_at( attrbuf_t* ab, ssize_t pos, ssize_t count, attr_t attr );+ic_private void           attrbuf_insert_at( attrbuf_t* ab, ssize_t pos, ssize_t count, attr_t attr );++ic_private attr_t         attrbuf_attr_at( attrbuf_t* ab, ssize_t pos );   +ic_private void           attrbuf_delete_at( attrbuf_t* ab, ssize_t pos, ssize_t count );++#endif // IC_ATTR_H
+ src/bbcode.c view
@@ -0,0 +1,841 @@+/* ----------------------------------------------------------------------------+  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.+-----------------------------------------------------------------------------*/+#include <string.h>+#include <stdio.h>+#include <stdarg.h>+#include <stdlib.h>  ++#include "common.h"+#include "attr.h"+#include "term.h"+#include "bbcode.h" ++//-------------------------------------------------------------+// HTML color table+//-------------------------------------------------------------++#include "bbcode_colors.c"++//-------------------------------------------------------------+// Types+//-------------------------------------------------------------++typedef struct style_s {+  const char*  name;  // name of the style+  attr_t  attr;  // attribute to apply+} style_t;++typedef enum align_e {+  IC_ALIGN_LEFT,+  IC_ALIGN_CENTER,+  IC_ALIGN_RIGHT+} align_t;++typedef struct width_s {+  ssize_t w;     // > 0+  align_t align;+  bool    dots;  // "..."  (e.g. "sentence...")+  char    fill;  // " "    (e.g. "hello      ")+} width_t;++typedef struct tag_s {  +  const char* name;   // tag open name+  attr_t  attr;       // the saved attribute before applying the style+  width_t width;      // start sequence of at most "width" columns+  ssize_t pos;        // start position in the output (used for width restriction)+} tag_t;+++static void tag_init(tag_t* tag) {+  memset(tag,0,sizeof(*tag));  +}++struct bbcode_s {+  tag_t*       tags;              // stack of tags; one entry for each open tag+  ssize_t      tags_capacity;+  ssize_t      tags_nesting;   +  style_t*     styles;            // list of used defined styles+  ssize_t      styles_capacity;+  ssize_t      styles_count;+  term_t*      term;              // terminal+  alloc_t*     mem;               // allocator+  // caches+  stringbuf_t*  out;              // print buffer+  attrbuf_t*    out_attrs;+  stringbuf_t*  vout;             // vprintf buffer +};+++//-------------------------------------------------------------+// Create, helpers+//-------------------------------------------------------------++ic_private bbcode_t* bbcode_new( alloc_t* mem, term_t* term ) {+  bbcode_t* bb = mem_zalloc_tp(mem,bbcode_t);+  if (bb==NULL) return NULL;+  bb->mem = mem;+  bb->term = term;+  bb->out = sbuf_new(mem);+  bb->out_attrs = attrbuf_new(mem);+  bb->vout = sbuf_new(mem);+  return bb;+}++ic_private void bbcode_free( bbcode_t* bb ) {+  for(ssize_t i = 0; i < bb->styles_count; i++) {+    mem_free(bb->mem, bb->styles[i].name);+  }+  mem_free(bb->mem, bb->tags);+  mem_free(bb->mem, bb->styles);+  sbuf_free(bb->vout);+  sbuf_free(bb->out);+  attrbuf_free(bb->out_attrs);+  mem_free(bb->mem, bb);+}++ic_private void bbcode_style_add( bbcode_t* bb, const char* style_name, attr_t attr ) {+  if (bb->styles_count >= bb->styles_capacity) {+    ssize_t newlen = bb->styles_capacity + 32;+    style_t* p = mem_realloc_tp( bb->mem, style_t, bb->styles, newlen );+    if (p == NULL) return;+    bb->styles = p;+    bb->styles_capacity = newlen;+  }+  assert(bb->styles_count < bb->styles_capacity);+  bb->styles[bb->styles_count].name = mem_strdup( bb->mem, style_name );+  bb->styles[bb->styles_count].attr = attr;+  bb->styles_count++;+}++static ssize_t bbcode_tag_push( bbcode_t* bb, const tag_t* tag ) {+  if (bb->tags_nesting >= bb->tags_capacity) {+    ssize_t newcap = bb->tags_capacity + 32;+    tag_t* p = mem_realloc_tp( bb->mem, tag_t, bb->tags, newcap );+    if (p == NULL) return -1;+    bb->tags = p;+    bb->tags_capacity = newcap;    +  }+  assert(bb->tags_nesting < bb->tags_capacity);+  bb->tags[bb->tags_nesting] = *tag;+  bb->tags_nesting++;+  return (bb->tags_nesting-1);+}++static void bbcode_tag_pop( bbcode_t* bb, tag_t* tag ) {+  if (bb->tags_nesting <= 0) {+    if (tag != NULL) { tag_init(tag); }+  }+  else {+    bb->tags_nesting--;+    if (tag != NULL) {+      *tag = bb->tags[bb->tags_nesting];+    }    +  }+}++//-------------------------------------------------------------+// Invalid parse/values/balance+//-------------------------------------------------------------++static void bbcode_invalid(const char* fmt, ... ) {+  if (getenv("ISOCLINE_BBCODE_DEBUG") != NULL) {+    va_list args;+    va_start(args,fmt);+    vfprintf(stderr,fmt,args);+    va_end(args);+  }+}++//-------------------------------------------------------------+// Set attributes+//-------------------------------------------------------------+++static attr_t bbcode_open( bbcode_t* bb, ssize_t out_pos, const tag_t* tag, attr_t current ) { +  // save current and set+  tag_t cur;+  tag_init(&cur);+  cur.name  = tag->name;+  cur.attr  = current;  +  cur.width = tag->width;+  cur.pos   = out_pos;+  bbcode_tag_push(bb,&cur);+  return attr_update_with( current, tag->attr );+}++static bool bbcode_close( bbcode_t* bb, ssize_t base, const char* name, tag_t* pprev ) {+  // pop until match+  while (bb->tags_nesting > base) {+    tag_t prev;+    bbcode_tag_pop(bb,&prev);+    if (name==NULL || prev.name==NULL || ic_stricmp(prev.name,name) == 0) {+      // matched+      if (pprev != NULL) { *pprev = prev; }+      return true;+    }+    else {+      // unbalanced: we either continue popping or restore the tags depending if there is a matching open tag in our tags.+      bool has_open_tag = false;+      if (name != NULL) {+        for( ssize_t i = bb->tags_nesting - 1; i > base; i--) {+          if (bb->tags[i].name != NULL && ic_stricmp(bb->tags[i].name, name) == 0) {+            has_open_tag = true;+            break;+          }+        }+      }+      bbcode_invalid("bbcode: unbalanced tags: open [%s], close [/%s]\n", prev.name, name);            +      if (!has_open_tag) {+        bbcode_tag_push( bb, &prev ); // restore the tags and ignore this close tag+        break;+      }+      else {+        // continue until we hit our open tag+      }+    }+  }+  if (pprev != NULL) { memset(pprev,0,sizeof(*pprev)); }+  return false;+}++//-------------------------------------------------------------+// Update attributes+//-------------------------------------------------------------++static const char* attr_update_bool( const char* fname, signed int* field, const char* value ) {+  if (value == NULL || value[0] == 0 || strcmp(value,"on") || strcmp(value,"true") || strcmp(value,"1")) {+    *field = IC_ON;+  }+  else if (strcmp(value,"off") || strcmp(value,"false") || strcmp(value,"0")) {+    *field = IC_OFF;+  }+  else {+    bbcode_invalid("bbcode: invalid %s value: %s\n", fname, value );+  }+  return fname;+}++static const char* attr_update_color( const char* fname, ic_color_t* field, const char* value ) {+  if (value == NULL || value[0] == 0 || strcmp(value,"none") == 0) {+    *field = IC_COLOR_NONE;+    return fname;+  }+  +  // hex value+  if (value[0] == '#') {+    uint32_t rgb = 0;+    if (sscanf(value,"#%x",&rgb) == 1) {+      *field = ic_rgb(rgb);+    } +    else {+      bbcode_invalid("bbcode: invalid color code: %s\n", value);+    }+    return fname;+  }++  // search color names+  ssize_t lo = 0;+  ssize_t hi = IC_HTML_COLOR_COUNT-1;+  while( lo <= hi ) {+    ssize_t mid = (lo + hi) / 2;+    style_color_t* info = &html_colors[mid];+    int cmp = strcmp(info->name,value);+    if (cmp < 0) {+      lo = mid+1;+    }+    else if (cmp > 0) {+      hi = mid-1;+    }+    else { +      *field = info->color;+      return fname;+    }    +  }+  bbcode_invalid("bbcode: unknown %s: %s\n", fname, value);+  *field = IC_COLOR_NONE;+  return fname;+}++static const char* attr_update_sgr( const char* fname, attr_t* attr, const char* value ) {+  *attr = attr_update_with(*attr, attr_from_sgr(value, ic_strlen(value)));+  return fname;+}++static void attr_update_width( width_t* pwidth, char default_fill, const char* value ) {+  // parse width value: <width>;<left|center|right>;<fill>;<cut>+  width_t width;+  memset(&width, 0, sizeof(width));+  width.fill = default_fill;   // use 0 for no-fill (as for max-width)+  if (ic_atoz(value, &width.w)) {     +    ssize_t i = 0;+    while( value[i] != ';' && value[i] != 0 ) { i++; }+    if (value[i] == ';') {+      i++;+      ssize_t len = 0;    +      while( value[i+len] != ';' && value[i+len] != 0 ) { len++; }+      if (len == 4 && ic_istarts_with(value+i,"left")) {+        width.align = IC_ALIGN_LEFT;+      }+      else if (len == 5 && ic_istarts_with(value+i,"right")) {+        width.align = IC_ALIGN_RIGHT;+      }+      else if (len == 6 && ic_istarts_with(value+i,"center")) {+        width.align = IC_ALIGN_CENTER;+      }+      i += len;+      if (value[i] == ';') {+        i++; len = 0;+        while( value[i+len] != ';' && value[i+len] != 0 ) { len++; }+        if (len == 1) { width.fill = value[i]; }+        i+= len;+        if (value[i] == ';') {+          i++; len = 0;+          while( value[i+len] != ';' && value[i+len] != 0 ) { len++; }+          if ((len == 2 && ic_istarts_with(value+i,"on")) || (len == 1 && value[i] == '1')) { width.dots = true; }+          i += len;+        }+      }+    }+  }+  else {+    bbcode_invalid("bbcode: illegal width: %s\n", value);+  }+  *pwidth = width;+}++static const char* attr_update_ansi_color( const char* fname, ic_color_t* color, const char* value ) {+  ssize_t num = 0;+  if (ic_atoz(value, &num) && num >= 0 && num <= 256) {+    *color = color_from_ansi256(num);+  }+  return fname;+}+++static const char* attr_update_property( tag_t* tag, const char* attr_name, const char* value ) {+  const char* fname = NULL;+  fname = "bold";+  if (strcmp(attr_name,fname) == 0) {    +    signed int b = IC_NONE;    +    attr_update_bool(fname,&b, value); +    if (b != IC_NONE) { tag->attr.x.bold = b; }+    return fname;+  }+  fname = "italic";+  if (strcmp(attr_name,fname) == 0) {    +    signed int b = IC_NONE;      +    attr_update_bool(fname,&b, value); +    if (b != IC_NONE) { tag->attr.x.italic = b; }+    return fname;+  }+  fname = "underline";+  if (strcmp(attr_name,fname) == 0) {  +    signed int b = IC_NONE;        +    attr_update_bool(fname,&b, value); +    if (b != IC_NONE) { tag->attr.x.underline = b; }+    return fname;+  }+  fname = "reverse";+  if (strcmp(attr_name,fname) == 0) {+    signed int b = IC_NONE;          +    attr_update_bool(fname,&b, value); +    if (b != IC_NONE) { tag->attr.x.reverse = b; }+    return fname;+  }+  fname = "color";+  if (strcmp(attr_name,fname) == 0) {+    unsigned int color = IC_COLOR_NONE;+    attr_update_color(fname, &color, value);+    if (color != IC_COLOR_NONE) { tag->attr.x.color = color; }+    return fname;+  }+  fname = "bgcolor";+  if (strcmp(attr_name,fname) == 0) {+    unsigned int color = IC_COLOR_NONE;+    attr_update_color(fname, &color, value);+    if (color != IC_COLOR_NONE) { tag->attr.x.bgcolor = color; }+    return fname;+  }+  fname = "ansi-sgr";+  if (strcmp(attr_name,fname) == 0) {+    attr_update_sgr(fname, &tag->attr, value);+    return fname;+  }+  fname = "ansi-color";+  if (strcmp(attr_name,fname) == 0) {+    ic_color_t color = IC_COLOR_NONE;;+    attr_update_ansi_color(fname, &color, value);+    if (color != IC_COLOR_NONE) { tag->attr.x.color = color; }+    return fname;+  }+  fname = "ansi-bgcolor";+  if (strcmp(attr_name,fname) == 0) {+    ic_color_t color = IC_COLOR_NONE;;+    attr_update_ansi_color(fname, &color, value);+    if (color != IC_COLOR_NONE) { tag->attr.x.bgcolor = color; }+    return fname;+  }  +  fname = "width";+  if (strcmp(attr_name,fname) == 0) {+    attr_update_width(&tag->width, ' ', value);+    return fname;+  }+  fname = "max-width";+  if (strcmp(attr_name,fname) == 0) {+    attr_update_width(&tag->width, 0, value);+    return "width";+  }    +  else {+    return NULL;+  }+}++static const style_t builtin_styles[] = {+  { "b",  { { IC_COLOR_NONE, IC_ON  , IC_NONE, IC_COLOR_NONE, IC_NONE, IC_NONE } } },+  { "r",  { { IC_COLOR_NONE, IC_NONE, IC_ON  , IC_COLOR_NONE, IC_NONE, IC_NONE } } },+  { "u",  { { IC_COLOR_NONE, IC_NONE, IC_NONE, IC_COLOR_NONE, IC_ON  , IC_NONE } } },+  { "i",  { { IC_COLOR_NONE, IC_NONE, IC_NONE, IC_COLOR_NONE, IC_NONE, IC_ON   } } },+  { "em", { { IC_COLOR_NONE, IC_ON  , IC_NONE, IC_COLOR_NONE, IC_NONE, IC_NONE } } }, // bold+  { "url",{ { IC_COLOR_NONE, IC_NONE, IC_NONE, IC_COLOR_NONE, IC_ON,   IC_NONE } } }, // underline+  { NULL, { { IC_COLOR_NONE, IC_NONE, IC_NONE, IC_COLOR_NONE, IC_NONE, IC_NONE } } }+};++static void attr_update_with_styles( tag_t* tag, const char* attr_name, const char* value, +                                             bool usebgcolor, const style_t* styles, ssize_t count ) +{+  // direct hex color?+  if (attr_name[0] == '#' && (value == NULL || value[0]==0)) {+    value = attr_name;+    attr_name = (usebgcolor ? "bgcolor" : "color");+  }+  // first try if it is a builtin property+  const char* name;+  if ((name = attr_update_property(tag,attr_name,value)) != NULL) {+    if (tag->name != NULL) tag->name = name;+    return;+  }+  // then check all styles+  while( count-- > 0 ) {+    const style_t* style = styles + count;+    if (strcmp(style->name,attr_name) == 0) {+      tag->attr = attr_update_with(tag->attr,style->attr);+      if (tag->name != NULL) tag->name = style->name;+      return;+    }    +  }+  // check builtin styles; todo: binary search?+  for( const style_t* style = builtin_styles; style->name != NULL; style++) {+    if (strcmp(style->name,attr_name) == 0) {+      tag->attr = attr_update_with(tag->attr,style->attr);+      if (tag->name != NULL) tag->name = style->name;+      return;+    }+  }+  // check colors as a style+  ssize_t lo = 0;+  ssize_t hi = IC_HTML_COLOR_COUNT-1;+  while( lo <= hi ) {+    ssize_t mid = (lo + hi) / 2;+    style_color_t* info = &html_colors[mid];+    int cmp = strcmp(info->name,attr_name);    +    if (cmp < 0) {+      lo = mid+1;+    }+    else if (cmp > 0) {+      hi = mid-1;+    }+    else {+      attr_t cattr = attr_none();+      if (usebgcolor) { cattr.x.bgcolor = info->color; }+                else  { cattr.x.color = info->color; }+      tag->attr = attr_update_with(tag->attr,cattr);+      if (tag->name != NULL) tag->name = info->name;+      return;+    }+  }+  // not found+  bbcode_invalid("bbcode: unknown style: %s\n", attr_name);  +}+++ic_private attr_t bbcode_style( bbcode_t* bb, const char* style_name ) {+  tag_t tag;+  tag_init(&tag);+  attr_update_with_styles( &tag, style_name, NULL, false, bb->styles, bb->styles_count );+  return tag.attr;+}++//-------------------------------------------------------------+// Parse tags+//-------------------------------------------------------------++ic_private const char* parse_skip_white(const char* s) {+  while( *s != 0 && *s != ']') {+    if (!(*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r')) break;+    s++;+  }+  return s;+}++ic_private const char* parse_skip_to_white(const char* s) {+  while( *s != 0 && *s != ']') {  +    if (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') break;+    s++;+  }+  return parse_skip_white(s);+}++ic_private const char* parse_skip_to_end(const char* s) {+  while( *s != 0 && *s != ']' ) { s++; }    +  return s;+}++ic_private const char* parse_attr_name(const char* s) {+  if (*s == '#') {+    s++; // hex rgb color as id+    while( *s != 0 && *s != ']') {+      if (!((*s >= 'a' && *s <= 'f') || (*s >= 'A' && *s <= 'Z') || (*s >= '0' && *s <= '9'))) break;+      s++;+    }+  }+  else {+    while( *s != 0 && *s != ']') {+      if (!((*s >= 'a' && *s <= 'z') || (*s >= 'A' && *s <= 'Z') || +            (*s >= '0' && *s <= '9') || *s == '_' || *s == '-')) break;+      s++;+    }+  }    +  return s;+}++ic_private const char* parse_value(const char* s, const char** start, const char** end) {+  if (*s == '"') {+    s++;+    *start = s;+    while( *s != 0 ) {+      if (*s == '"') break;+      s++;+    }+    *end = s;      +    if (*s == '"') { s++; }+  }+  else if (*s == '#') {+    *start = s;+    s++;+    while( *s != 0 ) {+      if (!((*s >= 'a' && *s <= 'f') || (*s >= 'A' && *s <= 'Z') || (*s >= '0' && *s <= '9'))) break;+      s++;+    }+    *end = s;+  }+  else {+    *start = s;+    while( *s != 0 ) {+      if (!((*s >= 'a' && *s <= 'z') || (*s >= 'A' && *s <= 'F') || (*s >= '0' && *s <= '9') || *s == '-' || *s == '_')) break;+      s++;+    }+    *end = s;+  }  +  return s;  +}++ic_private const char* parse_tag_value( tag_t* tag, char* idbuf, const char* s, const style_t* styles, ssize_t scount ) {+  // parse: \s*[\w-]+\s*(=\s*<value>)+  bool usebgcolor = false;+  const char* id = s;+  const char* idend = parse_attr_name(id);+  const char* val = NULL;+  const char* valend = NULL;  +  if (id == idend) {+    bbcode_invalid("bbcode: empty identifier? %.10s...\n", id );+    return parse_skip_to_white(id);+  }+  // "on" bgcolor?+  s = parse_skip_white(idend);+  if (idend - id == 2 && ic_strnicmp(id,"on",2) == 0 && *s != '=') {+    usebgcolor = true;+    id = s;+    idend = parse_attr_name(id);+    if (id == idend) {+      bbcode_invalid("bbcode: empty identifier follows 'on'? %.10s...\n", id );+      return parse_skip_to_white(id);+    }+    s = parse_skip_white(idend);      +  }+  // value+  if (*s == '=') {+    s++;+    s = parse_skip_white(s);+    s = parse_value(s, &val, &valend);+    s = parse_skip_white(s);+  }  +  // limit name and attr to 128 bytes+  char valbuf[128];+  ic_strncpy( idbuf, 128, id, idend - id);+  ic_strncpy( valbuf, 128, val, valend - val);+  ic_str_tolower(idbuf);+  ic_str_tolower(valbuf);+  attr_update_with_styles( tag, idbuf, valbuf, usebgcolor, styles, scount );  +  return s;+}++static const char* parse_tag_values( tag_t* tag, char* idbuf, const char* s, const style_t* styles, ssize_t scount ) {+  s = parse_skip_white(s);  +  idbuf[0] = 0;+  ssize_t count = 0;+  while( *s != 0 && *s != ']') {+    char idbuf_next[128];+    s = parse_tag_value(tag, (count==0 ? idbuf : idbuf_next), s, styles, scount);+    count++;+  }+  if (*s == ']') { s++; }+  return s;+}++static const char* parse_tag( tag_t* tag, char* idbuf, bool* open, bool* pre, const char* s, const style_t* styles, ssize_t scount ) {+  *open = true;+  *pre = false;+  if (*s != '[') return s;+  s = parse_skip_white(s+1);+  if (*s == '!') { // pre+    *pre = true;+    s = parse_skip_white(s+1);  +  }  +  else if (*s == '/') { +    *open = false; +    s = parse_skip_white(s+1); +  };+  s = parse_tag_values( tag, idbuf, s, styles, scount);+  return s;+}+++//---------------------------------------------------------+// Styles+//---------------------------------------------------------++static void bbcode_parse_tag_content( bbcode_t* bb, const char* s, tag_t* tag ) {+  tag_init(tag);+  if (s != NULL) { +    char idbuf[128];+    parse_tag_values(tag, idbuf, s, bb->styles, bb->styles_count);+  }+}++ic_private void bbcode_style_def( bbcode_t* bb, const char* style_name, const char* s ) {+  tag_t tag;+  bbcode_parse_tag_content( bb, s, &tag);+  bbcode_style_add(bb, style_name, tag.attr);+}++ic_private void bbcode_style_open( bbcode_t* bb, const char* fmt ) {+  tag_t tag;+  bbcode_parse_tag_content(bb, fmt, &tag);+  term_set_attr( bb->term, bbcode_open(bb, 0, &tag, term_get_attr(bb->term)) );+}++ic_private void bbcode_style_close( bbcode_t* bb, const char* fmt ) {+  const ssize_t base = bb->tags_nesting - 1; // as we end a style+  tag_t tag;+  bbcode_parse_tag_content(bb, fmt, &tag);  +  tag_t prev;+  if (bbcode_close(bb, base, tag.name, &prev)) {+    term_set_attr( bb->term, prev.attr );+  }+}++//---------------------------------------------------------+// Restrict to width+//---------------------------------------------------------++static void bbcode_restrict_width( ssize_t start, width_t width, stringbuf_t* out, attrbuf_t* attr_out ) {+  if (width.w <= 0) return;+  assert(start <= sbuf_len(out));+  assert(attr_out == NULL || sbuf_len(out) == attrbuf_len(attr_out));+  const char*   s   = sbuf_string(out) + start;+  const ssize_t len = sbuf_len(out) - start;+  const ssize_t w   = str_column_width(s);+  if (w == width.w) return; // fits exactly+  if (w > width.w) {+    // too large+    ssize_t innerw = (width.dots && width.w > 3 ? width.w-3 : width.w);+    if (width.align == IC_ALIGN_RIGHT) {+      // right align+      const ssize_t ndel = str_skip_until_fit( s, innerw );+      sbuf_delete_at( out, start, ndel );+      attrbuf_delete_at( attr_out, start, ndel );+      if (innerw < width.w) {+        // add dots+        sbuf_insert_at( out, "...", start );+        attr_t attr = attrbuf_attr_at(attr_out, start);+        attrbuf_insert_at( attr_out, start, 3, attr);+      }+    }+    else {+      // left or center align+      ssize_t count = str_take_while_fit( s, innerw );+      sbuf_delete_at( out, start + count, len - count );+      attrbuf_delete_at( attr_out, start + count, len - count );+      if (innerw < width.w) {+        // add dots+        attr_t attr = attrbuf_attr_at(attr_out,start);+        attrbuf_append_n( out, attr_out, "...", 3, attr );+      }+    }+  }+  else {+    // too short, pad to width+    const ssize_t diff = (width.w - w);+    const ssize_t pad_left  = (width.align == IC_ALIGN_RIGHT ? diff : (width.align == IC_ALIGN_LEFT  ? 0 : diff / 2));+    const ssize_t pad_right = (width.align == IC_ALIGN_LEFT  ? diff : (width.align == IC_ALIGN_RIGHT ? 0 : diff - pad_left));+    if (width.fill != 0 && pad_left > 0) {+      const attr_t attr = attrbuf_attr_at(attr_out,start);+      for( ssize_t i = 0; i < pad_left; i++) {  // todo: optimize+        sbuf_insert_char_at(out, width.fill, start);+      }+      attrbuf_insert_at( attr_out, start, pad_left, attr );+    }+    if (width.fill != 0 && pad_right > 0) {+      const attr_t attr = attrbuf_attr_at(attr_out,sbuf_len(out) - 1);+      char buf[2];+      buf[0] = width.fill;+      buf[1] = 0;        +      for( ssize_t i = 0; i < pad_right; i++) {  // todo: optimize+        attrbuf_append_n( out, attr_out, buf, 1, attr );+      }      +    }+  }+}++//---------------------------------------------------------+// Print+//---------------------------------------------------------++ic_private ssize_t bbcode_process_tag( bbcode_t* bb, const char* s, const ssize_t nesting_base, +                                        stringbuf_t* out, attrbuf_t* attr_out, attr_t* cur_attr ) {+  assert(*s == '[');+  tag_t tag;+  tag_init(&tag);  +  bool open = true;+  bool ispre = false;+  char idbuf[128];+  const char* end = parse_tag( &tag, idbuf, &open, &ispre, s, bb->styles, bb->styles_count ); // todo: styles+  assert(end > s);+  if (open) {+    if (!ispre) {+      // open tag+      *cur_attr = bbcode_open( bb, sbuf_len(out), &tag, *cur_attr );+    }+    else {+      // scan pre to end tag+      attr_t attr = attr_update_with(*cur_attr, tag.attr);+      char pre[132];+      if (snprintf(pre, 132, "[/%s]", idbuf) < ssizeof(pre)) {+        const char* etag = strstr(end,pre);+        if (etag == NULL) {+          const ssize_t len = ic_strlen(end);+          attrbuf_append_n(out, attr_out, end, len, attr);+          end += len;+        }+        else {+          attrbuf_append_n(out, attr_out, end, (etag - end), attr);+          end = etag + ic_strlen(pre);+        }+      }+    }+  }+  else {+    // pop the tag+    tag_t prev;+    if (bbcode_close( bb, nesting_base, tag.name, &prev)) {+      *cur_attr = prev.attr;+      if (prev.width.w > 0) {+        // closed a width tag; restrict the output to width+        bbcode_restrict_width( prev.pos, prev.width, out, attr_out);+      }+    }+  }  +  return (end - s);+}++ic_private void bbcode_append( bbcode_t* bb, const char* s, stringbuf_t* out, attrbuf_t* attr_out ) {+  attr_t attr = attr_none();+  const ssize_t base = bb->tags_nesting; // base; will not be popped+  ssize_t i = 0;+  while( s[i] != 0 ) {+    // handle no tags in bulk+    ssize_t nobb = 0;+    char c;+    while( (c = s[i+nobb]) != 0) {+      if (c == '[' || c == '\\') { break; }+      if (c == '\x1B' && s[i+nobb+1] == '[') {+        nobb++; // don't count 'ESC[' as a tag opener+      }+      nobb++;+    }+    if (nobb > 0) { attrbuf_append_n(out, attr_out, s+i, nobb, attr); }+    i += nobb;+    // tag+    if (s[i] == '[') {+      i += bbcode_process_tag(bb, s+i, base, out, attr_out, &attr);+    }+    else if (s[i] == '\\') {+      if (s[i+1] == '\\' || s[i+1] == '[') {+        attrbuf_append_n(out, attr_out, s+i+1, 1, attr); // escape '\[' and '\\' +        i += 2;+      }+      else {+        attrbuf_append_n(out, attr_out, s+i, 1, attr);  // pass '\\' as is+        i++;+      }+    }+  }+  // pop unclosed openings+  assert(bb->tags_nesting >= base);+  while( bb->tags_nesting > base ) {+    bbcode_tag_pop(bb,NULL);+  };+}++ic_private void bbcode_print( bbcode_t* bb, const char* s ) {+  if (bb->out == NULL || bb->out_attrs == NULL) return;+  assert(sbuf_len(bb->out) == 0 && attrbuf_len(bb->out_attrs) == 0);+  bbcode_append( bb, s, bb->out, bb->out_attrs );+  term_write_formatted( bb->term, sbuf_string(bb->out), attrbuf_attrs(bb->out_attrs,sbuf_len(bb->out)) );+  attrbuf_clear(bb->out_attrs);+  sbuf_clear(bb->out);+}++ic_private void bbcode_println( bbcode_t* bb, const char* s ) {+  bbcode_print(bb,s);+  term_writeln(bb->term, "");+}++ic_private void bbcode_vprintf( bbcode_t* bb, const char* fmt, va_list args  ) {+  if (bb->vout == NULL) return;+  assert(sbuf_len(bb->vout) == 0);+  sbuf_append_vprintf(bb->vout,fmt,args);+  bbcode_print(bb, sbuf_string(bb->vout));+  sbuf_clear(bb->vout);+}++ic_private void bbcode_printf( bbcode_t* bb, const char* fmt, ... ) {+  va_list args;+  va_start(args,fmt);+  bbcode_vprintf(bb,fmt,args);+  va_end(args);+}++ic_private ssize_t bbcode_column_width( bbcode_t* bb, const char* s ) {+  if (s==0 || s[0] == 0) return 0;+  if (bb->vout == NULL) { return str_column_width(s); }+  assert(sbuf_len(bb->vout) == 0); +  bbcode_append( bb, s, bb->vout, NULL);+  const ssize_t w = str_column_width(sbuf_string(bb->vout));+  sbuf_clear(bb->vout);+  return w;+}
+ src/bbcode.h view
@@ -0,0 +1,37 @@+/* ----------------------------------------------------------------------------+  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.+-----------------------------------------------------------------------------*/+#pragma once+#ifndef IC_BBCODE_H+#define IC_BBCODE_H++#include <stdarg.h>+#include "common.h"+#include "term.h"++struct bbcode_s;+typedef struct bbcode_s bbcode_t;++ic_private bbcode_t* bbcode_new( alloc_t* mem, term_t* term );+ic_private void bbcode_free( bbcode_t* bb );++ic_private void bbcode_style_add( bbcode_t* bb, const char* style_name, attr_t attr );+ic_private void bbcode_style_def( bbcode_t* bb, const char* style_name, const char* s );+ic_private void bbcode_style_open( bbcode_t* bb, const char* fmt );+ic_private void bbcode_style_close( bbcode_t* bb, const char* fmt );+ic_private attr_t bbcode_style( bbcode_t* bb, const char* style_name );++ic_private void bbcode_print( bbcode_t* bb, const char* s );+ic_private void bbcode_println( bbcode_t* bb, const char* s );+ic_private void bbcode_printf( bbcode_t* bb, const char* fmt, ... );+ic_private void bbcode_vprintf( bbcode_t* bb, const char* fmt, va_list args );++ic_private ssize_t bbcode_column_width( bbcode_t* bb, const char* s );++// allows `attr_out == NULL`.+ic_private void bbcode_append( bbcode_t* bb, const char* s, stringbuf_t* out, attrbuf_t* attr_out );++#endif // IC_BBCODE_H
+ src/bbcode_colors.c view
@@ -0,0 +1,194 @@+/* ----------------------------------------------------------------------------+  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.+-----------------------------------------------------------------------------*/++// This file is included from "bbcode.c" and contains html color names++#include "common.h"++typedef struct style_color_s {+  const char* name;+  ic_color_t color;+} style_color_t;++#define IC_HTML_COLOR_COUNT (172)++// ordered list of HTML color names (so we can use binary search)+static style_color_t html_colors[IC_HTML_COLOR_COUNT+1] = {+  { "aliceblue",      IC_RGB(0xf0f8ff) },+  { "ansi-aqua",      IC_ANSI_AQUA },+  { "ansi-black",     IC_ANSI_BLACK },+  { "ansi-blue",      IC_ANSI_BLUE },+  { "ansi-cyan",      IC_ANSI_CYAN },+  { "ansi-darkgray",  IC_ANSI_DARKGRAY },+  { "ansi-darkgrey",  IC_ANSI_DARKGRAY },+  { "ansi-default",   IC_ANSI_DEFAULT },+  { "ansi-fuchsia",   IC_ANSI_FUCHSIA },+  { "ansi-gray",      IC_ANSI_GRAY },+  { "ansi-green",     IC_ANSI_GREEN },+  { "ansi-grey",      IC_ANSI_GRAY },+  { "ansi-lightgray", IC_ANSI_LIGHTGRAY },+  { "ansi-lightgrey", IC_ANSI_LIGHTGRAY },+  { "ansi-lime" ,     IC_ANSI_LIME },+  { "ansi-magenta",   IC_ANSI_MAGENTA },+  { "ansi-maroon",    IC_ANSI_MAROON },+  { "ansi-navy",      IC_ANSI_NAVY },+  { "ansi-olive",     IC_ANSI_OLIVE },+  { "ansi-purple",    IC_ANSI_PURPLE },+  { "ansi-red",       IC_ANSI_RED },+  { "ansi-silver",    IC_ANSI_SILVER },+  { "ansi-teal",      IC_ANSI_TEAL },+  { "ansi-white",     IC_ANSI_WHITE },+  { "ansi-yellow",    IC_ANSI_YELLOW },+  { "antiquewhite",   IC_RGB(0xfaebd7) },+  { "aqua", IC_RGB(0x00ffff) },+  { "aquamarine", IC_RGB(0x7fffd4) },+  { "azure", IC_RGB(0xf0ffff) },+  { "beige", IC_RGB(0xf5f5dc) },+  { "bisque", IC_RGB(0xffe4c4) },+  { "black", IC_RGB(0x000000) },+  { "blanchedalmond", IC_RGB(0xffebcd) },+  { "blue", IC_RGB(0x0000ff) },+  { "blueviolet", IC_RGB(0x8a2be2) },+  { "brown", IC_RGB(0xa52a2a) },+  { "burlywood", IC_RGB(0xdeb887) },+  { "cadetblue", IC_RGB(0x5f9ea0) },+  { "chartreuse", IC_RGB(0x7fff00) },+  { "chocolate", IC_RGB(0xd2691e) },+  { "coral", IC_RGB(0xff7f50) },+  { "cornflowerblue", IC_RGB(0x6495ed) },+  { "cornsilk", IC_RGB(0xfff8dc) },+  { "crimson", IC_RGB(0xdc143c) },+  { "cyan", IC_RGB(0x00ffff) },+  { "darkblue", IC_RGB(0x00008b) },+  { "darkcyan", IC_RGB(0x008b8b) },+  { "darkgoldenrod", IC_RGB(0xb8860b) },+  { "darkgray", IC_RGB(0xa9a9a9) },+  { "darkgreen", IC_RGB(0x006400) },+  { "darkgrey", IC_RGB(0xa9a9a9) },+  { "darkkhaki", IC_RGB(0xbdb76b) },+  { "darkmagenta", IC_RGB(0x8b008b) },+  { "darkolivegreen", IC_RGB(0x556b2f) },+  { "darkorange", IC_RGB(0xff8c00) },+  { "darkorchid", IC_RGB(0x9932cc) },+  { "darkred", IC_RGB(0x8b0000) },+  { "darksalmon", IC_RGB(0xe9967a) },+  { "darkseagreen", IC_RGB(0x8fbc8f) },+  { "darkslateblue", IC_RGB(0x483d8b) },+  { "darkslategray", IC_RGB(0x2f4f4f) },+  { "darkslategrey", IC_RGB(0x2f4f4f) },+  { "darkturquoise", IC_RGB(0x00ced1) },+  { "darkviolet", IC_RGB(0x9400d3) },+  { "deeppink", IC_RGB(0xff1493) },+  { "deepskyblue", IC_RGB(0x00bfff) },+  { "dimgray", IC_RGB(0x696969) },+  { "dimgrey", IC_RGB(0x696969) },+  { "dodgerblue", IC_RGB(0x1e90ff) },+  { "firebrick", IC_RGB(0xb22222) },+  { "floralwhite", IC_RGB(0xfffaf0) },+  { "forestgreen", IC_RGB(0x228b22) },+  { "fuchsia", IC_RGB(0xff00ff) },+  { "gainsboro", IC_RGB(0xdcdcdc) },+  { "ghostwhite", IC_RGB(0xf8f8ff) },+  { "gold", IC_RGB(0xffd700) },+  { "goldenrod", IC_RGB(0xdaa520) },+  { "gray", IC_RGB(0x808080) },+  { "green", IC_RGB(0x008000) },+  { "greenyellow", IC_RGB(0xadff2f) },+  { "grey", IC_RGB(0x808080) },+  { "honeydew", IC_RGB(0xf0fff0) },+  { "hotpink", IC_RGB(0xff69b4) },+  { "indianred", IC_RGB(0xcd5c5c) },+  { "indigo", IC_RGB(0x4b0082) },+  { "ivory", IC_RGB(0xfffff0) },+  { "khaki", IC_RGB(0xf0e68c) },+  { "lavender", IC_RGB(0xe6e6fa) },+  { "lavenderblush", IC_RGB(0xfff0f5) },+  { "lawngreen", IC_RGB(0x7cfc00) },+  { "lemonchiffon", IC_RGB(0xfffacd) },+  { "lightblue", IC_RGB(0xadd8e6) },+  { "lightcoral", IC_RGB(0xf08080) },+  { "lightcyan", IC_RGB(0xe0ffff) },+  { "lightgoldenrodyellow", IC_RGB(0xfafad2) },+  { "lightgray", IC_RGB(0xd3d3d3) },+  { "lightgreen", IC_RGB(0x90ee90) },+  { "lightgrey", IC_RGB(0xd3d3d3) },+  { "lightpink", IC_RGB(0xffb6c1) },+  { "lightsalmon", IC_RGB(0xffa07a) },+  { "lightseagreen", IC_RGB(0x20b2aa) },+  { "lightskyblue", IC_RGB(0x87cefa) },+  { "lightslategray", IC_RGB(0x778899) },+  { "lightslategrey", IC_RGB(0x778899) },+  { "lightsteelblue", IC_RGB(0xb0c4de) },+  { "lightyellow", IC_RGB(0xffffe0) },+  { "lime", IC_RGB(0x00ff00) },+  { "limegreen", IC_RGB(0x32cd32) },+  { "linen", IC_RGB(0xfaf0e6) },+  { "magenta", IC_RGB(0xff00ff) },+  { "maroon", IC_RGB(0x800000) },+  { "mediumaquamarine", IC_RGB(0x66cdaa) },+  { "mediumblue", IC_RGB(0x0000cd) },+  { "mediumorchid", IC_RGB(0xba55d3) },+  { "mediumpurple", IC_RGB(0x9370db) },+  { "mediumseagreen", IC_RGB(0x3cb371) },+  { "mediumslateblue", IC_RGB(0x7b68ee) },+  { "mediumspringgreen", IC_RGB(0x00fa9a) },+  { "mediumturquoise", IC_RGB(0x48d1cc) },+  { "mediumvioletred", IC_RGB(0xc71585) },+  { "midnightblue", IC_RGB(0x191970) },+  { "mintcream", IC_RGB(0xf5fffa) },+  { "mistyrose", IC_RGB(0xffe4e1) },+  { "moccasin", IC_RGB(0xffe4b5) },+  { "navajowhite", IC_RGB(0xffdead) },+  { "navy", IC_RGB(0x000080) },+  { "oldlace", IC_RGB(0xfdf5e6) },+  { "olive", IC_RGB(0x808000) },+  { "olivedrab", IC_RGB(0x6b8e23) },+  { "orange", IC_RGB(0xffa500) },+  { "orangered", IC_RGB(0xff4500) },+  { "orchid", IC_RGB(0xda70d6) },+  { "palegoldenrod", IC_RGB(0xeee8aa) },+  { "palegreen", IC_RGB(0x98fb98) },+  { "paleturquoise", IC_RGB(0xafeeee) },+  { "palevioletred", IC_RGB(0xdb7093) },+  { "papayawhip", IC_RGB(0xffefd5) },+  { "peachpuff", IC_RGB(0xffdab9) },+  { "peru", IC_RGB(0xcd853f) },+  { "pink", IC_RGB(0xffc0cb) },+  { "plum", IC_RGB(0xdda0dd) },+  { "powderblue", IC_RGB(0xb0e0e6) },+  { "purple", IC_RGB(0x800080) },+  { "rebeccapurple", IC_RGB(0x663399) },+  { "red", IC_RGB(0xff0000) },+  { "rosybrown", IC_RGB(0xbc8f8f) },+  { "royalblue", IC_RGB(0x4169e1) },+  { "saddlebrown", IC_RGB(0x8b4513) },+  { "salmon", IC_RGB(0xfa8072) },+  { "sandybrown", IC_RGB(0xf4a460) },+  { "seagreen", IC_RGB(0x2e8b57) },+  { "seashell", IC_RGB(0xfff5ee) },+  { "sienna", IC_RGB(0xa0522d) },+  { "silver", IC_RGB(0xc0c0c0) },+  { "skyblue", IC_RGB(0x87ceeb) },+  { "slateblue", IC_RGB(0x6a5acd) },+  { "slategray", IC_RGB(0x708090) },+  { "slategrey", IC_RGB(0x708090) },+  { "snow", IC_RGB(0xfffafa) },+  { "springgreen", IC_RGB(0x00ff7f) },+  { "steelblue", IC_RGB(0x4682b4) },+  { "tan", IC_RGB(0xd2b48c) },+  { "teal", IC_RGB(0x008080) },+  { "thistle", IC_RGB(0xd8bfd8) },+  { "tomato", IC_RGB(0xff6347) },+  { "turquoise", IC_RGB(0x40e0d0) },+  { "violet", IC_RGB(0xee82ee) },+  { "wheat", IC_RGB(0xf5deb3) },+  { "white", IC_RGB(0xffffff) },+  { "whitesmoke", IC_RGB(0xf5f5f5) },+  { "yellow", IC_RGB(0xffff00) },+  { "yellowgreen", IC_RGB(0x9acd32) },+  {NULL, 0}+}; 
+ src/common.c view
@@ -0,0 +1,347 @@+/* ----------------------------------------------------------------------------+  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.+-----------------------------------------------------------------------------*/++#include <string.h>+#include <stdio.h>+#include <stdarg.h>+#include <stdlib.h>+#include "common.h"+++//-------------------------------------------------------------+// String wrappers for ssize_t+//-------------------------------------------------------------++ic_private ssize_t ic_strlen( const char* s ) {+  if (s==NULL) return 0;+  return to_ssize_t(strlen(s));+}++ic_private void ic_memmove( void* dest, const void* src, ssize_t n ) {+  assert(dest!=NULL && src != NULL);+  if (n <= 0) return;+  memmove(dest,src,to_size_t(n));+}+++ic_private void ic_memcpy( void* dest, const void* src, ssize_t n ) {+  assert(dest!=NULL && src != NULL);+  if (dest == NULL || src == NULL || n <= 0) return;+  memcpy(dest,src,to_size_t(n));+}++ic_private void ic_memset(void* dest, uint8_t value, ssize_t n) {+  assert(dest!=NULL);+  if (dest == NULL || n <= 0) return;+  memset(dest,(int8_t)value,to_size_t(n));+}++ic_private bool ic_memnmove( void* dest, ssize_t dest_size, const void* src, ssize_t n ) {+  assert(dest!=NULL && src != NULL);+  if (n <= 0) return true;+  if (dest_size < n) { assert(false); return false; }+  memmove(dest,src,to_size_t(n));+  return true;+}++ic_private bool ic_strcpy( char* dest, ssize_t dest_size /* including 0 */, const char* src) {+  assert(dest!=NULL && src != NULL);+  if (dest == NULL || dest_size <= 0) return false;+  ssize_t slen = ic_strlen(src);+  if (slen >= dest_size) return false;+  strcpy(dest,src);+  assert(dest[slen] == 0);+  return true;+}+++ic_private bool ic_strncpy( char* dest, ssize_t dest_size /* including 0 */, const char* src, ssize_t n) {+  assert(dest!=NULL && n < dest_size);+  if (dest == NULL || dest_size <= 0) return false;+  if (n >= dest_size) return false;+  if (src==NULL || n <= 0) {+    dest[0] = 0;+  }+  else {+    strncpy(dest,src,to_size_t(n));+    dest[n] = 0;  +  }+  return true;+}++//-------------------------------------------------------------+// String matching+//-------------------------------------------------------------++ic_public bool ic_starts_with( const char* s, const char* prefix ) {+  if (s==prefix) return true;+  if (prefix==NULL) return true;+  if (s==NULL) return false;++  ssize_t i;+  for( i = 0; s[i] != 0 && prefix[i] != 0; i++) {+    if (s[i] != prefix[i]) return false;+  }+  return (prefix[i] == 0);+}++ic_private char ic_tolower( char c ) {+  return (c >= 'A' && c <= 'Z'  ?  c - 'A' + 'a' : c);+}++ic_private void ic_str_tolower(char* s) {+  while(*s != 0) {+    *s = ic_tolower(*s);+    s++;+  }+}++ic_public bool ic_istarts_with( const char* s, const char* prefix ) {+  if (s==prefix) return true;+  if (prefix==NULL) return true;+  if (s==NULL) return false;++  ssize_t i;+  for( i = 0; s[i] != 0 && prefix[i] != 0; i++) {+    if (ic_tolower(s[i]) != ic_tolower(prefix[i])) return false;+  }+  return (prefix[i] == 0);+}+++ic_private int ic_strnicmp(const char* s1, const char* s2, ssize_t n) {+  if (s1 == NULL && s2 == NULL) return 0;+  if (s1 == NULL) return -1;+  if (s2 == NULL) return 1;+  ssize_t i;+  for (i = 0; s1[i] != 0 && i < n; i++) {  // note: if s2[i] == 0 the loop will stop as c1 != c2+    char c1 = ic_tolower(s1[i]);+    char c2 = ic_tolower(s2[i]);+    if (c1 < c2) return -1;+    if (c1 > c2) return 1;+  }+  return ((i >= n || s2[i] == 0) ? 0 : -1);+}++ic_private int ic_stricmp(const char* s1, const char* s2) {+  ssize_t len1 = ic_strlen(s1);+  ssize_t len2 = ic_strlen(s2);+  if (len1 < len2) return -1;+  if (len1 > len2) return 1;+  return (ic_strnicmp(s1, s2, (len1 >= len2 ? len1 : len2)));+}+++static const char* ic_stristr(const char* s, const char* pat) {+  if (s==NULL) return NULL;+  if (pat==NULL || pat[0] == 0) return s;+  ssize_t patlen = ic_strlen(pat);+  for (ssize_t i = 0; s[i] != 0; i++) {+    if (ic_strnicmp(s + i, pat, patlen) == 0) return (s+i);+  }+  return NULL;+}++ic_private bool ic_contains(const char* big, const char* s) {+  if (big == NULL) return false;+  if (s == NULL) return true;+  return (strstr(big,s) != NULL);+}++ic_private bool ic_icontains(const char* big, const char* s) {+  if (big == NULL) return false;+  if (s == NULL) return true;+  return (ic_stristr(big,s) != NULL);+}+++//-------------------------------------------------------------+// Unicode+// QUTF-8: See <https://github.com/koka-lang/koka/blob/master/kklib/include/kklib/string.h>+// Raw bytes are code points 0xEE000 - 0xEE0FF+//-------------------------------------------------------------+#define IC_UNICODE_RAW   ((unicode_t)(0xEE000U))++ic_private unicode_t unicode_from_raw(uint8_t c) {+  return (IC_UNICODE_RAW + c);+}++ic_private bool unicode_is_raw(unicode_t u, uint8_t* c) {+  if (u >= IC_UNICODE_RAW && u <= IC_UNICODE_RAW + 0xFF) {+    *c = (uint8_t)(u - IC_UNICODE_RAW);+    return true;+  }+  else {+    return false;+  }+}++ic_private void unicode_to_qutf8(unicode_t u, uint8_t buf[5]) {+  memset(buf, 0, 5);+  if (u <= 0x7F) {+    buf[0] = (uint8_t)u;+  }+  else if (u <= 0x07FF) {+    buf[0] = (0xC0 | ((uint8_t)(u >> 6)));+    buf[1] = (0x80 | (((uint8_t)u) & 0x3F));+  }+  else if (u <= 0xFFFF) {+    buf[0] = (0xE0 |  ((uint8_t)(u >> 12)));+    buf[1] = (0x80 | (((uint8_t)(u >>  6)) & 0x3F));+    buf[2] = (0x80 | (((uint8_t)u) & 0x3F));+  }+  else if (u <= 0x10FFFF) {+    if (unicode_is_raw(u, &buf[0])) {+      buf[1] = 0;+    }+    else {+      buf[0] = (0xF0 |  ((uint8_t)(u >> 18)));+      buf[1] = (0x80 | (((uint8_t)(u >> 12)) & 0x3F));+      buf[2] = (0x80 | (((uint8_t)(u >>  6)) & 0x3F));+      buf[3] = (0x80 | (((uint8_t)u) & 0x3F));+    }+  }+}++// is this a utf8 continuation byte?+ic_private bool utf8_is_cont(uint8_t c) {+  return ((c & 0xC0) == 0x80);+}++ic_private unicode_t unicode_from_qutf8(const uint8_t* s, ssize_t len, ssize_t* count) {+  unicode_t c0 = 0;+  if (len <= 0 || s == NULL) {+    goto fail;+  }+  // 1 byte+  c0 = s[0];+  if (c0 <= 0x7F && len >= 1) {+    if (count != NULL) *count = 1;+    return c0; +  }+  else if (c0 <= 0xC1) { // invalid continuation byte or invalid 0xC0, 0xC1+    goto fail;+  }+  // 2 bytes+  else if (c0 <= 0xDF && len >= 2 && utf8_is_cont(s[1])) { +    if (count != NULL) *count = 2;+    return (((c0 & 0x1F) << 6) | (s[1] & 0x3F));+  }+  // 3 bytes: reject overlong and surrogate halves+  else if (len >= 3 && +           ((c0 == 0xE0 && s[1] >= 0xA0 && s[1] <= 0xBF && utf8_is_cont(s[2])) ||+            (c0 >= 0xE1 && c0 <= 0xEC && utf8_is_cont(s[1]) && utf8_is_cont(s[2])) +          ))+  {+    if (count != NULL) *count = 3;+    return (((c0 & 0x0F) << 12) | ((unicode_t)(s[1] & 0x3F) << 6) | (s[2] & 0x3F));+  }+  // 4 bytes: reject overlong+  else if (len >= 4 && +           (((c0 == 0xF0 && s[1] >= 0x90 && s[1] <= 0xBF && utf8_is_cont(s[2]) && utf8_is_cont(s[3])) ||+            (c0 >= 0xF1 && c0 <= 0xF3 && utf8_is_cont(s[1]) && utf8_is_cont(s[2]) && utf8_is_cont(s[3])) ||+            (c0 == 0xF4 && s[1] >= 0x80 && s[1] <= 0x8F && utf8_is_cont(s[2]) && utf8_is_cont(s[3]))) +          )) +  {+    if (count != NULL) *count = 4;+    return (((c0 & 0x07) << 18) | ((unicode_t)(s[1] & 0x3F) << 12) | ((unicode_t)(s[2] & 0x3F) << 6) | (s[3] & 0x3F));+  }  +fail:+  if (count != NULL) *count = 1;+  return unicode_from_raw(s[0]);+}+++//-------------------------------------------------------------+// Debug+//-------------------------------------------------------------++#if defined(IC_NO_DEBUG_MSG) +// nothing+#elif !defined(IC_DEBUG_TO_FILE)+ic_private void debug_msg(const char* fmt, ...) {+  if (getenv("ISOCLINE_DEBUG")) {+    va_list args;+    va_start(args, fmt);+    vfprintf(stderr, fmt, args);+    va_end(args);+  }+}+#else+ic_private void debug_msg(const char* fmt, ...) {+  static int debug_init;+  static const char* debug_fname = "isocline.debug.txt";+  // initialize?+  if (debug_init==0) {+    debug_init = -1;+    const char* rdebug = getenv("ISOCLINE_DEBUG");+    if (rdebug!=NULL && strcmp(rdebug,"1") == 0) {+      FILE* fdbg = fopen(debug_fname, "w");+      if (fdbg!=NULL) {+        debug_init = 1;+        fclose(fdbg);+      }+    }+  }+  if (debug_init <= 0) return;++  // write debug messages+  FILE* fdbg = fopen(debug_fname, "a");+  if (fdbg==NULL) return;+  va_list args;+  va_start(args, fmt);+  vfprintf(fdbg, fmt, args);+  fclose(fdbg);+  va_end(args);+}+#endif+++//-------------------------------------------------------------+// Allocation+//-------------------------------------------------------------++ic_private void* mem_malloc(alloc_t* mem, ssize_t sz) {+  return mem->malloc(to_size_t(sz));+}++ic_private void* mem_zalloc(alloc_t* mem, ssize_t sz) {+  void* p = mem_malloc(mem, sz);+  if (p != NULL) memset(p, 0, to_size_t(sz));+  return p;+}++ic_private void* mem_realloc(alloc_t* mem, void* p, ssize_t newsz) {+  return mem->realloc(p, to_size_t(newsz));+}++ic_private void mem_free(alloc_t* mem, const void* p) {+  mem->free((void*)p);+}++ic_private char* mem_strdup(alloc_t* mem, const char* s) {+  if (s==NULL) return NULL;+  ssize_t n = ic_strlen(s);+  char* p = mem_malloc_tp_n(mem, char, n+1);+  if (p == NULL) return NULL;+  ic_memcpy(p, s, n+1);+  return p;+}++ic_private char* mem_strndup(alloc_t* mem, const char* s, ssize_t n) {+  if (s==NULL || n < 0) return NULL;+  char* p = mem_malloc_tp_n(mem, char, n+1);+  if (p == NULL) return NULL;+  ssize_t i;+  for (i = 0; i < n && s[i] != 0; i++) {+    p[i] = s[i];+  }+  assert(i <= n);+  p[i] = 0;+  return p;+}+
+ src/common.h view
@@ -0,0 +1,187 @@+/* ----------------------------------------------------------------------------+  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.+-----------------------------------------------------------------------------*/++#pragma once+#ifndef IC_COMMON_H+#define IC_COMMON_H++//-------------------------------------------------------------+// Headers and defines+//-------------------------------------------------------------++#include <sys/types.h>  // ssize_t+#include <limits.h>+#include <stddef.h>+#include <stdint.h>+#include <stdbool.h>+#include <assert.h>+#include "../include/isocline.h"  // ic_malloc_fun_t, ic_color_t etc.++# ifdef __cplusplus+#  define ic_extern_c   extern "C"+# else+#  define ic_extern_c+# endif++#if defined(IC_SEPARATE_OBJS)+#  define ic_public     ic_extern_c +# if defined(__GNUC__) // includes clang and icc      +#  define ic_private    __attribute__((visibility("hidden")))+# else+#  define ic_private  +# endif+#else+# define ic_private     static+# define ic_public      ic_extern_c+#endif++#define ic_unused(x)    (void)(x)+++//-------------------------------------------------------------+// ssize_t+//-------------------------------------------------------------++#if defined(_MSC_VER)+typedef intptr_t ssize_t;+#endif++#define ssizeof(tp)   (ssize_t)(sizeof(tp))+static inline size_t  to_size_t(ssize_t sz) { return (sz >= 0 ? (size_t)sz : 0); }+static inline ssize_t to_ssize_t(size_t sz) { return (sz <= SIZE_MAX/2 ? (ssize_t)sz : 0); }++ic_private void    ic_memmove(void* dest, const void* src, ssize_t n);+ic_private void    ic_memcpy(void* dest, const void* src, ssize_t n);+ic_private void    ic_memset(void* dest, uint8_t value, ssize_t n);+ic_private bool    ic_memnmove(void* dest, ssize_t dest_size, const void* src, ssize_t n);++ic_private ssize_t ic_strlen(const char* s);+ic_private bool    ic_strcpy(char* dest, ssize_t dest_size /* including 0 */, const char* src);+ic_private bool    ic_strncpy(char* dest, ssize_t dest_size /* including 0 */, const char* src, ssize_t n);++ic_private bool    ic_contains(const char* big, const char* s);+ic_private bool    ic_icontains(const char* big, const char* s);+ic_private char    ic_tolower(char c);+ic_private void    ic_str_tolower(char* s);+ic_private int     ic_stricmp(const char* s1, const char* s2);+ic_private int     ic_strnicmp(const char* s1, const char* s2, ssize_t n);++++//---------------------------------------------------------------------+// Unicode+//+// We use "qutf-8" (quite like utf-8) encoding and decoding. +// Internally we always use valid utf-8. If we encounter invalid+// utf-8 bytes (or bytes >= 0x80 from any other encoding) we encode+// these as special code points in the "raw plane" (0xEE000 - 0xEE0FF).+// When decoding we are then able to restore such raw bytes as-is.+// See <https://github.com/koka-lang/koka/blob/master/kklib/include/kklib/string.h>+//---------------------------------------------------------------------++typedef uint32_t  unicode_t;++ic_private void      unicode_to_qutf8(unicode_t u, uint8_t buf[5]);+ic_private unicode_t unicode_from_qutf8(const uint8_t* s, ssize_t len, ssize_t* nread); // validating++ic_private unicode_t unicode_from_raw(uint8_t c);+ic_private bool      unicode_is_raw(unicode_t u, uint8_t* c);++ic_private bool      utf8_is_cont(uint8_t c);+++//-------------------------------------------------------------+// Colors+//-------------------------------------------------------------++// A color is either RGB or an ANSI code.+// (RGB colors have bit 24 set to distinguish them from the ANSI color palette colors.)+// (Isocline will automatically convert from RGB on terminals that do not support full colors)+typedef uint32_t ic_color_t;++// Create a color from a 24-bit color value.+ic_private ic_color_t ic_rgb(uint32_t hex);++// Create a color from a 8-bit red/green/blue components.+// The value of each component is capped between 0 and 255.+ic_private ic_color_t ic_rgbx(ssize_t r, ssize_t g, ssize_t b);++#define IC_COLOR_NONE     (0)+#define IC_RGB(rgb)       (0x1000000 | (uint32_t)(rgb)) // ic_rgb(rgb)  // define to it can be used as a constant++// ANSI colors.+// The actual colors used is usually determined by the terminal theme+// See <https://en.wikipedia.org/wiki/ANSI_escape_code#3-bit_and_4-bit>+#define IC_ANSI_BLACK     (30)+#define IC_ANSI_MAROON    (31)+#define IC_ANSI_GREEN     (32)+#define IC_ANSI_OLIVE     (33)+#define IC_ANSI_NAVY      (34)+#define IC_ANSI_PURPLE    (35)+#define IC_ANSI_TEAL      (36)+#define IC_ANSI_SILVER    (37)+#define IC_ANSI_DEFAULT   (39)++#define IC_ANSI_GRAY      (90)+#define IC_ANSI_RED       (91)+#define IC_ANSI_LIME      (92)+#define IC_ANSI_YELLOW    (93)+#define IC_ANSI_BLUE      (94)+#define IC_ANSI_FUCHSIA   (95)+#define IC_ANSI_AQUA      (96)+#define IC_ANSI_WHITE     (97)++#define IC_ANSI_DARKGRAY  IC_ANSI_GRAY+#define IC_ANSI_LIGHTGRAY IC_ANSI_SILVER+#define IC_ANSI_MAGENTA   IC_ANSI_FUCHSIA+#define IC_ANSI_CYAN      IC_ANSI_AQUA++++//-------------------------------------------------------------+// Debug+//-------------------------------------------------------------++#if defined(IC_NO_DEBUG_MSG) +#define debug_msg(fmt,...)   (void)(0)+#else+ic_private void debug_msg( const char* fmt, ... );+#endif+++//-------------------------------------------------------------+// Abstract environment+//-------------------------------------------------------------+struct ic_env_s;+typedef struct ic_env_s ic_env_t;+++//-------------------------------------------------------------+// Allocation+//-------------------------------------------------------------++typedef struct alloc_s {+  ic_malloc_fun_t*  malloc;+  ic_realloc_fun_t* realloc;+  ic_free_fun_t*    free;+} alloc_t;+++ic_private void* mem_malloc( alloc_t* mem, ssize_t sz );+ic_private void* mem_zalloc( alloc_t* mem, ssize_t sz );+ic_private void* mem_realloc( alloc_t* mem, void* p, ssize_t newsz );+ic_private void  mem_free( alloc_t* mem, const void* p );+ic_private char* mem_strdup( alloc_t* mem, const char* s);+ic_private char* mem_strndup( alloc_t* mem, const char* s, ssize_t n);++#define mem_zalloc_tp(mem,tp)        (tp*)mem_zalloc(mem,ssizeof(tp))+#define mem_malloc_tp_n(mem,tp,n)    (tp*)mem_malloc(mem,(n)*ssizeof(tp))+#define mem_zalloc_tp_n(mem,tp,n)    (tp*)mem_zalloc(mem,(n)*ssizeof(tp))+#define mem_realloc_tp(mem,tp,p,n)   (tp*)mem_realloc(mem,p,(n)*ssizeof(tp))+++#endif // IC_COMMON_H
+ src/completers.c view
@@ -0,0 +1,675 @@+/* ----------------------------------------------------------------------------+  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.+-----------------------------------------------------------------------------*/+#include <string.h>+#include <stdio.h>++#include "../include/isocline.h"+#include "common.h"+#include "env.h"+#include "stringbuf.h"+#include "completions.h"++++//-------------------------------------------------------------+// Word completion +//-------------------------------------------------------------++// free variables for word completion+typedef struct word_closure_s {+  long                  delete_before_adjust;+  void*                 prev_env;+  ic_completion_fun_t*  prev_complete;+} word_closure_t;+++// word completion callback+static bool token_add_completion_ex(ic_env_t* env, void* closure, const char* replacement, const char* display, const char* help, long delete_before, long delete_after) {+  word_closure_t* wenv = (word_closure_t*)(closure);+  // call the previous completer with an adjusted delete-before+  return (*wenv->prev_complete)(env, wenv->prev_env, replacement, display, help, wenv->delete_before_adjust + delete_before, delete_after);+}+++ic_public void ic_complete_word(ic_completion_env_t* cenv, const char* prefix, ic_completer_fun_t* fun,+                                    ic_is_char_class_fun_t* is_word_char) +{+  if (is_word_char == NULL) is_word_char = &ic_char_is_nonseparator;+  +  ssize_t len = ic_strlen(prefix);+  ssize_t pos = len; // will be start of the 'word' (excluding a potential start quote)+  while (pos > 0) {+    // go back one code point+    ssize_t ofs = str_prev_ofs(prefix, pos, NULL);+    if (ofs <= 0) break;+    if (!(*is_word_char)(prefix + (pos - ofs), (long)ofs)) { +      break;+    }+    pos -= ofs;+  }+  if (pos < 0) { pos = 0; }+  +  // stop if empty word+  // if (len == pos) return;+  +  // set up the closure+  word_closure_t wenv;+  wenv.delete_before_adjust = (long)(len - pos);+  wenv.prev_complete = cenv->complete;+  wenv.prev_env = cenv->env;+  cenv->complete = &token_add_completion_ex;+  cenv->closure = &wenv;++  // and call the user completion routine+  (*fun)(cenv, prefix + pos);++  // restore the original environment+  cenv->complete = wenv.prev_complete;+  cenv->closure = wenv.prev_env;+}+++//-------------------------------------------------------------+// Quoted word completion (with escape characters)+//-------------------------------------------------------------++// free variables for word completion+typedef struct qword_closure_s {+  char         escape_char;+  char         quote;+  long         delete_before_adjust;+  stringbuf_t* sbuf;+  void*        prev_env;+  ic_is_char_class_fun_t* is_word_char;+  ic_completion_fun_t*    prev_complete;+} qword_closure_t;+++// word completion callback+static bool qword_add_completion_ex(ic_env_t* env, void* closure, const char* replacement, const char* display, const char* help, +                                       long delete_before, long delete_after) {+  qword_closure_t* wenv = (qword_closure_t*)(closure);+  sbuf_replace( wenv->sbuf, replacement );   +  if (wenv->quote != 0) {+    // add end quote+    sbuf_append_char( wenv->sbuf, wenv->quote);+  }+  else {+    // escape non-word characters if it was not quoted+    ssize_t pos = 0;+    ssize_t next;+    while ( (next = sbuf_next_ofs(wenv->sbuf, pos, NULL)) > 0 ) +    {+      if (!(*wenv->is_word_char)(sbuf_string(wenv->sbuf) + pos, (long)next)) { // strchr(wenv->non_word_char, sbuf_char_at( wenv->sbuf, pos )) != NULL) {+        sbuf_insert_char_at( wenv->sbuf, wenv->escape_char, pos);+        pos++;+      }+      pos += next;+    }+  }+  // and call the previous completion function+  return (*wenv->prev_complete)( env, wenv->prev_env, sbuf_string(wenv->sbuf), display, help, wenv->delete_before_adjust + delete_before, delete_after );  +}+++ic_public void ic_complete_qword( ic_completion_env_t* cenv, const char* prefix, ic_completer_fun_t* fun, ic_is_char_class_fun_t* is_word_char ) {+  ic_complete_qword_ex( cenv, prefix, fun, is_word_char, '\\', NULL);+}+++ic_public void ic_complete_qword_ex( ic_completion_env_t* cenv, const char* prefix, ic_completer_fun_t* fun, +                                        ic_is_char_class_fun_t* is_word_char, char escape_char, const char* quote_chars ) {+  if (is_word_char == NULL) is_word_char = &ic_char_is_nonseparator ;  +  if (quote_chars == NULL) quote_chars = "'\"";++  ssize_t len = ic_strlen(prefix);+  ssize_t pos; // will be start of the 'word' (excluding a potential start quote)+  char quote = 0;+  ssize_t quote_len = 0;+  +  // 1. look for a starting quote+  if (quote_chars[0] != 0) {+    // we go forward and count all quotes; if it is uneven, we need to complete quoted.+    ssize_t qpos_open = -1;+    ssize_t qpos_close = -1;+    ssize_t qcount = 0;+    pos = 0; +    while(pos < len) {+      if (prefix[pos] == escape_char && prefix[pos+1] != 0 && +           !(*is_word_char)(prefix + pos + 1, 1)) // strchr(non_word_char, prefix[pos+1]) != NULL+      {       +        pos++; // skip escape and next char+      }+      else if (qcount % 2 == 0 && strchr(quote_chars, prefix[pos]) != NULL) {+        // open quote +        qpos_open = pos;+        quote = prefix[pos];+        qcount++;+      }+      else if (qcount % 2 == 1 && prefix[pos] == quote) {+        // close quote+        qpos_close = pos;+        qcount++;+      }+      else if (!(*is_word_char)(prefix + pos, 1)) { //  strchr(non_word_char, prefix[pos]) != NULL) {+        qpos_close = -1;+      }+      ssize_t ofs = str_next_ofs( prefix, len, pos, NULL );+      if (ofs <= 0) break;+      pos += ofs;+    }    +    if ((qcount % 2 == 0 && qpos_close >= 0) || // if the last quote is only followed by word chars, we still complete it+        (qcount % 2 == 1))                     // opening quote found+    {+      quote_len = (len - qpos_open - 1);+      pos = qpos_open + 1;  // pos points to the word start just after the quote.+    }+    else {+      quote = 0;+    }+  }++  // 2. if we did not find a quoted word, look for non-word-chars+  if (quote == 0) {+    pos = len;+    while(pos > 0) {+      // go back one code point+      ssize_t ofs = str_prev_ofs(prefix, pos, NULL );+      if (ofs <= 0) break;+      if (!(*is_word_char)(prefix + (pos - ofs), (long)ofs)) { // strchr(non_word_char, prefix[pos - ofs]) != NULL) {+        // non word char, break if it is not escaped+        if (pos <= ofs || prefix[pos - ofs - 1] != escape_char) break; +        // otherwise go on+        pos--; // skip escaped char+      }+      pos -= ofs;+    }+  }++  // stop if empty word+  // if (len == pos) return;++  // allocate new unescaped word prefix+  char* word = mem_strndup( cenv->env->mem, prefix + pos, (quote==0 ? len - pos : quote_len));+  if (word == NULL) return;++  if (quote == 0) {+    // unescape prefix+    ssize_t wlen = len - pos;+    ssize_t wpos = 0;+    while (wpos < wlen) {+      ssize_t ofs = str_next_ofs(word, wlen, wpos, NULL);+      if (ofs <= 0) break;+      if (word[wpos] == escape_char && word[wpos+1] != 0 &&+           !(*is_word_char)(word + wpos + 1, (long)ofs)) // strchr(non_word_char, word[wpos+1]) != NULL) {+      {+        ic_memmove(word + wpos, word + wpos + 1, wlen - wpos /* including 0 */);+      }+      wpos += ofs;+    }+  }+  #ifdef _WIN32+  else {+    // remove inner quote: "c:\Program Files\"Win+    ssize_t wlen = len - pos;+    ssize_t wpos = 0;+    while (wpos < wlen) {+      ssize_t ofs = str_next_ofs(word, wlen, wpos, NULL);+      if (ofs <= 0) break;+      if (word[wpos] == escape_char && word[wpos+1] == quote) {+        word[wpos+1] = escape_char;+        ic_memmove(word + wpos, word + wpos + 1, wlen - wpos /* including 0 */);+      }+      wpos += ofs;+    }+  }+  #endif++  // set up the closure+  qword_closure_t wenv;+  wenv.quote          = quote;+  wenv.is_word_char   = is_word_char;+  wenv.escape_char    = escape_char;+  wenv.delete_before_adjust = (long)(len - pos);+  wenv.prev_complete  = cenv->complete;+  wenv.prev_env       =  cenv->env;+  wenv.sbuf = sbuf_new(cenv->env->mem);+  if (wenv.sbuf == NULL) { mem_free(cenv->env->mem, word); return; }+  cenv->complete = &qword_add_completion_ex;+  cenv->closure = &wenv;++  // and call the user completion routine+  (*fun)( cenv, word );++  // restore the original environment+  cenv->complete = wenv.prev_complete;+  cenv->closure = wenv.prev_env;++  sbuf_free(wenv.sbuf);+  mem_free(cenv->env->mem, word);  +}+++++//-------------------------------------------------------------+// Complete file names+// Listing files+//-------------------------------------------------------------+#include <stdlib.h>++typedef enum file_type_e {+  // must follow BSD style LSCOLORS order+  FT_DEFAULT = 0,+  FT_DIR,+  FT_SYM,+  FT_SOCK,+  FT_PIPE,+  FT_BLOCK,+  FT_CHAR,+  FT_SETUID,+  FT_SETGID,+  FT_DIR_OW_STICKY,+  FT_DIR_OW,+  FT_DIR_STICKY,+  FT_EXE,+  FT_LAST+} file_type_t;++static int         cli_color; // 1 enabled, 0 not initialized, -1 disabled+static const char* lscolors  = "exfxcxdxbxegedabagacad";  // default BSD setting+static const char* ls_colors;+static const char* ls_colors_names[] = { "no=","di=","ln=","so=","pi=","bd=","cd=","su=","sg=","tw=","ow=","st=","ex=", NULL };++static bool ls_colors_init(void) {+  if (cli_color != 0) return (cli_color >= 1);+  // colors enabled?+  const char* s = getenv("CLICOLOR");+  if (s==NULL || (strcmp(s, "1")!=0 && strcmp(s, "") != 0)) {+    cli_color = -1;+    return false;+  }+  cli_color = 1;+  s = getenv("LS_COLORS");+  if (s != NULL) { ls_colors = s;  }+  s = getenv("LSCOLORS");+  if (s != NULL) { lscolors = s; }  +  return true;+}++static bool ls_valid_esc(ssize_t c) {+  return ((c==0 || c==1 || c==4 || c==7 || c==22 || c==24  || c==27) ||+    (c >= 30 && c <= 37) || (c >= 40 && c <= 47) ||+    (c >= 90 && c <= 97) || (c >= 100 && c <= 107));+}++static bool ls_colors_from_key(stringbuf_t* sb, const char* key) {+  // find key+  ssize_t keylen = ic_strlen(key);+  if (keylen <= 0) return false;+  const char* p = strstr(ls_colors, key);+  if (p == NULL) return false;+  p += keylen;+  if (key[keylen-1] != '=') {+    if (*p != '=') return false;+    p++;+  }+  ssize_t len = 0;+  while (p[len] != 0 && p[len] != ':') {+    len++;+  }+  if (len <= 0) return false;+  sbuf_append(sb, "[ansi-sgr=\"" );+  sbuf_append_n(sb, p, len );+  sbuf_append(sb, "\"]");+  return true;+}++static int ls_colors_from_char(char c) {+  if (c >= 'a' && c <= 'h')      { return (c - 'a'); }+  else if (c >= 'A' && c <= 'H') { return (c - 'A') + 8; }+  else if (c == 'x')             { return 256; }+  else return 256; // default+}++static bool ls_colors_append(stringbuf_t* sb, file_type_t ft, const char* ext) {+  if (!ls_colors_init()) return false;+  if (ls_colors != NULL) {+    // GNU style+    if (ft == FT_DEFAULT && ext != NULL) {+      // first try extension match+      if (ls_colors_from_key(sb, ext)) return true;+    }+    if (ft >= FT_DEFAULT && ft < FT_LAST) {+      // then a filetype match+      const char* key = ls_colors_names[ft];+      if (ls_colors_from_key(sb, key)) return true;+    }    +  }+  else if (lscolors != NULL) {+    // BSD style+    char fg = 'x';+    char bg = 'x';+    if (ic_strlen(lscolors) > (2*(ssize_t)ft)+1) {+      fg = lscolors[2*ft];+      bg = lscolors[2*ft + 1];+    }+    sbuf_appendf(sb, "[ansi-color=%d ansi-bgcolor=%d]", ls_colors_from_char(fg), ls_colors_from_char(bg) );+    return true;+  }+  return false;+}++static void ls_colorize(bool no_lscolor, stringbuf_t* sb, file_type_t ft, const char* name, const char* ext, char dirsep) {+  bool close = (no_lscolor ? false : ls_colors_append( sb, ft, ext));+  sbuf_append(sb, "[!pre]" );+  sbuf_append(sb, name);+  if (dirsep != 0) sbuf_append_char(sb, dirsep);+  sbuf_append(sb,"[/pre]" );+  if (close) { sbuf_append(sb, "[/]"); }+}++#if defined(_WIN32)+#include <io.h>+#include <sys/stat.h>++static bool os_is_dir(const char* cpath) {+  struct _stat64 st = { 0 };+  _stat64(cpath, &st);+  return ((st.st_mode & _S_IFDIR) != 0);+}++static file_type_t os_get_filetype(const char* cpath) {+  struct _stat64 st = { 0 };+  _stat64(cpath, &st);+  if (((st.st_mode) & _S_IFDIR) != 0) return FT_DIR;+  if (((st.st_mode) & _S_IFCHR) != 0) return FT_CHAR;+  if (((st.st_mode) & _S_IFIFO) != 0) return FT_PIPE;+  if (((st.st_mode) & _S_IEXEC) != 0) return FT_EXE;+  return FT_DEFAULT;+}+++#define dir_cursor intptr_t+#define dir_entry  struct __finddata64_t++static bool os_findfirst(alloc_t* mem, const char* path, dir_cursor* d, dir_entry* entry) {+  stringbuf_t* spath = sbuf_new(mem);+  if (spath == NULL) return false;+  sbuf_append(spath, path);+  sbuf_append(spath, "\\*");+  *d = _findfirsti64(sbuf_string(spath), entry);+  mem_free(mem,spath);+  return (*d != -1);+}++static bool os_findnext(dir_cursor d, dir_entry* entry) {+  return (_findnexti64(d, entry) == 0);  +}++static void os_findclose(dir_cursor d) {+  _findclose(d);+}++static const char* os_direntry_name(dir_entry* entry) {+  return entry->name;  +}++static bool os_path_is_absolute( const char* path ) {+  if (path != NULL && path[0] != 0 && path[1] == ':' && (path[2] == '\\' || path[2] == '/' || path[2] == 0)) {+    char drive = path[0];+    return ((drive >= 'A' && drive <= 'Z') || (drive >= 'a' && drive <= 'z'));+  }+  else return false;+}++ic_private char ic_dirsep(void) {+  return '\\';+}+#else++#include <sys/types.h>+#include <sys/stat.h>+#include <dirent.h>+#include <errno.h>++static bool os_is_dir(const char* cpath) {+  struct stat st;+  memset(&st, 0, sizeof(st));+  stat(cpath, &st);+  return (S_ISDIR(st.st_mode));+}++static file_type_t os_get_filetype(const char* cpath) {+  struct stat st;+  memset(&st, 0, sizeof(st));+  lstat(cpath, &st);+  switch ((st.st_mode)&S_IFMT) {+    case S_IFSOCK: return FT_SOCK;+    case S_IFLNK: {+      return FT_SYM;+    }+    case S_IFIFO:  return FT_PIPE;+    case S_IFCHR:  return FT_CHAR;+    case S_IFBLK:  return FT_BLOCK;+    case S_IFDIR: {+      if ((st.st_mode & S_ISUID) != 0) return FT_SETUID;+      if ((st.st_mode & S_ISGID) != 0) return FT_SETGID;+      if ((st.st_mode & S_IWGRP) != 0 && (st.st_mode & S_ISVTX) != 0) return FT_DIR_OW_STICKY;+      if ((st.st_mode & S_IWGRP)) return FT_DIR_OW;+      if ((st.st_mode & S_ISVTX)) return FT_DIR_STICKY;+      return FT_DIR;+    }+    case S_IFREG:+    default: {+      if ((st.st_mode & S_IXUSR) != 0) return FT_EXE;+      return FT_DEFAULT;+    }+  }  +}+++#define dir_cursor DIR*+#define dir_entry  struct dirent*++static bool os_findnext(dir_cursor d, dir_entry* entry) {+  *entry = readdir(d);+  return (*entry != NULL);+}++static bool os_findfirst(alloc_t* mem, const char* cpath, dir_cursor* d, dir_entry* entry) {+  ic_unused(mem);+  *d = opendir(cpath);+  if (*d == NULL) {+    return false;+  }+  else {+    return os_findnext(*d, entry);+  }+}++static void os_findclose(dir_cursor d) {+  closedir(d);+}++static const char* os_direntry_name(dir_entry* entry) {+  return (*entry)->d_name;  +}++static bool os_path_is_absolute( const char* path ) {+  return (path != NULL && path[0] == '/');+}++ic_private char ic_dirsep(void) {+  return '/';+}+#endif++++//-------------------------------------------------------------+// File completion +//-------------------------------------------------------------++static bool ends_with_n(const char* name, ssize_t name_len, const char* ending, ssize_t len) {+  if (name_len < len) return false;+  if (ending == NULL || len <= 0) return true;+  for (ssize_t i = 1; i <= len; i++) {+    char c1 = name[name_len - i];+    char c2 = ending[len - i];+    #ifdef _WIN32+    if (ic_tolower(c1) != ic_tolower(c2)) return false;+    #else+    if (c1 != c2) return false;+    #endif+  }+  return true;+}++static bool match_extension(const char* name, const char* extensions) {+  if (extensions == NULL || extensions[0] == 0) return true;+  if (name == NULL) return false;+  ssize_t name_len = ic_strlen(name);+  ssize_t len = ic_strlen(extensions);+  ssize_t cur = 0;  +  //debug_msg("match extensions: %s ~ %s", name, extensions);+  for (ssize_t end = 0; end <= len; end++) {+    if (extensions[end] == ';' || extensions[end] == 0) {+      if (ends_with_n(name, name_len, extensions+cur, (end - cur))) {+        return true;+      }+      cur = end+1;+    }+  }+  return false;+}++static bool filename_complete_indir( ic_completion_env_t* cenv, stringbuf_t* dir, +                                      stringbuf_t* dir_prefix, stringbuf_t* display,+                                       const char* base_prefix, +                                        char dir_sep, const char* extensions ) +{+  dir_cursor d = 0;+  dir_entry entry;+  bool cont = true;+  if (os_findfirst(cenv->env->mem, sbuf_string(dir), &d, &entry)) {+    do {+      const char* name = os_direntry_name(&entry);+      if (name != NULL && strcmp(name, ".") != 0 && strcmp(name, "..") != 0 && +          ic_istarts_with(name, base_prefix))+      {+        // possible match, first check if it is a directory+        file_type_t ft;+        bool isdir;+        const ssize_t plen = sbuf_len(dir_prefix);+        sbuf_append(dir_prefix, name);+        { // check directory and potentially add a dirsep to the dir_prefix+          const ssize_t dlen = sbuf_len(dir);+          sbuf_append_char(dir,ic_dirsep());+          sbuf_append(dir,name);+          ft = os_get_filetype(sbuf_string(dir));+          isdir = os_is_dir(sbuf_string(dir));+          if (isdir && dir_sep != 0) {+            sbuf_append_char(dir_prefix,dir_sep); +          }+          sbuf_delete_from(dir,dlen);  // restore dir+        }+        if (isdir || match_extension(name, extensions)) {+          // add completion+          sbuf_clear(display);+          ls_colorize(cenv->env->no_lscolors, display, ft, name, NULL, (isdir ? dir_sep : 0));+          cont = ic_add_completion_ex(cenv, sbuf_string(dir_prefix), sbuf_string(display), NULL);+        }+        sbuf_delete_from( dir_prefix, plen ); // restore dir_prefix+      }+    } while (cont && os_findnext(d, &entry));+    os_findclose(d);+  }+  return cont;+}++typedef struct filename_closure_s {+  const char* roots;+  const char* extensions;+  char        dir_sep;+} filename_closure_t;++static void filename_completer( ic_completion_env_t* cenv, const char* prefix ) {+  if (prefix == NULL) return;+  filename_closure_t* fclosure = (filename_closure_t*)cenv->arg;  +  stringbuf_t* root_dir   = sbuf_new(cenv->env->mem);+  stringbuf_t* dir_prefix = sbuf_new(cenv->env->mem);+  stringbuf_t* display    = sbuf_new(cenv->env->mem);  +  if (root_dir!=NULL && dir_prefix != NULL && display != NULL) +  {+    // split prefix in dir_prefix / base.+    const char* base = strrchr(prefix,'/');+    #ifdef _WIN32+    const char* base2 = strrchr(prefix,'\\');+    if (base == NULL || base2 > base) base = base2;+    #endif+    if (base != NULL) {+      base++; +      sbuf_append_n(dir_prefix, prefix, base - prefix ); // includes dir separator+    }++    // absolute path+    if (os_path_is_absolute(prefix)) {+      // do not use roots but try to complete directly+      if (base != NULL) {+        sbuf_append_n( root_dir, prefix, (base - prefix));  // include dir separator+      }+      filename_complete_indir( cenv, root_dir, dir_prefix, display,  +                                (base != NULL ? base : prefix), +                                 fclosure->dir_sep, fclosure->extensions );   +    }+    else {+      // relative path, complete with respect to every root.+      const char* next;+      const char* root = fclosure->roots;+      while ( root != NULL ) {+        // create full root in `root_dir`+        sbuf_clear(root_dir);+        next = strchr(root,';');+        if (next == NULL) {+          sbuf_append( root_dir, root );+          root = NULL;+        }+        else {+          sbuf_append_n( root_dir, root, next - root );+          root = next + 1;+        }      +        sbuf_append_char( root_dir, ic_dirsep());+          +        // add the dir_prefix to the root+        if (base != NULL) {+          sbuf_append_n( root_dir, prefix, (base - prefix) - 1);+        }++        // and complete in this directory    +        filename_complete_indir( cenv, root_dir, dir_prefix, display,+                                  (base != NULL ? base : prefix), +                                   fclosure->dir_sep, fclosure->extensions);+      }+    }+  }+  sbuf_free(display);+  sbuf_free(root_dir);+  sbuf_free(dir_prefix);+}++ic_public void ic_complete_filename( ic_completion_env_t* cenv, const char* prefix, char dir_sep, const char* roots, const char* extensions ) {+  if (roots == NULL) roots = ".";+  if (extensions == NULL) extensions = "";+  if (dir_sep == 0) dir_sep = ic_dirsep();+  filename_closure_t fclosure;+  fclosure.dir_sep = dir_sep;+  fclosure.roots = roots; +  fclosure.extensions = extensions;+  cenv->arg = &fclosure;+  ic_complete_qword_ex( cenv, prefix, &filename_completer, &ic_char_is_filename_letter, '\\', "'\"");  +}
+ src/completions.c view
@@ -0,0 +1,312 @@+/* ----------------------------------------------------------------------------+  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.+-----------------------------------------------------------------------------*/+#include <string.h>+#include <stdio.h>+#include <stdlib.h>++#include "../include/isocline.h"+#include "common.h"+#include "env.h"+#include "stringbuf.h"+#include "completions.h"+++//-------------------------------------------------------------+// Completions+//-------------------------------------------------------------++typedef struct completion_s {+  const char* replacement;+  const char* display;+  const char* help;+  ssize_t     delete_before;+  ssize_t     delete_after;+} completion_t;++struct completions_s {+  ic_completer_fun_t* completer;+  void* completer_arg;+  ssize_t completer_max;+  ssize_t count;+  ssize_t len;+  completion_t* elems;+  alloc_t* mem;+};++static void default_filename_completer( ic_completion_env_t* cenv, const char* prefix );++ic_private completions_t* completions_new(alloc_t* mem) {+  completions_t* cms = mem_zalloc_tp(mem, completions_t);+  if (cms == NULL) return NULL;+  cms->mem = mem;+  cms->completer = &default_filename_completer;+  return cms;+}++ic_private void completions_free(completions_t* cms) {+  if (cms == NULL) return;+  completions_clear(cms);  +  if (cms->elems != NULL) {+    mem_free(cms->mem, cms->elems);+    cms->elems = NULL;+    cms->count = 0;+    cms->len = 0;+  }+  mem_free(cms->mem, cms); // free ourselves+}+++ic_private void completions_clear(completions_t* cms) {  +  while (cms->count > 0) {+    completion_t* cm = cms->elems + cms->count - 1;+    mem_free( cms->mem, cm->display);+    mem_free( cms->mem, cm->replacement);+    mem_free( cms->mem, cm->help);+    memset(cm,0,sizeof(*cm));+    cms->count--;    +  }+}++static void completions_push(completions_t* cms, const char* replacement, const char* display, const char* help, ssize_t delete_before, ssize_t delete_after) +{+  if (cms->count >= cms->len) {+    ssize_t newlen = (cms->len <= 0 ? 32 : cms->len*2);+    completion_t* newelems = mem_realloc_tp(cms->mem, completion_t, cms->elems, newlen );+    if (newelems == NULL) return;+    cms->elems = newelems;+    cms->len   = newlen;+  }+  assert(cms->count < cms->len);+  completion_t* cm  = cms->elems + cms->count;+  cm->replacement   = mem_strdup(cms->mem,replacement);+  cm->display       = mem_strdup(cms->mem,display);+  cm->help          = mem_strdup(cms->mem,help);+  cm->delete_before = delete_before;+  cm->delete_after  = delete_after;+  cms->count++;+}++ic_private ssize_t completions_count(completions_t* cms) {+  return cms->count;+}+++ic_private bool completions_add(completions_t* cms, const char* replacement, const char* display, const char* help, ssize_t delete_before, ssize_t delete_after) {+  if (cms->completer_max <= 0) return false;+  cms->completer_max--;+  //debug_msg("completion: add: %d,%d, %s\n", delete_before, delete_after, replacement);+  completions_push(cms, replacement, display, help, delete_before, delete_after);+  return true;+}++static completion_t* completions_get(completions_t* cms, ssize_t index) {+  if (index < 0 || cms->count <= 0 || index >= cms->count) return NULL;+  return &cms->elems[index];+}++ic_private const char* completions_get_display( completions_t* cms, ssize_t index, const char** help ) {+  if (help != NULL) { *help = NULL;  }+  completion_t* cm = completions_get(cms, index);+  if (cm == NULL) return NULL;+  if (help != NULL) { *help = cm->help; }+  return (cm->display != NULL ? cm->display : cm->replacement);+}++ic_private const char* completions_get_help( completions_t* cms, ssize_t index ) {+  completion_t* cm = completions_get(cms, index);+  if (cm == NULL) return NULL;+  return cm->help;+}++ic_private const char* completions_get_hint(completions_t* cms, ssize_t index, const char** help) {+  if (help != NULL) { *help = NULL; }+  completion_t* cm = completions_get(cms, index);+  if (cm == NULL) return NULL;+  ssize_t len = ic_strlen(cm->replacement);+  if (len < cm->delete_before) return NULL;+  const char* hint = (cm->replacement + cm->delete_before);+  if (*hint == 0 || utf8_is_cont((uint8_t)(*hint))) return NULL;  // utf8 boundary?+  if (help != NULL) { *help = cm->help; }+  return hint;+}++ic_private void completions_set_completer(completions_t* cms, ic_completer_fun_t* completer, void* arg) {+  cms->completer = completer;+  cms->completer_arg = arg;+}++ic_private void completions_get_completer(completions_t* cms, ic_completer_fun_t** completer, void** arg) {+  *completer = cms->completer;+  *arg = cms->completer_arg;+}++ic_public bool ic_has_completions( ic_completion_env_t* cenv ) {+  return (cenv->env->completions->count > 0);+}++ic_public bool ic_stop_completing(ic_completion_env_t* cenv) {+  return (cenv->env->completions->completer_max <= 0);+}+++static ssize_t completion_apply( completion_t* cm, stringbuf_t* sbuf, ssize_t pos ) {+  if (cm == NULL) return -1;  +  debug_msg( "completion: apply: %s at %zd\n", cm->replacement, pos);+  ssize_t start = pos - cm->delete_before;+  if (start < 0) start = 0;+  ssize_t n = cm->delete_before + cm->delete_after;+  if (ic_strlen(cm->replacement) == n && strncmp(sbuf_string_at(sbuf,start), cm->replacement, to_size_t(n)) == 0) {+    // no changes+    return -1;+  }+  else {+    sbuf_delete_from_to( sbuf, start, pos + cm->delete_after );+    return sbuf_insert_at(sbuf, cm->replacement, start); +  }+}++ic_private ssize_t completions_apply( completions_t* cms, ssize_t index, stringbuf_t* sbuf, ssize_t pos ) {+  completion_t* cm = completions_get(cms, index);+  return completion_apply( cm, sbuf, pos );+}+++static int completion_compare(const void* p1, const void* p2) {+  if (p1 == NULL || p2 == NULL) return 0;+  const completion_t* cm1 = (const completion_t*)p1;+  const completion_t* cm2 = (const completion_t*)p2;  +  return ic_stricmp(cm1->replacement, cm2->replacement);+}++ic_private void completions_sort(completions_t* cms) {+  if (cms->count <= 0) return;+  qsort(cms->elems, to_size_t(cms->count), sizeof(cms->elems[0]), &completion_compare);+}++#define IC_MAX_PREFIX  (256)++// find longest common prefix and complete with that.+ic_private ssize_t completions_apply_longest_prefix(completions_t* cms, stringbuf_t* sbuf, ssize_t pos) {+  if (cms->count <= 1) {+    return completions_apply(cms,0,sbuf,pos);+  }++  // set initial prefix to the first entry+  completion_t* cm = completions_get(cms, 0);+  if (cm == NULL) return -1;++  char prefix[IC_MAX_PREFIX+1];+  ssize_t delete_before = cm->delete_before;+  ic_strncpy( prefix, IC_MAX_PREFIX+1, cm->replacement, IC_MAX_PREFIX );+  prefix[IC_MAX_PREFIX] = 0;+  +  // and visit all others to find the longest common prefix+  for(ssize_t i = 1; i < cms->count; i++) {+    cm = completions_get(cms,i);+    if (cm->delete_before != delete_before) {  // deletions must match delete_before+      prefix[0] = 0;+      break;+    }+    // check if it is still a prefix+    const char* r = cm->replacement;    +    ssize_t j;+    for(j = 0; prefix[j] != 0 && r[j] != 0; j++) {+      if (prefix[j] != r[j]) break;+    }+    prefix[j] = 0;+    if (j <= 0) break;+  }++  // check the length+  ssize_t len = ic_strlen(prefix);+  if (len <= 0 || len < delete_before) return -1;++  // we found a prefix :-)+  completion_t cprefix;+  memset(&cprefix,0,sizeof(cprefix));+  cprefix.delete_before = delete_before;+  cprefix.replacement   = prefix;+  ssize_t newpos = completion_apply( &cprefix, sbuf, pos);+  if (newpos < 0) return newpos;  ++  // adjust all delete_before for the new replacement+  for( ssize_t i = 0; i < cms->count; i++) {+    cm = completions_get(cms,i);+    cm->delete_before = len;+  }++  return newpos;+}+++//-------------------------------------------------------------+// Completer functions+//-------------------------------------------------------------++ic_public bool ic_add_completions(ic_completion_env_t* cenv, const char* prefix, const char** completions) {+  for (const char** pc = completions; *pc != NULL; pc++) {+    if (ic_istarts_with(*pc, prefix)) {+      if (!ic_add_completion_ex(cenv, *pc, NULL, NULL)) return false;+    }+  }+  return true;+}++ic_public bool ic_add_completion(ic_completion_env_t* cenv, const char* replacement) {+  return ic_add_completion_ex(cenv, replacement, NULL, NULL);+}++ic_public bool ic_add_completion_ex( ic_completion_env_t* cenv, const char* replacement, const char* display, const char* help ) {+  return ic_add_completion_prim(cenv,replacement,display,help,0,0);+}++ic_public bool ic_add_completion_prim(ic_completion_env_t* cenv, const char* replacement, const char* display, const char* help, long delete_before, long delete_after) {+  return (*cenv->complete)(cenv->env, cenv->closure, replacement, display, help, delete_before, delete_after );+}++static bool prim_add_completion(ic_env_t* env, void* funenv, const char* replacement, const char* display, const char* help, long delete_before, long delete_after) {+  ic_unused(funenv);+  return completions_add(env->completions, replacement, display, help, delete_before, delete_after);+}++ic_public void ic_set_default_completer(ic_completer_fun_t* completer, void* arg) {+  ic_env_t* env = ic_get_env(); if (env == NULL) return;+  completions_set_completer(env->completions, completer, arg);+}++ic_private ssize_t completions_generate(struct ic_env_s* env, completions_t* cms, const char* input, ssize_t pos, ssize_t max) {+  completions_clear(cms);+  if (cms->completer == NULL || input == NULL || ic_strlen(input) < pos) return 0;++  // set up env+  ic_completion_env_t cenv;+  cenv.env = env;+  cenv.input = input,+  cenv.cursor = (long)pos;+  cenv.arg = cms->completer_arg;+  cenv.complete = &prim_add_completion;+  cenv.closure  = NULL;+  const char* prefix = mem_strndup(cms->mem, input, pos);+  cms->completer_max = max;+  +  // and complete+  cms->completer(&cenv,prefix);++  // restore+  mem_free(cms->mem,prefix);+  return completions_count(cms);+}++// The default completer is no completion is set+static void default_filename_completer( ic_completion_env_t* cenv, const char* prefix ) {+  #ifdef _WIN32+  const char sep = '\\';+  #else+  const char sep = '/';+  #endif+  ic_complete_filename( cenv, prefix, sep, ".", NULL);+}
+ src/completions.h view
@@ -0,0 +1,52 @@+/* ----------------------------------------------------------------------------+  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.+-----------------------------------------------------------------------------*/+#pragma once+#ifndef IC_COMPLETIONS_H+#define IC_COMPLETIONS_H++#include "common.h"+#include "stringbuf.h"+++//-------------------------------------------------------------+// Completions+//-------------------------------------------------------------+#define IC_MAX_COMPLETIONS_TO_SHOW  (1000)+#define IC_MAX_COMPLETIONS_TO_TRY   (IC_MAX_COMPLETIONS_TO_SHOW/4)++typedef struct completions_s completions_t;++ic_private completions_t* completions_new(alloc_t* mem);+ic_private void        completions_free(completions_t* cms);+ic_private void        completions_clear(completions_t* cms);+ic_private bool        completions_add(completions_t* cms , const char* replacement, const char* display, const char* help, ssize_t delete_before, ssize_t delete_after);+ic_private ssize_t     completions_count(completions_t* cms);+ic_private ssize_t     completions_generate(struct ic_env_s* env, completions_t* cms , const char* input, ssize_t pos, ssize_t max);+ic_private void        completions_sort(completions_t* cms);+ic_private void        completions_set_completer(completions_t* cms, ic_completer_fun_t* completer, void* arg);+ic_private const char* completions_get_display(completions_t* cms , ssize_t index, const char** help);+ic_private const char* completions_get_hint(completions_t* cms, ssize_t index, const char** help);+ic_private void        completions_get_completer(completions_t* cms, ic_completer_fun_t** completer, void** arg);++ic_private ssize_t     completions_apply(completions_t* cms, ssize_t index, stringbuf_t* sbuf, ssize_t pos);+ic_private ssize_t     completions_apply_longest_prefix(completions_t* cms, stringbuf_t* sbuf, ssize_t pos);++//-------------------------------------------------------------+// Completion environment+//-------------------------------------------------------------+typedef bool (ic_completion_fun_t)( ic_env_t* env, void* funenv, const char* replacement, const char* display, const char* help, long delete_before, long delete_after );++struct ic_completion_env_s {+  ic_env_t*   env;       // the isocline environment+  const char* input;     // current full input+  long        cursor;    // current cursor position+  void*       arg;       // argument given to `ic_set_completer`+  void*       closure;   // free variables for function composition+  ic_completion_fun_t* complete;  // function that adds a completion+};++#endif // IC_COMPLETIONS_H
+ src/editline.c view
@@ -0,0 +1,1132 @@+/* ----------------------------------------------------------------------------+  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.+-----------------------------------------------------------------------------*/+#include <stdio.h>+#include <string.h>++#include "common.h"+#include "term.h"+#include "tty.h"+#include "env.h"+#include "stringbuf.h"+#include "history.h"+#include "completions.h"+#include "undo.h"+#include "highlight.h"++//-------------------------------------------------------------+// The editor state+//-------------------------------------------------------------++++// editor state+typedef struct editor_s {+  stringbuf_t*  input;        // current user input+  stringbuf_t*  extra;        // extra displayed info (for completion menu etc)+  stringbuf_t*  hint;         // hint displayed as part of the input+  stringbuf_t*  hint_help;    // help for a hint.+  ssize_t       pos;          // current cursor position in the input+  ssize_t       cur_rows;     // current used rows to display our content (including extra content)+  ssize_t       cur_row;      // current row that has the cursor (0 based, relative to the prompt)+  ssize_t       termw;+  bool          modified;     // has a modification happened? (used for history navigation for example)  +  bool          disable_undo; // temporarily disable auto undo (for history search)+  ssize_t       history_idx;  // current index in the history +  editstate_t*  undo;         // undo buffer  +  editstate_t*  redo;         // redo buffer+  const char*   prompt_text;  // text of the prompt before the prompt marker    +  alloc_t*      mem;          // allocator+  // caches+  attrbuf_t*    attrs;        // reuse attribute buffers +  attrbuf_t*    attrs_extra; +} editor_t;++++++//-------------------------------------------------------------+// Main edit line +//-------------------------------------------------------------+static char* edit_line( ic_env_t* env, const char* prompt_text );  // defined at bottom+static void edit_refresh(ic_env_t* env, editor_t* eb);++ic_private char* ic_editline(ic_env_t* env, const char* prompt_text) {+  tty_start_raw(env->tty);+  term_start_raw(env->term);+  char* line = edit_line(env,prompt_text);+  term_end_raw(env->term,false);+  tty_end_raw(env->tty);+  term_writeln(env->term,"");+  term_flush(env->term);+  return line;+}+++//-------------------------------------------------------------+// Undo/Redo+//-------------------------------------------------------------++// capture the current edit state+static void editor_capture(editor_t* eb, editstate_t** es ) {+  if (!eb->disable_undo) {+    editstate_capture( eb->mem, es, sbuf_string(eb->input), eb->pos );+  }+}++static void editor_undo_capture(editor_t* eb ) {+  editor_capture(eb, &eb->undo );+}++static void editor_undo_forget(editor_t* eb) {+  if (eb->disable_undo) return;+  const char* input = NULL;+  ssize_t pos = 0;+  editstate_restore(eb->mem, &eb->undo, &input, &pos);+  mem_free(eb->mem, input);+}++static void editor_restore(editor_t* eb, editstate_t** from, editstate_t** to ) {+  if (eb->disable_undo) return;+  if (*from == NULL) return;+  const char* input;+  if (to != NULL) { editor_capture( eb, to ); }+  if (!editstate_restore( eb->mem, from, &input, &eb->pos )) return;+  sbuf_replace( eb->input, input );+  mem_free(eb->mem, input);+  eb->modified = false;+}++static void editor_undo_restore(editor_t* eb, bool with_redo ) {+  editor_restore(eb, &eb->undo, (with_redo ? &eb->redo : NULL));+}++static void editor_redo_restore(editor_t* eb ) {+  editor_restore(eb, &eb->redo, &eb->undo);+  eb->modified = false;+}++static void editor_start_modify(editor_t* eb ) {+  editor_undo_capture(eb);+  editstate_done(eb->mem, &eb->redo);  // clear redo+  eb->modified = true;+}++++static bool editor_pos_is_at_end(editor_t* eb ) {+  return (eb->pos == sbuf_len(eb->input));  +}++//-------------------------------------------------------------+// Row/Column width and positioning+//-------------------------------------------------------------+++static void edit_get_prompt_width( ic_env_t* env, editor_t* eb, bool in_extra, ssize_t* promptw, ssize_t* cpromptw ) {+  if (in_extra) {+    *promptw = 0;+    *cpromptw = 0;+  }+  else {+    // todo: cache prompt widths+    ssize_t textw = bbcode_column_width(env->bbcode, eb->prompt_text);+    ssize_t markerw = bbcode_column_width(env->bbcode, env->prompt_marker);+    ssize_t cmarkerw = bbcode_column_width(env->bbcode, env->cprompt_marker);+    *promptw = markerw + textw;+    *cpromptw = (env->no_multiline_indent || *promptw < cmarkerw ? cmarkerw : *promptw);+  }+}++static ssize_t edit_get_rowcol( ic_env_t* env, editor_t* eb, rowcol_t* rc ) {+  ssize_t promptw, cpromptw;+  edit_get_prompt_width(env, eb, false, &promptw, &cpromptw);+  return sbuf_get_rc_at_pos( eb->input, eb->termw, promptw, cpromptw, eb->pos, rc );+}++static void edit_set_pos_at_rowcol( ic_env_t* env, editor_t* eb, ssize_t row, ssize_t col ) {+  ssize_t promptw, cpromptw;+  edit_get_prompt_width(env, eb, false, &promptw, &cpromptw);+  ssize_t pos = sbuf_get_pos_at_rc( eb->input, eb->termw, promptw, cpromptw, row, col );+  if (pos < 0) return;+  eb->pos = pos;+  edit_refresh(env, eb);+}++static bool edit_pos_is_at_row_end( ic_env_t* env, editor_t* eb ) {+  rowcol_t rc;+  edit_get_rowcol( env, eb, &rc );+  return rc.last_on_row;+}++static void edit_write_prompt( ic_env_t* env, editor_t* eb, ssize_t row, bool in_extra ) {+  if (in_extra) return;+  bbcode_style_open(env->bbcode, "ic-prompt");+  if (row==0) {+    // regular prompt text    +    bbcode_print( env->bbcode, eb->prompt_text );+  }+  else if (!env->no_multiline_indent) {+    // multiline continuation indentation+    // todo: cache prompt widths+    ssize_t textw = bbcode_column_width(env->bbcode, eb->prompt_text );+    ssize_t markerw = bbcode_column_width(env->bbcode, env->prompt_marker);+    ssize_t cmarkerw = bbcode_column_width(env->bbcode, env->cprompt_marker);      +    if (cmarkerw < markerw + textw) {+      term_write_repeat(env->term, " ", markerw + textw - cmarkerw );+    }+  }+  // the marker+  bbcode_print(env->bbcode, (row == 0 ? env->prompt_marker : env->cprompt_marker ));   +  bbcode_style_close(env->bbcode,NULL);    +}++//-------------------------------------------------------------+// Refresh+//-------------------------------------------------------------++typedef struct refresh_info_s {+  ic_env_t*   env;+  editor_t*   eb;+  attrbuf_t*  attrs;+  bool        in_extra;+  ssize_t     first_row;+  ssize_t     last_row;+} refresh_info_t;++static bool edit_refresh_rows_iter(+    const char* s,+    ssize_t row, ssize_t row_start, ssize_t row_len, +    ssize_t startw, bool is_wrap, const void* arg, void* res)+{+  ic_unused(res); ic_unused(startw);+  const refresh_info_t* info = (const refresh_info_t*)(arg);+  term_t* term = info->env->term;++  // debug_msg("edit: line refresh: row %zd, len: %zd\n", row, row_len);+  if (row < info->first_row) return false;+  if (row > info->last_row)  return true; // should not occur+  +  // term_clear_line(term);+  edit_write_prompt(info->env, info->eb, row, info->in_extra);++  //' write output+  if (info->attrs == NULL || (info->env->no_highlight && info->env->no_bracematch)) {+    term_write_n( term, s + row_start, row_len );+  }+  else {+    term_write_formatted_n( term, s + row_start, attrbuf_attrs(info->attrs, row_start + row_len) + row_start, row_len );+  }++  // write line ending+  if (row < info->last_row) {+    if (is_wrap && tty_is_utf8(info->env->tty)) {       +      #ifndef __APPLE__+      bbcode_print( info->env->bbcode, "[ic-dim]\xE2\x86\x90");  // left arrow +      #else+      bbcode_print( info->env->bbcode, "[ic-dim]\xE2\x86\xB5" ); // return symbol+      #endif+    }+    term_clear_to_end_of_line(term);+    term_writeln(term, "");+  }+  else {+    term_clear_to_end_of_line(term);+  }+  return (row >= info->last_row);  +}++static void edit_refresh_rows(ic_env_t* env, editor_t* eb, stringbuf_t* input, attrbuf_t* attrs,+                               ssize_t promptw, ssize_t cpromptw, bool in_extra, +                                ssize_t first_row, ssize_t last_row) +{+  if (input == NULL) return;+  refresh_info_t info;+  info.env        = env;+  info.eb         = eb;+  info.attrs      = attrs;+  info.in_extra   = in_extra;+  info.first_row  = first_row;+  info.last_row   = last_row;+  sbuf_for_each_row( input, eb->termw, promptw, cpromptw, &edit_refresh_rows_iter, &info, NULL);+}+++static void edit_refresh(ic_env_t* env, editor_t* eb) +{+  // calculate the new cursor row and total rows needed+  ssize_t promptw, cpromptw;+  edit_get_prompt_width( env, eb, false, &promptw, &cpromptw );+  +  if (eb->attrs != NULL) {+    highlight( env->mem, env->bbcode, sbuf_string(eb->input), eb->attrs, +                 (env->no_highlight ? NULL : env->highlighter), env->highlighter_arg );+  }++  // highlight matching braces+  if (eb->attrs != NULL && !env->no_bracematch) {+    highlight_match_braces(sbuf_string(eb->input), eb->attrs, eb->pos, ic_env_get_match_braces(env),  +                              bbcode_style(env->bbcode,"ic-bracematch"), bbcode_style(env->bbcode,"ic-error"));+  }++  // insert hint  +  if (sbuf_len(eb->hint) > 0) {+    if (eb->attrs != NULL) {+      attrbuf_insert_at( eb->attrs, eb->pos, sbuf_len(eb->hint), bbcode_style(env->bbcode, "ic-hint") );+    }+    sbuf_insert_at(eb->input, sbuf_string(eb->hint), eb->pos );+  }++  // render extra (like a completion menu)+  stringbuf_t* extra = NULL;+  if (sbuf_len(eb->extra) > 0) {+    extra = sbuf_new(eb->mem);+    if (extra != NULL) {+      if (sbuf_len(eb->hint_help) > 0) {+        bbcode_append(env->bbcode, sbuf_string(eb->hint_help), extra, eb->attrs_extra);+      }+      bbcode_append(env->bbcode, sbuf_string(eb->extra), extra, eb->attrs_extra);+    }+  }++  // calculate rows and row/col position+  rowcol_t rc = { 0 };+  const ssize_t rows_input = sbuf_get_rc_at_pos( eb->input, eb->termw, promptw, cpromptw, eb->pos, &rc );+  rowcol_t rc_extra = { 0 };+  ssize_t rows_extra = 0;+  if (extra != NULL) { +    rows_extra = sbuf_get_rc_at_pos( extra, eb->termw, 0, 0, 0 /*pos*/, &rc_extra ); +  }+  const ssize_t rows = rows_input + rows_extra; +  debug_msg("edit: refresh: rows %zd, cursor: %zd,%zd (previous rows %zd, cursor row %zd)\n", rows, rc.row, rc.col, eb->cur_rows, eb->cur_row);+  +  // only render at most terminal height rows+  const ssize_t termh = term_get_height(env->term);+  ssize_t first_row = 0;                 // first visible row +  ssize_t last_row = rows - 1;           // last visible row+  if (rows > termh) {+    first_row = rc.row - termh + 1;      // ensure cursor is visible+    if (first_row < 0) first_row = 0;+    last_row = first_row + termh - 1;+  }+  assert(last_row - first_row < termh);+  +  // reduce flicker+  buffer_mode_t bmode = term_set_buffer_mode(env->term, BUFFERED);        ++  // back up to the first line+  term_start_of_line(env->term);+  term_up(env->term, (eb->cur_row >= termh ? termh-1 : eb->cur_row) );+  // term_clear_lines_to_end(env->term);  // gives flicker in old Windows cmd prompt ++  // render rows+  edit_refresh_rows( env, eb, eb->input, eb->attrs, promptw, cpromptw, false, first_row, last_row );  +  if (rows_extra > 0) {+    assert(extra != NULL);+    const ssize_t first_rowx = (first_row > rows_input ? first_row - rows_input : 0);+    const ssize_t last_rowx = last_row - rows_input; assert(last_rowx >= 0);+    edit_refresh_rows(env, eb, extra, eb->attrs_extra, 0, 0, true, first_rowx, last_rowx);+  }+    +  // overwrite trailing rows we do not use anymore  +  ssize_t rrows = last_row - first_row + 1;  // rendered rows+  if (rrows < termh && rows < eb->cur_rows) {+    ssize_t clear = eb->cur_rows - rows;+    while (rrows < termh && clear > 0) {+      clear--;+      rrows++;+      term_writeln(env->term,"");+      term_clear_line(env->term);+    }+  }+  +  // move cursor back to edit position+  term_start_of_line(env->term);+  term_up(env->term, first_row + rrows - 1 - rc.row );+  term_right(env->term, rc.col + (rc.row == 0 ? promptw : cpromptw));++  // and refresh+  term_flush(env->term);++  // stop buffering+  term_set_buffer_mode(env->term, bmode);++  // restore input by removing the hint+  sbuf_delete_at(eb->input, eb->pos, sbuf_len(eb->hint));+  sbuf_delete_at(eb->extra, 0, sbuf_len(eb->hint_help));+  attrbuf_clear(eb->attrs);+  attrbuf_clear(eb->attrs_extra);+  sbuf_free(extra);++  // update previous+  eb->cur_rows = rows;+  eb->cur_row = rc.row;+}++// clear current output+static void edit_clear(ic_env_t* env, editor_t* eb ) {+  term_attr_reset(env->term);  +  term_up(env->term, eb->cur_row);+  +  // overwrite all rows+  for( ssize_t i = 0; i < eb->cur_rows; i++) {+    term_clear_line(env->term);+    term_writeln(env->term, "");    +  }+  +  // move cursor back +  term_up(env->term, eb->cur_rows - eb->cur_row );  +}+++// clear screen and refresh+static void edit_clear_screen(ic_env_t* env, editor_t* eb ) {+  ssize_t cur_rows = eb->cur_rows;+  eb->cur_rows = term_get_height(env->term) - 1;+  edit_clear(env,eb);+  eb->cur_rows = cur_rows;+  edit_refresh(env,eb);+}+++// refresh after a terminal window resized (but before doing further edit operations!)+static bool edit_resize(ic_env_t* env, editor_t* eb ) {+  // update dimensions+  term_update_dim(env->term);+  ssize_t newtermw = term_get_width(env->term);+  if (eb->termw == newtermw) return false;+  +  // recalculate the row layout assuming the hardwrapping for the new terminal width+  ssize_t promptw, cpromptw;+  edit_get_prompt_width( env, eb, false, &promptw, &cpromptw );+  sbuf_insert_at(eb->input, sbuf_string(eb->hint), eb->pos); // insert used hint    +  +  // render extra (like a completion menu)+  stringbuf_t* extra = NULL;+  if (sbuf_len(eb->extra) > 0) {+    extra = sbuf_new(eb->mem);+    if (extra != NULL) {+      if (sbuf_len(eb->hint_help) > 0) {+        bbcode_append(env->bbcode, sbuf_string(eb->hint_help), extra, NULL);+      }+      bbcode_append(env->bbcode, sbuf_string(eb->extra), extra, NULL);+    }+  }+  rowcol_t rc = { 0 };+  const ssize_t rows_input = sbuf_get_wrapped_rc_at_pos( eb->input, eb->termw, newtermw, promptw, cpromptw, eb->pos, &rc );+  rowcol_t rc_extra = { 0 };+  ssize_t rows_extra = 0;+  if (extra != NULL) {+    rows_extra = sbuf_get_wrapped_rc_at_pos(extra, eb->termw, newtermw, 0, 0, 0 /*pos*/, &rc_extra);+  }  +  ssize_t rows = rows_input + rows_extra;+  debug_msg("edit: resize: new rows: %zd, cursor row: %zd (previous: rows: %zd, cursor row %zd)\n", rows, rc.row, eb->cur_rows, eb->cur_row);+  +  // update the newly calculated row and rows+  eb->cur_row = rc.row;+  if (rows > eb->cur_rows) {+    eb->cur_rows = rows;+  }+  eb->termw = newtermw;     +  edit_refresh(env,eb); ++  // remove hint again+  sbuf_delete_at(eb->input, eb->pos, sbuf_len(eb->hint));+  sbuf_free(extra);+  return true;+} ++static void editor_append_hint_help(editor_t* eb, const char* help) {+  sbuf_clear(eb->hint_help);+  if (help != NULL) {+    sbuf_replace(eb->hint_help, "[ic-info]");+    sbuf_append(eb->hint_help, help);+    sbuf_append(eb->hint_help, "[/ic-info]\n");+  }+}++// refresh with possible hint+static void edit_refresh_hint(ic_env_t* env, editor_t* eb) {+  if (env->no_hint || env->hint_delay > 0) {+    // refresh without hint first+    edit_refresh(env, eb);+    if (env->no_hint) return;+  }+    +  // and see if we can construct a hint (displayed after a delay)+  ssize_t count = completions_generate(env, env->completions, sbuf_string(eb->input), eb->pos, 2);+  if (count == 1) {+    const char* help = NULL;+    const char* hint = completions_get_hint(env->completions, 0, &help);+    if (hint != NULL) {+      sbuf_replace(eb->hint, hint); +      editor_append_hint_help(eb, help);+      // do auto-tabbing?+      if (env->complete_autotab) {+        stringbuf_t* sb = sbuf_new(env->mem);  // temporary buffer for completion+        if (sb != NULL) { +          sbuf_replace( sb, sbuf_string(eb->input) ); +          ssize_t pos = eb->pos;+          const char* extra_hint = hint;+          do {+            ssize_t newpos = sbuf_insert_at( sb, extra_hint, pos );+            if (newpos <= pos) break;+            pos = newpos;+            count = completions_generate(env, env->completions, sbuf_string(sb), pos, 2);+            if (count == 1) {+              const char* extra_help = NULL;+              extra_hint = completions_get_hint(env->completions, 0, &extra_help);+              if (extra_hint != NULL) {+                editor_append_hint_help(eb, extra_help);+                sbuf_append(eb->hint, extra_hint);+              }+            }+          }+          while(count == 1);       +          sbuf_free(sb);+        }          +      }      +    }+  }++  if (env->hint_delay <= 0) {+    // refresh with hint directly+    edit_refresh(env, eb);+  }+}++//-------------------------------------------------------------+// Edit operations+//-------------------------------------------------------------++static void edit_history_prev(ic_env_t* env, editor_t* eb);+static void edit_history_next(ic_env_t* env, editor_t* eb);++static void edit_undo_restore(ic_env_t* env, editor_t* eb) {+  editor_undo_restore(eb, true);+  edit_refresh(env,eb);+}++static void edit_redo_restore(ic_env_t* env, editor_t* eb) {+  editor_redo_restore(eb);+  edit_refresh(env,eb);+}++static void edit_cursor_left(ic_env_t* env, editor_t* eb) {+  ssize_t cwidth = 1;+  ssize_t prev = sbuf_prev(eb->input,eb->pos,&cwidth);+  if (prev < 0) return;+  rowcol_t rc;+  edit_get_rowcol( env, eb, &rc);  +  eb->pos = prev;  +  edit_refresh(env,eb);  +}++static void edit_cursor_right(ic_env_t* env, editor_t* eb) {+  ssize_t cwidth = 1;+  ssize_t next = sbuf_next(eb->input,eb->pos,&cwidth);+  if (next < 0) return;+  rowcol_t rc;+  edit_get_rowcol( env, eb, &rc);  +  eb->pos = next;  +  edit_refresh(env,eb);+}++static void edit_cursor_line_end(ic_env_t* env, editor_t* eb) {+  ssize_t end = sbuf_find_line_end(eb->input,eb->pos);+  if (end < 0) return;  +  eb->pos = end; +  edit_refresh(env,eb);+}++static void edit_cursor_line_start(ic_env_t* env, editor_t* eb) {+  ssize_t start = sbuf_find_line_start(eb->input,eb->pos);+  if (start < 0) return;+  eb->pos = start;+  edit_refresh(env,eb);+}++static void edit_cursor_next_word(ic_env_t* env, editor_t* eb) {+  ssize_t end = sbuf_find_word_end(eb->input,eb->pos);+  if (end < 0) return;+  eb->pos = end;+  edit_refresh(env,eb);+}++static void edit_cursor_prev_word(ic_env_t* env, editor_t* eb) {+  ssize_t start = sbuf_find_word_start(eb->input,eb->pos);+  if (start < 0) return;+  eb->pos = start;+  edit_refresh(env,eb);+}++static void edit_cursor_next_ws_word(ic_env_t* env, editor_t* eb) {+  ssize_t end = sbuf_find_ws_word_end(eb->input, eb->pos);+  if (end < 0) return;+  eb->pos = end;+  edit_refresh(env, eb);+}++static void edit_cursor_prev_ws_word(ic_env_t* env, editor_t* eb) {+  ssize_t start = sbuf_find_ws_word_start(eb->input, eb->pos);+  if (start < 0) return;+  eb->pos = start;+  edit_refresh(env, eb);+}++static void edit_cursor_to_start(ic_env_t* env, editor_t* eb) {+  eb->pos = 0; +  edit_refresh(env,eb);+}++static void edit_cursor_to_end(ic_env_t* env, editor_t* eb) {+  eb->pos = sbuf_len(eb->input); +  edit_refresh(env,eb);+}+++static void edit_cursor_row_up(ic_env_t* env, editor_t* eb) {+  rowcol_t rc;+  edit_get_rowcol( env, eb, &rc);+  if (rc.row == 0) {+    edit_history_prev(env,eb);+  }+  else {+    edit_set_pos_at_rowcol( env, eb, rc.row - 1, rc.col );+  }+}++static void edit_cursor_row_down(ic_env_t* env, editor_t* eb) {+  rowcol_t rc;+  ssize_t rows = edit_get_rowcol( env, eb, &rc);+  if (rc.row + 1 >= rows) {+    edit_history_next(env,eb);+  }+  else {+    edit_set_pos_at_rowcol( env, eb, rc.row + 1, rc.col );+  }+}+++static void edit_cursor_match_brace(ic_env_t* env, editor_t* eb) {+  ssize_t match = find_matching_brace( sbuf_string(eb->input), eb->pos, ic_env_get_match_braces(env), NULL );+  if (match < 0) return;+  eb->pos = match;+  edit_refresh(env,eb);+}++static void edit_backspace(ic_env_t* env, editor_t* eb) {+  if (eb->pos <= 0) return;+  editor_start_modify(eb);+  eb->pos = sbuf_delete_char_before(eb->input,eb->pos);+  edit_refresh(env,eb);+}++static void edit_delete_char(ic_env_t* env, editor_t* eb) {+  if (eb->pos >= sbuf_len(eb->input)) return;+  editor_start_modify(eb);+  sbuf_delete_char_at(eb->input,eb->pos);+  edit_refresh(env,eb);+}++static void edit_delete_all(ic_env_t* env, editor_t* eb) {+  if (sbuf_len(eb->input) <= 0) return;+  editor_start_modify(eb);+  sbuf_clear(eb->input);+  eb->pos = 0;+  edit_refresh(env,eb);+}++static void edit_delete_to_end_of_line(ic_env_t* env, editor_t* eb) { +  ssize_t start = sbuf_find_line_start(eb->input,eb->pos);+  if (start < 0) return;+  ssize_t end = sbuf_find_line_end(eb->input,eb->pos);+  if (end < 0) return;+  editor_start_modify(eb);+  // if on an empty line, remove it completely    +  if (start == end && sbuf_char_at(eb->input,end) == '\n') {+    end++;+  }+  else if (start == end && sbuf_char_at(eb->input,start - 1) == '\n') {+    eb->pos--;+  }+  sbuf_delete_from_to( eb->input, eb->pos, end );+  edit_refresh(env,eb);+}++static void edit_delete_to_start_of_line(ic_env_t* env, editor_t* eb) {+  ssize_t start = sbuf_find_line_start(eb->input,eb->pos);+  if (start < 0) return;+  ssize_t end   = sbuf_find_line_end(eb->input,eb->pos);+  if (end < 0) return;+  editor_start_modify(eb);+  // delete start newline if it was an empty line+  bool goright = false;+  if (start > 0 && sbuf_char_at(eb->input,start-1) == '\n' && start == end) {+    // if it is an empty line remove it+    start--;+    // afterwards, move to start of next line if it exists (so the cursor stays on the same row)+    goright = true;+  }+  sbuf_delete_from_to( eb->input, start, eb->pos );+  eb->pos = start;+  if (goright) edit_cursor_right(env,eb); +  edit_refresh(env,eb);+}++static void edit_delete_line(ic_env_t* env, editor_t* eb) {+  ssize_t start = sbuf_find_line_start(eb->input,eb->pos);+  if (start < 0) return;+  ssize_t end   = sbuf_find_line_end(eb->input,eb->pos);+  if (end < 0) return;+  editor_start_modify(eb);+  // delete newline as well so no empty line is left;+  bool goright = false;+  if (start > 0 && sbuf_char_at(eb->input,start-1) == '\n') {+    start--;+    // afterwards, move to start of next line if it exists (so the cursor stays on the same row)+    goright = true;+  }+  else if (sbuf_char_at(eb->input,end) == '\n') {+    end++;+  }+  sbuf_delete_from_to(eb->input,start,end);+  eb->pos = start;+  if (goright) edit_cursor_right(env,eb); +  edit_refresh(env,eb);+}+ +static void edit_delete_to_start_of_word(ic_env_t* env, editor_t* eb) {+   ssize_t start = sbuf_find_word_start(eb->input,eb->pos);+  if (start < 0) return;+  editor_start_modify(eb);+  sbuf_delete_from_to( eb->input, start, eb->pos );+  eb->pos = start;+  edit_refresh(env,eb);+}++static void edit_delete_to_end_of_word(ic_env_t* env, editor_t* eb) {+  ssize_t end = sbuf_find_word_end(eb->input,eb->pos);+  if (end < 0) return;+  editor_start_modify(eb);+  sbuf_delete_from_to( eb->input, eb->pos, end );+  edit_refresh(env,eb);+}++static void edit_delete_to_start_of_ws_word(ic_env_t* env, editor_t* eb) {+  ssize_t start = sbuf_find_ws_word_start(eb->input, eb->pos);+  if (start < 0) return;+  editor_start_modify(eb);+  sbuf_delete_from_to(eb->input, start, eb->pos);+  eb->pos = start;+  edit_refresh(env, eb);+}++static void edit_delete_to_end_of_ws_word(ic_env_t* env, editor_t* eb) {+  ssize_t end = sbuf_find_ws_word_end(eb->input, eb->pos);+  if (end < 0) return;+  editor_start_modify(eb);+  sbuf_delete_from_to(eb->input, eb->pos, end);+  edit_refresh(env, eb);+}+++static void edit_delete_word(ic_env_t* env, editor_t* eb) {+  ssize_t start = sbuf_find_word_start(eb->input,eb->pos);+  if (start < 0) return;+  ssize_t end   = sbuf_find_word_end(eb->input,eb->pos);+  if (end < 0) return;+  editor_start_modify(eb);  +  sbuf_delete_from_to(eb->input,start,end);+  eb->pos = start;+  edit_refresh(env,eb);+}++static void edit_swap_char( ic_env_t* env, editor_t* eb ) { +  if (eb->pos <= 0 || eb->pos == sbuf_len(eb->input)) return;+  editor_start_modify(eb);+  eb->pos = sbuf_swap_char(eb->input,eb->pos);+  edit_refresh(env,eb);+}++static void edit_multiline_eol(ic_env_t* env, editor_t* eb) {+  if (eb->pos <= 0) return;+  if (sbuf_string(eb->input)[eb->pos-1] != env->multiline_eol) return;+  editor_start_modify(eb);+  // replace line continuation with a real newline+  sbuf_delete_at( eb->input, eb->pos-1, 1);+  sbuf_insert_at( eb->input, "\n", eb->pos-1);  +  edit_refresh(env,eb);+}++static void edit_insert_unicode(ic_env_t* env, editor_t* eb, unicode_t u) {+  editor_start_modify(eb);+  ssize_t nextpos = sbuf_insert_unicode_at(eb->input, u, eb->pos);+  if (nextpos >= 0) eb->pos = nextpos;  +  edit_refresh_hint(env, eb);+}++static void edit_auto_brace(ic_env_t* env, editor_t* eb, char c) {+  if (env->no_autobrace) return;+  const char* braces = ic_env_get_auto_braces(env);+  for (const char* b = braces; *b != 0; b += 2) {+    if (*b == c) {+      const char close = b[1];+      //if (sbuf_char_at(eb->input, eb->pos) != close) {+        sbuf_insert_char_at(eb->input, close, eb->pos);+        bool balanced = false;+        find_matching_brace(sbuf_string(eb->input), eb->pos, braces, &balanced );+        if (!balanced) {+          // don't insert if it leads to an unbalanced expression.+          sbuf_delete_char_at(eb->input, eb->pos);+        }+      //}+      return;+    }+    else if (b[1] == c) {+      // close brace, check if there we don't overwrite to the right+      if (sbuf_char_at(eb->input, eb->pos) == c) {+        sbuf_delete_char_at(eb->input, eb->pos);+      }+      return;+    }+  }+}++static void editor_auto_indent(editor_t* eb, const char* pre, const char* post ) {+  assert(eb->pos > 0 && sbuf_char_at(eb->input,eb->pos-1) == '\n');+  ssize_t prelen = ic_strlen(pre);+  if (prelen > 0) {+    if (eb->pos - 1 < prelen) return;+    if (!ic_starts_with(sbuf_string(eb->input) + eb->pos - 1 - prelen, pre)) return;+    if (!ic_starts_with(sbuf_string(eb->input) + eb->pos, post)) return;+    eb->pos = sbuf_insert_at(eb->input, "  ", eb->pos);+    sbuf_insert_char_at(eb->input, '\n', eb->pos);+  }+}++static void edit_insert_char(ic_env_t* env, editor_t* eb, char c) {+  editor_start_modify(eb);+  ssize_t nextpos = sbuf_insert_char_at( eb->input, c, eb->pos );+  if (nextpos >= 0) eb->pos = nextpos;+  edit_auto_brace(env, eb, c);+  if (c=='\n') {+    editor_auto_indent(eb, "{", "}");  // todo: custom auto indent tokens?+  }+  edit_refresh_hint(env,eb);  +}++//-------------------------------------------------------------+// Help+//-------------------------------------------------------------++#include "editline_help.c"++//-------------------------------------------------------------+// History+//-------------------------------------------------------------++#include "editline_history.c"++//-------------------------------------------------------------+// Completion+//-------------------------------------------------------------++#include "editline_completion.c"+++//-------------------------------------------------------------+// Edit line: main edit loop+//-------------------------------------------------------------++static char* edit_line( ic_env_t* env, const char* prompt_text )+{+  // set up an edit buffer+  editor_t eb;+  memset(&eb, 0, sizeof(eb));+  eb.mem      = env->mem;+  eb.input    = sbuf_new(env->mem);+  eb.extra    = sbuf_new(env->mem);+  eb.hint     = sbuf_new(env->mem);+  eb.hint_help= sbuf_new(env->mem);+  eb.termw    = term_get_width(env->term);  +  eb.pos      = 0;+  eb.cur_rows = 1; +  eb.cur_row  = 0; +  eb.modified = false;  +  eb.prompt_text   = (prompt_text != NULL ? prompt_text : "");+  eb.history_idx   = 0;  +  editstate_init(&eb.undo);+  editstate_init(&eb.redo);+  if (eb.input==NULL || eb.extra==NULL || eb.hint==NULL || eb.hint_help==NULL) {+    return NULL;+  }++  // caching+  if (!(env->no_highlight && env->no_bracematch)) {+    eb.attrs = attrbuf_new(env->mem);+    eb.attrs_extra = attrbuf_new(env->mem);+  }+  +  // show prompt+  edit_write_prompt(env, &eb, 0, false);   ++  // always a history entry for the current input+  history_push(env->history, "");++  // process keys+  code_t c;          // current key code+  while(true) {    +    // read a character+    term_flush(env->term);+    if (env->hint_delay <= 0 || sbuf_len(eb.hint) == 0) {+      // blocking read+      c = tty_read(env->tty);+    }+    else {+      // timeout to display hint+      if (!tty_read_timeout(env->tty, env->hint_delay, &c)) {+        // timed-out+        if (sbuf_len(eb.hint) > 0) {+          // display hint+          edit_refresh(env, &eb);+        }+        c = tty_read(env->tty);+      }+      else {+        // clear the pending hint if we got input before the delay expired+        sbuf_clear(eb.hint);+        sbuf_clear(eb.hint_help);+      }+    }+    +    // update terminal in case of a resize+    if (tty_term_resize_event(env->tty)) {+      edit_resize(env,&eb);            +    }++    // clear hint only after a potential resize (so resize row calculations are correct)+    const bool had_hint = (sbuf_len(eb.hint) > 0);+    sbuf_clear(eb.hint);+    sbuf_clear(eb.hint_help);++    // if the user tries to move into a hint with left-cursor or end, we complete it first+    if ((c == KEY_RIGHT || c == KEY_END) && had_hint) {+      edit_generate_completions(env, &eb, true);+      c = KEY_NONE;      +    }++    // Operations that may return+    if (c == KEY_ENTER) {+      if (!env->singleline_only && eb.pos > 0 && +           sbuf_string(eb.input)[eb.pos-1] == env->multiline_eol && +            edit_pos_is_at_row_end(env,&eb)) +      {+        // replace line-continuation with newline+        edit_multiline_eol(env,&eb);        +      }+      else {+        // otherwise done+        break;+      }+    } +    else if (c == KEY_CTRL_D) {+      if (eb.pos == 0 && editor_pos_is_at_end(&eb)) break; // ctrl+D on empty quits with NULL+      edit_delete_char(env,&eb);     // otherwise it is like delete+    } +    else if (c == KEY_CTRL_C || c == KEY_EVENT_STOP) {+      break; // ctrl+C or STOP event quits with NULL+    }+    else if (c == KEY_ESC) {+      if (eb.pos == 0 && editor_pos_is_at_end(&eb)) break;  // ESC on empty input returns with empty input+      edit_delete_all(env,&eb);      // otherwise delete the current input+      // edit_delete_line(env,&eb);  // otherwise delete the current line+    }+    else if (c == KEY_BELL /* ^G */) {+      edit_delete_all(env,&eb);+      break; // ctrl+G cancels (and returns empty input)+    }++    // Editing Operations+    else switch(c) {+      // events+      case KEY_EVENT_RESIZE:  // not used+        edit_resize(env,&eb);+        break;+      case KEY_EVENT_AUTOTAB:+        edit_generate_completions(env, &eb, true);+        break;++      // completion, history, help, undo+      case KEY_TAB:+      case WITH_ALT('?'):+        edit_generate_completions(env,&eb,false);+        break;+      case KEY_CTRL_R:+      case KEY_CTRL_S:+        edit_history_search_with_current_word(env,&eb);+        break;+      case KEY_CTRL_P:+        edit_history_prev(env, &eb);+        break;+      case KEY_CTRL_N:+        edit_history_next(env, &eb);+        break;+      case KEY_CTRL_L:+        edit_clear_screen(env, &eb);+        break;+      case KEY_CTRL_Z:+      case WITH_CTRL('_'):+        edit_undo_restore(env, &eb);+        break;+      case KEY_CTRL_Y:+        edit_redo_restore(env, &eb);+        break;+      case KEY_F1:+        edit_show_help(env, &eb);+        break;++      // navigation+      case KEY_LEFT:+      case KEY_CTRL_B:+        edit_cursor_left(env,&eb);+        break;+      case KEY_RIGHT:+      case KEY_CTRL_F:+        edit_cursor_right(env,&eb);+        break;+      case KEY_UP:+        edit_cursor_row_up(env,&eb);+        break;+      case KEY_DOWN:+        edit_cursor_row_down(env,&eb);+        break;                 +      case KEY_HOME:+      case KEY_CTRL_A:+        edit_cursor_line_start(env,&eb);+        break;+      case KEY_END:+      case KEY_CTRL_E:+        edit_cursor_line_end(env,&eb);+        break;+      case KEY_CTRL_LEFT:+      case WITH_SHIFT(KEY_LEFT):    +      case WITH_ALT('b'):+        edit_cursor_prev_word(env,&eb);+        break;+      case KEY_CTRL_RIGHT:+      case WITH_SHIFT(KEY_RIGHT):      +      case WITH_ALT('f'):+        edit_cursor_next_word(env,&eb);+        break;      +      case KEY_CTRL_HOME:+      case WITH_SHIFT(KEY_HOME):      +      case KEY_PAGEUP:+      case WITH_ALT('<'):+        edit_cursor_to_start(env,&eb);+        break;+      case KEY_CTRL_END:+      case WITH_SHIFT(KEY_END):      +      case KEY_PAGEDOWN:+      case WITH_ALT('>'):+        edit_cursor_to_end(env,&eb);+        break;+      case WITH_ALT('m'):+        edit_cursor_match_brace(env,&eb);+        break;++      // deletion+      case KEY_BACKSP:+        edit_backspace(env,&eb);+        break;+      case KEY_DEL:+        edit_delete_char(env,&eb);+        break;+      case WITH_ALT('d'):+        edit_delete_to_end_of_word(env,&eb);+        break;+      case KEY_CTRL_W:+        edit_delete_to_start_of_ws_word(env, &eb);+        break;+      case WITH_ALT(KEY_DEL):+      case WITH_ALT(KEY_BACKSP):+        edit_delete_to_start_of_word(env,&eb);+        break;      +      case KEY_CTRL_U:+        edit_delete_to_start_of_line(env,&eb);+        break;+      case KEY_CTRL_K:+        edit_delete_to_end_of_line(env,&eb);+        break;+      case KEY_CTRL_T:+        edit_swap_char(env,&eb);+        break;++      // Editing+      case KEY_SHIFT_TAB:+      case KEY_LINEFEED: // '\n' (ctrl+J, shift+enter)+        if (!env->singleline_only) { +          edit_insert_char(env, &eb, '\n'); +        }+        break;+      default: {+        char chr;+        unicode_t uchr;+        if (code_is_ascii_char(c,&chr)) {+          edit_insert_char(env,&eb,chr);+        }+        else if (code_is_unicode(c, &uchr)) {+          edit_insert_unicode(env,&eb, uchr);+        }+        else {+          debug_msg( "edit: ignore code: 0x%04x\n", c);+        }+        break;+      }+    }++  }++  // goto end+  eb.pos = sbuf_len(eb.input);++  // refresh once more but without brace matching+  bool bm = env->no_bracematch;+  env->no_bracematch = true;+  edit_refresh(env,&eb);+  env->no_bracematch = bm;+  +  // save result+  char* res; +  if ((c == KEY_CTRL_D && sbuf_len(eb.input) == 0) || c == KEY_CTRL_C || c == KEY_EVENT_STOP) {+    res = NULL;+  }+  else if (!tty_is_utf8(env->tty)) {+    res = sbuf_strdup_from_utf8(eb.input);+  }+  else {+    res = sbuf_strdup(eb.input);+  }++  // update history+  history_update(env->history, sbuf_string(eb.input));+  if (res == NULL || sbuf_len(eb.input) <= 1) { ic_history_remove_last(); } // no empty or single-char entries+  history_save(env->history);++  // free resources +  editstate_done(env->mem, &eb.undo);+  editstate_done(env->mem, &eb.redo);+  attrbuf_free(eb.attrs);+  attrbuf_free(eb.attrs_extra);+  sbuf_free(eb.input);+  sbuf_free(eb.extra);+  sbuf_free(eb.hint);+  sbuf_free(eb.hint_help);++  return res;+}+
+ src/editline_completion.c view
@@ -0,0 +1,277 @@+/* ----------------------------------------------------------------------------+  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.+-----------------------------------------------------------------------------*/++//-------------------------------------------------------------+// Completion menu: this file is included in editline.c+//-------------------------------------------------------------++// return true if anything changed+static bool edit_complete(ic_env_t* env, editor_t* eb, ssize_t idx) {+  editor_start_modify(eb);+  ssize_t newpos = completions_apply(env->completions, idx, eb->input, eb->pos);+  if (newpos < 0) {+    editor_undo_restore(eb,false);+    return false;+  }+  eb->pos = newpos;+  edit_refresh(env,eb);  +  return true;+}++static bool edit_complete_longest_prefix(ic_env_t* env, editor_t* eb ) {+  editor_start_modify(eb);+  ssize_t newpos = completions_apply_longest_prefix( env->completions, eb->input, eb->pos );+  if (newpos < 0) {+    editor_undo_restore(eb,false);+    return false;+  }+  eb->pos = newpos;+  edit_refresh(env,eb);+  return true;+}++ic_private void sbuf_append_tagged( stringbuf_t* sb, const char* tag, const char* content ) {+  sbuf_appendf(sb, "[%s]", tag);  +  sbuf_append(sb,content);+  sbuf_append(sb,"[/]");+}++static void editor_append_completion(ic_env_t* env, editor_t* eb, ssize_t idx, ssize_t width, bool numbered, bool selected ) {+  const char* help = NULL;+  const char* display = completions_get_display(env->completions, idx, &help);+  if (display == NULL) return;+  if (numbered) {+    sbuf_appendf(eb->extra, "[ic-info]%s%zd [/]", (selected ? (tty_is_utf8(env->tty) ? "\xE2\x86\x92" : "*") : " "), 1 + idx);+    width -= 3;+  }++  if (width > 0) {+    sbuf_appendf(eb->extra, "[width=\"%zd;left; ;on\"]", width );+  }+  if (selected) {+    sbuf_append(eb->extra, "[ic-emphasis]");+  }+  sbuf_append(eb->extra, display);+  if (selected) { sbuf_append(eb->extra,"[/ic-emphasis]"); }+  if (help != NULL) {+    sbuf_append(eb->extra, "  ");+    sbuf_append_tagged(eb->extra, "ic-info", help );      +  }+  if (width > 0) { sbuf_append(eb->extra,"[/width]"); }  +}++// 2 and 3 column output up to 80 wide+#define IC_DISPLAY2_MAX    34+#define IC_DISPLAY2_COL    (3+IC_DISPLAY2_MAX)+#define IC_DISPLAY2_WIDTH  (2*IC_DISPLAY2_COL + 2)    // 75++#define IC_DISPLAY3_MAX    21+#define IC_DISPLAY3_COL    (3+IC_DISPLAY3_MAX)+#define IC_DISPLAY3_WIDTH  (3*IC_DISPLAY3_COL + 2*2)  // 76++static void editor_append_completion2(ic_env_t* env, editor_t* eb, ssize_t col_width, ssize_t idx1, ssize_t idx2, ssize_t selected ) {  +  editor_append_completion(env, eb, idx1, col_width, true, (idx1 == selected) );+  sbuf_append( eb->extra, "  ");+  editor_append_completion(env, eb, idx2, col_width, true, (idx2 == selected) );+}++static void editor_append_completion3(ic_env_t* env, editor_t* eb, ssize_t col_width, ssize_t idx1, ssize_t idx2, ssize_t idx3, ssize_t selected ) {  +  editor_append_completion(env, eb, idx1, col_width, true, (idx1 == selected) );+  sbuf_append( eb->extra, "  ");+  editor_append_completion(env, eb, idx2, col_width, true, (idx2 == selected));+  sbuf_append( eb->extra, "  ");+  editor_append_completion(env, eb, idx3, col_width, true, (idx3 == selected) );+}++static ssize_t edit_completions_max_width( ic_env_t* env, ssize_t count ) {+  ssize_t max_width = 0;+  for( ssize_t i = 0; i < count; i++) {+    const char* help = NULL;+    ssize_t w = bbcode_column_width(env->bbcode, completions_get_display(env->completions, i, &help));+    if (help != NULL) {+      w += 2 + bbcode_column_width(env->bbcode, help);+    }+    if (w > max_width) {+      max_width = w;+    }+  }+  return max_width;+}++static void edit_completion_menu(ic_env_t* env, editor_t* eb, bool more_available) {+  ssize_t count = completions_count(env->completions);+  ssize_t count_displayed = count;+  assert(count > 1);+  ssize_t selected = (env->complete_nopreview ? 0 : -1); // select first or none+  ssize_t percolumn = count;++again:+  // show first 9 (or 8) completions+  sbuf_clear(eb->extra);+  ssize_t twidth = term_get_width(env->term) - 1;+  ssize_t colwidth;+  if (count > 3 && ((colwidth = 3 + edit_completions_max_width(env, 9))*3 + 2*2) < twidth) {+    // display as a 3 column block+    count_displayed = (count > 9 ? 9 : count);+    percolumn = 3;+    for (ssize_t rw = 0; rw < percolumn; rw++) {+      if (rw > 0) sbuf_append(eb->extra, "\n");+      editor_append_completion3(env, eb, colwidth, rw, percolumn+rw, (2*percolumn)+rw, selected);+    }+  }+  else if (count > 4 && ((colwidth = 3 + edit_completions_max_width(env, 8))*2 + 2) < twidth) {+    // display as a 2 column block if some entries are too wide for three columns+    count_displayed = (count > 8 ? 8 : count);+    percolumn = (count_displayed <= 6 ? 3 : 4);+    for (ssize_t rw = 0; rw < percolumn; rw++) {+      if (rw > 0) sbuf_append(eb->extra, "\n");+      editor_append_completion2(env, eb, colwidth, rw, percolumn+rw, selected);+    }+  }+  else {+    // display as a list+    count_displayed = (count > 9 ? 9 : count);+    percolumn = count_displayed;+    for (ssize_t i = 0; i < count_displayed; i++) {+      if (i > 0) sbuf_append(eb->extra, "\n");+      editor_append_completion(env, eb, i, -1, true /* numbered */, selected == i);+    }+  }+  if (count > count_displayed) {+    if (more_available) {+      sbuf_append(eb->extra, "\n[ic-info](press page-down (or ctrl-j) to see all further completions)[/]");+    }+    else {+      sbuf_appendf(eb->extra, "\n[ic-info](press page-down (or ctrl-j) to see all %zd completions)[/]", count );+    }+  }+  if (!env->complete_nopreview && selected >= 0 && selected <= count_displayed) {+    edit_complete(env,eb,selected);+    editor_undo_restore(eb,false);+  }+  else {+    edit_refresh(env, eb);+  }++  // read here; if not a valid key, push it back and return to main event loop+  code_t c = tty_read(env->tty);+  if (tty_term_resize_event(env->tty)) {+    edit_resize(env, eb);+  }+  sbuf_clear(eb->extra);+  +  // direct selection?+  if (c >= '1' && c <= '9') {+    ssize_t i = (c - '1');+    if (i < count) {+      selected = i;+      c = KEY_ENTER;+    }+  }++  // process commands+  if (c == KEY_DOWN || c == KEY_TAB) {+    selected++;+    if (selected >= count_displayed) {+      //term_beep(env->term);+      selected = 0;+    }+    goto again;+  }+  else if (c == KEY_UP || c == KEY_SHIFT_TAB) {+    selected--;+    if (selected < 0) {+      selected = count_displayed - 1;+      //term_beep(env->term);+    }+    goto again;+  }+  else if (c == KEY_F1) {+    edit_show_help(env, eb);+    goto again;+  }+  else if (c == KEY_ESC) {+    completions_clear(env->completions);+    edit_refresh(env,eb);+    c = 0; // ignore and return+  }+  else if (selected >= 0 && (c == KEY_ENTER || c == KEY_RIGHT || c == KEY_END)) /* || c == KEY_TAB*/ {  +    // select the current entry+    assert(selected < count);+    c = 0;      +    edit_complete(env, eb, selected);    +    if (env->complete_autotab) {+      tty_code_pushback(env->tty,KEY_EVENT_AUTOTAB); // immediately try to complete again        +    }+  }+  else if (!env->complete_nopreview && !code_is_virt_key(c)) {+    // if in preview mode, select the current entry and exit the menu+    assert(selected < count);+    edit_complete(env, eb, selected); +  }+  else if ((c == KEY_PAGEDOWN || c == KEY_LINEFEED) && count > 9) {+    // show all completions+    c = 0;+    if (more_available) {+      // generate all entries (up to the max (= 1000))+      count = completions_generate(env, env->completions, sbuf_string(eb->input), eb->pos, IC_MAX_COMPLETIONS_TO_SHOW);+    }+    rowcol_t rc;+    edit_get_rowcol(env,eb,&rc);+    edit_clear(env,eb);+    edit_write_prompt(env,eb,0,false);+    term_writeln(env->term, "");+    for(ssize_t i = 0; i < count; i++) {+      const char* display = completions_get_display(env->completions, i, NULL);+      if (display != NULL) {+        bbcode_println(env->bbcode, display);+      }+    }+    if (count >= IC_MAX_COMPLETIONS_TO_SHOW) {+      bbcode_println(env->bbcode, "[ic-info]... and more.[/]");+    }+    else {+      bbcode_printf(env->bbcode, "[ic-info](%zd possible completions)[/]\n", count );+    }+    for(ssize_t i = 0; i < rc.row+1; i++) {+      term_write(env->term, " \n");+    }+    eb->cur_rows = 0;+    edit_refresh(env,eb);      +  }+  else {+    edit_refresh(env,eb);+  }+  // done+  completions_clear(env->completions);      +  if (c != 0) tty_code_pushback(env->tty,c);+}++static void edit_generate_completions(ic_env_t* env, editor_t* eb, bool autotab) {+  debug_msg( "edit: complete: %zd: %s\n", eb->pos, sbuf_string(eb->input) );+  if (eb->pos < 0) return;+  ssize_t count = completions_generate(env, env->completions, sbuf_string(eb->input), eb->pos, IC_MAX_COMPLETIONS_TO_TRY);+  bool more_available = (count >= IC_MAX_COMPLETIONS_TO_TRY);+  if (count <= 0) {+    // no completions+    if (!autotab) { term_beep(env->term); }+  }+  else if (count == 1) {+    // complete if only one match    +    if (edit_complete(env,eb,0 /*idx*/) && env->complete_autotab) {+      tty_code_pushback(env->tty,KEY_EVENT_AUTOTAB);+    }    +  }+  else {+    //term_beep(env->term); +    if (!more_available) { +      edit_complete_longest_prefix(env,eb);+    }    +    completions_sort(env->completions);+    edit_completion_menu( env, eb, more_available);    +  }+}
+ src/editline_help.c view
@@ -0,0 +1,140 @@+/* ----------------------------------------------------------------------------+  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.+-----------------------------------------------------------------------------*/++//-------------------------------------------------------------+// Help: this is included into editline.c+//-------------------------------------------------------------++static const char* help[] = {+  "","Navigation:",+  "left,"+  "^b",         "go one character to the left",+  "right,"+  "^f",         "go one character to the right",+  "up",         "go one row up, or back in the history",+  "down",       "go one row down, or forward in the history",+  #ifdef __APPLE__+  "shift-left",+  #else+  "^left",+  #endif+                "go to the start of the previous word",+  #ifdef __APPLE__+  "shift-right",+  #else+  "^right",+  #endif+                "go to the end the current word",+  "home,"+  "^a",         "go to the start of the current line",+  "end,"+  "^e",         "go to the end of the current line",+  "pgup,"+  "^home",       "go to the start of the current input",+  "pgdn,"+  "^end",       "go to the end of the current input",+  "alt-m",      "jump to matching brace",+  "^p",         "go back in the history",+  "^n",         "go forward in the history",+  "^r,^s",      "search the history starting with the current word",+  "","",++  "", "Deletion:",+  "del,^d",     "delete the current character",+  "backsp,^h",  "delete the previous character",+  "^w",         "delete to preceding white space",+  "alt-backsp", "delete to the start of the current word",+  "alt-d",      "delete to the end of the current word",+  "^u",         "delete to the start of the current line",+  "^k",         "delete to the end of the current line",+  "esc",        "delete the current input, or done with empty input",+  "","",++  "", "Editing:",+  "enter",      "accept current input",+  #ifndef __APPLE__+  "^enter, ^j", "",+  "shift-tab",+  #else+  "shift-tab,^j",+  #endif+                "create a new line for multi-line input",+  //" ",          "(or type '\\' followed by enter)",+  "^l",         "clear screen",+  "^t",         "swap with previous character (move character backward)",+  "^z,^_",      "undo",+  "^y",         "redo",+  //"^C",         "done with empty input",+  //"F1",         "show this help",+  "tab",        "try to complete the current input",+  "","",+  "","In the completion menu:",+  "enter,left", "use the currently selected completion",+  "1 - 9",      "use completion N from the menu",+  "tab,down",   "select the next completion",+  "shift-tab,up","select the previous completion",+  "esc",        "exit menu without completing",+  "pgdn,^j",    "show all further possible completions",+  "","",+  "","In incremental history search:",+  "enter",      "use the currently found history entry",+  "backsp,"+  "^z",         "go back to the previous match (undo)",+  "tab,"+  "^r",         "find the next match",+  "shift-tab,"+  "^s",         "find an earlier match",+  "esc",        "exit search",+  " ","",+  NULL, NULL+};++static const char* help_initial = +  "[ic-info]"+  "Isocline v1.0, copyright (c) 2021 Daan Leijen.\n"+  "This is free software; you can redistribute it and/or\n"+  "modify it under the terms of the MIT License.\n"+  "See <[url]https://github.com/daanx/isocline[/url]> for further information.\n"+  "We use ^<key> as a shorthand for ctrl-<key>.\n"+  "\n"+  "Overview:\n"+  "\n[ansi-lightgray]"+  "       home,ctrl-a      cursor     end,ctrl-e\n"+  "         ┌────────────────┼───────────────┐    (navigate)\n"+  //"       │                │               │\n"+  #ifndef __APPLE__+  "         │    ctrl-left   │  ctrl-right   │\n"+  #else+  "         │     alt-left   │   alt-right   │\n"+  #endif+  "         │        ┌───────┼──────┐        │    ctrl-r   : search history\n"+  "         ▼        ▼       ▼      ▼        ▼    tab      : complete word\n"+  "  prompt> [ansi-darkgray]it's the quintessential language[/]     shift-tab: insert new line\n"+  "         ▲        ▲              ▲        ▲    esc      : delete input, done\n"+  "         │        └──────────────┘        │    ctrl-z   : undo\n"+  "         │   alt-backsp        alt-d      │\n"+  //"       │                │               │\n"+  "         └────────────────────────────────┘    (delete)\n"+  "       ctrl-u                          ctrl-k\n"+  "[/ansi-lightgray][/ic-info]\n";++static void edit_show_help(ic_env_t* env, editor_t* eb) {+  edit_clear(env, eb);+  bbcode_println(env->bbcode, help_initial);+  for (ssize_t i = 0; help[i] != NULL && help[i+1] != NULL; i += 2) {+    if (help[i][0] == 0) {  +      bbcode_printf(env->bbcode, "[ic-info]%s[/]\n", help[i+1]);+    }+    else {+      bbcode_printf(env->bbcode, "  [ic-emphasis]%-13s[/][ansi-lightgray]%s%s[/]\n", help[i], (help[i+1][0] == 0 ? "" : ": "), help[i+1]);+    }+  }++  eb->cur_rows = 0;+  eb->cur_row = 0;+  edit_refresh(env, eb);+}
+ src/editline_history.c view
@@ -0,0 +1,260 @@+/* ----------------------------------------------------------------------------+  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.+-----------------------------------------------------------------------------*/++//-------------------------------------------------------------+// History search: this file is included in editline.c+//-------------------------------------------------------------++static void edit_history_at(ic_env_t* env, editor_t* eb, int ofs ) +{+  if (eb->modified) { +    history_update(env->history, sbuf_string(eb->input)); // update first entry if modified+    eb->history_idx = 0;          // and start again +    eb->modified = false;    +  }+  const char* entry = history_get(env->history,eb->history_idx + ofs);+  // debug_msg( "edit: history: at: %d + %d, found: %s\n", eb->history_idx, ofs, entry);+  if (entry == NULL) {+    term_beep(env->term);+  }+  else {+    eb->history_idx += ofs;+    sbuf_replace(eb->input, entry);+    if (ofs > 0) {+      // at end of first line when scrolling up+      ssize_t end = sbuf_find_line_end(eb->input,0);+      eb->pos = (end < 0 ? 0 : end);+    }+    else {+      eb->pos = sbuf_len(eb->input);    // at end of last line when scrolling down+    }+    edit_refresh(env, eb);+  }+}++static void edit_history_prev(ic_env_t* env, editor_t* eb) {+  edit_history_at(env,eb, 1 );+}++static void edit_history_next(ic_env_t* env, editor_t* eb) {+  edit_history_at(env,eb, -1 );+}++typedef struct hsearch_s {+  struct hsearch_s* next;+  ssize_t hidx;+  ssize_t match_pos;+  ssize_t match_len;+  bool cinsert;+} hsearch_t;++static void hsearch_push( alloc_t* mem, hsearch_t** hs, ssize_t hidx, ssize_t mpos, ssize_t mlen, bool cinsert ) {+  hsearch_t* h = mem_zalloc_tp( mem, hsearch_t );+  if (h == NULL) return;+  h->hidx = hidx;+  h->match_pos = mpos;+  h->match_len = mlen;+  h->cinsert = cinsert;+  h->next = *hs;+  *hs = h;+}++static bool hsearch_pop( alloc_t* mem, hsearch_t** hs, ssize_t* hidx, ssize_t* match_pos, ssize_t* match_len, bool* cinsert ) {+  hsearch_t* h = *hs;+  if (h == NULL) return false;+  *hs = h->next;+  if (hidx != NULL)      *hidx = h->hidx;+  if (match_pos != NULL) *match_pos = h->match_pos;+  if (match_len != NULL) *match_len = h->match_len;+  if (cinsert != NULL)   *cinsert = h->cinsert;+  mem_free(mem, h);+  return true;+}++static void hsearch_done( alloc_t* mem, hsearch_t* hs ) {+  while (hs != NULL) {+    hsearch_t* next = hs->next;+    mem_free(mem, hs);+    hs = next;+  }+}++static void edit_history_search(ic_env_t* env, editor_t* eb, char* initial ) {+  if (history_count( env->history ) <= 0) {+    term_beep(env->term);+    return;+  }++  // update history+  if (eb->modified) { +    history_update(env->history, sbuf_string(eb->input)); // update first entry if modified+    eb->history_idx = 0;               // and start again +    eb->modified = false;+  }++  // set a search prompt and remember the previous state+  editor_undo_capture(eb);+  eb->disable_undo = true;+  bool old_hint = ic_enable_hint(false);  +  const char* prompt_text = eb->prompt_text;+  eb->prompt_text = "history search";+  +  // search state+  hsearch_t* hs = NULL;        // search undo +  ssize_t hidx = 1;            // current history entry+  ssize_t match_pos = 0;       // current matched position+  ssize_t match_len = 0;       // length of the match+  const char* hentry = NULL;   // current history entry+  +  // Simulate per character searches for each letter in `initial` (so backspace works)+  if (initial != NULL) {+    const ssize_t initial_len = ic_strlen(initial);+    ssize_t ipos = 0;+    while( ipos < initial_len ) {+      ssize_t next = str_next_ofs( initial, initial_len, ipos, NULL );+      if (next < 0) break;+      hsearch_push( eb->mem, &hs, hidx, match_pos, match_len, true);+      char c = initial[ipos + next];  // terminate temporarily+      initial[ipos + next] = 0;+      if (history_search( env->history, hidx, initial, true, &hidx, &match_pos )) {+        match_len = ipos + next;+      }      +      else if (ipos + next >= initial_len) {+        term_beep(env->term);+      }+      initial[ipos + next] = c;       // restore+      ipos += next;+    }+    sbuf_replace( eb->input, initial);+    eb->pos = ipos;+  }+  else {+    sbuf_clear( eb->input );+    eb->pos = 0;+  }++  // Incremental search+again:+  hentry = history_get(env->history,hidx);+  if (hentry != NULL) {+    sbuf_appendf(eb->extra, "[ic-info]%zd. [/][ic-diminish][!pre]", hidx);+    sbuf_append_n( eb->extra, hentry, match_pos );      +    sbuf_append(eb->extra, "[/pre][u ic-emphasis][!pre]" ); +    sbuf_append_n( eb->extra, hentry + match_pos, match_len );+    sbuf_append(eb->extra, "[/pre][/u][!pre]" ); +    sbuf_append(eb->extra, hentry + match_pos + match_len );+    sbuf_append(eb->extra, "[/pre][/ic-diminish]");+    if (!env->no_help) {+      sbuf_append(eb->extra, "\n[ic-info](use tab for the next match)[/]");+    }+    sbuf_append(eb->extra, "\n" );+  }+  edit_refresh(env, eb);++  // Wait for input+  code_t c = (hentry == NULL ? KEY_ESC : tty_read(env->tty));+  if (tty_term_resize_event(env->tty)) {+    edit_resize(env, eb);+  }+  sbuf_clear(eb->extra);++  // Process commands+  if (c == KEY_ESC || c == KEY_BELL /* ^G */ || c == KEY_CTRL_C) {+    c = 0;  +    eb->disable_undo = false;+    editor_undo_restore(eb, false);+  } +  else if (c == KEY_ENTER) {+    c = 0;+    editor_undo_forget(eb);+    sbuf_replace( eb->input, hentry );+    eb->pos = sbuf_len(eb->input);+    eb->modified = false;+    eb->history_idx = hidx;+  }  +  else if (c == KEY_BACKSP || c == KEY_CTRL_Z) {+    // undo last search action+    bool cinsert;+    if (hsearch_pop(env->mem,&hs, &hidx, &match_pos, &match_len, &cinsert)) {+      if (cinsert) edit_backspace(env,eb);+    }+    goto again;+  }+  else if (c == KEY_CTRL_R || c == KEY_TAB || c == KEY_UP) {    +    // search backward+    hsearch_push(env->mem, &hs, hidx, match_pos, match_len, false);+    if (!history_search( env->history, hidx+1, sbuf_string(eb->input), true, &hidx, &match_pos )) {+      hsearch_pop(env->mem,&hs,NULL,NULL,NULL,NULL);+      term_beep(env->term);+    };+    goto again;+  }  +  else if (c == KEY_CTRL_S || c == KEY_SHIFT_TAB || c == KEY_DOWN) {    +    // search forward+    hsearch_push(env->mem, &hs, hidx, match_pos, match_len, false);+    if (!history_search( env->history, hidx-1, sbuf_string(eb->input), false, &hidx, &match_pos )) {+      hsearch_pop(env->mem, &hs,NULL,NULL,NULL,NULL);+      term_beep(env->term);+    };+    goto again;+  }+  else if (c == KEY_F1) {+    edit_show_help(env, eb);+    goto again;+  }+  else {+    // insert character and search further backward+    char chr;+    unicode_t uchr;+    if (code_is_ascii_char(c,&chr)) {+      hsearch_push(env->mem, &hs, hidx, match_pos, match_len, true);+      edit_insert_char(env,eb,chr);      +    }+    else if (code_is_unicode(c,&uchr)) {+      hsearch_push(env->mem, &hs, hidx, match_pos, match_len, true);+      edit_insert_unicode(env,eb,uchr);+    }+    else {+      // ignore command+      term_beep(env->term);+      goto again;+    }+    // search for the new input+    if (history_search( env->history, hidx, sbuf_string(eb->input), true, &hidx, &match_pos )) {+      match_len = sbuf_len(eb->input);+    }+    else {+      term_beep(env->term);+    };+    goto again;+  }++  // done+  eb->disable_undo = false;+  hsearch_done(env->mem,hs);+  eb->prompt_text = prompt_text;+  ic_enable_hint(old_hint);+  edit_refresh(env,eb);+  if (c != 0) tty_code_pushback(env->tty, c);+}++// Start an incremental search with the current word +static void edit_history_search_with_current_word(ic_env_t* env, editor_t* eb) {+  char* initial = NULL;+  ssize_t start = sbuf_find_word_start( eb->input, eb->pos );+  if (start >= 0) {+    const ssize_t next = sbuf_next(eb->input, start, NULL);+    if (!ic_char_is_idletter(sbuf_string(eb->input) + start, (long)(next - start))) { +      start = next; +    }+    if (start >= 0 && start < eb->pos) {+      initial = mem_strndup(eb->mem, sbuf_string(eb->input) + start, eb->pos - start);+    }+  }+  edit_history_search( env, eb, initial);+  mem_free(env->mem, initial);+}
+ src/env.h view
@@ -0,0 +1,60 @@+/* ----------------------------------------------------------------------------+  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.+-----------------------------------------------------------------------------*/+#pragma once+#ifndef IC_ENV_H+#define IC_ENV_H++#include "../include/isocline.h"+#include "common.h"+#include "term.h"+#include "tty.h"+#include "stringbuf.h"+#include "history.h"+#include "completions.h"+#include "bbcode.h"++//-------------------------------------------------------------+// Environment+//-------------------------------------------------------------++struct ic_env_s {+  alloc_t*        mem;              // potential custom allocator+  ic_env_t*       next;             // next environment (used for proper deallocation)+  term_t*         term;             // terminal+  tty_t*          tty;              // keyboard (NULL if stdin is a pipe, file, etc)+  completions_t*  completions;      // current completions+  history_t*      history;          // edit history+  bbcode_t*       bbcode;           // print with bbcodes+  const char*     prompt_marker;    // the prompt marker (defaults to "> ")+  const char*     cprompt_marker;   // prompt marker for continuation lines (defaults to `prompt_marker`)+  ic_highlight_fun_t* highlighter;  // highlight callback+  void*           highlighter_arg;  // user state for the highlighter.+  const char*     match_braces;     // matching braces, e.g "()[]{}"+  const char*     auto_braces;      // auto insertion braces, e.g "()[]{}\"\"''"+  char            multiline_eol;    // character used for multiline input ("\") (set to 0 to disable)+  bool            initialized;      // are we initialized?+  bool            noedit;           // is rich editing possible (tty != NULL)+  bool            singleline_only;  // allow only single line editing?+  bool            complete_nopreview; // do not show completion preview for each selection in the completion menu?+  bool            complete_autotab; // try to keep completing after a completion?+  bool            no_multiline_indent; // indent continuation lines to line up under the initial prompt +  bool            no_help;          // show short help line for history search etc.+  bool            no_hint;          // allow hinting?+  bool            no_highlight;     // enable highlighting?+  bool            no_bracematch;    // enable brace matching?+  bool            no_autobrace;     // enable automatic brace insertion?+  bool            no_lscolors;      // use LSCOLORS/LS_COLORS to colorize file name completions?+  long            hint_delay;       // delay before displaying a hint in milliseconds+};++ic_private char*        ic_editline(ic_env_t* env, const char* prompt_text);++ic_private ic_env_t*    ic_get_env(void);+ic_private const char*  ic_env_get_auto_braces(ic_env_t* env);+ic_private const char*  ic_env_get_match_braces(ic_env_t* env);++#endif // IC_ENV_H
+ src/highlight.c view
@@ -0,0 +1,259 @@+/* ----------------------------------------------------------------------------+  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.+-----------------------------------------------------------------------------*/++#include <string.h>+#include "common.h"+#include "term.h"+#include "stringbuf.h"+#include "attr.h"+#include "bbcode.h"++//-------------------------------------------------------------+// Syntax highlighting+//-------------------------------------------------------------++struct ic_highlight_env_s {+  attrbuf_t*    attrs;+  const char*   input;   +  ssize_t       input_len;     +  bbcode_t*     bbcode;+  alloc_t*      mem;+  ssize_t       cached_upos;  // cached unicode position+  ssize_t       cached_cpos;  // corresponding utf-8 byte position+};+++ic_private void highlight( alloc_t* mem, bbcode_t* bb, const char* s, attrbuf_t* attrs, ic_highlight_fun_t* highlighter, void* arg ) {+  const ssize_t len = ic_strlen(s);+  if (len <= 0) return;+  attrbuf_set_at(attrs,0,len,attr_none()); // fill to length of s+  if (highlighter != NULL) {+    ic_highlight_env_t henv;+    henv.attrs = attrs;+    henv.input = s;     +    henv.input_len = len;+    henv.bbcode = bb;+    henv.mem = mem;+    henv.cached_cpos = 0;+    henv.cached_upos = 0;+    (*highlighter)( &henv, s, arg );    +  }+}+++//-------------------------------------------------------------+// Client interface+//-------------------------------------------------------------++static void pos_adjust( ic_highlight_env_t* henv, ssize_t* ppos, ssize_t* plen ) {+  ssize_t pos = *ppos;+  ssize_t len = *plen;+  if (pos >= henv->input_len) return;+  if (pos >= 0 && len >= 0) return;   // already character positions+  if (henv->input == NULL) return;++  if (pos < 0) {+    // negative `pos` is used as the unicode character position (for easy interfacing with Haskell)+    ssize_t upos = -pos;+    ssize_t cpos = 0;+    ssize_t ucount = 0;+    if (henv->cached_upos <= upos) {  // if we have a cached position, start from there+      ucount = henv->cached_upos;+      cpos = henv->cached_cpos;+    }+    while ( ucount < upos ) {+      ssize_t next = str_next_ofs(henv->input, henv->input_len, cpos, NULL);+      if (next <= 0) return;+      ucount++;+      cpos += next;+    }+    *ppos = pos = cpos;+    // and cache it to avoid quadratic behavior+    henv->cached_upos = upos;+    henv->cached_cpos = cpos;+  }+  if (len < 0) {+    // negative `len` is used as a unicode character length+    len = -len;+    ssize_t ucount = 0;+    ssize_t clen   = 0;    +    while (ucount < len) {+      ssize_t next = str_next_ofs(henv->input, henv->input_len, pos + clen, NULL);+      if (next <= 0) return;+      ucount++;+      clen += next;+    }+    *plen = len = clen;+    // and update cache if possible+    if (henv->cached_cpos == pos) {+      henv->cached_upos += ucount;+      henv->cached_cpos += clen;+    }+  } +}++static void highlight_attr(ic_highlight_env_t* henv, ssize_t pos, ssize_t count, attr_t attr ) {+  if (henv==NULL) return;+  pos_adjust(henv,&pos,&count);+  if (pos < 0 || count <= 0) return;+  attrbuf_update_at(henv->attrs, pos, count, attr);+}++ic_public void ic_highlight(ic_highlight_env_t* henv, long pos, long count, const char* style ) {+  if (henv == NULL || style==NULL || style[0]==0 || pos < 0) return;  +  highlight_attr(henv,pos,count,bbcode_style( henv->bbcode, style ));+}++ic_public void ic_highlight_formatted(ic_highlight_env_t* henv, const char* s, const char* fmt) {+  if (s==NULL || s[0] == 0 || fmt==NULL) return;+  attrbuf_t* attrs = attrbuf_new(henv->mem);+  stringbuf_t* out = sbuf_new(henv->mem);  // todo: avoid allocating out?+  if (attrs!=NULL && out != NULL) {+    bbcode_append( henv->bbcode, fmt, out, attrs);+    const ssize_t len = ic_strlen(s);+    if (sbuf_len(out) != len) {+      debug_msg("highlight: formatted string content differs from the original input:\n  original: %s\n  formatted: %s\n", s, fmt);+    }+    for( ssize_t i = 0; i < len; i++) {+      attrbuf_update_at(henv->attrs, i, 1, attrbuf_attr_at(attrs,i));+    }+  }+  sbuf_free(out);+  attrbuf_free(attrs);+}++//-------------------------------------------------------------+// Brace matching+//-------------------------------------------------------------+#define MAX_NESTING (64)++typedef struct brace_s {+  char    close;+  bool    at_cursor;+  ssize_t pos;+} brace_t;++ic_private void highlight_match_braces(const char* s, attrbuf_t* attrs, ssize_t cursor_pos, const char* braces, attr_t match_attr, attr_t error_attr) +{+  brace_t open[MAX_NESTING+1];+  ssize_t nesting = 0;+  const ssize_t brace_len = ic_strlen(braces);+  for (long i = 0; i < ic_strlen(s); i++) {+    const char c = s[i];+    // push open brace+    bool found_open = false;+    for (ssize_t b = 0; b < brace_len; b += 2) {+      if (c == braces[b]) {+        // open brace+        if (nesting >= MAX_NESTING) return; // give up+        open[nesting].close = braces[b+1];+        open[nesting].pos = i;+        open[nesting].at_cursor = (i == cursor_pos - 1);+        nesting++;+        found_open = true;+        break;+      }+    }+    if (found_open) continue;++    // pop to closing brace and potentially highlight+    for (ssize_t b = 1; b < brace_len; b += 2) {+      if (c == braces[b]) {+        // close brace+        if (nesting <= 0) {+          // unmatched close brace+          attrbuf_update_at( attrs, i, 1, error_attr);+        }+        else {+          // can we fix an unmatched brace where we can match by popping just one?+          if (open[nesting-1].close != c && nesting > 1 && open[nesting-2].close == c) {+            // assume previous open brace was wrong+            attrbuf_update_at(attrs, open[nesting-1].pos, 1, error_attr);+            nesting--;+          }+          if (open[nesting-1].close != c) {+            // unmatched open brace+            attrbuf_update_at( attrs, i, 1, error_attr);+          }+          else {+            // matching brace+            nesting--;+            if (i == cursor_pos - 1 || (open[nesting].at_cursor && open[nesting].pos != i - 1)) {+              // highlight matching brace+              attrbuf_update_at(attrs, open[nesting].pos, 1, match_attr);+              attrbuf_update_at(attrs, i, 1, match_attr);              +            }+          }+        }+        break;+      }+    }+  }+  // note: don't mark further unmatched open braces as in error+}+++ic_private ssize_t find_matching_brace(const char* s, ssize_t cursor_pos, const char* braces, bool* is_balanced) +{+  if (is_balanced != NULL) { *is_balanced = false; }+  bool balanced = true;+  ssize_t match = -1;+  brace_t open[MAX_NESTING+1];+  ssize_t nesting = 0;+  const ssize_t brace_len = ic_strlen(braces);+  for (long i = 0; i < ic_strlen(s); i++) {+    const char c = s[i];+    // push open brace+    bool found_open = false;+    for (ssize_t b = 0; b < brace_len; b += 2) {+      if (c == braces[b]) {+        // open brace+        if (nesting >= MAX_NESTING) return -1; // give up+        open[nesting].close = braces[b+1];+        open[nesting].pos = i;+        open[nesting].at_cursor = (i == cursor_pos - 1);+        nesting++;+        found_open = true;+        break;+      }+    }+    if (found_open) continue;++    // pop to closing brace +    for (ssize_t b = 1; b < brace_len; b += 2) {+      if (c == braces[b]) {+        // close brace+        if (nesting <= 0) {+          // unmatched close brace+          balanced = false;          +        }+        else {+          if (open[nesting-1].close != c) {+            // unmatched open brace+            balanced = false;+          }+          else {+            // matching brace+            nesting--;+            if (i == cursor_pos - 1) {+              // found matching open brace+              match = open[nesting].pos + 1;+            }+            else if (open[nesting].at_cursor) {+              // found matching close brace+              match = i + 1;+            }+          }+        }+        break; +      }+    }+  }+  if (nesting != 0) { balanced = false; }+  if (is_balanced != NULL) { *is_balanced = balanced; }+  return match;+}
+ src/highlight.h view
@@ -0,0 +1,24 @@+/* ----------------------------------------------------------------------------+  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.+-----------------------------------------------------------------------------*/+#pragma once+#ifndef IC_HIGHLIGHT_H+#define IC_HIGHLIGHT_H++#include "common.h"+#include "attr.h"+#include "term.h"+#include "bbcode.h"++//-------------------------------------------------------------+// Syntax highlighting+//-------------------------------------------------------------++ic_private void highlight( alloc_t* mem, bbcode_t* bb, const char* s, attrbuf_t* attrs, ic_highlight_fun_t* highlighter, void* arg );+ic_private void highlight_match_braces(const char* s, attrbuf_t* attrs, ssize_t cursor_pos, const char* braces, attr_t match_attr, attr_t error_attr);+ic_private ssize_t find_matching_brace(const char* s, ssize_t cursor_pos, const char* braces, bool* is_balanced);++#endif // IC_HIGHLIGHT_H
+ src/history.c view
@@ -0,0 +1,269 @@+/* ----------------------------------------------------------------------------+  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.+-----------------------------------------------------------------------------*/+#include <stdio.h>+#include <string.h>  +#include <sys/stat.h>++#include "../include/isocline.h"+#include "common.h"+#include "history.h"+#include "stringbuf.h"++#define IC_MAX_HISTORY (200)++struct history_s {+  ssize_t  count;              // current number of entries in use+  ssize_t  len;                // size of elems +  const char** elems;         // history items (up to count)+  const char*  fname;         // history file+  alloc_t* mem;+  bool     allow_duplicates;   // allow duplicate entries?+};++ic_private history_t* history_new(alloc_t* mem) {+  history_t* h = mem_zalloc_tp(mem,history_t);+  h->mem = mem;+  return h;+}++ic_private void history_free(history_t* h) {+  if (h == NULL) return;+  history_clear(h);+  if (h->len > 0) {+    mem_free( h->mem, h->elems );+    h->elems = NULL;+    h->len = 0;+  }+  mem_free(h->mem, h->fname);+  h->fname = NULL;+  mem_free(h->mem, h); // free ourselves+}++ic_private bool history_enable_duplicates( history_t* h, bool enable ) {+  bool prev = h->allow_duplicates;+  h->allow_duplicates = enable;+  return prev;+}++ic_private ssize_t  history_count(const history_t* h) {+  return h->count;+}++//-------------------------------------------------------------+// push/clear+//-------------------------------------------------------------++ic_private bool history_update( history_t* h, const char* entry ) {+  if (entry==NULL) return false;+  history_remove_last(h);+  history_push(h,entry);+  //debug_msg("history: update: with %s; now at %s\n", entry, history_get(h,0));+  return true;+}++static void history_delete_at( history_t* h, ssize_t idx ) {+  if (idx < 0 || idx >= h->count) return;+  mem_free(h->mem, h->elems[idx]);+  for(ssize_t i = idx+1; i < h->count; i++) {+    h->elems[i-1] = h->elems[i];+  }+  h->count--;+}++ic_private bool history_push( history_t* h, const char* entry ) {+  if (h->len <= 0 || entry==NULL)  return false;+  // remove any older duplicate+  if (!h->allow_duplicates) {+    for( int i = 0; i < h->count; i++) {+      if (strcmp(h->elems[i],entry) == 0) {+        history_delete_at(h,i);+      }+    }+  }+  // insert at front+  if (h->count == h->len) {+    // delete oldest entry+    history_delete_at(h,0);    +  }+  assert(h->count < h->len);+  h->elems[h->count] = mem_strdup(h->mem,entry);+  h->count++;+  return true;+}+++static void history_remove_last_n( history_t* h, ssize_t n ) {+  if (n <= 0) return;+  if (n > h->count) n = h->count;+  for( ssize_t i = h->count - n; i < h->count; i++) {+    mem_free( h->mem, h->elems[i] );+  }+  h->count -= n;+  assert(h->count >= 0);    +}++ic_private void history_remove_last(history_t* h) {+  history_remove_last_n(h,1);+}++ic_private void history_clear(history_t* h) {+  history_remove_last_n( h, h->count );+}++ic_private const char* history_get( const history_t* h, ssize_t n ) {+  if (n < 0 || n >= h->count) return NULL;+  return h->elems[h->count - n - 1];+}++ic_private bool history_search( const history_t* h, ssize_t from /*including*/, const char* search, bool backward, ssize_t* hidx, ssize_t* hpos ) {+  const char* p = NULL;+  ssize_t i;+  if (backward) {+    for( i = from; i < h->count; i++ ) {+      p = strstr( history_get(h,i), search);+      if (p != NULL) break;+    }+  }+  else {+    for( i = from; i >= 0; i-- ) {+      p = strstr( history_get(h,i), search);+      if (p != NULL) break;+    }+  }+  if (p == NULL) return false;+  if (hidx != NULL) *hidx = i;+  if (hpos != NULL) *hpos = (p - history_get(h,i));+  return true;+}++//-------------------------------------------------------------+// +//-------------------------------------------------------------++ic_private void history_load_from(history_t* h, const char* fname, long max_entries ) {+  history_clear(h);+  h->fname = mem_strdup(h->mem,fname);+  if (max_entries == 0) {+    assert(h->elems == NULL);+    return;+  }+  if (max_entries < 0 || max_entries > IC_MAX_HISTORY) max_entries = IC_MAX_HISTORY;+  h->elems = (const char**)mem_zalloc_tp_n(h->mem, char*, max_entries );+  if (h->elems == NULL) return;+  h->len = max_entries;+  history_load(h);+}+++++//-------------------------------------------------------------+// save/load history to file+//-------------------------------------------------------------++static char from_xdigit( int c ) {+  if (c >= '0' && c <= '9') return (char)(c - '0');+  if (c >= 'A' && c <= 'F') return (char)(10 + (c - 'A'));+  if (c >= 'a' && c <= 'f') return (char)(10 + (c - 'a'));+  return 0;+}++static char to_xdigit( uint8_t c ) {+  if (c <= 9) return ((char)c + '0');+  if (c >= 10 && c <= 15) return ((char)c - 10 + 'A');+  return '0';+}++static bool ic_isxdigit( int c ) {+  return ((c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F') || (c >= '0' && c <= '9'));+}++static bool history_read_entry( history_t* h, FILE* f, stringbuf_t* sbuf ) {+  sbuf_clear(sbuf);+  while( !feof(f)) {+    int c = fgetc(f);+    if (c == EOF || c == '\n') break;+    if (c == '\\') {+      c = fgetc(f);+      if (c == 'n')       sbuf_append(sbuf,"\n");+      else if (c == 'r')  sbuf_append(sbuf,"\r");+      else if (c == 't')  sbuf_append(sbuf,"\t");+      else if (c == '\\') sbuf_append(sbuf,"\\");+      else if (c == 'x') {+        int c1 = fgetc(f);         +        int c2 = fgetc(f);+        if (ic_isxdigit(c1) && ic_isxdigit(c2)) {+          char chr = from_xdigit(c1)*16 + from_xdigit(c2);+          sbuf_append_char(sbuf,chr);+        }+        else return false;+      }+      else return false;+    }+    else sbuf_append_char(sbuf,(char)c);+  }+  if (sbuf_len(sbuf)==0 || sbuf_string(sbuf)[0] == '#') return true;+  return history_push(h, sbuf_string(sbuf));+}++static bool history_write_entry( const char* entry, FILE* f, stringbuf_t* sbuf ) {+  sbuf_clear(sbuf);+  //debug_msg("history: write: %s\n", entry);+  while( entry != NULL && *entry != 0 ) {+    char c = *entry++;+    if (c == '\\')      { sbuf_append(sbuf,"\\\\"); }+    else if (c == '\n') { sbuf_append(sbuf,"\\n"); }+    else if (c == '\r') { sbuf_append(sbuf,"\\r"); }+    else if (c == '\t') { sbuf_append(sbuf,"\\t"); }+    else if (c < ' ' || c > '~' || c == '#') {+      char c1 = to_xdigit( (uint8_t)c / 16 );+      char c2 = to_xdigit( (uint8_t)c % 16 );+      sbuf_append(sbuf,"\\x"); +      sbuf_append_char(sbuf,c1); +      sbuf_append_char(sbuf,c2);            +    }+    else sbuf_append_char(sbuf,c);+  }+  //debug_msg("history: write buf: %s\n", sbuf_string(sbuf));+  +  if (sbuf_len(sbuf) > 0) {+    sbuf_append(sbuf,"\n");+    fputs(sbuf_string(sbuf),f);+  }+  return true;+}++ic_private void history_load( history_t* h ) {+  if (h->fname == NULL) return;+  FILE* f = fopen(h->fname, "r");+  if (f == NULL) return;+  stringbuf_t* sbuf = sbuf_new(h->mem);+  if (sbuf != NULL) {+    while (!feof(f)) {+      if (!history_read_entry(h,f,sbuf)) break; // error+    }+    sbuf_free(sbuf);+  }+  fclose(f);+}++ic_private void history_save( const history_t* h ) {+  if (h->fname == NULL) return;+  FILE* f = fopen(h->fname, "w");+  if (f == NULL) return;+  #ifndef _WIN32+  chmod(h->fname,S_IRUSR|S_IWUSR);+  #endif+  stringbuf_t* sbuf = sbuf_new(h->mem);+  if (sbuf != NULL) {+    for( int i = 0; i < h->count; i++ )  {+      if (!history_write_entry(h->elems[i],f,sbuf)) break;  // error+    }+    sbuf_free(sbuf);+  }+  fclose(f);  +}
+ src/history.h view
@@ -0,0 +1,38 @@+/* ----------------------------------------------------------------------------+  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.+-----------------------------------------------------------------------------*/+#pragma once+#ifndef IC_HISTORY_H+#define IC_HISTORY_H++#include "common.h"++//-------------------------------------------------------------+// History+//-------------------------------------------------------------++struct history_s;+typedef struct history_s history_t;++ic_private history_t* history_new(alloc_t* mem);+ic_private void     history_free(history_t* h);+ic_private void     history_clear(history_t* h);+ic_private bool     history_enable_duplicates( history_t* h, bool enable );+ic_private ssize_t  history_count(const history_t* h);++ic_private void     history_load_from(history_t* h, const char* fname, long max_entries);+ic_private void     history_load( history_t* h );+ic_private void     history_save( const history_t* h );++ic_private bool     history_push( history_t* h, const char* entry );+ic_private bool     history_update( history_t* h, const char* entry );+ic_private const char* history_get( const history_t* h, ssize_t n );+ic_private void     history_remove_last(history_t* h);++ic_private bool     history_search( const history_t* h, ssize_t from, const char* search, bool backward, ssize_t* hidx, ssize_t* hpos);+++#endif // IC_HISTORY_H
+ src/isocline.c view
@@ -0,0 +1,589 @@+/* ----------------------------------------------------------------------------+  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.+-----------------------------------------------------------------------------*/++//-------------------------------------------------------------+// Usually we include all sources one file so no internal +// symbols are public in the libray.+// +// You can compile the entire library just as: +// $ gcc -c src/isocline.c+//-------------------------------------------------------------+#if !defined(IC_SEPARATE_OBJS)+# define _CRT_SECURE_NO_WARNINGS  // for msvc+# define _XOPEN_SOURCE   700      // for wcwidth+# define _DEFAULT_SOURCE          // ensure usleep stays visible with _XOPEN_SOURCE >= 700+# include "attr.c"+# include "bbcode.c"+# include "editline.c"+# include "highlight.c"+# include "undo.c"+# include "history.c"+# include "completers.c"+# include "completions.c"+# include "term.c"+# include "tty_esc.c"+# include "tty.c"+# include "stringbuf.c"+# include "common.c"+#endif++//-------------------------------------------------------------+// includes+//-------------------------------------------------------------+#include <stdlib.h>+#include <stdio.h>+#include <string.h>+#include <stdarg.h>++#include "../include/isocline.h"+#include "common.h"+#include "env.h"+++//-------------------------------------------------------------+// Readline+//-------------------------------------------------------------++static char*  ic_getline( alloc_t* mem );++ic_public char* ic_readline(const char* prompt_text) +{+  ic_env_t* env = ic_get_env();+  if (env == NULL) return NULL;+  if (!env->noedit) {+    // terminal editing enabled+    return ic_editline(env, prompt_text);   // in editline.c+  } +  else {+    // no editing capability (pipe, dumb terminal, etc)+    if (env->tty != NULL && env->term != NULL) {+      // if the terminal is not interactive, but we are reading from the tty (keyboard), we display a prompt+      term_start_raw(env->term);  // set utf8 mode on windows+      if (prompt_text != NULL) {+        term_write(env->term, prompt_text);+      }+      term_write(env->term, env->prompt_marker);    +      term_end_raw(env->term, false);+    }+    // read directly from stdin+    return ic_getline(env->mem);+  }+}+++//-------------------------------------------------------------+// Read a line from the stdin stream if there is no editing +// support (like from a pipe, file, or dumb terminal).+//-------------------------------------------------------------++static char* ic_getline(alloc_t* mem)+{  +  // read until eof or newline+  stringbuf_t* sb = sbuf_new(mem);+  int c;+  while (true) {+    c = fgetc(stdin);+    if (c==EOF || c=='\n') {+      break;+    }+    else {+      sbuf_append_char(sb, (char)c);+    }+  }+  return sbuf_free_dup(sb);+}+++//-------------------------------------------------------------+// Formatted output+//-------------------------------------------------------------+++ic_public void ic_printf(const char* fmt, ...) {+  va_list ap;+  va_start(ap, fmt);+  ic_vprintf(fmt, ap);+  va_end(ap);+}++ic_public void ic_vprintf(const char* fmt, va_list args) {+  ic_env_t* env = ic_get_env(); if (env==NULL || env->bbcode == NULL) return;+  bbcode_vprintf(env->bbcode, fmt, args);+}++ic_public void ic_print(const char* s) {+  ic_env_t* env = ic_get_env(); if (env==NULL || env->bbcode==NULL) return;+  bbcode_print(env->bbcode, s);+}++ic_public void ic_println(const char* s) {+  ic_env_t* env = ic_get_env(); if (env==NULL || env->bbcode==NULL) return;+  bbcode_println(env->bbcode, s);+}++void ic_style_def(const char* name, const char* fmt) {+  ic_env_t* env = ic_get_env(); if (env==NULL || env->bbcode==NULL) return;+  bbcode_style_def(env->bbcode, name, fmt);+}++void ic_style_open(const char* fmt) {+  ic_env_t* env = ic_get_env(); if (env==NULL || env->bbcode==NULL) return;+  bbcode_style_open(env->bbcode, fmt);+}++void ic_style_close(void) {+  ic_env_t* env = ic_get_env(); if (env==NULL || env->bbcode==NULL) return;+  bbcode_style_close(env->bbcode, NULL);+}+++//-------------------------------------------------------------+// Interface+//-------------------------------------------------------------++ic_public bool ic_async_stop(void) {+  ic_env_t* env = ic_get_env(); if (env==NULL) return false;+  if (env->tty==NULL) return false;+  return tty_async_stop(env->tty);+}++static void set_prompt_marker(ic_env_t* env, const char* prompt_marker, const char* cprompt_marker) {+  if (prompt_marker == NULL) prompt_marker = "> ";+  if (cprompt_marker == NULL) cprompt_marker = prompt_marker;+  mem_free(env->mem, env->prompt_marker);+  mem_free(env->mem, env->cprompt_marker);+  env->prompt_marker = mem_strdup(env->mem, prompt_marker);+  env->cprompt_marker = mem_strdup(env->mem, cprompt_marker);+}++ic_public const char* ic_get_prompt_marker(void) {+  ic_env_t* env = ic_get_env(); if (env==NULL) return NULL;+  return env->prompt_marker;+}++ic_public const char* ic_get_continuation_prompt_marker(void) {+  ic_env_t* env = ic_get_env(); if (env==NULL) return NULL;+  return env->cprompt_marker;+}++ic_public void ic_set_prompt_marker( const char* prompt_marker, const char* cprompt_marker ) {+  ic_env_t* env = ic_get_env(); if (env==NULL) return;+  set_prompt_marker(env, prompt_marker, cprompt_marker);+}++ic_public bool ic_enable_multiline( bool enable ) {+  ic_env_t* env = ic_get_env(); if (env==NULL) return false;+  bool prev = env->singleline_only;+  env->singleline_only = !enable;+  return !prev;+}++ic_public bool ic_enable_beep( bool enable ) {+  ic_env_t* env = ic_get_env(); if (env==NULL) return false;+  return term_enable_beep(env->term, enable);+}++ic_public bool ic_enable_color( bool enable ) {+  ic_env_t* env = ic_get_env(); if (env==NULL) return false;+  return term_enable_color( env->term, enable );+}++ic_public bool ic_enable_history_duplicates( bool enable ) {+  ic_env_t* env = ic_get_env(); if (env==NULL) return false;+  return history_enable_duplicates(env->history, enable);+}++ic_public void ic_set_history(const char* fname, long max_entries ) {+  ic_env_t* env = ic_get_env(); if (env==NULL) return;+  history_load_from(env->history, fname, max_entries );+}++ic_public void ic_history_remove_last(void) {+  ic_env_t* env = ic_get_env(); if (env==NULL) return;+  history_remove_last(env->history);+}++ic_public void ic_history_add( const char* entry ) {+  ic_env_t* env = ic_get_env(); if (env==NULL) return;+  history_push( env->history, entry );+}++ic_public void ic_history_clear(void) {+  ic_env_t* env = ic_get_env(); if (env==NULL) return;+  history_clear(env->history);+}++ic_public bool ic_enable_auto_tab( bool enable ) {+  ic_env_t* env = ic_get_env(); if (env==NULL) return false;+  bool prev = env->complete_autotab;+  env->complete_autotab = enable;+  return prev;+}++ic_public bool ic_enable_completion_preview( bool enable ) {+  ic_env_t* env = ic_get_env(); if (env==NULL) return false;+  bool prev = env->complete_nopreview;+  env->complete_nopreview = !enable;+  return !prev;+}++ic_public bool ic_enable_multiline_indent(bool enable) {+  ic_env_t* env = ic_get_env(); if (env==NULL) return false;+  bool prev = env->no_multiline_indent;+  env->no_multiline_indent = !enable;+  return !prev;+}++ic_public bool ic_enable_hint(bool enable) {+  ic_env_t* env = ic_get_env(); if (env==NULL) return false;+  bool prev = env->no_hint;+  env->no_hint = !enable;+  return !prev;+}++ic_public long ic_set_hint_delay(long delay_ms) {+  ic_env_t* env = ic_get_env(); if (env==NULL) return false;+  long prev = env->hint_delay;+  env->hint_delay = (delay_ms < 0 ? 0 : (delay_ms > 5000 ? 5000 : delay_ms));+  return prev;+}++ic_public void ic_set_tty_esc_delay(long initial_delay_ms, long followup_delay_ms ) {+  ic_env_t* env = ic_get_env(); if (env==NULL) return;+  if (env->tty == NULL) return;+  tty_set_esc_delay(env->tty, initial_delay_ms, followup_delay_ms);+}+++ic_public bool ic_enable_highlight(bool enable) {+  ic_env_t* env = ic_get_env(); if (env==NULL) return false;+  bool prev = env->no_highlight;+  env->no_highlight = !enable;+  return !prev;+}++ic_public bool ic_enable_inline_help(bool enable) {+  ic_env_t* env = ic_get_env(); if (env==NULL) return false;+  bool prev = env->no_help;+  env->no_help = !enable;+  return !prev;+}++ic_public bool ic_enable_brace_matching(bool enable) {+  ic_env_t* env = ic_get_env(); if (env==NULL) return false;+  bool prev = env->no_bracematch;+  env->no_bracematch = !enable;+  return !prev;+}++ic_public void ic_set_matching_braces(const char* brace_pairs) {+  ic_env_t* env = ic_get_env(); if (env==NULL) return;+  mem_free(env->mem, env->match_braces);+  env->match_braces = NULL;+  if (brace_pairs != NULL) {+    ssize_t len = ic_strlen(brace_pairs);+    if (len > 0 && (len % 2) == 0) {+      env->match_braces = mem_strdup(env->mem, brace_pairs);+    }+  }+}++ic_public bool ic_enable_brace_insertion(bool enable) {+  ic_env_t* env = ic_get_env(); if (env==NULL) return false;+  bool prev = env->no_autobrace;+  env->no_autobrace = !enable;+  return !prev;+}++ic_public void ic_set_insertion_braces(const char* brace_pairs) {+  ic_env_t* env = ic_get_env(); if (env==NULL) return;+  mem_free(env->mem, env->auto_braces);+  env->auto_braces = NULL;+  if (brace_pairs != NULL) {+    ssize_t len = ic_strlen(brace_pairs);+    if (len > 0 && (len % 2) == 0) { +      env->auto_braces = mem_strdup(env->mem, brace_pairs);+    }+  }+}++ic_private const char* ic_env_get_match_braces(ic_env_t* env) {+  return (env->match_braces == NULL ? "()[]{}" : env->match_braces);+}++ic_private const char* ic_env_get_auto_braces(ic_env_t* env) {+  return (env->auto_braces == NULL ? "()[]{}\"\"''" : env->auto_braces);+}++ic_public void ic_set_default_highlighter(ic_highlight_fun_t* highlighter, void* arg) {+  ic_env_t* env = ic_get_env(); if (env==NULL) return;+  env->highlighter = highlighter;+  env->highlighter_arg = arg;+}+++ic_public void ic_free( void* p ) {+  ic_env_t* env = ic_get_env(); if (env==NULL) return;+  mem_free(env->mem, p);+}++ic_public void* ic_malloc(size_t sz) {+  ic_env_t* env = ic_get_env(); if (env==NULL) return NULL;+  return mem_malloc(env->mem, to_ssize_t(sz));+}++ic_public const char* ic_strdup( const char* s ) {+  if (s==NULL) return NULL;+  ic_env_t* env = ic_get_env(); if (env==NULL) return NULL;+  ssize_t len = ic_strlen(s);+  char* p = mem_malloc_tp_n( env->mem, char, len + 1 );+  if (p == NULL) return NULL;+  ic_memcpy( p, s, len );+  p[len] = 0;+  return p;+}++//-------------------------------------------------------------+// Terminal+//-------------------------------------------------------------++ic_public void ic_term_init(void) {+  ic_env_t* env = ic_get_env(); if (env==NULL) return;+  if (env->term==NULL) return;+  term_start_raw(env->term);+}++ic_public void ic_term_done(void) {+  ic_env_t* env = ic_get_env(); if (env==NULL) return;+  if (env->term==NULL) return;+  term_end_raw(env->term,false);+}++ic_public void ic_term_flush(void) {+  ic_env_t* env = ic_get_env(); if (env==NULL) return;+  if (env->term==NULL) return;+  term_flush(env->term);+}++ic_public void ic_term_write(const char* s) {+  ic_env_t* env = ic_get_env(); if (env==NULL) return;+  if (env->term == NULL) return;+  term_write(env->term, s);+}++ic_public void ic_term_writeln(const char* s) {+  ic_env_t* env = ic_get_env(); if (env==NULL) return;+  if (env->term == NULL) return;+  term_writeln(env->term, s);+}++ic_public void ic_term_writef(const char* fmt, ...) {+  va_list ap;+  va_start(ap, fmt);+  ic_term_vwritef(fmt, ap);+  va_end(ap);+}++ic_public void ic_term_vwritef(const char* fmt, va_list args) {+  ic_env_t* env = ic_get_env(); if (env==NULL) return;+  if (env->term == NULL) return;+  term_vwritef(env->term, fmt, args);+}++ic_public void ic_term_reset( void )  {+  ic_env_t* env = ic_get_env(); if (env==NULL) return;+  if (env->term == NULL) return;+  term_attr_reset(env->term);+}++ic_public void ic_term_style( const char* style ) {+  ic_env_t* env = ic_get_env(); if (env==NULL) return;+  if (env->term == NULL || env->bbcode == NULL) return;+  term_set_attr( env->term, bbcode_style(env->bbcode, style));+}++ic_public int ic_term_get_color_bits(void) {+  ic_env_t* env = ic_get_env(); +  if (env==NULL || env->term==NULL) return 4;  +  return term_get_color_bits(env->term);+}++ic_public void ic_term_bold(bool enable) {+  ic_env_t* env = ic_get_env(); if (env==NULL || env->term==NULL) return;+  term_bold(env->term, enable);+}++ic_public void ic_term_underline(bool enable) {+  ic_env_t* env = ic_get_env(); if (env==NULL || env->term==NULL) return;+  term_underline(env->term, enable);+}++ic_public void ic_term_italic(bool enable) {+  ic_env_t* env = ic_get_env(); if (env==NULL || env->term==NULL) return;+  term_italic(env->term, enable);+}++ic_public void ic_term_reverse(bool enable) {+  ic_env_t* env = ic_get_env(); if (env==NULL || env->term==NULL) return;+  term_reverse(env->term, enable);+}++ic_public void ic_term_color_ansi(bool foreground, int ansi_color) {+  ic_env_t* env = ic_get_env(); if (env==NULL || env->term==NULL) return;+  ic_color_t color = color_from_ansi256(ansi_color);+  if (foreground) { term_color(env->term, color); }+             else { term_bgcolor(env->term, color); }+}++ic_public void ic_term_color_rgb(bool foreground, uint32_t hcolor) {+  ic_env_t* env = ic_get_env(); if (env==NULL || env->term==NULL) return;+  ic_color_t color = ic_rgb(hcolor);+  if (foreground) { term_color(env->term, color); }+             else { term_bgcolor(env->term, color); }+}+++//-------------------------------------------------------------+// Readline with temporary completer and highlighter+//-------------------------------------------------------------++ic_public char* ic_readline_ex(const char* prompt_text,+                                ic_completer_fun_t* completer, void* completer_arg,+                                 ic_highlight_fun_t* highlighter, void* highlighter_arg )+{+  ic_env_t* env = ic_get_env(); if (env == NULL) return NULL;+  // save previous+  ic_completer_fun_t* prev_completer;+  void* prev_completer_arg;+  completions_get_completer(env->completions, &prev_completer, &prev_completer_arg);+  ic_highlight_fun_t* prev_highlighter = env->highlighter;+  void* prev_highlighter_arg = env->highlighter_arg;+  // call with current+  if (completer != NULL)   { ic_set_default_completer(completer, completer_arg); }+  if (highlighter != NULL) { ic_set_default_highlighter(highlighter, highlighter_arg); }+  char* res = ic_readline(prompt_text);+  // restore previous+  ic_set_default_completer(prev_completer, prev_completer_arg);+  ic_set_default_highlighter(prev_highlighter, prev_highlighter_arg);+  return res;+}+++//-------------------------------------------------------------+// Initialize+//-------------------------------------------------------------++static void ic_atexit(void);++static void ic_env_free(ic_env_t* env) {+  if (env == NULL) return;+  history_save(env->history);+  history_free(env->history);+  completions_free(env->completions);+  bbcode_free(env->bbcode);+  term_free(env->term);+  tty_free(env->tty);+  mem_free(env->mem, env->cprompt_marker);+  mem_free(env->mem,env->prompt_marker);+  mem_free(env->mem, env->match_braces);+  mem_free(env->mem, env->auto_braces);+  env->prompt_marker = NULL;+  +  // and deallocate ourselves+  alloc_t* mem = env->mem;  +  mem_free(mem, env);++  // and finally the custom memory allocation structure+  mem_free(mem, mem);+}+++static ic_env_t* ic_env_create( ic_malloc_fun_t* _malloc, ic_realloc_fun_t* _realloc, ic_free_fun_t* _free )  +{+  if (_malloc == NULL)  _malloc = &malloc;+  if (_realloc == NULL) _realloc = &realloc;+  if (_free == NULL)    _free = &free;+  // allocate+  alloc_t* mem = (alloc_t*)_malloc(sizeof(alloc_t));+  if (mem == NULL) return NULL;+  mem->malloc = _malloc;+  mem->realloc = _realloc;+  mem->free = _free;+  ic_env_t* env = mem_zalloc_tp(mem, ic_env_t);+  if (env==NULL) {+    mem->free(mem);+    return NULL;+  }+  env->mem = mem;++  // Initialize+  env->tty         = tty_new(env->mem, -1);  // can return NULL+  env->term        = term_new(env->mem, env->tty, false, false, -1 );  +  env->history     = history_new(env->mem);+  env->completions = completions_new(env->mem);+  env->bbcode      = bbcode_new(env->mem, env->term);+  env->hint_delay  = 400;   +  +  if (env->tty == NULL || env->term==NULL ||+      env->completions == NULL || env->history == NULL || env->bbcode == NULL ||+      !term_is_interactive(env->term)) +  {+    env->noedit = true;+  }+  env->multiline_eol = '\\';+  +  bbcode_style_def(env->bbcode, "ic-prompt",    "ansi-green" );+  bbcode_style_def(env->bbcode, "ic-info",      "ansi-darkgray" );+  bbcode_style_def(env->bbcode, "ic-diminish",  "ansi-lightgray" );+  bbcode_style_def(env->bbcode, "ic-emphasis",  "color=#FFFFD7" );+  bbcode_style_def(env->bbcode, "ic-hint",      "ansi-darkgray" );+  bbcode_style_def(env->bbcode, "ic-error",     "color=#D70000" );+  bbcode_style_def(env->bbcode, "ic-bracematch","color=#F7DC6F" );++  bbcode_style_def(env->bbcode, "keyword",  "#569cd6" );+  bbcode_style_def(env->bbcode, "control",  "#c586c0" );+  bbcode_style_def(env->bbcode, "number",   "#b5cea8" );+  bbcode_style_def(env->bbcode, "string",   "#ce9178" );+  bbcode_style_def(env->bbcode, "comment",  "#6A9955" );+  bbcode_style_def(env->bbcode, "type",     "darkcyan" );+  bbcode_style_def(env->bbcode, "constant", "#569cd6" );++  set_prompt_marker(env, NULL, NULL);+  return env;+}++static ic_env_t* rpenv;++static void ic_atexit(void) {+  if (rpenv != NULL) {+    ic_env_free(rpenv);+    rpenv = NULL;+  }+}++ic_private ic_env_t* ic_get_env(void) {  +  if (rpenv==NULL) {+    rpenv = ic_env_create( NULL, NULL, NULL );+    if (rpenv != NULL) { atexit( &ic_atexit ); }+  }+  return rpenv;+}++ic_public void ic_init_custom_malloc( ic_malloc_fun_t* _malloc, ic_realloc_fun_t* _realloc, ic_free_fun_t* _free ) {+  assert(rpenv == NULL);+  if (rpenv != NULL) {+    ic_env_free(rpenv);    +    rpenv = ic_env_create( _malloc, _realloc, _free ); +  }+  else {+    rpenv = ic_env_create( _malloc, _realloc, _free ); +    if (rpenv != NULL) {+      atexit( &ic_atexit );+    }+  }+}+
+ src/stringbuf.c view
@@ -0,0 +1,1034 @@+/* ----------------------------------------------------------------------------+  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.+-----------------------------------------------------------------------------*/++// get `wcwidth` for the column width of unicode characters+#if defined(__linux__) || defined(__freebsd__)+// use the system supplied one+#if !defined(_XOPEN_SOURCE)+#define  _XOPEN_SOURCE  700    // so wcwidth is visible+#endif+#include <wchar.h>+#else+// use our own (also on APPLE as that fails within vscode)+#define  wcwidth(c)  mk_wcwidth(c)+#include "wcwidth.c"+#endif++#include <stdio.h>+#include <string.h>+#include <inttypes.h>++#include "common.h"+#include "stringbuf.h"++//-------------------------------------------------------------+// In place growable utf-8 strings+//-------------------------------------------------------------++struct stringbuf_s {+  char*     buf;+  ssize_t   buflen;+  ssize_t   count;  +  alloc_t*  mem;+};+++//-------------------------------------------------------------+// String column width+//-------------------------------------------------------------++// column width of a utf8 single character sequence.+static ssize_t utf8_char_width( const char* s, ssize_t n ) {+  if (n <= 0) return 0;++  uint8_t b = (uint8_t)s[0];+  int32_t c;+  if (b < ' ') {+    return 0;+  }+  else if (b <= 0x7F) {+    return 1;+  }+  else if (b <= 0xC1) { // invalid continuation byte or invalid 0xC0, 0xC1 (check is strictly not necessary as we don't validate..)+    return 1;+  }+  else if (b <= 0xDF && n >= 2) { // b >= 0xC2  // 2 bytes+    c = (((b & 0x1F) << 6) | (s[1] & 0x3F));+    assert(c < 0xD800 || c > 0xDFFF);+    int w = wcwidth(c);+    return w;+  }+  else if (b <= 0xEF && n >= 3) { // b >= 0xE0  // 3 bytes +    c = (((b & 0x0F) << 12) | ((s[1] & 0x3F) << 6) | (s[2] & 0x3F));+    return wcwidth(c);    +  }+  else if (b <= 0xF4 && n >= 4) { // b >= 0xF0  // 4 bytes +    c = (((b & 0x07) << 18) | ((s[1] & 0x3F) << 12) | ((s[2] & 0x3F) << 6) | (s[3] & 0x3F));+    return wcwidth(c);+  }+  else {+    // failed+    return 1;+  }+}+++// The column width of a codepoint (0, 1, or 2)+static ssize_t char_column_width( const char* s, ssize_t n ) {+  if (s == NULL || n <= 0) return 0;+  else if ((uint8_t)(*s) < ' ') return 0;   // also for CSI escape sequences+  else {+    ssize_t w = utf8_char_width(s, n);+    #ifdef _WIN32+    return (w <= 0 ? 1 : w); // windows console seems to use at least one column+    #else+    return w;+    #endif+  }+}++static ssize_t str_column_width_n( const char* s, ssize_t len ) {+  if (s == NULL || len <= 0) return 0;+  ssize_t pos = 0;+  ssize_t cwidth = 0;+  ssize_t cw;+  ssize_t ofs;+  while (s[pos] != 0 && (ofs = str_next_ofs(s, len, pos, &cw)) > 0) {+    cwidth += cw;+    pos += ofs;+  }  +  return cwidth;+}++ic_private ssize_t str_column_width( const char* s ) {+  return str_column_width_n( s, ic_strlen(s) );+}++ic_private ssize_t str_skip_until_fit( const char* s, ssize_t max_width ) {+  if (s == NULL) return 0;+  ssize_t cwidth = str_column_width(s);+  ssize_t len    = ic_strlen(s);+  ssize_t pos = 0;+  ssize_t next;+  ssize_t cw;+  while (cwidth > max_width && (next = str_next_ofs(s, len, pos, &cw)) > 0) {+    cwidth -= cw;+    pos += next;+  }+  return pos;+}++ic_private ssize_t str_take_while_fit( const char* s, ssize_t max_width) {+  if (s == NULL) return 0;+  const ssize_t len = ic_strlen(s);+  ssize_t pos = 0;+  ssize_t next;+  ssize_t cw;+  ssize_t cwidth = 0;+  while ((next = str_next_ofs(s, len, pos, &cw)) > 0) {+    if (cwidth + cw > max_width) break;+    cwidth += cw;+    pos += next;+  }+  return pos;+}+++//-------------------------------------------------------------+// String navigation +//-------------------------------------------------------------++// get offset of the previous codepoint. does not skip back over CSI sequences.+ic_private ssize_t str_prev_ofs( const char* s, ssize_t pos, ssize_t* width ) {+  ssize_t ofs = 0;+  if (s != NULL && pos > 0) {+    ofs = 1;+    while (pos > ofs) {+      uint8_t u = (uint8_t)s[pos - ofs];+      if (u < 0x80 || u > 0xBF) break;  // continue while follower+      ofs++;+    }+  }+  if (width != NULL) *width = char_column_width( s+(pos-ofs), ofs );+  return ofs;+}++// skip an escape sequence+// <https://www.xfree86.org/current/ctlseqs.html>+ic_private bool skip_esc( const char* s, ssize_t len, ssize_t* esclen ) {  +  if (s == NULL || len <= 1 || s[0] != '\x1B') return false;+  if (esclen != NULL) *esclen = 0;+  if (strchr("[PX^_]",s[1]) != NULL) {+    // CSI (ESC [), DCS (ESC P), SOS (ESC X), PM (ESC ^), APC (ESC _), and OSC (ESC ]): terminated with a special sequence+    bool finalCSI = (s[1] == '[');  // CSI terminates with 0x40-0x7F; otherwise ST (bell or ESC \)+    ssize_t n = 2;+    while (len > n) {+      char c = s[n++];+      if ((finalCSI && (uint8_t)c >= 0x40 && (uint8_t)c <= 0x7F) ||  // terminating byte: @A–Z[\]^_`a–z{|}~+          (!finalCSI && c == '\x07') ||   // bell+          (c == '\x02'))                  // STX terminates as well+      {+        if (esclen != NULL) *esclen = n;+        return true;+      }+      else if (!finalCSI && c == '\x1B' && len > n && s[n] == '\\') {  // ST (ESC \)+        n++;+        if (esclen != NULL) *esclen = n;+        return true;+      }+    }+  }+  if (strchr(" #%()*+",s[1]) != NULL) {+    // assume escape sequence of length 3 (like ESC % G)+    if (esclen != NULL) *esclen = 2;+    return true;+  }+  else {+    // assume single character escape code (like ESC 7)+    if (esclen != NULL) *esclen = 2;+    return true;+  }+  return false;+}++// Offset to the next codepoint, treats CSI escape sequences as a single code point.+ic_private ssize_t str_next_ofs( const char* s, ssize_t len, ssize_t pos, ssize_t* cwidth ) {+  ssize_t ofs = 0;+  if (s != NULL && len > pos) {+    if (skip_esc(s+pos,len-pos,&ofs)) {+      // skip escape sequence      +    }+    else {+      ofs = 1;+      // utf8 extended character?+      while(len > pos + ofs) {+        uint8_t u = (uint8_t)s[pos + ofs];+        if (u < 0x80 || u > 0xBF) break;  // break if not a follower+        ofs++;+      }      +    } +  }+  if (cwidth != NULL) *cwidth = char_column_width( s+pos, ofs );+  return ofs;+}++static ssize_t str_limit_to_length( const char* s, ssize_t n ) {+  ssize_t i;+  for(i = 0; i < n && s[i] != 0; i++) { /* nothing */ }+  return i;+}+++//-------------------------------------------------------------+// String searching prev/next word, line, ws_word+//-------------------------------------------------------------+++static ssize_t str_find_backward( const char* s, ssize_t len, ssize_t pos, ic_is_char_class_fun_t* match, bool skip_immediate_matches ) {+  if (pos > len) pos = len;+  if (pos < 0) pos = 0;+  ssize_t i = pos;+  // skip matching first (say, whitespace in case of the previous start-of-word)+  if (skip_immediate_matches) {+    do {+      ssize_t prev = str_prev_ofs(s, i, NULL); +      if (prev <= 0) break;+      assert(i - prev >= 0);+      if (!match(s + i - prev, (long)prev)) break;+      i -= prev;+    } while (i > 0);  +  }+  // find match+  do {+    ssize_t prev = str_prev_ofs(s, i, NULL); +    if (prev <= 0) break;+    assert(i - prev >= 0);+    if (match(s + i - prev, (long)prev)) {+      return i;  // found;+    }+    i -= prev;+  } while (i > 0);+  return -1; // not found+}++static ssize_t str_find_forward( const char* s, ssize_t len, ssize_t pos, ic_is_char_class_fun_t* match, bool skip_immediate_matches ) {+  if (s == NULL || len < 0) return -1;+  if (pos > len) pos = len;+  if (pos < 0) pos = 0;  +  ssize_t i = pos;+  ssize_t next;+  // skip matching first (say, whitespace in case of the next end-of-word)+  if (skip_immediate_matches) {+    do {+      next = str_next_ofs(s, len, i, NULL); +      if (next <= 0) break;+      assert( i + next <= len);+      if (!match(s + i, (long)next)) break;+      i += next;+    } while (i < len);  +  }+  // and then look+  do {+    next = str_next_ofs(s, len, i, NULL); +    if (next <= 0) break;+    assert( i + next <= len);+    if (match(s + i, (long)next)) {+      return i; // found+    }+    i += next;+  } while (i < len);+  return -1;+} ++static bool char_is_linefeed( const char* s, long n ) {  +  return (n == 1 && (*s == '\n' || *s == 0));+}++static ssize_t str_find_line_start( const char* s, ssize_t len, ssize_t pos) {+  ssize_t start = str_find_backward(s,len,pos,&char_is_linefeed,false /* don't skip immediate matches */);+  return (start < 0 ? 0 : start); +}++static ssize_t str_find_line_end( const char* s, ssize_t len, ssize_t pos) {+  ssize_t end = str_find_forward(s,len,pos, &char_is_linefeed, false);+  return (end < 0 ? len : end);+}++static ssize_t str_find_word_start( const char* s, ssize_t len, ssize_t pos) {+  ssize_t start = str_find_backward(s,len,pos, &ic_char_is_idletter,true /* skip immediate matches */);+  return (start < 0 ? 0 : start); +}++static ssize_t str_find_word_end( const char* s, ssize_t len, ssize_t pos) {+  ssize_t end = str_find_forward(s,len,pos,&ic_char_is_idletter,true /* skip immediate matches */);+  return (end < 0 ? len : end); +}++static ssize_t str_find_ws_word_start( const char* s, ssize_t len, ssize_t pos) {+  ssize_t start = str_find_backward(s,len,pos,&ic_char_is_white,true /* skip immediate matches */);+  return (start < 0 ? 0 : start); +}++static ssize_t str_find_ws_word_end( const char* s, ssize_t len, ssize_t pos) {+  ssize_t end = str_find_forward(s,len,pos,&ic_char_is_white,true /* skip immediate matches */);+  return (end < 0 ? len : end); +}+++//-------------------------------------------------------------+// String row/column iteration+//-------------------------------------------------------------++// invoke a function for each terminal row; returns total row count.+static ssize_t str_for_each_row( const char* s, ssize_t len, ssize_t termw, ssize_t promptw, ssize_t cpromptw,+                                 row_fun_t* fun, const void* arg, void* res ) +{+  if (s == NULL) s = "";+  ssize_t i;+  ssize_t rcount = 0;+  ssize_t rcol = 0;+  ssize_t rstart = 0;  +  ssize_t startw  = promptw; +  for(i = 0; i < len; ) {+    ssize_t w;+    ssize_t next = str_next_ofs(s, len, i, &w);    +    if (next <= 0) {+      debug_msg("str: foreach row: next<=0: len %zd, i %zd, w %zd, buf %s\n", len, i, w, s );+      assert(false);+      break;+    }+    startw = (rcount == 0 ? promptw : cpromptw);+    ssize_t termcol = rcol + w + startw + 1 /* for the cursor */;+    if (termw != 0 && i != 0 && termcol >= termw) {  +      // wrap+      if (fun != NULL) {+        if (fun(s,rcount,rstart,i - rstart,startw,true,arg,res)) return rcount;+      }+      rcount++;+      rstart = i;+      rcol   = 0;+    }+    if (s[i] == '\n') {+      // newline+      if (fun != NULL) {+        if (fun(s,rcount,rstart,i - rstart,startw,false,arg,res)) return rcount;+      }+      rcount++;+      rstart = i+1;+      rcol = 0;+    }+    assert (s[i] != 0);+    i += next;+    rcol += w;+  }+  if (fun != NULL) {+    if (fun(s,rcount,rstart,i - rstart,startw,false,arg,res)) return rcount;+  }+  return rcount+1;+}++//-------------------------------------------------------------+// String: get row/column position+//-------------------------------------------------------------+++static bool str_get_current_pos_iter(+    const char* s,+    ssize_t row, ssize_t row_start, ssize_t row_len, +    ssize_t startw, bool is_wrap, const void* arg, void* res)+{+  ic_unused(is_wrap); ic_unused(startw);+  rowcol_t* rc = (rowcol_t*)res;+  ssize_t pos = *((ssize_t*)arg);++  if (pos >= row_start && pos <= (row_start + row_len)) {+    // found the cursor row+    rc->row_start = row_start;+    rc->row_len   = row_len;+    rc->row = row;+    rc->col = str_column_width_n( s + row_start, pos - row_start );+    rc->first_on_row = (pos == row_start);+    if (is_wrap) {+      // if wrapped, we check if the next character is at row_len+      ssize_t next = str_next_ofs(s, row_start + row_len, pos, NULL);+      rc->last_on_row = (pos + next >= row_start + row_len);+    }+    else {+      // normal last position is right after the last character+      rc->last_on_row = (pos >= row_start + row_len); +    }+    // debug_msg("edit; pos iter: pos: %zd (%c), row_start: %zd, rowlen: %zd\n", pos, s[pos], row_start, row_len);    +  }  +  return false; // always continue to count all rows+}++static ssize_t str_get_rc_at_pos(const char* s, ssize_t len, ssize_t termw, ssize_t promptw, ssize_t cpromptw, ssize_t pos, rowcol_t* rc) {+  memset(rc, 0, sizeof(*rc));+  ssize_t rows = str_for_each_row(s, len, termw, promptw, cpromptw, &str_get_current_pos_iter, &pos, rc);+  // debug_msg("edit: current pos: (%d, %d) %s %s\n", rc->row, rc->col, rc->first_on_row ? "first" : "", rc->last_on_row ? "last" : "");+  return rows;+}++++//-------------------------------------------------------------+// String: get row/column position for a resized terminal+// with potentially "hard-wrapped" rows+//-------------------------------------------------------------+typedef struct wrapped_arg_s {+  ssize_t  pos;+  ssize_t  newtermw;+} wrapped_arg_t;++typedef struct wrowcol_s {+  rowcol_t rc;+  ssize_t  hrows;  // count of hard-wrapped extra rows+} wrowcol_t;++static bool str_get_current_wrapped_pos_iter(+    const char* s,+    ssize_t row, ssize_t row_start, ssize_t row_len, +    ssize_t startw, bool is_wrap, const void* arg, void* res)+{+  ic_unused(is_wrap);+  wrowcol_t*     wrc = (wrowcol_t*)res;+  const wrapped_arg_t* warg = (const wrapped_arg_t*)arg;++  // iterate through the row and record the postion and hard-wraps+  ssize_t hwidth = startw;+  ssize_t i = 0;+  while( i <= row_len ) {  // include rowlen as the cursor position can be just after the last character+    // get next position and column width+    ssize_t cw;+    ssize_t next;+    bool is_cursor = (warg->pos == row_start+i);+    if (i < row_len) {+      next = str_next_ofs(s + row_start, row_len, i, &cw);+    }+    else {+      // end of row: take wrap or cursor into account+      // (wrap has width 2 as it displays a back-arrow but also has an invisible newline that wraps)+      cw = (is_wrap ? 2 : (is_cursor ? 1 : 0));+      next = 1;+    }++    if (next > 0) {+      if (hwidth + cw > warg->newtermw) {+        // hardwrap+        hwidth = 0;+        wrc->hrows++;+        debug_msg("str: found hardwrap: row: %zd, hrows: %zd\n", row, wrc->hrows);      +      }+    }    +    else {+      next++; // ensure we terminate (as we go up to rowlen)      +    }++    // did we find our position?+    if (is_cursor) {+      debug_msg("str: found position: row: %zd, hrows: %zd\n", row, wrc->hrows);+      wrc->rc.row_start = row_start;+      wrc->rc.row_len   = row_len;      +      wrc->rc.row       = wrc->hrows + row;+      wrc->rc.col       = hwidth;      +      wrc->rc.first_on_row = (i==0);+      wrc->rc.last_on_row  = (i+next >= row_len - (is_wrap ? 1 : 0)); +    }++    // advance+    hwidth += cw;+    i += next;    +  }+  return false; // always continue to count all rows+}+++static ssize_t str_get_wrapped_rc_at_pos(const char* s, ssize_t len, ssize_t termw, ssize_t newtermw, ssize_t promptw, ssize_t cpromptw, ssize_t pos, rowcol_t* rc) {+  wrapped_arg_t warg;+  warg.pos = pos;+  warg.newtermw = newtermw;+  wrowcol_t wrc;+  memset(&wrc,0,sizeof(wrc));+  ssize_t rows = str_for_each_row(s, len, termw, promptw, cpromptw, &str_get_current_wrapped_pos_iter, &warg, &wrc);+  debug_msg("edit: wrapped pos: (%zd,%zd) rows %zd %s %s, hrows: %zd\n", wrc.rc.row, wrc.rc.col, rows, wrc.rc.first_on_row ? "first" : "", wrc.rc.last_on_row ? "last" : "", wrc.hrows);+  *rc = wrc.rc;+  return (rows + wrc.hrows);+}+++//-------------------------------------------------------------+// Set position+//-------------------------------------------------------------++static bool str_set_pos_iter(+    const char* s,+    ssize_t row, ssize_t row_start, ssize_t row_len, +    ssize_t startw, bool is_wrap, const void* arg, void* res)+{+  ic_unused(arg); ic_unused(is_wrap); ic_unused(startw);+  rowcol_t* rc = (rowcol_t*)arg;+  if (rc->row != row) return false; // keep searching+  // we found our row+  ssize_t col = 0; +  ssize_t i   = row_start;+  ssize_t end = row_start + row_len;+  while (col < rc->col && i < end) {+    ssize_t cw;+    ssize_t next = str_next_ofs(s, row_start + row_len, i, &cw);+    if (next <= 0) break;+    i   += next;+    col += cw;+  }+  *((ssize_t*)res) = i;+  return true; // stop iteration+}++static ssize_t str_get_pos_at_rc(const char* s, ssize_t len, ssize_t termw, ssize_t promptw, ssize_t cpromptw, ssize_t row, ssize_t col /* without prompt */) {+  rowcol_t rc;+  memset(&rc,0,ssizeof(rc));+  rc.row = row;+  rc.col = col;+  ssize_t pos = -1;+  str_for_each_row(s,len,termw,promptw,cpromptw,&str_set_pos_iter,&rc,&pos);  +  return pos;+}+++//-------------------------------------------------------------+// String buffer+//-------------------------------------------------------------+static bool sbuf_ensure_extra(stringbuf_t* s, ssize_t extra) +{+  if (s->buflen >= s->count + extra) return true;   +  // reallocate; pick good initial size and multiples to increase reuse on allocation+  ssize_t newlen = (s->buflen <= 0 ? 120 : (s->buflen > 1000 ? s->buflen + 1000 : 2*s->buflen));+  if (newlen < s->count + extra) newlen = s->count + extra;+  if (s->buflen > 0) {+    debug_msg("stringbuf: reallocate: old %zd, new %zd\n", s->buflen, newlen);+  }+  char* newbuf = mem_realloc_tp(s->mem, char, s->buf, newlen+1); // one more for terminating zero+  if (newbuf == NULL) {+    assert(false);+    return false;+  }+  s->buf = newbuf;+  s->buflen = newlen;+  s->buf[s->count] = s->buf[s->buflen] = 0;+  assert(s->buflen >= s->count + extra);+  return true;+}++static void sbuf_init( stringbuf_t* sbuf, alloc_t* mem ) {+  sbuf->mem = mem;+  sbuf->buf = NULL;+  sbuf->buflen = 0;+  sbuf->count = 0;+}++static void sbuf_done( stringbuf_t* sbuf ) {+  mem_free( sbuf->mem, sbuf->buf );+  sbuf->buf = NULL;+  sbuf->buflen = 0;+  sbuf->count = 0;+}+++ic_private void sbuf_free( stringbuf_t* sbuf ) {+  if (sbuf==NULL) return;+  sbuf_done(sbuf);+  mem_free(sbuf->mem, sbuf);+}++ic_private stringbuf_t*  sbuf_new( alloc_t* mem ) {+  stringbuf_t* sbuf = mem_zalloc_tp(mem,stringbuf_t);+  if (sbuf == NULL) return NULL;+  sbuf_init(sbuf,mem);+  return sbuf;+}++// free the sbuf and return the current string buffer as the result+ic_private char* sbuf_free_dup(stringbuf_t* sbuf) {+  if (sbuf == NULL) return NULL;+  char* s = NULL;+  if (sbuf->buf != NULL) {+    s = mem_realloc_tp(sbuf->mem, char, sbuf->buf, sbuf_len(sbuf)+1);+    if (s == NULL) { s = sbuf->buf; }+    sbuf->buf = 0;+    sbuf->buflen = 0;+    sbuf->count = 0;+  }+  sbuf_free(sbuf);+  return s;+}++ic_private const char* sbuf_string_at( stringbuf_t* sbuf, ssize_t pos ) {+  if (pos < 0 || sbuf->count < pos) return NULL;+  if (sbuf->buf == NULL) return "";+  assert(sbuf->buf[sbuf->count] == 0);+  return sbuf->buf + pos;+}++ic_private const char* sbuf_string( stringbuf_t* sbuf ) {+  return sbuf_string_at( sbuf, 0 );+}++ic_private char sbuf_char_at(stringbuf_t* sbuf, ssize_t pos) {+  if (sbuf->buf == NULL || pos < 0 || sbuf->count < pos) return 0;+  return sbuf->buf[pos];+}++ic_private char* sbuf_strdup_at( stringbuf_t* sbuf, ssize_t pos ) {+  return mem_strdup(sbuf->mem, sbuf_string_at(sbuf,pos));+}++ic_private char* sbuf_strdup( stringbuf_t* sbuf ) {+  return mem_strdup(sbuf->mem, sbuf_string(sbuf));+}++ic_private ssize_t sbuf_len(const stringbuf_t* s) {+  if (s == NULL) return 0;+  return s->count;+}++ic_private ssize_t sbuf_append_vprintf(stringbuf_t* sb, const char* fmt, va_list args) {+  const ssize_t min_needed = ic_strlen(fmt);+  if (!sbuf_ensure_extra(sb,min_needed + 16)) return sb->count;+  ssize_t avail = sb->buflen - sb->count;+  va_list args0;+  va_copy(args0, args);+  ssize_t needed = vsnprintf(sb->buf + sb->count, to_size_t(avail), fmt, args0);+  if (needed > avail) {+    sb->buf[sb->count] = 0;+    if (!sbuf_ensure_extra(sb, needed)) return sb->count;+    avail = sb->buflen - sb->count;+    needed = vsnprintf(sb->buf + sb->count, to_size_t(avail), fmt, args);+  }+  assert(needed <= avail);+  sb->count += (needed > avail ? avail : (needed >= 0 ? needed : 0));+  assert(sb->count <= sb->buflen);+  sb->buf[sb->count] = 0;+  return sb->count;+}++ic_private ssize_t sbuf_appendf(stringbuf_t* sb, const char* fmt, ...) {+  va_list args;+  va_start( args, fmt);+  ssize_t res = sbuf_append_vprintf( sb, fmt, args );+  va_end(args);+  return res;+}+++ic_private ssize_t sbuf_insert_at_n(stringbuf_t* sbuf, const char* s, ssize_t n, ssize_t pos ) {+  if (pos < 0 || pos > sbuf->count || s == NULL) return pos;+  n = str_limit_to_length(s,n);+  if (n <= 0 || !sbuf_ensure_extra(sbuf,n)) return pos;+  ic_memmove(sbuf->buf + pos + n, sbuf->buf + pos, sbuf->count - pos);+  ic_memcpy(sbuf->buf + pos, s, n);+  sbuf->count += n;+  sbuf->buf[sbuf->count] = 0;+  return (pos + n);+}++ic_private stringbuf_t* sbuf_split_at( stringbuf_t* sb, ssize_t pos ) {+  stringbuf_t* res = sbuf_new(sb->mem);+  if (res==NULL || pos < 0) return NULL;+  if (pos < sb->count) {+    sbuf_append_n(res, sb->buf + pos, sb->count - pos);+    sb->count = pos;+  }+  return res;+}++ic_private ssize_t sbuf_insert_at(stringbuf_t* sbuf, const char* s, ssize_t pos ) {+  return sbuf_insert_at_n( sbuf, s, ic_strlen(s), pos );+}++ic_private ssize_t sbuf_insert_char_at(stringbuf_t* sbuf, char c, ssize_t pos ) {+  char s[2];+  s[0] = c;+  s[1] = 0;+  return sbuf_insert_at_n( sbuf, s, 1, pos);+}++ic_private ssize_t sbuf_insert_unicode_at(stringbuf_t* sbuf, unicode_t u, ssize_t pos) {+  uint8_t s[5];+  unicode_to_qutf8(u, s);+  return sbuf_insert_at(sbuf, (const char*)s, pos);+}++++ic_private void sbuf_delete_at( stringbuf_t* sbuf, ssize_t pos, ssize_t count ) {+  if (pos < 0 || pos >= sbuf->count) return;+  if (pos + count > sbuf->count) count = sbuf->count - pos;+  ic_memmove(sbuf->buf + pos, sbuf->buf + pos + count, sbuf->count - pos - count);+  sbuf->count -= count;+  sbuf->buf[sbuf->count] = 0;+}++ic_private void sbuf_delete_from_to( stringbuf_t* sbuf, ssize_t pos, ssize_t end ) {+  if (end <= pos) return;+  sbuf_delete_at( sbuf, pos, end - pos);+}++ic_private void  sbuf_delete_from(stringbuf_t* sbuf, ssize_t pos ) {+  sbuf_delete_at(sbuf, pos, sbuf_len(sbuf) - pos );+}+++ic_private void sbuf_clear( stringbuf_t* sbuf ) {+  sbuf_delete_at(sbuf, 0, sbuf_len(sbuf));+}++ic_private ssize_t sbuf_append_n( stringbuf_t* sbuf, const char* s, ssize_t n ) {+  return sbuf_insert_at_n( sbuf, s, n, sbuf_len(sbuf));+}++ic_private ssize_t sbuf_append( stringbuf_t* sbuf, const char* s ) {+  return sbuf_insert_at( sbuf, s, sbuf_len(sbuf));+}++ic_private ssize_t sbuf_append_char( stringbuf_t* sbuf, char c ) {+  char buf[2];+  buf[0] = c;+  buf[1] = 0;+  return sbuf_append( sbuf, buf );+}++ic_private void sbuf_replace(stringbuf_t* sbuf, const char* s) {+  sbuf_clear(sbuf);+  sbuf_append(sbuf,s);+}++ic_private ssize_t sbuf_next_ofs( stringbuf_t* sbuf, ssize_t pos, ssize_t* cwidth ) {+  return str_next_ofs( sbuf->buf, sbuf->count, pos, cwidth);+}++ic_private ssize_t sbuf_prev_ofs( stringbuf_t* sbuf, ssize_t pos, ssize_t* cwidth ) {+  return str_prev_ofs( sbuf->buf, pos, cwidth);+}++ic_private ssize_t sbuf_next( stringbuf_t* sbuf, ssize_t pos, ssize_t* cwidth) {+  ssize_t ofs = sbuf_next_ofs(sbuf,pos,cwidth);+  if (ofs <= 0) return -1;+  assert(pos + ofs <= sbuf->count);+  return pos + ofs; +}++ic_private ssize_t sbuf_prev( stringbuf_t* sbuf, ssize_t pos, ssize_t* cwidth) {+  ssize_t ofs = sbuf_prev_ofs(sbuf,pos,cwidth);+  if (ofs <= 0) return -1;+  assert(pos - ofs >= 0);+  return pos - ofs;+}++ic_private ssize_t sbuf_delete_char_before( stringbuf_t* sbuf, ssize_t pos ) {+  ssize_t n = sbuf_prev_ofs(sbuf, pos, NULL);+  if (n <= 0) return 0;  +  assert( pos - n >= 0 );+  sbuf_delete_at(sbuf, pos - n, n);+  return pos - n;+}++ic_private void sbuf_delete_char_at( stringbuf_t* sbuf, ssize_t pos ) {+  ssize_t n = sbuf_next_ofs(sbuf, pos, NULL);+  if (n <= 0) return;  +  assert( pos + n <= sbuf->count );+  sbuf_delete_at(sbuf, pos, n);+  return;+}++ic_private ssize_t sbuf_swap_char( stringbuf_t* sbuf, ssize_t pos ) {+  ssize_t next = sbuf_next_ofs(sbuf, pos, NULL);+  if (next <= 0) return 0;  +  ssize_t prev = sbuf_prev_ofs(sbuf, pos, NULL);+  if (prev <= 0) return 0;  +  char buf[64];+  if (prev >= 63) return 0;+  ic_memcpy(buf, sbuf->buf + pos - prev, prev );+  ic_memmove(sbuf->buf + pos - prev, sbuf->buf + pos, next);+  ic_memmove(sbuf->buf + pos - prev + next, buf, prev);+  return pos - prev;+}++ic_private ssize_t sbuf_find_line_start( stringbuf_t* sbuf, ssize_t pos ) {+  return str_find_line_start( sbuf->buf, sbuf->count, pos);+}++ic_private ssize_t sbuf_find_line_end( stringbuf_t* sbuf, ssize_t pos ) {+  return str_find_line_end( sbuf->buf, sbuf->count, pos);+}++ic_private ssize_t sbuf_find_word_start( stringbuf_t* sbuf, ssize_t pos ) {+  return str_find_word_start( sbuf->buf, sbuf->count, pos);+}++ic_private ssize_t sbuf_find_word_end( stringbuf_t* sbuf, ssize_t pos ) {+  return str_find_word_end( sbuf->buf, sbuf->count, pos);+}++ic_private ssize_t sbuf_find_ws_word_start( stringbuf_t* sbuf, ssize_t pos ) {+  return str_find_ws_word_start( sbuf->buf, sbuf->count, pos);+}++ic_private ssize_t sbuf_find_ws_word_end( stringbuf_t* sbuf, ssize_t pos ) {+  return str_find_ws_word_end( sbuf->buf, sbuf->count, pos);+}++// find row/col position+ic_private ssize_t sbuf_get_pos_at_rc( stringbuf_t* sbuf, ssize_t termw, ssize_t promptw, ssize_t cpromptw, ssize_t row, ssize_t col ) {+  return str_get_pos_at_rc( sbuf->buf, sbuf->count, termw, promptw, cpromptw, row, col);+}++// get row/col for a given position+ic_private ssize_t sbuf_get_rc_at_pos( stringbuf_t* sbuf, ssize_t termw, ssize_t promptw, ssize_t cpromptw, ssize_t pos, rowcol_t* rc ) {+  return str_get_rc_at_pos( sbuf->buf, sbuf->count, termw, promptw, cpromptw, pos, rc);+}++ic_private ssize_t sbuf_get_wrapped_rc_at_pos( stringbuf_t* sbuf, ssize_t termw, ssize_t newtermw, ssize_t promptw, ssize_t cpromptw, ssize_t pos, rowcol_t* rc ) {+  return str_get_wrapped_rc_at_pos( sbuf->buf, sbuf->count, termw, newtermw, promptw, cpromptw, pos, rc);+}++ic_private ssize_t sbuf_for_each_row( stringbuf_t* sbuf, ssize_t termw, ssize_t promptw, ssize_t cpromptw, row_fun_t* fun, void* arg, void* res ) {+  if (sbuf == NULL) return 0;+  return str_for_each_row( sbuf->buf, sbuf->count, termw, promptw, cpromptw, fun, arg, res);+}+++// Duplicate and decode from utf-8 (for non-utf8 terminals)+ic_private char* sbuf_strdup_from_utf8(stringbuf_t* sbuf) {+  ssize_t len = sbuf_len(sbuf);+  if (sbuf == NULL || len <= 0) return NULL;+  char* s = mem_zalloc_tp_n(sbuf->mem, char, len);+  if (s == NULL) return NULL;+  ssize_t dest = 0;+  for (ssize_t i = 0; i < len; ) {+    ssize_t ofs = sbuf_next_ofs(sbuf, i, NULL);+    if (ofs <= 0) {+      // invalid input+      break;+    }+    else if (ofs == 1) {+      // regular character+      s[dest++] = sbuf->buf[i];+    }+    else if (sbuf->buf[i] == '\x1B') {+      // skip escape sequences+    }+    else {+      // decode unicode+      ssize_t nread;+      unicode_t uchr = unicode_from_qutf8( (const uint8_t*)(sbuf->buf + i), ofs, &nread);+      uint8_t c;+      if (unicode_is_raw(uchr, &c)) {+        // raw byte, output as is (this will take care of locale specific input)+        s[dest++] = (char)c;+      }+      else if (uchr <= 0x7F) {+        // allow ascii+        s[dest++] = (char)uchr;+      }+      else {+        // skip unknown unicode characters..+        // todo: convert according to locale?+      }+    }+    i += ofs;+  }+  assert(dest <= len);+  s[dest] = 0;+  return s;+}++//-------------------------------------------------------------+// String helpers+//-------------------------------------------------------------++ic_public long ic_prev_char( const char* s, long pos ) {+  ssize_t len = ic_strlen(s);+  if (pos < 0 || pos > len) return -1;+  ssize_t ofs = str_prev_ofs( s, pos, NULL );+  if (ofs <= 0) return -1;+  return (long)(pos - ofs);+}++ic_public long ic_next_char( const char* s, long pos ) {+  ssize_t len = ic_strlen(s);+  if (pos < 0 || pos > len) return -1;+  ssize_t ofs = str_next_ofs( s, len, pos, NULL );+  if (ofs <= 0) return -1;+  return (long)(pos + ofs);+}+++// parse a decimal (leave pi unchanged on error)+ic_private bool ic_atoz(const char* s, ssize_t* pi) {+  return (sscanf(s, "%zd", pi) == 1);+}++// parse two decimals separated by a semicolon +ic_private bool ic_atoz2(const char* s, ssize_t* pi, ssize_t* pj) {+  return (sscanf(s, "%zd;%zd", pi, pj) == 2);+}++// parse unsigned 32-bit (leave pu unchanged on error)+ic_private bool ic_atou32(const char* s, uint32_t* pu) {+  return (sscanf(s, "%" SCNu32, pu) == 1);+}+++// Convenience: character class for whitespace `[ \t\r\n]`.+ic_public bool ic_char_is_white(const char* s, long len) {+  if (s == NULL || len != 1) return false;+  const char c = *s;+  return (c==' ' || c == '\t' || c == '\n' || c == '\r');+}++// Convenience: character class for non-whitespace `[^ \t\r\n]`.+ic_public bool ic_char_is_nonwhite(const char* s, long len) {+  return !ic_char_is_white(s, len);+}++// Convenience: character class for separators `[ \t\r\n,.;:/\\\(\)\{\}\[\]]`.+ic_public bool ic_char_is_separator(const char* s, long len) {+  if (s == NULL || len != 1) return false;+  const char c = *s;+  return (strchr(" \t\r\n,.;:/\\(){}[]", c) != NULL);+}++// Convenience: character class for non-separators.+ic_public bool ic_char_is_nonseparator(const char* s, long len) {+  return !ic_char_is_separator(s, len);+}+++// Convenience: character class for digits (`[0-9]`).+ic_public bool ic_char_is_digit(const char* s, long len) {+  if (s == NULL || len != 1) return false;+  const char c = *s;+  return (c >= '0' && c <= '9');+}++// Convenience: character class for hexadecimal digits (`[A-Fa-f0-9]`).+ic_public bool ic_char_is_hexdigit(const char* s, long len) {+  if (s == NULL || len != 1) return false;+  const char c = *s;+  return ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'));+}++// Convenience: character class for letters (`[A-Za-z]` and any unicode > 0x80).+ic_public bool ic_char_is_letter(const char* s, long len) {+  if (s == NULL || len <= 0) return false;+  const char c = *s;+  return ((uint8_t)c >= 0x80 || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'));+}++// Convenience: character class for identifier letters (`[A-Za-z0-9_-]` and any unicode > 0x80).+ic_public bool ic_char_is_idletter(const char* s, long len) {+  if (s == NULL || len <= 0) return false;+  const char c = *s;+  return ((uint8_t)c >= 0x80 || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || (c == '_') || (c == '-'));+}++// Convenience: character class for filename letters (`[^ \t\r\n`@$><=;|&{(]`).+ic_public bool ic_char_is_filename_letter(const char* s, long len) {+  if (s == NULL || len <= 0) return false;+  const char c = *s;+  return ((uint8_t)c >= 0x80 || (strchr(" \t\r\n`@$><=;|&{}()[]", c) == NULL));+}++// Convenience: If this is a token start, returns the length (or <= 0 if not found).+ic_public long ic_is_token(const char* s, long pos, ic_is_char_class_fun_t* is_token_char) {+  if (s == NULL || pos < 0 || is_token_char == NULL) return -1;+  ssize_t len = ic_strlen(s);+  if (pos >= len) return -1;+  if (pos > 0 && is_token_char(s + pos -1, 1)) return -1; // token start?+  ssize_t i = pos;+  while ( i < len ) {+    ssize_t next = str_next_ofs(s, len, i, NULL);+    if (next <= 0) return -1;+    if (!is_token_char(s + i, (long)next)) break;+    i += next;+  }+  return (long)(i - pos);+}+++static int ic_strncmp(const char* s1, const char* s2, ssize_t n) {+  return strncmp(s1, s2, to_size_t(n));+}++// Convenience: Does this match the specified token? +// Ensures not to match prefixes or suffixes, and returns the length of the match (in bytes).+// E.g. `ic_match_token("function",0,&ic_char_is_letter,"fun")` returns 0.+ic_public long ic_match_token(const char* s, long pos, ic_is_char_class_fun_t* is_token_char, const char* token) {+  long n = ic_is_token(s, pos, is_token_char);+  if (n > 0 && token != NULL && n == ic_strlen(token) && ic_strncmp(s + pos, token, n) == 0) {+    return n;+  }+  else {+    return 0;+  }+}+++// Convenience: Do any of the specified tokens match? +// Ensures not to match prefixes or suffixes, and returns the length of the match (in bytes).+// Ensures not to match prefixes or suffixes. +// E.g. `ic_match_any_token("function",0,&ic_char_is_letter,{"fun","func",NULL})` returns 0.+ic_public long ic_match_any_token(const char* s, long pos, ic_is_char_class_fun_t* is_token_char, const char** tokens) {+  long n = ic_is_token(s, pos, is_token_char);+  if (n <= 0 || tokens == NULL) return 0;+  for (const char** token = tokens; *token != NULL; token++) {+    if (n == ic_strlen(*token) && ic_strncmp(s + pos, *token, n) == 0) {+      return n;+    }+  }+  return 0;+}+
+ src/stringbuf.h view
@@ -0,0 +1,121 @@+/* ----------------------------------------------------------------------------+  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.+-----------------------------------------------------------------------------*/+#pragma once+#ifndef IC_STRINGBUF_H+#define IC_STRINGBUF_H++#include <stdarg.h>+#include "common.h"++//-------------------------------------------------------------+// string buffer+// in-place modified buffer with edit operations +// that grows on demand.+//-------------------------------------------------------------++// abstract string buffer+struct stringbuf_s;+typedef struct stringbuf_s stringbuf_t;++ic_private stringbuf_t*  sbuf_new( alloc_t* mem );+ic_private void    sbuf_free( stringbuf_t* sbuf );+ic_private char*   sbuf_free_dup(stringbuf_t* sbuf);+ic_private ssize_t sbuf_len(const stringbuf_t* s);++ic_private const char* sbuf_string_at( stringbuf_t* sbuf, ssize_t pos );+ic_private const char* sbuf_string( stringbuf_t* sbuf );+ic_private char    sbuf_char_at(stringbuf_t* sbuf, ssize_t pos);+ic_private char*   sbuf_strdup_at( stringbuf_t* sbuf, ssize_t pos );+ic_private char*   sbuf_strdup( stringbuf_t* sbuf );+ic_private char*   sbuf_strdup_from_utf8(stringbuf_t* sbuf);  // decode to locale+++ic_private ssize_t sbuf_appendf(stringbuf_t* sb, const char* fmt, ...);+ic_private ssize_t sbuf_append_vprintf(stringbuf_t* sb, const char* fmt, va_list args);++ic_private stringbuf_t* sbuf_split_at( stringbuf_t* sb, ssize_t pos );++// primitive edit operations (inserts return the new position)+ic_private void    sbuf_clear(stringbuf_t* sbuf);+ic_private void    sbuf_replace(stringbuf_t* sbuf, const char* s);+ic_private void    sbuf_delete_at(stringbuf_t* sbuf, ssize_t pos, ssize_t count);+ic_private void    sbuf_delete_from_to(stringbuf_t* sbuf, ssize_t pos, ssize_t end);+ic_private void    sbuf_delete_from(stringbuf_t* sbuf, ssize_t pos );+ic_private ssize_t sbuf_insert_at_n(stringbuf_t* sbuf, const char* s, ssize_t n, ssize_t pos );+ic_private ssize_t sbuf_insert_at(stringbuf_t* sbuf, const char* s, ssize_t pos );+ic_private ssize_t sbuf_insert_char_at(stringbuf_t* sbuf, char c, ssize_t pos );+ic_private ssize_t sbuf_insert_unicode_at(stringbuf_t* sbuf, unicode_t u, ssize_t pos);+ic_private ssize_t sbuf_append_n(stringbuf_t* sbuf, const char* s, ssize_t n);+ic_private ssize_t sbuf_append(stringbuf_t* sbuf, const char* s);+ic_private ssize_t sbuf_append_char(stringbuf_t* sbuf, char c);++// high level edit operations (return the new position)+ic_private ssize_t sbuf_next( stringbuf_t* sbuf, ssize_t pos, ssize_t* cwidth );+ic_private ssize_t sbuf_prev( stringbuf_t* sbuf, ssize_t pos, ssize_t* cwidth );+ic_private ssize_t sbuf_next_ofs(stringbuf_t* sbuf, ssize_t pos, ssize_t* cwidth);++ic_private ssize_t sbuf_delete_char_before( stringbuf_t* sbuf, ssize_t pos );+ic_private void    sbuf_delete_char_at( stringbuf_t* sbuf, ssize_t pos );+ic_private ssize_t sbuf_swap_char( stringbuf_t* sbuf, ssize_t pos );++ic_private ssize_t sbuf_find_line_start( stringbuf_t* sbuf, ssize_t pos );+ic_private ssize_t sbuf_find_line_end( stringbuf_t* sbuf, ssize_t pos );+ic_private ssize_t sbuf_find_word_start( stringbuf_t* sbuf, ssize_t pos );+ic_private ssize_t sbuf_find_word_end( stringbuf_t* sbuf, ssize_t pos );+ic_private ssize_t sbuf_find_ws_word_start( stringbuf_t* sbuf, ssize_t pos );+ic_private ssize_t sbuf_find_ws_word_end( stringbuf_t* sbuf, ssize_t pos );++// parse a decimal +ic_private bool ic_atoz(const char* s, ssize_t* i);+// parse two decimals separated by a semicolon+ic_private bool ic_atoz2(const char* s, ssize_t* i, ssize_t* j);+ic_private bool ic_atou32(const char* s, uint32_t* pu);++// row/column info+typedef struct rowcol_s {+  ssize_t row;+  ssize_t col;+  ssize_t row_start;+  ssize_t row_len;+  bool    first_on_row;+  bool    last_on_row;+} rowcol_t;++// find row/col position+ic_private ssize_t sbuf_get_pos_at_rc( stringbuf_t* sbuf, ssize_t termw, ssize_t promptw, ssize_t cpromptw, +                                       ssize_t row, ssize_t col );+// get row/col for a given position+ic_private ssize_t sbuf_get_rc_at_pos( stringbuf_t* sbuf, ssize_t termw, ssize_t promptw, ssize_t cpromptw, +                                       ssize_t pos, rowcol_t* rc );++ic_private ssize_t sbuf_get_wrapped_rc_at_pos( stringbuf_t* sbuf, ssize_t termw, ssize_t newtermw, ssize_t promptw, ssize_t cpromptw, +                                       ssize_t pos, rowcol_t* rc );+                                       +// row iteration+typedef bool (row_fun_t)(const char* s,+                          ssize_t row, ssize_t row_start, ssize_t row_len, +                          ssize_t startw, // prompt width+                          bool is_wrap, const void* arg, void* res);++ic_private ssize_t sbuf_for_each_row( stringbuf_t* sbuf, ssize_t termw, ssize_t promptw, ssize_t cpromptw, +                                      row_fun_t* fun, void* arg, void* res );+++//-------------------------------------------------------------+// Strings+//-------------------------------------------------------------++// skip a single CSI sequence (ESC [ ...)+ic_private bool    skip_csi_esc( const char* s, ssize_t len, ssize_t* esclen ); // used in term.c++ic_private ssize_t str_column_width( const char* s );+ic_private ssize_t str_prev_ofs( const char* s, ssize_t pos, ssize_t* cwidth );+ic_private ssize_t str_next_ofs( const char* s, ssize_t len, ssize_t pos, ssize_t* cwidth );+ic_private ssize_t str_skip_until_fit( const char* s, ssize_t max_width);  // tail that fits+ic_private ssize_t str_take_while_fit( const char* s, ssize_t max_width);  // prefix that fits++#endif // IC_STRINGBUF_H
+ src/term.c view
@@ -0,0 +1,1113 @@+/* ----------------------------------------------------------------------------+  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.+-----------------------------------------------------------------------------*/+#include <string.h>+#include <stdio.h>+#include <stdarg.h>+#include <stdlib.h>  // getenv+#include <inttypes.h>++#include "common.h"+#include "tty.h"+#include "term.h"+#include "stringbuf.h" // str_next_ofs++#if defined(_WIN32)+#include <windows.h>+#define STDOUT_FILENO 1+#else+#include <unistd.h>+#include <errno.h>+#include <sys/ioctl.h>+#if defined(__linux__)+#include <linux/kd.h>+#endif+#endif++#define IC_CSI      "\x1B["++// color support; colors are auto mapped smaller palettes if needed. (see `term_color.c`)+typedef enum palette_e {+  MONOCHROME,  // no color+  ANSI8,       // only basic 8 ANSI color     (ESC[<idx>m, idx: 30-37, +10 for background)+  ANSI16,      // basic + bright ANSI colors  (ESC[<idx>m, idx: 30-37, 90-97, +10 for background)+  ANSI256,     // ANSI 256 color palette      (ESC[38;5;<idx>m, idx: 0-15 standard color, 16-231 6x6x6 rbg colors, 232-255 gray shades)+  ANSIRGB      // direct rgb colors supported (ESC[38;2;<r>;<g>;<b>m)+} palette_t;++// The terminal screen+struct term_s {+  int           fd_out;             // output handle+  ssize_t       width;              // screen column width+  ssize_t       height;             // screen row height+  ssize_t       raw_enabled;        // is raw mode active? counted by start/end pairs+  bool          nocolor;            // show colors?+  bool          silent;             // enable beep?+  bool          is_utf8;            // utf-8 output? determined by the tty+  attr_t   attr;               // current text attributes+  palette_t     palette;            // color support+  buffer_mode_t bufmode;            // buffer mode+  stringbuf_t*  buf;                // buffer for buffered output+  tty_t*        tty;                // used on posix to get the cursor position+  alloc_t*      mem;                // allocator+  #ifdef _WIN32+  HANDLE        hcon;               // output console handler+  WORD          hcon_default_attr;  // default text attributes+  WORD          hcon_orig_attr;     // original text attributes+  DWORD         hcon_orig_mode;     // original console mode+  DWORD         hcon_mode;          // used console mode+  UINT          hcon_orig_cp;       // original console code-page (locale)+  COORD         hcon_save_cursor;   // saved cursor position (for escape sequence emulation)+  #endif+};++static bool term_write_direct(term_t* term, const char* s, ssize_t n );+static void term_append_buf(term_t* term, const char* s, ssize_t n);++//-------------------------------------------------------------+// Colors+//-------------------------------------------------------------++#include "term_color.c"++//-------------------------------------------------------------+// Helpers+//-------------------------------------------------------------++ic_private void term_left(term_t* term, ssize_t n) {+  if (n <= 0) return;+  term_writef( term, IC_CSI "%zdD", n );+}++ic_private void term_right(term_t* term, ssize_t n) {+  if (n <= 0) return;+  term_writef( term, IC_CSI "%zdC", n );+}++ic_private void term_up(term_t* term, ssize_t n) {+  if (n <= 0) return;+  term_writef( term, IC_CSI "%zdA", n );+}++ic_private void term_down(term_t* term, ssize_t n) {+  if (n <= 0) return;+  term_writef( term, IC_CSI "%zdB", n );+}++ic_private void term_clear_line(term_t* term) {+  term_write( term, "\r" IC_CSI "K");+}++ic_private void term_clear_to_end_of_line(term_t* term) {+  term_write(term, IC_CSI "K");+}++ic_private void term_start_of_line(term_t* term) {+  term_write( term, "\r" );+}++ic_private ssize_t term_get_width(term_t* term) {+  return term->width;+}++ic_private ssize_t term_get_height(term_t* term) {+  return term->height;+}++ic_private void term_attr_reset(term_t* term) {+  term_write(term, IC_CSI "m" );+}++ic_private void term_underline(term_t* term, bool on) {+  term_write(term, on ? IC_CSI "4m" : IC_CSI "24m" );+}++ic_private void term_reverse(term_t* term, bool on) {+  term_write(term, on ? IC_CSI "7m" : IC_CSI "27m");+}++ic_private void term_bold(term_t* term, bool on) {+  term_write(term, on ? IC_CSI "1m" : IC_CSI "22m" );+}++ic_private void term_italic(term_t* term, bool on) {+  term_write(term, on ? IC_CSI "3m" : IC_CSI "23m" );+}++ic_private void term_writeln(term_t* term, const char* s) {+  term_write(term,s);+  term_write(term,"\n");+}++ic_private void term_write_char(term_t* term, char c) {+  char buf[2];+  buf[0] = c;+  buf[1] = 0;+  term_write_n(term, buf, 1 );+}++ic_private attr_t term_get_attr( const term_t* term ) {+  return term->attr;+}++ic_private void term_set_attr( term_t* term, attr_t attr ) {+  if (attr.x.color != term->attr.x.color && attr.x.color != IC_COLOR_NONE) {+    term_color(term,attr.x.color);+    if (term->palette < ANSIRGB && color_is_rgb(attr.x.color)) {+      term->attr.x.color = attr.x.color; // actual color may have been approximated but we keep the actual color to avoid updating every time+    }+  }+  if (attr.x.bgcolor != term->attr.x.bgcolor && attr.x.bgcolor != IC_COLOR_NONE) {+    term_bgcolor(term,attr.x.bgcolor);+    if (term->palette < ANSIRGB && color_is_rgb(attr.x.bgcolor)) {+      term->attr.x.bgcolor = attr.x.bgcolor; +    }+  }+  if (attr.x.bold != term->attr.x.bold && attr.x.bold != IC_NONE) {+    term_bold(term,attr.x.bold == IC_ON);+  }+  if (attr.x.underline != term->attr.x.underline && attr.x.underline != IC_NONE) {+    term_underline(term,attr.x.underline == IC_ON);+  }+  if (attr.x.reverse != term->attr.x.reverse && attr.x.reverse != IC_NONE) {+    term_reverse(term,attr.x.reverse == IC_ON);+  }+  if (attr.x.italic != term->attr.x.italic && attr.x.italic != IC_NONE) {+    term_italic(term,attr.x.italic == IC_ON);+  }+  assert(attr.x.color == term->attr.x.color || attr.x.color == IC_COLOR_NONE);+  assert(attr.x.bgcolor == term->attr.x.bgcolor || attr.x.bgcolor == IC_COLOR_NONE);+  assert(attr.x.bold == term->attr.x.bold || attr.x.bold == IC_NONE);+  assert(attr.x.reverse == term->attr.x.reverse || attr.x.reverse == IC_NONE);+  assert(attr.x.underline == term->attr.x.underline || attr.x.underline == IC_NONE);+  assert(attr.x.italic == term->attr.x.italic || attr.x.italic == IC_NONE);+}+++/*+ic_private void term_clear_lines_to_end(term_t* term) {+  term_write(term, "\r" IC_CSI "J");+}++ic_private void term_show_cursor(term_t* term, bool on) {+  term_write(term, on ? IC_CSI "?25h" : IC_CSI "?25l");+}+*/++//-------------------------------------------------------------+// Formatted output+//-------------------------------------------------------------++ic_private void term_writef(term_t* term, const char* fmt, ...) {+  va_list ap;+  va_start(ap, fmt);+  term_vwritef(term,fmt,ap);+  va_end(ap);  +}++ic_private void term_vwritef(term_t* term, const char* fmt, va_list args ) {+  sbuf_append_vprintf(term->buf, fmt, args);+}++ic_private void term_write_formatted( term_t* term, const char* s, const attr_t* attrs ) {+  term_write_formatted_n( term, s, attrs, ic_strlen(s));+}++ic_private void term_write_formatted_n( term_t* term, const char* s, const attr_t* attrs, ssize_t len ) {+  if (attrs == NULL) {+    // write directly+    term_write(term,s);+  }+  else {+    // ensure raw mode from now on+    if (term->raw_enabled <= 0) {+      term_start_raw(term);+    }+    // and output with text attributes+    const attr_t default_attr = term_get_attr(term);+    attr_t attr = attr_none();+    ssize_t i = 0;+    ssize_t n = 0;+    while( i+n < len && s[i+n] != 0 ) {+      if (!attr_is_eq(attr,attrs[i+n])) {+        if (n > 0) { +          term_write_n( term, s+i, n );+          i += n;+          n = 0;+        }+        attr = attrs[i];+        term_set_attr( term, attr_update_with(default_attr,attr) );+      }  +      n++;    +    }+    if (n > 0) {+      term_write_n( term, s+i, n );+      i += n;+      n = 0;    +    }+    assert(s[i] != 0 || i == len);+    term_set_attr(term, default_attr);+  }+}++//-------------------------------------------------------------+// Write to the terminal+// The buffered functions are used to reduce cursor flicker+// during refresh+//-------------------------------------------------------------++ic_private void term_beep(term_t* term) {+  if (term->silent) return;+  fprintf(stderr,"\x7");+  fflush(stderr);+}++ic_private void term_write_repeat(term_t* term, const char* s, ssize_t count) {+  for (; count > 0; count--) {+    term_write(term, s);+  }+}++ic_private void term_write(term_t* term, const char* s) {+  if (s == NULL || s[0] == 0) return;+  ssize_t n = ic_strlen(s);+  term_write_n(term,s,n);+}++// Primitive terminal write; all writes go through here+ic_private void term_write_n(term_t* term, const char* s, ssize_t n) {+  if (s == NULL || n <= 0) return;+  // write to buffer to reduce flicker and to process escape sequences (this may flush too)+  term_append_buf(term, s, n);  +}+++//-------------------------------------------------------------+// Buffering+//-------------------------------------------------------------+++ic_private void term_flush(term_t* term) {+  if (sbuf_len(term->buf) > 0) {+    //term_show_cursor(term,false);+    term_write_direct(term, sbuf_string(term->buf), sbuf_len(term->buf));+    //term_show_cursor(term,true);+    sbuf_clear(term->buf);+  }  +}++ic_private buffer_mode_t term_set_buffer_mode(term_t* term, buffer_mode_t mode) {+  buffer_mode_t oldmode = term->bufmode;+  if (oldmode != mode) {+    if (mode == UNBUFFERED) {+      term_flush(term);+    }+    term->bufmode = mode;+  }+  return oldmode;+}++static void term_check_flush(term_t* term, bool contains_nl) {+  if (term->bufmode == UNBUFFERED || +      sbuf_len(term->buf) > 4000 ||+      (term->bufmode == LINEBUFFERED && contains_nl)) +  {+    term_flush(term);+  }  +}++//-------------------------------------------------------------+// Init+//-------------------------------------------------------------++static void term_init_raw(term_t* term);++ic_private term_t* term_new(alloc_t* mem, tty_t* tty, bool nocolor, bool silent, int fd_out ) +{+  term_t* term = mem_zalloc_tp(mem, term_t);+  if (term == NULL) return NULL;++  term->fd_out  = (fd_out < 0 ? STDOUT_FILENO : fd_out);+  term->nocolor = nocolor;+  term->silent  = silent;  +  term->mem     = mem;+  term->tty     = tty;+  term->width   = 80;+  term->height  = 25;+  term->is_utf8 = tty_is_utf8(tty);+  term->palette = ANSI16; // almost universally supported+  term->buf     = sbuf_new(mem);  +  term->bufmode = LINEBUFFERED;+  term->attr    = attr_default();++  // respect NO_COLOR+  if (getenv("NO_COLOR") != NULL) {+    term->nocolor = true;+  }++  // detect color palette+  // COLORTERM takes precedence+  const char* colorterm = getenv("COLORTERM");  +  if (ic_contains(colorterm,"24bit") || ic_contains(colorterm,"truecolor") || ic_contains(colorterm,"direct")) { +    term->palette = ANSIRGB; +  }+  else if (ic_contains(colorterm,"8bit") || ic_contains(colorterm,"256color")) { term->palette = ANSI256; } +  else if (ic_contains(colorterm,"4bit") || ic_contains(colorterm,"16color"))  { term->palette = ANSI16; }+  else if (ic_contains(colorterm,"3bit") || ic_contains(colorterm,"8color"))   { term->palette = ANSI8; }+  else if (ic_contains(colorterm,"1bit") || ic_contains(colorterm,"nocolor") || ic_contains(colorterm,"monochrome")) { +    term->palette = MONOCHROME; +  }+  // otherwise check for some specific terminals+  else if (getenv("WT_SESSION") != NULL) { term->palette = ANSIRGB; } // Windows terminal+  else if (getenv("ITERM_SESSION_ID") != NULL) { term->palette = ANSIRGB; } // iTerm2 terminal+  else if (getenv("VSCODE_PID") != NULL) { term->palette = ANSIRGB; } // vscode terminal+  else {+    // and otherwise fall back to checking TERM+    const char* eterm = getenv("TERM");+    if (ic_contains(eterm,"truecolor") || ic_contains(eterm,"direct") || ic_contains(colorterm,"24bit")) {+      term->palette = ANSIRGB;+    }+    else if (ic_contains(eterm,"alacritty") || ic_contains(eterm,"kitty")) {+      term->palette = ANSIRGB;+    }+    else if (ic_contains(eterm,"256color") || ic_contains(eterm,"gnome")) { +      term->palette = ANSI256;+    }  +    else if (ic_contains(eterm,"16color")){ term->palette = ANSI16; }+    else if (ic_contains(eterm,"8color")) { term->palette = ANSI8; }+    else if (ic_contains(eterm,"monochrome") || ic_contains(eterm,"nocolor") || ic_contains(eterm,"dumb")) { +      term->palette = MONOCHROME; +    }+  }+  debug_msg("term: color-bits: %d (COLORTERM=%s, TERM=%s)\n", term_get_color_bits(term), colorterm, term);+  +  // read COLUMS/LINES from the environment for a better initial guess.+  const char* env_columns = getenv("COLUMNS");+  if (env_columns != NULL) { ic_atoz(env_columns, &term->width); }+  const char* env_lines = getenv("LINES");+  if (env_lines != NULL)   { ic_atoz(env_lines, &term->height); }+  +  // initialize raw terminal output and terminal dimensions+  term_init_raw(term);+  term_update_dim(term);+  term_attr_reset(term);  // ensure we are at default settings++  return term;+}++ic_private bool term_is_interactive(const term_t* term) {+  ic_unused(term);+  // check dimensions (0 is used for debuggers)+  // if (term->width <= 0) return false; +  +  // check editing support+  const char* eterm = getenv("TERM");+  debug_msg("term: TERM=%s\n", eterm);+  if (eterm != NULL &&+      (strstr("dumb|DUMB|cons25|CONS25|emacs|EMACS",eterm) != NULL)) {+    return false;+  }++  return true;+}++ic_private bool term_enable_beep(term_t* term, bool enable) {+  bool prev = term->silent;+  term->silent = !enable;+  return prev;+}++ic_private bool term_enable_color(term_t* term, bool enable) {+  bool prev = !term->nocolor;+  term->nocolor = !enable;+  return prev;+}++ic_private void term_free(term_t* term) {+  if (term == NULL) return;+  term_flush(term);+  term_end_raw(term, true);+  sbuf_free(term->buf); term->buf = NULL;+  mem_free(term->mem, term);+}++//-------------------------------------------------------------+// For best portability and applications inserting CSI SGR (ESC[ .. m)+// codes themselves in strings, we interpret these at the +// lowest level so we can have a `term_get_attr` function which+// is needed for bracketed styles etc.+//-------------------------------------------------------------++static void term_append_esc(term_t* term, const char* const s, ssize_t len) {+  if (s[1]=='[' && s[len-1] == 'm') {    +    // it is a CSI SGR sequence: ESC[ ... m+    if (term->nocolor) return;       // ignore escape sequences if nocolor is set+    term->attr = attr_update_with(term->attr, attr_from_esc_sgr(s,len));+  }+  // and write out the escape sequence as-is+  sbuf_append_n(term->buf, s, len);+}+++static void term_append_utf8(term_t* term, const char* s, ssize_t len) {+  ssize_t nread;+  unicode_t uchr = unicode_from_qutf8((const uint8_t*)s, len, &nread);+  uint8_t c;+  if (unicode_is_raw(uchr, &c)) {+    // write bytes as is; this also ensure that on non-utf8 terminals characters between 0x80-0xFF+    // go through _as is_ due to the qutf8 encoding.+    sbuf_append_char(term->buf,(char)c);+  }+  else if (!term->is_utf8) {+    // on non-utf8 terminals still send utf-8 and hope for the best+    // todo: we could try to convert to the locale first?+    sbuf_append_n(term->buf, s, len);+    // sbuf_appendf(term->buf, "\x1B[%" PRIu32 "u", uchr); // unicode escape code+  }+  else {+    // write utf-8 as is+    sbuf_append_n(term->buf, s, len);+  }+}++static void term_append_buf( term_t* term, const char* s, ssize_t len ) {+  ssize_t pos = 0;+  bool newline = false;+  while (pos < len) {+    // handle ascii sequences in bulk+    ssize_t ascii = 0;+    ssize_t next;+    while ((next = str_next_ofs(s, len, pos+ascii, NULL)) > 0 && +            (uint8_t)s[pos + ascii] > '\x1B' && (uint8_t)s[pos + ascii] <= 0x7F ) +    {+      if (s[pos+ascii] == '\n') { newline = true; }+      ascii += next;      +    }+    if (ascii > 0) {+      sbuf_append_n(term->buf, s+pos, ascii);+      pos += ascii;+    }+    if (next <= 0) break;++    const uint8_t c = (uint8_t)s[pos];+    // handle utf8 sequences (for non-utf8 terminals)+    if (c >= 0x80) {+      term_append_utf8(term, s+pos, next);+    }+    // handle escape sequence (note: str_next_ofs considers whole CSI escape sequences at a time)+    else if (next > 1 && c == '\x1B') {+      term_append_esc(term, s+pos, next);+    }+    else if (c < ' ' && c != 0 && (c < '\x07' || c > '\x0D')) {+      // ignore control characters except \a, \b, \t, \n, \r, and form-feed and vertical tab.+    }+    else {+      sbuf_append_n(term->buf, s+pos, next);+    }+    pos += next;+  }  +  // possibly flush+  term_check_flush(term, newline);  +}++//-------------------------------------------------------------+// Platform dependent: Write directly to the terminal+//-------------------------------------------------------------++#if !defined(_WIN32)++// write to the console without further processing+static bool term_write_direct(term_t* term, const char* s, ssize_t n) {+  ssize_t count = 0; +  while( count < n ) {+    ssize_t nwritten = write(term->fd_out, s + count, to_size_t(n - count));+    if (nwritten > 0) {+      count += nwritten;+    }+    else if (errno != EINTR && errno != EAGAIN) {+      debug_msg("term: write failed: length %i, errno %i: \"%s\"\n", n, errno, s);+      return false;+    }+  }+  return true;+}++#else++//----------------------------------------------------------------------------------+// On windows we use the new virtual terminal processing if it is available (Windows Terminal)+// but fall back to  ansi escape emulation on older systems but also for example+// the PS terminal+//+// note: we use row/col as 1-based ANSI escape while windows X/Y coords are 0-based.+//-----------------------------------------------------------------------------------++#if !defined(ENABLE_VIRTUAL_TERMINAL_PROCESSING)+#define ENABLE_VIRTUAL_TERMINAL_PROCESSING (0)+#endif+#if !defined(ENABLE_LVB_GRID_WORLDWIDE)+#define ENABLE_LVB_GRID_WORLDWIDE (0)+#endif++// direct write to the console without further processing+static bool term_write_console(term_t* term, const char* s, ssize_t n ) {+  DWORD written;+  WriteConsoleA(term->hcon, s, (DWORD)(to_size_t(n)), &written, NULL);+  return (written == (DWORD)(to_size_t(n)));+}++static bool term_get_cursor_pos( term_t* term, ssize_t* row, ssize_t* col) {+  *row = 0;+  *col = 0;+  CONSOLE_SCREEN_BUFFER_INFO info;+  if (!GetConsoleScreenBufferInfo(term->hcon, &info)) return false;+  *row = (ssize_t)info.dwCursorPosition.Y + 1;+  *col = (ssize_t)info.dwCursorPosition.X + 1;+  return true;+}++static void term_move_cursor_to( term_t* term, ssize_t row, ssize_t col ) {+  CONSOLE_SCREEN_BUFFER_INFO info;+  if (!GetConsoleScreenBufferInfo( term->hcon, &info )) return;+  if (col > info.dwSize.X) col = info.dwSize.X;+  if (row > info.dwSize.Y) row = info.dwSize.Y;+  if (col <= 0) col = 1;+  if (row <= 0) row = 1;+  COORD coord;+  coord.X = (SHORT)col - 1;+  coord.Y = (SHORT)row - 1;+  SetConsoleCursorPosition( term->hcon, coord);+}++static void term_cursor_save(term_t* term) {+  memset(&term->hcon_save_cursor, 0, sizeof(term->hcon_save_cursor));+  CONSOLE_SCREEN_BUFFER_INFO info;+  if (!GetConsoleScreenBufferInfo(term->hcon, &info)) return;+  term->hcon_save_cursor = info.dwCursorPosition;+}++static void term_cursor_restore(term_t* term) {+  if (term->hcon_save_cursor.X == 0) return;+  SetConsoleCursorPosition(term->hcon, term->hcon_save_cursor);+}++static void term_move_cursor( term_t* term, ssize_t drow, ssize_t dcol, ssize_t n ) {+  CONSOLE_SCREEN_BUFFER_INFO info;+  if (!GetConsoleScreenBufferInfo( term->hcon, &info )) return;+  COORD cur = info.dwCursorPosition;+  ssize_t col = (ssize_t)cur.X + 1 + n*dcol;+  ssize_t row = (ssize_t)cur.Y + 1 + n*drow;+  term_move_cursor_to( term, row, col );+}++static void term_cursor_visible( term_t* term, bool visible ) {+  CONSOLE_CURSOR_INFO info;+  if (!GetConsoleCursorInfo(term->hcon,&info)) return;+  info.bVisible = visible;+  SetConsoleCursorInfo(term->hcon,&info);+}++static void term_erase_line( term_t* term, ssize_t mode ) {  +  CONSOLE_SCREEN_BUFFER_INFO info;+  if (!GetConsoleScreenBufferInfo( term->hcon, &info )) return;+  DWORD written;+  COORD start;+  ssize_t length;+  if (mode == 2) {+    // entire line+    start.X = 0;+    start.Y = info.dwCursorPosition.Y;+    length = (ssize_t)info.srWindow.Right + 1;+  }+  else if (mode == 1) {+    // to start of line+    start.X = 0;+    start.Y = info.dwCursorPosition.Y;+    length  = info.dwCursorPosition.X;+  }+  else {+    // to end of line    +    length = (ssize_t)info.srWindow.Right - info.dwCursorPosition.X + 1;+    start = info.dwCursorPosition;+  }+  FillConsoleOutputAttribute( term->hcon, term->hcon_default_attr, (DWORD)length, start, &written );+  FillConsoleOutputCharacterA( term->hcon, ' ', (DWORD)length, start, &written );+}++static void term_clear_screen(term_t* term, ssize_t mode) {+  CONSOLE_SCREEN_BUFFER_INFO info;+  if (!GetConsoleScreenBufferInfo(term->hcon, &info)) return;+  COORD start;+  start.X = 0;+  start.Y = 0;+  ssize_t length;+  ssize_t width = (ssize_t)info.dwSize.X;+  if (mode == 2) {+    // entire screen+    length = width * info.dwSize.Y;    +  }+  else if (mode == 1) {+    // to cursor+    length = (width * ((ssize_t)info.dwCursorPosition.Y - 1)) + info.dwCursorPosition.X;+  }+  else {+    // from cursor+    start  = info.dwCursorPosition;+    length = (width * ((ssize_t)info.dwSize.Y - info.dwCursorPosition.Y)) + (width - info.dwCursorPosition.X + 1);+  }+  DWORD written;+  FillConsoleOutputAttribute(term->hcon, term->hcon_default_attr, (DWORD)length, start, &written);+  FillConsoleOutputCharacterA(term->hcon, ' ', (DWORD)length, start, &written);+}++static WORD attr_color[8] = {+  0,                                  // black+  FOREGROUND_RED,                     // maroon+  FOREGROUND_GREEN,                   // green+  FOREGROUND_RED | FOREGROUND_GREEN,  // orange+  FOREGROUND_BLUE,                    // navy+  FOREGROUND_RED | FOREGROUND_BLUE,   // purple+  FOREGROUND_GREEN | FOREGROUND_BLUE, // teal+  FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE, // light gray+};++static void term_set_win_attr( term_t* term, attr_t ta ) {+  WORD def_attr = term->hcon_default_attr;+  CONSOLE_SCREEN_BUFFER_INFO info;+  if (!GetConsoleScreenBufferInfo( term->hcon, &info )) return;  +  WORD cur_attr = info.wAttributes;+  WORD attr = cur_attr; +  if (ta.x.color != IC_COLOR_NONE) {+    if (ta.x.color >= IC_ANSI_BLACK && ta.x.color <= IC_ANSI_SILVER) {+      attr = (attr & 0xFFF0) | attr_color[ta.x.color - IC_ANSI_BLACK];+    }+    else if (ta.x.color >= IC_ANSI_GRAY && ta.x.color <= IC_ANSI_WHITE) {+      attr = (attr & 0xFFF0) | attr_color[ta.x.color - IC_ANSI_GRAY] | FOREGROUND_INTENSITY;+    }+    else if (ta.x.color == IC_ANSI_DEFAULT) {+      attr = (attr & 0xFFF0) | (def_attr & 0x000F);+    }+  }+  if (ta.x.bgcolor != IC_COLOR_NONE) {+    if (ta.x.bgcolor >= IC_ANSI_BLACK && ta.x.bgcolor <= IC_ANSI_SILVER) {+      attr = (attr & 0xFF0F) | (WORD)(attr_color[ta.x.bgcolor - IC_ANSI_BLACK] << 4);+    }+    else if (ta.x.bgcolor >= IC_ANSI_GRAY && ta.x.bgcolor <= IC_ANSI_WHITE) {+      attr = (attr & 0xFF0F) | (WORD)(attr_color[ta.x.bgcolor - IC_ANSI_GRAY] << 4) | BACKGROUND_INTENSITY;+    } +    else if (ta.x.bgcolor == IC_ANSI_DEFAULT) {+      attr = (attr & 0xFF0F) | (def_attr & 0x00F0);+    }+  }+  if (ta.x.underline != IC_NONE) {+    attr = (attr & ~COMMON_LVB_UNDERSCORE) | (ta.x.underline == IC_ON ? COMMON_LVB_UNDERSCORE : 0);+  }+  if (ta.x.reverse != IC_NONE) {+    attr = (attr & ~COMMON_LVB_REVERSE_VIDEO) | (ta.x.reverse == IC_ON ? COMMON_LVB_REVERSE_VIDEO : 0);+  }  +  if (attr != cur_attr) {+    SetConsoleTextAttribute(term->hcon, attr);+  }+}++static ssize_t esc_param( const char* s, ssize_t def ) {+  if (*s == '?') s++;+  ssize_t n = def;+  ic_atoz(s, &n);+  return n;+}++static void esc_param2( const char* s, ssize_t* p1, ssize_t* p2, ssize_t def ) {+  if (*s == '?') s++; +  *p1 = def;+  *p2 = def;+  ic_atoz2(s, p1, p2);  +}++// Emulate escape sequences on older windows.+static void term_write_esc( term_t* term, const char* s, ssize_t len ) {+  ssize_t row;+  ssize_t col;++  if (s[1] == '[') {+    switch (s[len-1]) {+    case 'A':+      term_move_cursor(term, -1, 0, esc_param(s+2, 1));+      break;+    case 'B':+      term_move_cursor(term, 1, 0, esc_param(s+2, 1));+      break;+    case 'C':+      term_move_cursor(term, 0, 1, esc_param(s+2, 1));+      break;+    case 'D':+      term_move_cursor(term, 0, -1, esc_param(s+2, 1));+      break;+    case 'H': +      esc_param2(s+2, &row, &col, 1);+      term_move_cursor_to(term, row, col);+      break;+    case 'K':+      term_erase_line(term, esc_param(s+2, 0));+      break;+    case 'm': +      term_set_win_attr( term, attr_from_esc_sgr(s,len) ); +      break;++    // support some less standard escape codes (currently not used by isocline)+    case 'E':  // line down+      term_get_cursor_pos(term, &row, &col);+      row += esc_param(s+2, 1);+      term_move_cursor_to(term, row, 1);+      break;+    case 'F':  // line up+      term_get_cursor_pos(term, &row, &col);+      row -= esc_param(s+2, 1);+      term_move_cursor_to(term, row, 1);+      break;+    case 'G':  // absolute column+      term_get_cursor_pos(term, &row, &col);+      col = esc_param(s+2, 1);+      term_move_cursor_to(term, row, col);+      break;+    case 'J': +      term_clear_screen(term, esc_param(s+2, 0));+      break;+    case 'h':+      if (strncmp(s+2, "?25h", 4) == 0) {+        term_cursor_visible(term, true);+      }+      break;+    case 'l': +      if (strncmp(s+2, "?25l", 4) == 0) {+        term_cursor_visible(term, false);+      }+      break;+    case 's': +      term_cursor_save(term);+      break;    +    case 'u':+      term_cursor_restore(term);+      break;+    // otherwise ignore+    }+  }+  else if (s[1] == '7') {+    term_cursor_save(term);+  }+  else if (s[1] == '8') {+    term_cursor_restore(term);+  }+  else {+    // otherwise ignore+  }+}++static bool term_write_direct(term_t* term, const char* s, ssize_t len ) {+  term_cursor_visible(term,false); // reduce flicker+  ssize_t pos = 0;    +  if ((term->hcon_mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0) {+    // use the builtin virtual terminal processing. (enables truecolor for example)+    term_write_console(term, s, len);   +    pos = len;+  }+  else {+    // emulate escape sequences+    while( pos < len ) {+      // handle non-control in bulk (including utf-8 sequences)+      // (We don't need to handle utf-8 separately as we set the codepage to always be in utf-8 mode)+      ssize_t nonctrl = 0;+      ssize_t next;+      while( (next = str_next_ofs( s, len, pos+nonctrl, NULL )) > 0 && +              (uint8_t)s[pos + nonctrl] >= ' ' && (uint8_t)s[pos + nonctrl] <= 0x7F) {+        nonctrl += next;+      }+      if (nonctrl > 0) {+        term_write_console(term, s+pos, nonctrl);+        pos += nonctrl;+      }    +      if (next <= 0) break;++      if ((uint8_t)s[pos] >= 0x80) {+        // utf8 is already processed+        term_write_console(term, s+pos, next);+      }+      else if (next > 1 && s[pos] == '\x1B') {                                +        // handle control (note: str_next_ofs considers whole CSI escape sequences at a time)+        term_write_esc(term, s+pos, next);+      }+      else if (next == 1 && (s[pos] == '\r' || s[pos] == '\n' || s[pos] == '\t' || s[pos] == '\b')) {+        term_write_console( term, s+pos, next);+      }+      else {+        // ignore+      }+      pos += next;+    }+  }+  term_cursor_visible(term,true);+  assert(pos == len);+  return (pos == len); ++}+#endif++++//-------------------------------------------------------------+// Update terminal dimensions+//-------------------------------------------------------------++#if !defined(_WIN32)++// send escape query that may return a response on the tty+static bool term_esc_query_raw( term_t* term, const char* query, char* buf, ssize_t buflen ) +{+  if (buf==NULL || buflen <= 0 || query[0] == 0) return false;+  bool osc = (query[1] == ']');+  if (!term_write_direct(term, query, ic_strlen(query))) return false;+  debug_msg("term: read tty query response to: ESC %s\n", query + 1);  +  return tty_read_esc_response( term->tty, query[1], osc, buf, buflen );+}++static bool term_esc_query( term_t* term, const char* query, char* buf, ssize_t buflen ) +{+  if (!tty_start_raw(term->tty)) return false;  +  bool ok = term_esc_query_raw(term,query,buf,buflen);  +  tty_end_raw(term->tty);+  return ok;+}++// get the cursor position via an ESC[6n+static bool term_get_cursor_pos( term_t* term, ssize_t* row, ssize_t* col) +{+  // send escape query+  char buf[128];+  if (!term_esc_query(term,"\x1B[6n",buf,128)) return false; +  if (!ic_atoz2(buf,row,col)) return false;+  return true;+}++static void term_set_cursor_pos( term_t* term, ssize_t row, ssize_t col ) {+  term_writef( term, IC_CSI "%zd;%zdH", row, col );+}++ic_private bool term_update_dim(term_t* term) {  +  ssize_t cols = 0;+  ssize_t rows = 0;+  struct winsize ws;+  if (ioctl(term->fd_out, TIOCGWINSZ, &ws) >= 0) {+    // ioctl succeeded+    cols = ws.ws_col;  // debuggers return 0 for the column+    rows = ws.ws_row;+  }+  else {+    // determine width by querying the cursor position+    debug_msg("term: ioctl term-size failed: %d,%d\n", ws.ws_row, ws.ws_col);+    ssize_t col0 = 0;+    ssize_t row0 = 0;+    if (term_get_cursor_pos(term,&row0,&col0)) {+      term_set_cursor_pos(term,999,999);+      ssize_t col1 = 0;+      ssize_t row1 = 0;+      if (term_get_cursor_pos(term,&row1,&col1)) {+        cols = col1;+        rows = row1;+      }+      term_set_cursor_pos(term,row0,col0);+    }+    else {+      // cannot query position+      // return 0 column+    }+  }++  // update width and return whether it changed.+  bool changed = (term->width != cols || term->height != rows);+  debug_msg("terminal dim: %zd,%zd: %s\n", rows, cols, changed ? "changed" : "unchanged");  +  if (cols > 0) { +    term->width = cols;+    term->height = rows;+  }+  return changed;  +}++#else++ic_private bool term_update_dim(term_t* term) {+  if (term->hcon == 0) {+    term->hcon = GetConsoleWindow();+  }+  ssize_t rows = 0;+  ssize_t cols = 0;  +  CONSOLE_SCREEN_BUFFER_INFO sbinfo;  +  if (GetConsoleScreenBufferInfo(term->hcon, &sbinfo)) {+     cols = (ssize_t)sbinfo.srWindow.Right - (ssize_t)sbinfo.srWindow.Left + 1;+     rows = (ssize_t)sbinfo.srWindow.Bottom - (ssize_t)sbinfo.srWindow.Top + 1;+  }+  bool changed = (term->width != cols || term->height != rows);+  term->width = cols;+  term->height = rows;+  debug_msg("term: update dim: %zd, %zd\n", term->height, term->width );+  return changed;+}++#endif++++//-------------------------------------------------------------+// Enable/disable terminal raw mode+//-------------------------------------------------------------++#if !defined(_WIN32)++// On non-windows, the terminal is set in raw mode by the tty.++ic_private void term_start_raw(term_t* term) {+  term->raw_enabled++;+}++ic_private void term_end_raw(term_t* term, bool force) {+  if (term->raw_enabled <= 0) return;+  if (!force) {+    term->raw_enabled--;+  }+  else {+    term->raw_enabled = 0;+  }+}++static bool term_esc_query_color_raw(term_t* term, int color_idx, uint32_t* color ) {+  char buf[128+1];+  snprintf(buf,128,"\x1B]4;%d;?\x1B\\", color_idx);+  if (!term_esc_query_raw( term, buf, buf, 128 )) return false;+  if (buf[0] != '4') return false;+  const char* rgb = strchr(buf,':');+  if (rgb==NULL) return false;+  rgb++; // skip ':'+  unsigned int r,g,b;+  if (sscanf(rgb,"%x/%x/%x",&r,&g,&b) != 3) return false;+  if (rgb[2]!='/') { // 48-bit rgb, hexadecimal round to 24-bit     +    r = (r+0x7F)/0x100;   // note: can "overflow", e.g. 0xFFFF -> 0x100. (and we need `ic_cap8` to convert.)+    g = (g+0x7F)/0x100;+    b = (b+0x7F)/0x100; +  }+  *color = (ic_cap8(r)<<16) | (ic_cap8(g)<<8) | ic_cap8(b);+  debug_msg("color query: %02x,%02x,%02x: %06x\n", r, g, b, *color);  +  return true;+}++// update ansi 16 color palette for better color approximation+static void term_update_ansi16(term_t* term) {+  #if defined(GIO_CMAP)+  // try ioctl first (on Linux)+  uint8_t cmap[48];+  memset(cmap,0,48);+  if (ioctl(term->fd_out,GIO_CMAP,&cmap) >= 0) {+    // success+    for(ssize_t i = 0; i < 48; i+=3) {+      uint32_t color = ((uint32_t)(cmap[i]) << 16) | ((uint32_t)(cmap[i+1]) << 8) | cmap[i+2];+      debug_msg("term (ioctl) ansi color %d: 0x%06x\n", i, color);+      ansi256[i] = color;+    }+    return;+  }+  else {+    debug_msg("ioctl GIO_CMAP failed: entry 1: 0x%02x%02x%02x\n", cmap[3], cmap[4], cmap[5]);+  }+  #endif+  // otherwise use OSC 4 escape sequence query+  tty_start_raw(term->tty);+  for(ssize_t i = 0; i < 16; i++) {+    uint32_t color;+    if (!term_esc_query_color_raw(term, i, &color)) break;+    debug_msg("term ansi color %d: 0x%06x\n", i, color);+    ansi256[i] = color;+  }  +  tty_end_raw(term->tty);  +}++static void term_init_raw(term_t* term) {+  if (term->palette < ANSIRGB) {+    term_update_ansi16(term);+  }+}++#else++ic_private void term_start_raw(term_t* term) {+  if (term->raw_enabled++ > 0) return;  +  CONSOLE_SCREEN_BUFFER_INFO info;+  if (GetConsoleScreenBufferInfo(term->hcon, &info)) {+    term->hcon_orig_attr = info.wAttributes;+  }+  term->hcon_orig_cp = GetConsoleOutputCP();+  SetConsoleOutputCP(CP_UTF8);+  if (term->hcon_mode == 0) {+    // first time initialization+    DWORD mode = ENABLE_PROCESSED_OUTPUT | ENABLE_LVB_GRID_WORLDWIDE;   // for \r \n and \b    +    // use escape sequence handling if available and the terminal supports it (so we can use rgb colors in Windows terminal)+    // Unfortunately, in plain powershell, we can successfully enable terminal processing+    // but it still fails to render correctly; so we require the palette be large enough (like in Windows Terminal)+    if (term->palette >= ANSI256 && SetConsoleMode(term->hcon, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING)) {+      term->hcon_mode = mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING;+      debug_msg("term: console mode: virtual terminal processing enabled\n");+    }+    // no virtual terminal processing, emulate instead+    else if (SetConsoleMode(term->hcon, mode)) {+      term->hcon_mode = mode;+      term->palette = ANSI16;+    }+    GetConsoleMode(term->hcon, &mode);+    debug_msg("term: console mode: orig: 0x%x, new: 0x%x, current 0x%x\n", term->hcon_orig_mode, term->hcon_mode, mode);+  }+  else {+    SetConsoleMode(term->hcon, term->hcon_mode);+  }  +}++ic_private void term_end_raw(term_t* term, bool force) {+  if (term->raw_enabled <= 0) return;+  if (!force && term->raw_enabled > 1) {+    term->raw_enabled--;+  }+  else {+    term->raw_enabled = 0;+    SetConsoleMode(term->hcon, term->hcon_orig_mode);+    SetConsoleOutputCP(term->hcon_orig_cp);+    SetConsoleTextAttribute(term->hcon, term->hcon_orig_attr);+  }+}++static void term_init_raw(term_t* term) {+  term->hcon = GetStdHandle(STD_OUTPUT_HANDLE);+  GetConsoleMode(term->hcon, &term->hcon_orig_mode);+  CONSOLE_SCREEN_BUFFER_INFOEX info;+  memset(&info, 0, sizeof(info));+  info.cbSize = sizeof(info);+  if (GetConsoleScreenBufferInfoEx(term->hcon, &info)) {+    // store default attributes+    term->hcon_default_attr = info.wAttributes;+    // update our color table with the actual colors used.    +    for (unsigned i = 0; i < 16; i++) {+      COLORREF cr = info.ColorTable[i];+      uint32_t color = (ic_cap8(GetRValue(cr))<<16) | (ic_cap8(GetGValue(cr))<<8) | ic_cap8(GetBValue(cr)); // COLORREF = BGR+      // index is also in reverse in the bits 0 and 2 +      unsigned j = (i&0x08) | ((i&0x04)>>2) | (i&0x02) | (i&0x01)<<2;+      debug_msg("term: ansi color %d is 0x%06x\n", j, color);+      ansi256[j] = color;+    }    +  }+  else {+    DWORD err = GetLastError();+    debug_msg("term: cannot get console screen buffer: %d %x", err, err);+  }+  term_start_raw(term); // initialize the hcon_mode+  term_end_raw(term,false);+}++#endif
+ src/term.h view
@@ -0,0 +1,85 @@+/* ----------------------------------------------------------------------------+  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.+-----------------------------------------------------------------------------*/+#pragma once+#ifndef IC_TERM_H+#define IC_TERM_H++#include "common.h"+#include "tty.h"+#include "stringbuf.h"+#include "attr.h"++struct term_s;+typedef struct term_s term_t;++typedef enum buffer_mode_e {+  UNBUFFERED,+  LINEBUFFERED,+  BUFFERED,+} buffer_mode_t;++// Primitives+ic_private term_t* term_new(alloc_t* mem, tty_t* tty, bool nocolor, bool silent, int fd_out);+ic_private void term_free(term_t* term);++ic_private bool term_is_interactive(const term_t* term);+ic_private void term_start_raw(term_t* term);+ic_private void term_end_raw(term_t* term, bool force);++ic_private bool term_enable_beep(term_t* term, bool enable);+ic_private bool term_enable_color(term_t* term, bool enable);++ic_private void term_flush(term_t* term);+ic_private buffer_mode_t term_set_buffer_mode(term_t* term, buffer_mode_t mode);++ic_private void term_write_n(term_t* term, const char* s, ssize_t n);+ic_private void term_write(term_t* term, const char* s);+ic_private void term_writeln(term_t* term, const char* s);+ic_private void term_write_char(term_t* term, char c);++ic_private void term_write_repeat(term_t* term, const char* s, ssize_t count );+ic_private void term_beep(term_t* term);++ic_private bool term_update_dim(term_t* term);++ic_private ssize_t term_get_width(term_t* term);+ic_private ssize_t term_get_height(term_t* term);+ic_private int  term_get_color_bits(term_t* term);++// Helpers+ic_private void term_writef(term_t* term, const char* fmt, ...);+ic_private void term_vwritef(term_t* term, const char* fmt, va_list args);++ic_private void term_left(term_t* term, ssize_t n);+ic_private void term_right(term_t* term, ssize_t n);+ic_private void term_up(term_t* term, ssize_t n);+ic_private void term_down(term_t* term, ssize_t n);+ic_private void term_start_of_line(term_t* term );+ic_private void term_clear_line(term_t* term);+ic_private void term_clear_to_end_of_line(term_t* term);+// ic_private void term_clear_lines_to_end(term_t* term);+++ic_private void term_attr_reset(term_t* term);+ic_private void term_underline(term_t* term, bool on);+ic_private void term_reverse(term_t* term, bool on);+ic_private void term_bold(term_t* term, bool on);+ic_private void term_italic(term_t* term, bool on);++ic_private void term_color(term_t* term, ic_color_t color);+ic_private void term_bgcolor(term_t* term, ic_color_t color);++// Formatted output++ic_private attr_t term_get_attr( const term_t* term );+ic_private void   term_set_attr( term_t* term, attr_t attr );+ic_private void   term_write_formatted( term_t* term, const char* s, const attr_t* attrs );+ic_private void   term_write_formatted_n( term_t* term, const char* s, const attr_t* attrs, ssize_t n );++ic_private ic_color_t color_from_ansi256(ssize_t i);++#endif // IC_TERM_H
+ src/term_color.c view
@@ -0,0 +1,371 @@+/* ----------------------------------------------------------------------------+  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.+-----------------------------------------------------------------------------*/++// This file is included in "term.c"++//-------------------------------------------------------------+// Standard ANSI palette for 256 colors+//-------------------------------------------------------------++static uint32_t ansi256[256] = {   +  // not const as on some platforms (e.g. Windows, xterm) we update the first 16 entries with the actual used colors.+  // 0, standard ANSI+  0x000000, 0x800000, 0x008000, 0x808000, 0x000080, 0x800080, +  0x008080, 0xc0c0c0,+  // 8, bright ANSI+  0x808080, 0xff0000, 0x00ff00, 0xffff00, 0x0000ff, 0xff00ff, +  0x00ffff, 0xffffff,+  // 6x6x6 RGB colors+  // 16+  0x000000, 0x00005f, 0x000087, 0x0000af, 0x0000d7, 0x0000ff,+  0x005f00, 0x005f5f, 0x005f87, 0x005faf, 0x005fd7, 0x005fff,+  0x008700, 0x00875f, 0x008787, 0x0087af, 0x0087d7, 0x0087ff,+  0x00af00, 0x00af5f, 0x00af87, 0x00afaf, 0x00afd7, 0x00afff,+  0x00d700, 0x00d75f, 0x00d787, 0x00d7af, 0x00d7d7, 0x00d7ff,+  0x00ff00, 0x00ff5f, 0x00ff87, 0x00ffaf, 0x00ffd7, 0x00ffff,+  // 52+  0x5f0000, 0x5f005f, 0x5f0087, 0x5f00af, 0x5f00d7, 0x5f00ff,+  0x5f5f00, 0x5f5f5f, 0x5f5f87, 0x5f5faf, 0x5f5fd7, 0x5f5fff,+  0x5f8700, 0x5f875f, 0x5f8787, 0x5f87af, 0x5f87d7, 0x5f87ff,+  0x5faf00, 0x5faf5f, 0x5faf87, 0x5fafaf, 0x5fafd7, 0x5fafff,+  0x5fd700, 0x5fd75f, 0x5fd787, 0x5fd7af, 0x5fd7d7, 0x5fd7ff,+  0x5fff00, 0x5fff5f, 0x5fff87, 0x5fffaf, 0x5fffd7, 0x5fffff,+  // 88+  0x870000, 0x87005f, 0x870087, 0x8700af, 0x8700d7, 0x8700ff,+  0x875f00, 0x875f5f, 0x875f87, 0x875faf, 0x875fd7, 0x875fff,+  0x878700, 0x87875f, 0x878787, 0x8787af, 0x8787d7, 0x8787ff,+  0x87af00, 0x87af5f, 0x87af87, 0x87afaf, 0x87afd7, 0x87afff,+  0x87d700, 0x87d75f, 0x87d787, 0x87d7af, 0x87d7d7, 0x87d7ff,+  0x87ff00, 0x87ff5f, 0x87ff87, 0x87ffaf, 0x87ffd7, 0x87ffff,+  // 124+  0xaf0000, 0xaf005f, 0xaf0087, 0xaf00af, 0xaf00d7, 0xaf00ff,+  0xaf5f00, 0xaf5f5f, 0xaf5f87, 0xaf5faf, 0xaf5fd7, 0xaf5fff,+  0xaf8700, 0xaf875f, 0xaf8787, 0xaf87af, 0xaf87d7, 0xaf87ff,+  0xafaf00, 0xafaf5f, 0xafaf87, 0xafafaf, 0xafafd7, 0xafafff,+  0xafd700, 0xafd75f, 0xafd787, 0xafd7af, 0xafd7d7, 0xafd7ff,+  0xafff00, 0xafff5f, 0xafff87, 0xafffaf, 0xafffd7, 0xafffff,+  // 160+  0xd70000, 0xd7005f, 0xd70087, 0xd700af, 0xd700d7, 0xd700ff,+  0xd75f00, 0xd75f5f, 0xd75f87, 0xd75faf, 0xd75fd7, 0xd75fff,+  0xd78700, 0xd7875f, 0xd78787, 0xd787af, 0xd787d7, 0xd787ff,+  0xd7af00, 0xd7af5f, 0xd7af87, 0xd7afaf, 0xd7afd7, 0xd7afff,+  0xd7d700, 0xd7d75f, 0xd7d787, 0xd7d7af, 0xd7d7d7, 0xd7d7ff,+  0xd7ff00, 0xd7ff5f, 0xd7ff87, 0xd7ffaf, 0xd7ffd7, 0xd7ffff,+  // 196+  0xff0000, 0xff005f, 0xff0087, 0xff00af, 0xff00d7, 0xff00ff,+  0xff5f00, 0xff5f5f, 0xff5f87, 0xff5faf, 0xff5fd7, 0xff5fff,+  0xff8700, 0xff875f, 0xff8787, 0xff87af, 0xff87d7, 0xff87ff,+  0xffaf00, 0xffaf5f, 0xffaf87, 0xffafaf, 0xffafd7, 0xffafff,+  0xffd700, 0xffd75f, 0xffd787, 0xffd7af, 0xffd7d7, 0xffd7ff,+  0xffff00, 0xffff5f, 0xffff87, 0xffffaf, 0xffffd7, 0xffffff,+  // 232, gray scale+  0x080808, 0x121212, 0x1c1c1c, 0x262626, 0x303030, 0x3a3a3a, +  0x444444, 0x4e4e4e, 0x585858, 0x626262, 0x6c6c6c, 0x767676, +  0x808080, 0x8a8a8a, 0x949494, 0x9e9e9e, 0xa8a8a8, 0xb2b2b2, +  0xbcbcbc, 0xc6c6c6, 0xd0d0d0, 0xdadada, 0xe4e4e4, 0xeeeeee   +};+++//-------------------------------------------------------------+// Create colors+//-------------------------------------------------------------++// Create a color from a 24-bit color value.+ic_private ic_color_t ic_rgb(uint32_t hex) {+  return (ic_color_t)(0x1000000 | (hex & 0xFFFFFF));+}++// Limit an int to values between 0 and 255.+static uint32_t ic_cap8(ssize_t i) {+  return (i < 0 ? 0 : (i > 255 ? 255 : (uint32_t)i));+}++// Create a color from a 24-bit color value.+ic_private ic_color_t ic_rgbx(ssize_t r, ssize_t g, ssize_t b) {+  return ic_rgb( (ic_cap8(r)<<16) | (ic_cap8(g)<<8) | ic_cap8(b) );+}+++//-------------------------------------------------------------+// Match an rgb color to a ansi8, ansi16, or ansi256+//-------------------------------------------------------------++static bool color_is_rgb( ic_color_t color ) {+  return (color >= IC_RGB(0));  // bit 24 is set for rgb colors+}++static void color_to_rgb(ic_color_t color, int* r, int* g, int* b) {+  assert(color_is_rgb(color));+  *r = ((color >> 16) & 0xFF);+  *g = ((color >> 8) & 0xFF);+  *b = (color & 0xFF);+}++ic_private ic_color_t color_from_ansi256(ssize_t i) {+  if (i >= 0 && i < 8) {+    return (IC_ANSI_BLACK + (uint32_t)i);+  }+  else if (i >= 8 && i < 16) {+    return (IC_ANSI_DARKGRAY + (uint32_t)(i - 8));+  }+  else if (i >= 16 && i <= 255) {+    return ic_rgb( ansi256[i] );+  }+  else if (i == 256) {+    return IC_ANSI_DEFAULT;+  }+  else {+    return IC_ANSI_DEFAULT;+  }+}++static bool is_grayish(int r, int g, int b) {+  return (abs(r-g) <= 4) && (abs((r+g)/2 - b) <= 4);+}++static bool is_grayish_color( uint32_t rgb ) {+  int r, g, b;+  color_to_rgb(IC_RGB(rgb),&r,&g,&b);+  return is_grayish(r,g,b);+}++static int_least32_t sqr(int_least32_t x) {+  return x*x;+}++// Approximation to delta-E CIE color distance using much +// simpler calculations. See <https://www.compuphase.com/cmetric.htm>.+// This is essentialy weighted euclidean distance but the weight distribution+// depends on how big the "red" component of the color is.+// We do not take the square root as we only need to find +// the minimal distance (and multiply by 256 to increase precision).+// Needs at least 28-bit signed integers to avoid overflow. +static int_least32_t rgb_distance_rmean( uint32_t color, int r2, int g2, int b2 ) {+  int r1, g1, b1;+  color_to_rgb(IC_RGB(color),&r1,&g1,&b1);+  int_least32_t rmean = (r1 + r2) / 2;+  int_least32_t dr2 = sqr(r1 - r2);+  int_least32_t dg2 = sqr(g1 - g2);+  int_least32_t db2 = sqr(b1 - b2);+  int_least32_t dist = ((512+rmean)*dr2) + 1024*dg2 + ((767-rmean)*db2);    +  return dist;+}++// Another approximation to delta-E CIE color distance using+// simpler calculations. Similar to `rmean` but adds an adjustment factor+// based on the "red/blue" difference.+static int_least32_t rgb_distance_rbmean( uint32_t color, int r2, int g2, int b2 ) {+  int r1, g1, b1;+  color_to_rgb(IC_RGB(color),&r1,&g1,&b1);+  int_least32_t rmean = (r1 + r2) / 2;+  int_least32_t dr2 = sqr(r1 - r2);+  int_least32_t dg2 = sqr(g1 - g2);+  int_least32_t db2 = sqr(b1 - b2);+  int_least32_t dist = 2*dr2 + 4*dg2 + 3*db2 + ((rmean*(dr2 - db2))/256);  +  return dist;+}+++// Maintain a small cache of recently used colors. Should be short enough to be effectively constant time.+// If we ever use a more expensive color distance method, we may increase the size a bit (64?) +// (Initial zero initialized cache is valid.)+#define RGB_CACHE_LEN (16)+typedef struct rgb_cache_s {+  int        last;+  int        indices[RGB_CACHE_LEN];+  ic_color_t colors[RGB_CACHE_LEN];+} rgb_cache_t;++// remember a color in the LRU cache+void rgb_remember( rgb_cache_t* cache, ic_color_t color, int idx ) {+  if (cache == NULL) return;+  cache->colors[cache->last] = color;+  cache->indices[cache->last] = idx;+  cache->last++;+  if (cache->last >= RGB_CACHE_LEN) { cache->last = 0; }+}++// quick lookup in cache; -1 on failure+int rgb_lookup( const rgb_cache_t* cache, ic_color_t color ) {+  if (cache != NULL) {+    for(int i = 0; i < RGB_CACHE_LEN; i++) {+      if (cache->colors[i] == color) return cache->indices[i];+    }+  }+  return -1;+}++// return the index of the closest matching color+static int rgb_match( uint32_t* palette, int start, int len, rgb_cache_t* cache, ic_color_t color ) {+  assert(color_is_rgb(color));+  // in cache?+  int min = rgb_lookup(cache,color);+  if (min >= 0) {+    return min;+  }+  // otherwise find closest color match in the palette+  int r, g, b;+  color_to_rgb(color,&r,&g,&b);+  min = start;+  int_least32_t mindist = (INT_LEAST32_MAX)/4;+  for(int i = start; i < len; i++) {+    //int_least32_t dist = rgb_distance_rbmean(palette[i],r,g,b);+    int_least32_t dist = rgb_distance_rmean(palette[i],r,g,b);+    if (is_grayish_color(palette[i]) != is_grayish(r, g, b)) { +      // with few colors, make it less eager to substitute a gray for a non-gray (or the other way around)+      if (len <= 16) {+        dist *= 4;+      } +      else {+        dist = (dist/4)*5;+      }+    } +    if (dist < mindist) {+      min = i;+      mindist = dist;+    }+  }+  rgb_remember(cache,color,min);+  return min;+}+++// Match RGB to an index in the ANSI 256 color table+static int rgb_to_ansi256(ic_color_t color) {+  static rgb_cache_t ansi256_cache;+  int c = rgb_match(ansi256, 16, 256, &ansi256_cache, color); // not the first 16 ANSI colors as those may be different +  //debug_msg("term: rgb %x -> ansi 256: %d\n", color, c );+  return c;+}++// Match RGB to an ANSI 16 color code (30-37, 90-97)+static int color_to_ansi16(ic_color_t color) {+  if (!color_is_rgb(color)) {+    return (int)color;+  }+  else {+    static rgb_cache_t ansi16_cache;+    int c = rgb_match(ansi256, 0, 16, &ansi16_cache, color);+    //debug_msg("term: rgb %x -> ansi 16: %d\n", color, c );+    return (c < 8 ? 30 + c : 90 + c - 8); +  }+}++// Match RGB to an ANSI 16 color code (30-37, 90-97)+// but assuming the bright colors are simulated using 'bold'.+static int color_to_ansi8(ic_color_t color) {+  if (!color_is_rgb(color)) {+    return (int)color;+  }+  else {+    // match to basic 8 colors first+    static rgb_cache_t ansi8_cache;+    int c = 30 + rgb_match(ansi256, 0, 8, &ansi8_cache, color);+    // and then adjust for brightness+    int r, g, b;+    color_to_rgb(color,&r,&g,&b);+    if (r>=196 || g>=196 || b>=196) c += 60;+    //debug_msg("term: rgb %x -> ansi 8: %d\n", color, c );+    return c;+  }+}+++//-------------------------------------------------------------+// Emit color escape codes based on the terminal capability+//-------------------------------------------------------------++static void fmt_color_ansi8( char* buf, ssize_t len, ic_color_t color, bool bg ) {+  int c = color_to_ansi8(color) + (bg ? 10 : 0);+  if (c >= 90) {+    snprintf(buf, to_size_t(len), IC_CSI "1;%dm", c - 60);    +  }+  else {+    snprintf(buf, to_size_t(len), IC_CSI "22;%dm", c );  +  }+}++static void fmt_color_ansi16( char* buf, ssize_t len, ic_color_t color, bool bg ) {+  snprintf( buf, to_size_t(len), IC_CSI "%dm", color_to_ansi16(color) + (bg ? 10 : 0) );  +}++static void fmt_color_ansi256( char* buf, ssize_t len,  ic_color_t color, bool bg ) {+  if (!color_is_rgb(color)) {+    fmt_color_ansi16(buf,len,color,bg);+  }+  else {+    snprintf( buf, to_size_t(len), IC_CSI "%d;5;%dm", (bg ? 48 : 38), rgb_to_ansi256(color) );  +  }+}++static void fmt_color_rgb( char* buf, ssize_t len, ic_color_t color, bool bg ) {+  if (!color_is_rgb(color)) {+    fmt_color_ansi16(buf,len,color,bg);+  }+  else {+    int r,g,b;+    color_to_rgb(color, &r,&g,&b);+    snprintf( buf, to_size_t(len), IC_CSI "%d;2;%d;%d;%dm", (bg ? 48 : 38), r, g, b );  +  }+}++static void fmt_color_ex(char* buf, ssize_t len, palette_t palette, ic_color_t color, bool bg) {+  if (color == IC_COLOR_NONE || palette == MONOCHROME) return;+  if (palette == ANSI8) {+    fmt_color_ansi8(buf,len,color,bg);+  }+  else if (!color_is_rgb(color) || palette == ANSI16) {+    fmt_color_ansi16(buf,len,color,bg);+  }+  else if (palette == ANSI256) {+    fmt_color_ansi256(buf,len,color,bg);+  }+  else {+    fmt_color_rgb(buf,len,color,bg);+  }+}++static void term_color_ex(term_t* term, ic_color_t color, bool bg) {+  char buf[128+1];+  fmt_color_ex(buf,128,term->palette,color,bg);+  term_write(term,buf);+}++//-------------------------------------------------------------+// Main API functions+//-------------------------------------------------------------++ic_private void term_color(term_t* term, ic_color_t color) {+  term_color_ex(term,color,false);+}++ic_private void term_bgcolor(term_t* term, ic_color_t color) {+  term_color_ex(term,color,true);+}++ic_private void term_append_color(term_t* term, stringbuf_t* sbuf, ic_color_t color) {+  char buf[128+1];+  fmt_color_ex(buf,128,term->palette,color,false);+  sbuf_append(sbuf,buf);+}++ic_private void term_append_bgcolor(term_t* term, stringbuf_t* sbuf, ic_color_t color) {+  char buf[128+1];+  fmt_color_ex(buf, 128, term->palette, color, true);+  sbuf_append(sbuf, buf);+}++ic_private int term_get_color_bits(term_t* term) {+  switch (term->palette) {+  case MONOCHROME: return 1;+  case ANSI8:      return 3;+  case ANSI16:     return 4;+  case ANSI256:    return 8;+  case ANSIRGB:    return 24;+  default:         return 4;+  }+}
+ src/tty.c view
@@ -0,0 +1,881 @@+/* ----------------------------------------------------------------------------+  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.+-----------------------------------------------------------------------------*/+#include <string.h>+#include <stdbool.h>+#include <stdio.h>+#include <stdarg.h>+#include <locale.h>++#include "tty.h"++#if defined(_WIN32)+#include <windows.h>+#include <io.h>+#define isatty(fd)     _isatty(fd)+#define read(fd,s,n)   _read(fd,s,n)+#define STDIN_FILENO   0+#else+#include <signal.h>+#include <errno.h>+#include <unistd.h>+#include <termios.h>+#include <sys/ioctl.h>+#include <sys/select.h>+#if !defined(FIONREAD)+#include <fcntl.h>+#endif+#endif++#define TTY_PUSH_MAX (32)++struct tty_s {+  int       fd_in;                  // input handle+  bool      raw_enabled;            // is raw mode enabled?+  bool      is_utf8;                // is the input stream in utf-8 mode?+  bool      has_term_resize_event;  // are resize events generated?+  bool      term_resize_event;      // did a term resize happen?+  alloc_t*  mem;                    // memory allocator+  code_t    pushbuf[TTY_PUSH_MAX];  // push back buffer for full key codes  +  ssize_t   push_count;               +  uint8_t   cpushbuf[TTY_PUSH_MAX]; // low level push back buffer for bytes+  ssize_t   cpush_count;+  long      esc_initial_timeout;    // initial ms wait to see if ESC starts an escape sequence+  long      esc_timeout;            // follow up delay for characters in an escape sequence+  #if defined(_WIN32)               +  HANDLE    hcon;                   // console input handle+  DWORD     hcon_orig_mode;         // original console mode+  #else+  struct termios  orig_ios;         // original terminal settings+  struct termios  raw_ios;          // raw terminal settings+  #endif+};+++//-------------------------------------------------------------+// Forward declarations of platform dependent primitives below+//-------------------------------------------------------------++ic_private bool tty_readc_noblock(tty_t* tty, uint8_t* c, long timeout_ms);  // does not modify `c` when no input (false is returned)++//-------------------------------------------------------------+// Key code helpers+//-------------------------------------------------------------++ic_private bool code_is_ascii_char(code_t c, char* chr ) {+  if (c >= ' ' && c <= 0x7F) {+    if (chr != NULL) *chr = (char)c;+    return true;+  }+  else {+    if (chr != NULL) *chr = 0;+    return false;+  }+}++ic_private bool code_is_unicode(code_t c, unicode_t* uchr) {+  if (c <= KEY_UNICODE_MAX) {+    if (uchr != NULL) *uchr = c;+    return true;+  }+  else {+    if (uchr != NULL) *uchr = 0;+    return false;+  }+}++ic_private bool code_is_virt_key(code_t c ) {+  return (KEY_NO_MODS(c) <= 0x20 || KEY_NO_MODS(c) >= KEY_VIRT);+}+++//-------------------------------------------------------------+// Read a key code+//-------------------------------------------------------------+static code_t modify_code( code_t code );++static code_t tty_read_utf8( tty_t* tty, uint8_t c0 ) {+  uint8_t buf[5];+  memset(buf, 0, 5);++  // try to read as many bytes as potentially needed+  buf[0] = c0;+  ssize_t count = 1;+  if (c0 > 0x7F) {+    if (tty_readc_noblock(tty, buf+count, tty->esc_timeout)) {+      count++;+      if (c0 > 0xDF) {+        if (tty_readc_noblock(tty, buf+count, tty->esc_timeout)) {+          count++;+          if (c0 > 0xEF) {+            if (tty_readc_noblock(tty, buf+count, tty->esc_timeout)) {+              count++;+            }+          }+        }+      }+    }+  }+  +  buf[count] = 0;+  debug_msg("tty: read utf8: count: %zd: %02x,%02x,%02x,%02x", count, buf[0], buf[1], buf[2], buf[3]);++  // decode the utf8 to unicode+  ssize_t read = 0;+  code_t code = key_unicode(unicode_from_qutf8(buf, count, &read));++  // push back unused bytes (in the case of invalid utf8)+  while (count > read) {+    count--;+    if (count >= 0 && count <= 4) {  // to help the static analyzer+      tty_cpush_char(tty, buf[count]);+    }+  }+  return code;+}++// pop a code from the pushback buffer.+static bool tty_code_pop(tty_t* tty, code_t* code);+++// read a single char/key +ic_private bool tty_read_timeout(tty_t* tty, long timeout_ms, code_t* code) +{+  // is there a push_count back code?+  if (tty_code_pop(tty,code)) {+    return code;+  }++  // read a single char/byte from a character stream+  uint8_t c;+  if (!tty_readc_noblock(tty, &c, timeout_ms)) return false;+  +  if (c == KEY_ESC) {+    // escape sequence?+    *code = tty_read_esc(tty, tty->esc_initial_timeout, tty->esc_timeout);+  }+  else if (c <= 0x7F) {+    // ascii+    *code = key_unicode(c);+  }+  else if (tty->is_utf8) {+    // utf8 sequence+    *code = tty_read_utf8(tty,c);+  }+  else {+    // c >= 0x80 but tty is not utf8; use raw plane so we can translate it back in the end+    *code = key_unicode( unicode_from_raw(c) );+  }++  *code = modify_code(*code);+  return true;+}++// Transform virtual keys to be more portable across platforms+static code_t modify_code( code_t code ) {+  code_t key  = KEY_NO_MODS(code);+  code_t mods = KEY_MODS(code);+  debug_msg( "tty: readc %s%s%s 0x%03x ('%c')\n", +              mods&KEY_MOD_SHIFT ? "shift+" : "",  mods&KEY_MOD_CTRL  ? "ctrl+" : "", mods&KEY_MOD_ALT   ? "alt+" : "",+              key, (key >= ' ' && key <= '~' ? key : ' '));++  // treat KEY_RUBOUT (0x7F) as KEY_BACKSP+  if (key == KEY_RUBOUT) {+    code = KEY_BACKSP | mods;+  }+  // ctrl+'_' is translated to '\x1F' on Linux, translate it back +  else if (key == key_char('\x1F') && (mods & KEY_MOD_ALT) == 0) {+    key = '_';+    code = WITH_CTRL(key_char('_'));+  }+  // treat ctrl/shift + enter always as KEY_LINEFEED for portability+  else if (key == KEY_ENTER && (mods == KEY_MOD_SHIFT || mods == KEY_MOD_ALT || mods == KEY_MOD_CTRL)) {+    code = KEY_LINEFEED;+  }+  // treat ctrl+tab always as shift+tab for portability+  else if (code == WITH_CTRL(KEY_TAB)) {+    code = KEY_SHIFT_TAB;+  }+  // treat ctrl+end/alt+>/alt-down and ctrl+home/alt+</alt-up always as pagedown/pageup for portability+  else if (code == WITH_ALT(KEY_DOWN) || code == WITH_ALT('>') || code == WITH_CTRL(KEY_END)) {+    code = KEY_PAGEDOWN;+  }+  else if (code == WITH_ALT(KEY_UP) || code == WITH_ALT('<') || code == WITH_CTRL(KEY_HOME)) {+    code = KEY_PAGEUP;+  }+  +  // treat C0 codes without KEY_MOD_CTRL+  if (key < ' ' && (mods&KEY_MOD_CTRL) != 0) {+    code &= ~KEY_MOD_CTRL; +  }+  +  return code;+}+++// read a single char/key +ic_private code_t tty_read(tty_t* tty)+{+  code_t code;+  if (!tty_read_timeout(tty, -1, &code)) return KEY_NONE;+  return code;+}++//-------------------------------------------------------------+// Read back an ANSI query response+//-------------------------------------------------------------++ic_private bool tty_read_esc_response(tty_t* tty, char esc_start, bool final_st, char* buf, ssize_t buflen ) +{+  buf[0] = 0;+  ssize_t len = 0;+  uint8_t c = 0;+  if (!tty_readc_noblock(tty, &c, tty->esc_initial_timeout) || c != '\x1B') return false;+  if (!tty_readc_noblock(tty, &c, tty->esc_timeout) || (c != esc_start)) return false;+  while( len < buflen ) {+    if (!tty_readc_noblock(tty, &c, tty->esc_timeout)) return false;+    if (final_st) {+      // OSC is terminated by BELL, or ESC \ (ST)  (and STX)+      if (c=='\x07' || c=='\x02') {+        break;+      }+      else if (c=='\x1B') {+        uint8_t c1;+        if (!tty_readc_noblock(tty, &c1, tty->esc_timeout)) return false;+        if (c1=='\\') break;+        tty_cpush_char(tty,c1);+      }+    }+    else {+      if (c == '\x02') { // STX+        break;+      }+      else if (!((c >= '0' && c <= '9') || strchr("<=>?;:",c) != NULL)) {+        buf[len++] = (char)c; // for non-OSC save the terminating character+        break;+      }+    }+    buf[len++] = (char)c; +  }+  buf[len] = 0;+  debug_msg("tty: escape query response: %s\n", buf);+  return true;+}++//-------------------------------------------------------------+// High level code pushback+//-------------------------------------------------------------++static bool tty_code_pop( tty_t* tty, code_t* code ) {+  if (tty->push_count <= 0) return false;+  tty->push_count--;+  *code = tty->pushbuf[tty->push_count];+  return true;+}++ic_private void tty_code_pushback( tty_t* tty, code_t c ) {   +  // note: must be signal safe+  if (tty->push_count >= TTY_PUSH_MAX) return;+  tty->pushbuf[tty->push_count] = c;+  tty->push_count++;+}+++//-------------------------------------------------------------+// low-level character pushback (for escape sequences and windows)+//-------------------------------------------------------------++ic_private bool tty_cpop(tty_t* tty, uint8_t* c) {  +  if (tty->cpush_count <= 0) {  // do not modify c on failure (see `tty_decode_unicode`)+    return false;+  }+  else {+    tty->cpush_count--;+    *c = tty->cpushbuf[tty->cpush_count];+    return true;+  }+}++static void tty_cpush(tty_t* tty, const char* s) {+  ssize_t len = ic_strlen(s);+  if (tty->push_count + len > TTY_PUSH_MAX) {+    debug_msg("tty: cpush buffer full! (pushing %s)\n", s);+    assert(false);+    return;+  }+  for (ssize_t i = 0; i < len; i++) {+    tty->cpushbuf[tty->cpush_count + i] = (uint8_t)( s[len - i - 1] );+  }+  tty->cpush_count += len;+  return;+}++// convenience function for small sequences+static void tty_cpushf(tty_t* tty, const char* fmt, ...) {+  va_list args;+  va_start(args,fmt);+  char buf[TTY_PUSH_MAX+1];+  vsnprintf(buf,TTY_PUSH_MAX,fmt,args);+  buf[TTY_PUSH_MAX] = 0;+  tty_cpush(tty,buf);+  va_end(args);+  return;+}++ic_private void tty_cpush_char(tty_t* tty, uint8_t c) {  +  uint8_t buf[2];+  buf[0] = c;+  buf[1] = 0;+  tty_cpush(tty, (const char*)buf);+}+++//-------------------------------------------------------------+// Push escape codes (used on Windows to insert keys)+//-------------------------------------------------------------++static unsigned csi_mods(code_t mods) {+  unsigned m = 1;+  if (mods&KEY_MOD_SHIFT) m += 1;+  if (mods&KEY_MOD_ALT)   m += 2;+  if (mods&KEY_MOD_CTRL)  m += 4;+  return m;+}++// Push ESC [ <vtcode> ; <mods> ~+static void tty_cpush_csi_vt( tty_t* tty, code_t mods, uint32_t vtcode ) {+  tty_cpushf(tty,"\x1B[%u;%u~", vtcode, csi_mods(mods) );+}++// push ESC [ 1 ; <mods> <xcmd>+static void tty_cpush_csi_xterm( tty_t* tty, code_t mods, char xcode ) {+  tty_cpushf(tty,"\x1B[1;%u%c", csi_mods(mods), xcode );+}++// push ESC [ <unicode> ; <mods> u+static void tty_cpush_csi_unicode( tty_t* tty, code_t mods, uint32_t unicode ) {+  if ((unicode < 0x80 && mods == 0) || +      (mods == KEY_MOD_CTRL && unicode < ' ' && unicode != KEY_TAB && unicode != KEY_ENTER +                        && unicode != KEY_LINEFEED && unicode != KEY_BACKSP) ||+      (mods == KEY_MOD_SHIFT && unicode >= ' ' && unicode <= KEY_RUBOUT)) {+    tty_cpush_char(tty,(uint8_t)unicode);+  }+  else {+    tty_cpushf(tty,"\x1B[%u;%uu", unicode, csi_mods(mods) );+  }+}++//-------------------------------------------------------------+// Init+//-------------------------------------------------------------++static bool tty_init_raw(tty_t* tty);+static void tty_done_raw(tty_t* tty);++static bool tty_init_utf8(tty_t* tty) {+  #ifdef _WIN32+  tty->is_utf8 = true;+  #else+  const char* loc = setlocale(LC_ALL,"");+  tty->is_utf8 = (ic_icontains(loc,"UTF-8") || ic_icontains(loc,"utf8") || ic_stricmp(loc,"C") == 0);+  debug_msg("tty: utf8: %s (loc=%s)\n", tty->is_utf8 ? "true" : "false", loc);+  #endif+  return true;+}++ic_private tty_t* tty_new(alloc_t* mem, int fd_in) +{+  tty_t* tty = mem_zalloc_tp(mem, tty_t);+  tty->mem = mem;+  tty->fd_in = (fd_in < 0 ? STDIN_FILENO : fd_in);+  #if defined(__APPLE__)+  tty->esc_initial_timeout = 200;  // apple use ESC+<key> for alt-<key>+  #else+  tty->esc_initial_timeout = 100; +  #endif+  tty->esc_timeout = 10;+  if (!(isatty(tty->fd_in) && tty_init_raw(tty) && tty_init_utf8(tty))) {+    tty_free(tty);+    return NULL;+  }+  return tty;+}++ic_private void tty_free(tty_t* tty) {+  if (tty==NULL) return;+  tty_end_raw(tty);+  tty_done_raw(tty);+  mem_free(tty->mem,tty);+}++ic_private bool tty_is_utf8(const tty_t* tty) {+  if (tty == NULL) return true;+  return (tty->is_utf8);+}++ic_private bool tty_term_resize_event(tty_t* tty) {+  if (tty == NULL) return true;+  if (tty->has_term_resize_event) {+    if (!tty->term_resize_event) return false;+    tty->term_resize_event = false;  // reset.   +  }+  return true;  // always return true on systems without a resize event (more expensive but still ok)+}++ic_private void tty_set_esc_delay(tty_t* tty, long initial_delay_ms, long followup_delay_ms) {+  tty->esc_initial_timeout = (initial_delay_ms < 0 ? 0 : (initial_delay_ms > 1000 ? 1000 : initial_delay_ms));+  tty->esc_timeout = (followup_delay_ms < 0 ? 0 : (followup_delay_ms > 1000 ? 1000 : followup_delay_ms));+}++//-------------------------------------------------------------+// Unix+//-------------------------------------------------------------+#if !defined(_WIN32)++static bool tty_readc_blocking(tty_t* tty, uint8_t* c) {+  if (tty_cpop(tty,c)) return true;+  *c = 0;+  ssize_t nread = read(tty->fd_in, (char*)c, 1);+  if (nread < 0 && errno == EINTR) {+    // can happen on SIGWINCH signal for terminal resize+  }+  return (nread == 1);+}+++// non blocking read -- with a small timeout used for reading escape sequences.+ic_private bool tty_readc_noblock(tty_t* tty, uint8_t* c, long timeout_ms) +{+  // in our pushback buffer?+  if (tty_cpop(tty, c)) return true;++  // blocking read?+  if (timeout_ms < 0) {+    return tty_readc_blocking(tty,c);+  }++  // if supported, peek first if any char is available.+  #if defined(FIONREAD)+  { int navail = 0;+    if (ioctl(0, FIONREAD, &navail) == 0) {+      if (navail >= 1) {+        return tty_readc_blocking(tty, c);+      }+      else if (timeout_ms == 0) {+        return false;  // return early if there is no input available (with a zero timeout)+      }+    }+  }+  #endif++  // otherwise block for at most timeout milliseconds+  #if defined(FD_SET)   +    // we can use select to detect when input becomes available+    fd_set readset;+    struct timeval time;+    FD_ZERO(&readset);+    FD_SET(tty->fd_in, &readset);+    time.tv_sec  = (timeout_ms > 0 ? timeout_ms / 1000 : 0);+    time.tv_usec = (timeout_ms > 0 ? 1000*(timeout_ms % 1000) : 0);      +    if (select(tty->fd_in + 1, &readset, NULL, NULL, &time) == 1) {+      // input available+      return tty_readc_blocking(tty, c);+    }    +  #else+    // no select, we cannot timeout; use usleeps :-(+    // todo: this seems very rare nowadays; should be even support this?+    do {+      // peek ahead if possible+      #if defined(FIONREAD)+      int navail = 0;+      if (ioctl(0, FIONREAD, &navail) == 0 && navail >= 1) {+        return tty_readc_blocking(tty, c);+      }+      #elif defined(O_NONBLOCK)+      // use a temporary non-blocking read mode+      int fstatus = fcntl(tty->fd_in, F_GETFL, 0);+      if (fstatus != -1) {+        if (fcntl(tty->fd_in, F_SETFL, (fstatus | O_NONBLOCK)) != -1) {+          char buf[2] = { 0, 0 };+          ssize_t nread = read(tty->fd_in, buf, 1);+          fcntl(tty->fd_in, F_SETFL, fstatus);+          if (nread >= 1) {+            *c = (uint8_t)buf[0];+            return true;+          }+        }+      }+      #else+      #error "define an nonblocking read for this platform"+      #endif+      // and sleep a bit+      if (timeout_ms > 0) {+        usleep(50*1000L); // sleep at most 0.05s at a time+        timeout_ms -= 100;+        if (timeout_ms < 0) { timeout_ms = 0; }+      }      +    } +    while (timeout_ms > 0);+  #endif  +  return false;+}++#if defined(TIOCSTI) +ic_private bool tty_async_stop(const tty_t* tty) {+  // insert ^C in the input stream+  char c = KEY_CTRL_C;+  return (ioctl(tty->fd_in, TIOCSTI, &c) >= 0);+}+#else+ic_private bool tty_async_stop(const tty_t* tty) {+  return false;+}+#endif++// We install various signal handlers to restore the terminal settings+// in case of a terminating signal. This is also used to catch terminal window resizes.+// This is not strictly needed so this can be disabled on +// (older) platforms that do not support signal handling well.+#if defined(SIGWINCH) && defined(SA_RESTART)  // ensure basic signal functionality is defined++// store the tty in a global so we access it on unexpected termination+static tty_t* sig_tty; // = NULL++// Catch all termination signals (and SIGWINCH)+typedef struct signal_handler_s {+  int signum;+  union {+    int _avoid_warning;+    struct sigaction previous;+  } action;+} signal_handler_t;++static signal_handler_t sighandlers[] = {+  { SIGWINCH, {0} },+  { SIGTERM , {0} }, +  { SIGINT  , {0} }, +  { SIGQUIT , {0} }, +  { SIGHUP  , {0} },+  { SIGSEGV , {0} }, +  { SIGTRAP , {0} }, +  { SIGBUS  , {0} }, +  { SIGTSTP , {0} },+  { SIGTTIN , {0} }, +  { SIGTTOU , {0} },+  { 0       , {0} }+};++static bool sigaction_is_valid( struct sigaction* sa ) {+  return (sa->sa_sigaction != NULL && sa->sa_handler != SIG_DFL && sa->sa_handler != SIG_IGN);+}++// Generic signal handler+static void sig_handler(int signum, siginfo_t* siginfo, void* uap ) {+  if (signum == SIGWINCH) {+    if (sig_tty != NULL) {+      sig_tty->term_resize_event = true;+    }+  }+  else {+    // the rest are termination signals; restore the terminal mode. (`tcsetattr` is signal-safe)+    if (sig_tty != NULL && sig_tty->raw_enabled) {+      tcsetattr(sig_tty->fd_in, TCSAFLUSH, &sig_tty->orig_ios);+      sig_tty->raw_enabled = false;+    }+  }+  // call previous handler+  signal_handler_t* sh = sighandlers;+  while( sh->signum != 0 && sh->signum != signum) { sh++; }+  if (sh->signum == signum) {+    if (sigaction_is_valid(&sh->action.previous)) {+      (sh->action.previous.sa_sigaction)(signum, siginfo, uap);+    }+  }+}++static void signals_install(tty_t* tty) {+  sig_tty = tty;+  // generic signal handler+  struct sigaction handler;+  memset(&handler,0,sizeof(handler));+  sigemptyset(&handler.sa_mask);+  handler.sa_sigaction = &sig_handler; +  handler.sa_flags = SA_RESTART;+  // install for all signals+  for( signal_handler_t* sh = sighandlers; sh->signum != 0; sh++ ) {+    if (sigaction( sh->signum, NULL, &sh->action.previous) == 0) {            // get previous+      if (sh->action.previous.sa_handler != SIG_IGN) {                        // if not to be ignored+        if (sigaction( sh->signum, &handler, &sh->action.previous ) < 0) {    // install our handler+          sh->action.previous.sa_sigaction = NULL;       // do not restore on error+        }+        else if (sh->signum == SIGWINCH) {+          sig_tty->has_term_resize_event = true;+        };+      }+    }    +  }+}++static void signals_restore(void) {+  // restore all signal handlers+  for( signal_handler_t* sh = sighandlers; sh->signum != 0; sh++ ) {+    if (sigaction_is_valid(&sh->action.previous)) {+      sigaction( sh->signum, &sh->action.previous, NULL );+    };+  }+  sig_tty = NULL;+}++#else+static void signals_install(tty_t* tty) {+  ic_unused(tty);+  // nothing+}+static void signals_restore(void) {+  // nothing+}++#endif++ic_private bool tty_start_raw(tty_t* tty) {+  if (tty->raw_enabled) return true;+  if (tcsetattr(tty->fd_in,TCSAFLUSH,&tty->raw_ios) < 0) return false;  +  tty->raw_enabled = true;+  return true;+}++ic_private void tty_end_raw(tty_t* tty) {+  if (!tty->raw_enabled) return;+  tty->cpush_count = 0;+  if (tcsetattr(tty->fd_in,TCSAFLUSH,&tty->orig_ios) < 0) return;+  tty->raw_enabled = false;+}++static bool tty_init_raw(tty_t* tty) +{  +  // Set input to raw mode. See <https://man7.org/linux/man-pages/man3/termios.3.html>.+  if (tcgetattr(tty->fd_in,&tty->orig_ios) == -1) return false;+  tty->raw_ios = tty->orig_ios; +  // input: no break signal, no \r to \n, no parity check, no 8-bit to 7-bit, no flow control+  tty->raw_ios.c_iflag &= ~(unsigned long)(BRKINT | ICRNL | INPCK | ISTRIP | IXON);+  // control: allow 8-bit+  tty->raw_ios.c_cflag |= CS8;  +  // local: no echo, no line-by-line (canonical), no extended input processing, no signals for ^z,^c+  tty->raw_ios.c_lflag &= ~(unsigned long)(ECHO | ICANON | IEXTEN | ISIG);+  // 1 byte at a time, no delay+  tty->raw_ios.c_cc[VTIME] = 0;+  tty->raw_ios.c_cc[VMIN] = 1;++  // store in global so our signal handlers can restore the terminal mode+  signals_install(tty);+  +  return true;+}++static void tty_done_raw(tty_t* tty) {+  ic_unused(tty);+  signals_restore();+}+++#else++//-------------------------------------------------------------+// Windows+// For best portability we push CSI escape sequences directly+// to the character stream (instead of returning key codes).+//-------------------------------------------------------------++static void tty_waitc_console(tty_t* tty, long timeout_ms);++ic_private bool tty_readc_noblock(tty_t* tty, uint8_t* c, long timeout_ms) {  // don't modify `c` if there is no input+  // in our pushback buffer?+  if (tty_cpop(tty, c)) return true;+  // any events in the input queue?+  tty_waitc_console(tty, timeout_ms);+  return tty_cpop(tty, c);+}++// Read from the console input events and push escape codes into the tty cbuffer.+static void tty_waitc_console(tty_t* tty, long timeout_ms) +{+  //  wait for a key down event+  INPUT_RECORD inp;+	DWORD count;+  uint32_t surrogate_hi = 0;+  while (true) {+    // check if there are events if in non-blocking timeout mode+    if (timeout_ms >= 0) {+      // first peek ahead+      if (!GetNumberOfConsoleInputEvents(tty->hcon, &count)) return;  +      if (count == 0) {+        if (timeout_ms == 0) {+          // out of time+          return;+        }+        else {+          // wait for input events for at most timeout milli seconds+          ULONGLONG start_ms = GetTickCount64();+          DWORD res = WaitForSingleObject(tty->hcon, (DWORD)timeout_ms);+          switch (res) {+            case WAIT_OBJECT_0: {+              // input is available, decrease our timeout+              ULONGLONG waited_ms = (GetTickCount64() - start_ms);+              timeout_ms -= (long)waited_ms;+              if (timeout_ms < 0) {+                timeout_ms = 0;+              }+              break;+            }+            case WAIT_TIMEOUT:+            case WAIT_ABANDONED:+            case WAIT_FAILED:+            default: +              return;+          }+        }+      }+    }++    // (blocking) Read from the input+    if (!ReadConsoleInputW(tty->hcon, &inp, 1, &count)) return;+    if (count != 1) return;++    // resize event?+    if (inp.EventType == WINDOW_BUFFER_SIZE_EVENT) {+      tty->term_resize_event = true;+      continue;+    }++    // wait for key down events +    if (inp.EventType != KEY_EVENT) continue;++    // the modifier state+    DWORD modstate = inp.Event.KeyEvent.dwControlKeyState;+    +    // we need to handle shift up events separately+    if (!inp.Event.KeyEvent.bKeyDown && inp.Event.KeyEvent.wVirtualKeyCode == VK_SHIFT) {+      modstate &= (DWORD)~SHIFT_PRESSED;+    }++    // ignore AltGr+    DWORD altgr = LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED;+    if ((modstate & altgr) == altgr) { modstate &= ~altgr; }++    +    // get modifiers+    code_t mods = 0;+    if ((modstate & ( RIGHT_CTRL_PRESSED | LEFT_CTRL_PRESSED )) != 0) mods |= KEY_MOD_CTRL;+    if ((modstate & ( RIGHT_ALT_PRESSED | LEFT_ALT_PRESSED )) != 0)   mods |= KEY_MOD_ALT;+    if ((modstate & SHIFT_PRESSED) != 0)                              mods |= KEY_MOD_SHIFT;++    // virtual keys+    uint32_t chr = (uint32_t)inp.Event.KeyEvent.uChar.UnicodeChar;+    WORD     virt = inp.Event.KeyEvent.wVirtualKeyCode;+    debug_msg("tty: console %s: %s%s%s virt 0x%04x, chr 0x%04x ('%c')\n", inp.Event.KeyEvent.bKeyDown ? "down" : "up", mods&KEY_MOD_CTRL ? "ctrl-" : "", mods&KEY_MOD_ALT ? "alt-" : "", mods&KEY_MOD_SHIFT ? "shift-" : "", virt, chr, chr);++    // only process keydown events (except for Alt-up which is used for unicode pasting...)+    if (!inp.Event.KeyEvent.bKeyDown && virt != VK_MENU) {+			continue;+		}+    +    if (chr == 0) { +      switch (virt) {+        case VK_UP:     tty_cpush_csi_xterm(tty, mods, 'A'); return;+        case VK_DOWN:   tty_cpush_csi_xterm(tty, mods, 'B'); return;+        case VK_RIGHT:  tty_cpush_csi_xterm(tty, mods, 'C'); return;+        case VK_LEFT:   tty_cpush_csi_xterm(tty, mods, 'D'); return;+        case VK_END:    tty_cpush_csi_xterm(tty, mods, 'F'); return; +        case VK_HOME:   tty_cpush_csi_xterm(tty, mods, 'H'); return;+        case VK_DELETE: tty_cpush_csi_vt(tty,mods,3); return;+        case VK_PRIOR:  tty_cpush_csi_vt(tty,mods,5); return;   //page up+        case VK_NEXT:   tty_cpush_csi_vt(tty,mods,6); return;   //page down+        case VK_TAB:    tty_cpush_csi_unicode(tty,mods,9);  return; +        case VK_RETURN: tty_cpush_csi_unicode(tty,mods,13); return;+        default: {+          uint32_t vtcode = 0;+          if (virt >= VK_F1 && virt <= VK_F5) {+            vtcode = 10 + (virt - VK_F1);+          }+          else if (virt >= VK_F6 && virt <= VK_F10) {+            vtcode = 17 + (virt - VK_F6);+          }+          else if (virt >= VK_F11 && virt <= VK_F12) {+            vtcode = 13 + (virt - VK_F11);+          }+          if (vtcode > 0) {+            tty_cpush_csi_vt(tty,mods,vtcode);+            return;+          }+        }+      }    +      // ignore other control keys (shift etc).+    }+    // high surrogate pair+    else if (chr >= 0xD800 && chr <= 0xDBFF) {+			surrogate_hi = (chr - 0xD800);			+    }+    // low surrogate pair+    else if (chr >= 0xDC00 && chr <= 0xDFFF) {+			chr = ((surrogate_hi << 10) + (chr - 0xDC00) + 0x10000);+      tty_cpush_csi_unicode(tty,mods,chr);+      surrogate_hi = 0;+      return;+		}+    // regular character+    else {+      tty_cpush_csi_unicode(tty,mods,chr);+			return;+    }+  }+}  ++ic_private bool tty_async_stop(const tty_t* tty) {+  // send ^c+  INPUT_RECORD events[2];+  memset(events, 0, 2*sizeof(INPUT_RECORD));+  events[0].EventType = KEY_EVENT;+  events[0].Event.KeyEvent.bKeyDown = TRUE;+  events[0].Event.KeyEvent.uChar.AsciiChar = KEY_CTRL_C;+  events[1] = events[0];+  events[1].Event.KeyEvent.bKeyDown = FALSE;+  DWORD nwritten = 0;+  WriteConsoleInput(tty->hcon, events, 2, &nwritten);+  return (nwritten == 2);+}++ic_private bool tty_start_raw(tty_t* tty) {+  if (tty->raw_enabled) return true;+  GetConsoleMode(tty->hcon,&tty->hcon_orig_mode);+  DWORD mode = ENABLE_QUICK_EDIT_MODE   // cut&paste allowed +             | ENABLE_WINDOW_INPUT      // to catch resize events +             // | ENABLE_VIRTUAL_TERMINAL_INPUT +             // | ENABLE_PROCESSED_INPUT+             ;+  SetConsoleMode(tty->hcon, mode );+  tty->raw_enabled = true; +  return true; +}++ic_private void tty_end_raw(tty_t* tty) {+  if (!tty->raw_enabled) return;+  SetConsoleMode(tty->hcon, tty->hcon_orig_mode );+  tty->raw_enabled = false;+}++static bool tty_init_raw(tty_t* tty) {+  tty->hcon = GetStdHandle( STD_INPUT_HANDLE );  +  tty->has_term_resize_event = true;+  return true;+}++static void tty_done_raw(tty_t* tty) {+  ic_unused(tty);+}++#endif++
+ src/tty.h view
@@ -0,0 +1,160 @@+/* ----------------------------------------------------------------------------+  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.+-----------------------------------------------------------------------------*/+#pragma once+#ifndef IC_TTY_H+#define IC_TTY_H++#include "common.h"++//-------------------------------------------------------------+// TTY/Keyboard input +//-------------------------------------------------------------++// Key code+typedef uint32_t  code_t;++// TTY interface+struct tty_s;+typedef struct tty_s tty_t;+++ic_private tty_t* tty_new(alloc_t* mem, int fd_in);+ic_private void   tty_free(tty_t* tty);++ic_private bool   tty_is_utf8(const tty_t* tty);+ic_private bool   tty_start_raw(tty_t* tty);+ic_private void   tty_end_raw(tty_t* tty);+ic_private code_t tty_read(tty_t* tty);+ic_private bool   tty_read_timeout(tty_t* tty, long timeout_ms, code_t* c );++ic_private void   tty_code_pushback( tty_t* tty, code_t c );+ic_private bool   code_is_ascii_char(code_t c, char* chr );+ic_private bool   code_is_unicode(code_t c, unicode_t* uchr);+ic_private bool   code_is_virt_key(code_t c );++ic_private bool   tty_term_resize_event(tty_t* tty); // did the terminal resize?+ic_private bool   tty_async_stop(const tty_t* tty);  // unblock the read asynchronously+ic_private void   tty_set_esc_delay(tty_t* tty, long initial_delay_ms, long followup_delay_ms);++// shared between tty.c and tty_esc.c: low level character push+ic_private void   tty_cpush_char(tty_t* tty, uint8_t c);+ic_private bool   tty_cpop(tty_t* tty, uint8_t* c);+ic_private bool   tty_readc_noblock(tty_t* tty, uint8_t* c, long timeout_ms);+ic_private code_t tty_read_esc(tty_t* tty, long esc_initial_timeout, long esc_timeout); // in tty_esc.c++// used by term.c to read back ANSI escape responses+ic_private bool   tty_read_esc_response(tty_t* tty, char esc_start, bool final_st, char* buf, ssize_t buflen ); +++//-------------------------------------------------------------+// Key codes: a code_t is 32 bits.+// we use the bottom 24 (nah, 21) bits for unicode (up to x0010FFFF)+// The codes after x01000000 are for virtual keys +// and events use  x02000000.+// The top 4 bits are used for modifiers.+//-------------------------------------------------------------++static inline code_t key_char( char c ) {+  // careful about signed character conversion (negative char ~> 0x80 - 0xFF)+  return ((uint8_t)c);+}++static inline code_t key_unicode( unicode_t u ) {+  return u;+}+++#define KEY_MOD_SHIFT     (0x10000000U)+#define KEY_MOD_ALT       (0x20000000U)+#define KEY_MOD_CTRL      (0x40000000U)++#define KEY_NO_MODS(k)    (k & 0x0FFFFFFFU)+#define KEY_MODS(k)       (k & 0xF0000000U)++#define WITH_SHIFT(x)     (x | KEY_MOD_SHIFT)+#define WITH_ALT(x)       (x | KEY_MOD_ALT)+#define WITH_CTRL(x)      (x | KEY_MOD_CTRL)++#define KEY_NONE          (0)+#define KEY_CTRL_A        (1)+#define KEY_CTRL_B        (2)+#define KEY_CTRL_C        (3)+#define KEY_CTRL_D        (4)+#define KEY_CTRL_E        (5)+#define KEY_CTRL_F        (6)+#define KEY_BELL          (7)+#define KEY_BACKSP        (8)+#define KEY_TAB           (9)+#define KEY_LINEFEED      (10)   // ctrl/shift + enter is considered KEY_LINEFEED+#define KEY_CTRL_K        (11)+#define KEY_CTRL_L        (12)+#define KEY_ENTER         (13)+#define KEY_CTRL_N        (14)+#define KEY_CTRL_O        (15)+#define KEY_CTRL_P        (16)+#define KEY_CTRL_Q        (17)+#define KEY_CTRL_R        (18)+#define KEY_CTRL_S        (19)+#define KEY_CTRL_T        (20)+#define KEY_CTRL_U        (21)+#define KEY_CTRL_V        (22)+#define KEY_CTRL_W        (23)+#define KEY_CTRL_X        (24)+#define KEY_CTRL_Y        (25)+#define KEY_CTRL_Z        (26)+#define KEY_ESC           (27)+#define KEY_SPACE         (32)+#define KEY_RUBOUT        (127)  // always translated to KEY_BACKSP+#define KEY_UNICODE_MAX   (0x0010FFFFU)+++#define KEY_VIRT          (0x01000000U)  +#define KEY_UP            (KEY_VIRT+0)+#define KEY_DOWN          (KEY_VIRT+1)+#define KEY_LEFT          (KEY_VIRT+2)+#define KEY_RIGHT         (KEY_VIRT+3)+#define KEY_HOME          (KEY_VIRT+4)+#define KEY_END           (KEY_VIRT+5)+#define KEY_DEL           (KEY_VIRT+6)+#define KEY_PAGEUP        (KEY_VIRT+7)+#define KEY_PAGEDOWN      (KEY_VIRT+8)+#define KEY_INS           (KEY_VIRT+9)++#define KEY_F1            (KEY_VIRT+11)+#define KEY_F2            (KEY_VIRT+12)+#define KEY_F3            (KEY_VIRT+13)+#define KEY_F4            (KEY_VIRT+14)+#define KEY_F5            (KEY_VIRT+15)+#define KEY_F6            (KEY_VIRT+16)+#define KEY_F7            (KEY_VIRT+17)+#define KEY_F8            (KEY_VIRT+18)+#define KEY_F9            (KEY_VIRT+19)+#define KEY_F10           (KEY_VIRT+20)+#define KEY_F11           (KEY_VIRT+21)+#define KEY_F12           (KEY_VIRT+22)+#define KEY_F(n)          (KEY_F1 + (n) - 1)++#define KEY_EVENT_BASE    (0x02000000U)+#define KEY_EVENT_RESIZE  (KEY_EVENT_BASE+1)+#define KEY_EVENT_AUTOTAB (KEY_EVENT_BASE+2)+#define KEY_EVENT_STOP    (KEY_EVENT_BASE+3)++// Convenience+#define KEY_CTRL_UP       (WITH_CTRL(KEY_UP))+#define KEY_CTRL_DOWN     (WITH_CTRL(KEY_DOWN))+#define KEY_CTRL_LEFT     (WITH_CTRL(KEY_LEFT))+#define KEY_CTRL_RIGHT    (WITH_CTRL(KEY_RIGHT))+#define KEY_CTRL_HOME     (WITH_CTRL(KEY_HOME))+#define KEY_CTRL_END      (WITH_CTRL(KEY_END))+#define KEY_CTRL_DEL      (WITH_CTRL(KEY_DEL))+#define KEY_CTRL_PAGEUP   (WITH_CTRL(KEY_PAGEUP))+#define KEY_CTRL_PAGEDOWN (WITH_CTRL(KEY_PAGEDOWN)))+#define KEY_CTRL_INS      (WITH_CTRL(KEY_INS))++#define KEY_SHIFT_TAB     (WITH_SHIFT(KEY_TAB))++#endif // IC_TTY_H
+ src/tty_esc.c view
@@ -0,0 +1,375 @@+/* ----------------------------------------------------------------------------+  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.+-----------------------------------------------------------------------------*/+#include <string.h>+#include "tty.h"++/*-------------------------------------------------------------+Decoding escape sequences to key codes.+This is a bit tricky there are many variants to encode keys as escape sequences, see for example:+- <http://www.leonerd.org.uk/hacks/fixterms/>.+- <https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_(Control_Sequence_Introducer)_sequences>+- <https://www.xfree86.org/current/ctlseqs.html>+- <https://vt100.net/docs/vt220-rm/contents.html>+- <https://www.ecma-international.org/wp-content/uploads/ECMA-48_5th_edition_june_1991.pdf>++Generally, for our purposes we accept a subset of escape sequences as:++  escseq ::= ESC +          |  ESC char+          |  ESC start special? (number (';' modifiers)?)? final++where:+  char         ::= [\x00-\xFF]               # any character+  special      ::= [:<=>?]             +  number       ::= [0-9+]+  modifiers    ::= [1-9]      +  intermediate ::= [\x20-\x2F]               # !"#$%&'()*+,-./+  final        ::= [\x40-\x7F]               # @A–Z[\]^_`a–z{|}~+  ESC          ::= '\x1B'+  CSI          ::= ESC '['+  SS3          ::= ESC 'O'++In ECMA48 `special? (number (';' modifiers)?)?` is the more liberal `[\x30-\x3F]*` +but that seems never used for key codes. If the number (vtcode or unicode) or the +modifiers are not given, we assume these are '1'. +We then accept the following key sequences:++  key ::= ESC                                              # lone ESC+       |  ESC char                                         # Alt+char+       |  ESC '[' special? vtcode  ';' modifiers '~'       # vt100 codes+       |  ESC '[' special? '1'     ';' modifiers [A-Z]     # xterm codes+       |  ESC 'O' special? '1'     ';' modifiers [A-Za-z]  # SS3 codes+       |  ESC '[' special? unicode ';' modifiers 'u'       # direct unicode code++Moreover, we translate the following special cases that do not fit into the above grammar.+First we translate away special starter sequences:+---------------------------------------------------------------------+  ESC '[' '[' ..      ~>  ESC '[' ..                  # Linux sometimes uses extra '[' for CSI+  ESC '[' 'O' ..      ~>  ESC 'O' ..                  # Linux sometimes uses extra '[' for SS3+  ESC 'o' ..          ~>  ESC 'O' ..                  # Eterm: ctrl + SS3+  ESC '?' ..          ~>  ESC 'O' ..                  # vt52 treated as SS3++And then translate the following special cases into a standard form:+---------------------------------------------------------------------+  ESC '[' .. '@'      ~>  ESC '[' '3' '~'             # Del on Mach+  ESC '[' .. '9'      ~>  ESC '[' '2' '~'             # Ins on Mach+  ESC .. [^@$]        ~>  ESC .. '~'                  # ETerm,xrvt,urxt: ^ = ctrl, $ = shift, @ = alt+  ESC '[' [a-d]       ~>  ESC '[' '1' ';' '2' [A-D]   # Eterm shift+<cursor>+  ESC 'O' [1-9] final ~>  ESC 'O' '1' ';' [1-9] final # modifiers as parameter 1 (like on Haiku)+  ESC '[' [1-9] [^~u] ~>  ESC 'O' '1' ';' [1-9] final # modifiers as parameter 1++The modifier keys are encoded as "(modifiers-1) & mask" where the +shift mask is 0x01, alt 0x02 and ctrl 0x04. Therefore:+------------------------------------------------------------+  1:  -           5: ctrl            9: alt  (for minicom)+  2:  shift       6: shift+ctrl+  3:  alt         7: alt+ctrl+  4:  shift+alt   8: shift+alt+ctrl++The different encodings fox vt100, xterm, and SS3 are:++vt100:  ESC [ vtcode ';' modifiers '~'+--------------------------------------+  1:  Home       10-15: F1-F5+  2:  Ins        16   : F5+  3:  Del        17-21: F6-F10+  4:  End        23-26: F11-F14+  5:  PageUp     28   : F15+  6:  PageDn     29   : F16+  7:  Home       31-34: F17-F20+  8:  End++xterm: ESC [ 1 ';' modifiers [A-Z]+-----------------------------------+  A: Up          N: F2        +  B: Down        O: F3       +  C: Right       P: F4       +  D: Left        Q: F5       +  E: '5'         R: F6       +  F: End         S: F7       +  G:             T: F8       +  H: Home        U: PageDn       +  I: PageUp      V: PageUp       +  J:             W: F11      +  K:             X: F12      +  L: Ins         Y: End      +  M: F1          Z: shift+Tab++SS3:   ESC 'O' 1 ';' modifiers [A-Za-z]+---------------------------------------+  (normal)                       (numpad)+  A: Up          N:              a: Up        n:           +  B: Down        O:              b: Down      o: +  C: Right       P: F1           c: Right     p: Ins  +  D: Left        Q: F2           d: Left      q: End  +  E: '5'         R: F3           e:           r: Down +  F: End         S: F4           f:           s: PageDn+  G:             T: F5           g:           t: Left +  H: Home        U: F6           h:           u: '5'+  I: Tab         V: F7           i:           v: Right+  J:             W: F8           j: '*'       w: Home +  K:             X: F9           k: '+'       x: Up +  L:             Y: F10          l: ','       y: PageUp +  M: \x0A '\n'   Z: shift+Tab    m: '-'       z:  +    +-------------------------------------------------------------*/++//-------------------------------------------------------------+// Decode escape sequences+//-------------------------------------------------------------++static code_t esc_decode_vt(uint32_t vt_code ) {+  switch(vt_code) {+    case 1: return KEY_HOME; +    case 2: return KEY_INS;+    case 3: return KEY_DEL;+    case 4: return KEY_END;          +    case 5: return KEY_PAGEUP;+    case 6: return KEY_PAGEDOWN;+    case 7: return KEY_HOME;+    case 8: return KEY_END;          +    default: +      if (vt_code >= 10 && vt_code <= 15) return KEY_F(1  + (vt_code - 10));+      if (vt_code == 16) return KEY_F5; // minicom+      if (vt_code >= 17 && vt_code <= 21) return KEY_F(6  + (vt_code - 17));+      if (vt_code >= 23 && vt_code <= 26) return KEY_F(11 + (vt_code - 23));+      if (vt_code >= 28 && vt_code <= 29) return KEY_F(15 + (vt_code - 28));+      if (vt_code >= 31 && vt_code <= 34) return KEY_F(17 + (vt_code - 31));+  }+  return KEY_NONE;+}++static code_t esc_decode_xterm( uint8_t xcode ) {+  // ESC [+  switch(xcode) {+    case 'A': return KEY_UP;+    case 'B': return KEY_DOWN;+    case 'C': return KEY_RIGHT;+    case 'D': return KEY_LEFT;+    case 'E': return '5';          // numpad 5+    case 'F': return KEY_END;+    case 'H': return KEY_HOME;+    case 'Z': return KEY_TAB | KEY_MOD_SHIFT;+    // Freebsd:+    case 'I': return KEY_PAGEUP;  +    case 'L': return KEY_INS;   +    case 'M': return KEY_F1;+    case 'N': return KEY_F2;+    case 'O': return KEY_F3;+    case 'P': return KEY_F4;       // note: differs from <https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_(Control_Sequence_Introducer)_sequences>+    case 'Q': return KEY_F5;+    case 'R': return KEY_F6;+    case 'S': return KEY_F7;+    case 'T': return KEY_F8;+    case 'U': return KEY_PAGEDOWN; // Mach+    case 'V': return KEY_PAGEUP;   // Mach+    case 'W': return KEY_F11;+    case 'X': return KEY_F12;    +    case 'Y': return KEY_END;      // Mach       +  }+  return KEY_NONE;+}++static code_t esc_decode_ss3( uint8_t ss3_code ) {+  // ESC O +  switch(ss3_code) {+    case 'A': return KEY_UP;+    case 'B': return KEY_DOWN;+    case 'C': return KEY_RIGHT;+    case 'D': return KEY_LEFT;+    case 'E': return '5';           // numpad 5+    case 'F': return KEY_END;+    case 'H': return KEY_HOME;+    case 'I': return KEY_TAB;+    case 'Z': return KEY_TAB | KEY_MOD_SHIFT;+    case 'M': return KEY_LINEFEED; +    case 'P': return KEY_F1;+    case 'Q': return KEY_F2;+    case 'R': return KEY_F3;+    case 'S': return KEY_F4;+    // on Mach+    case 'T': return KEY_F5;+    case 'U': return KEY_F6;+    case 'V': return KEY_F7;+    case 'W': return KEY_F8;+    case 'X': return KEY_F9;  // '=' on vt220+    case 'Y': return KEY_F10;+    // numpad+    case 'a': return KEY_UP;+    case 'b': return KEY_DOWN;+    case 'c': return KEY_RIGHT;+    case 'd': return KEY_LEFT;+    case 'j': return '*';+    case 'k': return '+';+    case 'l': return ',';+    case 'm': return '-'; +    case 'n': return KEY_DEL; // '.'+    case 'o': return '/'; +    case 'p': return KEY_INS;+    case 'q': return KEY_END;  +    case 'r': return KEY_DOWN; +    case 's': return KEY_PAGEDOWN; +    case 't': return KEY_LEFT; +    case 'u': return '5';+    case 'v': return KEY_RIGHT;+    case 'w': return KEY_HOME;  +    case 'x': return KEY_UP; +    case 'y': return KEY_PAGEUP;   +  }+  return KEY_NONE;+}++static void tty_read_csi_num(tty_t* tty, uint8_t* ppeek, uint32_t* num, long esc_timeout) {+  *num = 1; // default+  ssize_t count = 0;+  uint32_t i = 0;+  while (*ppeek >= '0' && *ppeek <= '9' && count < 16) {    +    uint8_t digit = *ppeek - '0';+    if (!tty_readc_noblock(tty,ppeek,esc_timeout)) break;  // peek is not modified in this case +    count++;+    i = 10*i + digit; +  }+  if (count > 0) *num = i;+}++static code_t tty_read_csi(tty_t* tty, uint8_t c1, uint8_t peek, code_t mods0, long esc_timeout) {+  // CSI starts with 0x9b (c1=='[') | ESC [ (c1=='[') | ESC [Oo?] (c1 == 'O')  /* = SS3 */+  +  // check for extra starter '[' (Linux sends ESC [ [ 15 ~  for F5 for example)+  if (c1 == '[' && strchr("[Oo", (char)peek) != NULL) {+    uint8_t cx = peek;+    if (tty_readc_noblock(tty,&peek,esc_timeout)) {+      c1 = cx;+    }+  }++  // "special" characters ('?' is used for private sequences)+  uint8_t special = 0;+  if (strchr(":<=>?",(char)peek) != NULL) { +    special = peek;+    if (!tty_readc_noblock(tty,&peek,esc_timeout)) {  +      tty_cpush_char(tty,special); // recover+      return (key_unicode(c1) | KEY_MOD_ALT);       // Alt+<anychar>+    }+  }++  // up to 2 parameters that default to 1+  uint32_t num1 = 1;+  uint32_t num2 = 1;+  tty_read_csi_num(tty,&peek,&num1,esc_timeout);+  if (peek == ';') {+    if (!tty_readc_noblock(tty,&peek,esc_timeout)) return KEY_NONE;+    tty_read_csi_num(tty,&peek,&num2,esc_timeout);+  }++  // the final character (we do not allow 'intermediate characters')+  uint8_t final = peek;+  code_t  modifiers = mods0;++  debug_msg("tty: escape sequence: ESC %c %c %d;%d %c\n", c1, (special == 0 ? '_' : special), num1, num2, final);+  +  // Adjust special cases into standard ones.+  if ((final == '@' || final == '9') && c1 == '[' && num1 == 1) {+    // ESC [ @, ESC [ 9  : on Mach+    if (final == '@')      num1 = 3; // DEL+    else if (final == '9') num1 = 2; // INS +    final = '~';+  }+  else if (final == '^' || final == '$' || final == '@') {  +    // Eterm/rxvt/urxt  +    if (final=='^') modifiers |= KEY_MOD_CTRL;+    if (final=='$') modifiers |= KEY_MOD_SHIFT;+    if (final=='@') modifiers |= KEY_MOD_SHIFT | KEY_MOD_CTRL;+    final = '~';+  }+  else if (c1 == '[' && final >= 'a' && final <= 'd') {  // note: do not catch ESC [ .. u  (for unicode)+    // ESC [ [a-d]  : on Eterm for shift+ cursor+    modifiers |= KEY_MOD_SHIFT;+    final = 'A' + (final - 'a');+  }+  +  if (((c1 == 'O') || (c1=='[' && final != '~' && final != 'u')) &&+      (num2 == 1 && num1 > 1 && num1 <= 8)) +  {+    // on haiku the modifier can be parameter 1, make it parameter 2 instead+    num2 = num1;+    num1 = 1;+  }++  // parameter 2 determines the modifiers+  if (num2 > 1 && num2 <= 9) {+    if (num2 == 9) num2 = 3; // iTerm2 in xterm mode+    num2--;+    if (num2 & 0x1) modifiers |= KEY_MOD_SHIFT;+    if (num2 & 0x2) modifiers |= KEY_MOD_ALT;+    if (num2 & 0x4) modifiers |= KEY_MOD_CTRL;+  }++  // and translate+  code_t code = KEY_NONE;+  if (final == '~') {+    // vt codes+    code = esc_decode_vt(num1);+  }+  else if (c1 == '[' && final == 'u') {+    // unicode+    code = key_unicode(num1);+  }+  else if (c1 == 'O' && ((final >= 'A' && final <= 'Z') || (final >= 'a' && final <= 'z'))) {+    // ss3+    code = esc_decode_ss3(final);+  }+  else if (num1 == 1 && final >= 'A' && final <= 'Z') {+    // xterm +    code = esc_decode_xterm(final);+  }+  else if (c1 == '[' && final == 'R') {+    // cursor position+    code = KEY_NONE;+  }  ++  if (code == KEY_NONE && final != 'R') { +    debug_msg("tty: ignore escape sequence: ESC %c %zu;%zu %c\n", c1, num1, num2, final); +  }+  return (code != KEY_NONE ? (code | modifiers) : KEY_NONE);+}++ic_private code_t tty_read_esc(tty_t* tty, long esc_initial_timeout, long esc_timeout) {+  code_t  mods = 0;+  uint8_t peek = 0;+  +  // lone ESC?+  if (!tty_readc_noblock(tty, &peek, esc_initial_timeout)) return KEY_ESC;++  // treat ESC ESC as Alt modifier (macOS sends ESC ESC [ [A-D] for alt-<cursor>)+  if (peek == KEY_ESC) {+    if (!tty_readc_noblock(tty, &peek, esc_timeout)) goto alt;+    mods |= KEY_MOD_ALT;+  }++  // CSI ?+  if (peek == '[') {+    if (!tty_readc_noblock(tty, &peek, esc_timeout)) goto alt;+    return tty_read_csi(tty, '[', peek, mods, esc_timeout);  // ESC [ ...+  }++  // SS3?+  if (peek == 'O' || peek == 'o' || peek == '?' /*vt52*/) {+    uint8_t c1 = peek;+    if (!tty_readc_noblock(tty, &peek, esc_timeout)) goto alt;+    if (c1 == 'o') { +      // ETerm uses this for ctrl+<cursor>+      mods |= KEY_MOD_CTRL;+    }+    // treat all as standard SS3 'O'+    return tty_read_csi(tty,'O',peek,mods, esc_timeout);  // ESC [Oo?] ...+  }++alt:  +  // Alt+<char>+  return (key_unicode(peek) | KEY_MOD_ALT);  // ESC <anychar>+}
+ src/undo.c view
@@ -0,0 +1,67 @@+/* ----------------------------------------------------------------------------+  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.+-----------------------------------------------------------------------------*/+#include <string.h>+#include <stdio.h>++#include "../include/isocline.h"+#include "common.h"+#include "env.h"+#include "stringbuf.h"+#include "completions.h"+#include "undo.h"++++//-------------------------------------------------------------+// edit state+//-------------------------------------------------------------+struct editstate_s {+  struct editstate_s* next;+  const char* input;          // input+  ssize_t     pos;            // cursor position+};++ic_private void editstate_init( editstate_t** es ) {+  *es = NULL;+}++ic_private void editstate_done( alloc_t* mem, editstate_t** es ) {+  while (*es != NULL) {+    editstate_t* next = (*es)->next;+    mem_free(mem, (*es)->input);+    mem_free(mem, *es );+    *es = next;+  }+  *es = NULL;+}++ic_private void editstate_capture( alloc_t* mem, editstate_t** es, const char* input, ssize_t pos) {+  if (input==NULL) input = "";+  // alloc+  editstate_t* entry = mem_zalloc_tp(mem, editstate_t);+  if (entry == NULL) return;+  // initialize+  entry->input = mem_strdup( mem, input);+  entry->pos   = pos;+  if (entry->input == NULL) { mem_free(mem, entry); return; }+  // and push+  entry->next = *es;+  *es = entry;+}++// caller should free *input+ic_private bool editstate_restore( alloc_t* mem, editstate_t** es, const char** input, ssize_t* pos ) {+  if (*es == NULL) return false;+  // pop +  editstate_t* entry = *es;+  *es = entry->next;+  *input = entry->input;+  *pos = entry->pos;+  mem_free(mem, entry);+  return true;+}+
+ src/undo.h view
@@ -0,0 +1,24 @@+/* ----------------------------------------------------------------------------+  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.+-----------------------------------------------------------------------------*/+#pragma once+#ifndef IC_UNDO_H+#define IC_UNDO_H++#include "common.h"++//-------------------------------------------------------------+// Edit state+//-------------------------------------------------------------+struct editstate_s;+typedef struct editstate_s editstate_t;++ic_private void editstate_init( editstate_t** es );+ic_private void editstate_done( alloc_t* mem, editstate_t** es );+ic_private void editstate_capture( alloc_t* mem, editstate_t** es, const char* input, ssize_t pos);+ic_private bool editstate_restore( alloc_t* mem, editstate_t** es, const char** input, ssize_t* pos ); // caller needs to free input++#endif // IC_UNDO_H
+ src/wcwidth.c view
@@ -0,0 +1,292 @@+// include in "stringbuf.c"+/*+ * This is an implementation of wcwidth() and wcswidth() (defined in+ * IEEE Std 1002.1-2001) for Unicode.+ *+ * http://www.opengroup.org/onlinepubs/007904975/functions/wcwidth.html+ * http://www.opengroup.org/onlinepubs/007904975/functions/wcswidth.html+ *+ * In fixed-width output devices, Latin characters all occupy a single+ * "cell" position of equal width, whereas ideographic CJK characters+ * occupy two such cells. Interoperability between terminal-line+ * applications and (teletype-style) character terminals using the+ * UTF-8 encoding requires agreement on which character should advance+ * the cursor by how many cell positions. No established formal+ * standards exist at present on which Unicode character shall occupy+ * how many cell positions on character terminals. These routines are+ * a first attempt of defining such behavior based on simple rules+ * applied to data provided by the Unicode Consortium.+ *+ * For some graphical characters, the Unicode standard explicitly+ * defines a character-cell width via the definition of the East Asian+ * FullWidth (F), Wide (W), Half-width (H), and Narrow (Na) classes.+ * In all these cases, there is no ambiguity about which width a+ * terminal shall use. For characters in the East Asian Ambiguous (A)+ * class, the width choice depends purely on a preference of backward+ * compatibility with either historic CJK or Western practice.+ * Choosing single-width for these characters is easy to justify as+ * the appropriate long-term solution, as the CJK practice of+ * displaying these characters as double-width comes from historic+ * implementation simplicity (8-bit encoded characters were displayed+ * single-width and 16-bit ones double-width, even for Greek,+ * Cyrillic, etc.) and not any typographic considerations.+ *+ * Much less clear is the choice of width for the Not East Asian+ * (Neutral) class. Existing practice does not dictate a width for any+ * of these characters. It would nevertheless make sense+ * typographically to allocate two character cells to characters such+ * as for instance EM SPACE or VOLUME INTEGRAL, which cannot be+ * represented adequately with a single-width glyph. The following+ * routines at present merely assign a single-cell width to all+ * neutral characters, in the interest of simplicity. This is not+ * entirely satisfactory and should be reconsidered before+ * establishing a formal standard in this area. At the moment, the+ * decision which Not East Asian (Neutral) characters should be+ * represented by double-width glyphs cannot yet be answered by+ * applying a simple rule from the Unicode database content. Setting+ * up a proper standard for the behavior of UTF-8 character terminals+ * will require a careful analysis not only of each Unicode character,+ * but also of each presentation form, something the author of these+ * routines has avoided to do so far.+ *+ * http://www.unicode.org/unicode/reports/tr11/+ *+ * Markus Kuhn -- 2007-05-26 (Unicode 5.0)+ *+ * Permission to use, copy, modify, and distribute this software+ * for any purpose and without fee is hereby granted. The author+ * disclaims all warranties with regard to this software.+ *+ * Latest version: http://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c+ */++#include <stdint.h>+#include <string.h>++struct interval {+	int32_t first;+	int32_t last;+};++/* auxiliary function for binary search in interval table */+static int bisearch(int32_t ucs, const struct interval *table, int max) {+	int min = 0;+	int mid;++	if (ucs < table[0].first || ucs > table[max].last)+		return 0;+	while (max >= min) {+		mid = (min + max) / 2;+		if (ucs > table[mid].last)+			min = mid + 1;+		else if (ucs < table[mid].first)+			max = mid - 1;+		else+			return 1;+	}++	return 0;+}+++/* The following two functions define the column width of an ISO 10646+ * character as follows:+ *+ *		- The null character (U+0000) has a column width of 0.+ *+ *		- Other C0/C1 control characters and DEL will lead to a return+ *			value of -1.+ *+ *		- Non-spacing and enclosing combining characters (general+ *			category code Mn or Me in the Unicode database) have a+ *			column width of 0.+ *+ *		- SOFT HYPHEN (U+00AD) has a column width of 1.+ *+ *		- Other format characters (general category code Cf in the Unicode+ *			database) and ZERO WIDTH SPACE (U+200B) have a column width of 0.+ *+ *		- Hangul Jamo medial vowels and final consonants (U+1160-U+11FF)+ *			have a column width of 0.+ *+ *		- Spacing characters in the East Asian Wide (W) or East Asian+ *			Full-width (F) category as defined in Unicode Technical+ *			Report #11 have a column width of 2.+ *+ *		- All remaining characters (including all printable+ *			ISO 8859-1 and WGL4 characters, Unicode control characters,+ *			etc.) have a column width of 1.+ *+ * This implementation assumes that wchar_t characters are encoded+ * in ISO 10646.+ */++static int mk_is_wide_char(int32_t ucs) {+  static const struct interval wide[] = {+    {0x1100, 0x115f}, {0x231a, 0x231b}, {0x2329, 0x232a},+    {0x23e9, 0x23ec}, {0x23f0, 0x23f0}, {0x23f3, 0x23f3},+    {0x25fd, 0x25fe}, {0x2614, 0x2615}, {0x2648, 0x2653},+    {0x267f, 0x267f}, {0x2693, 0x2693}, {0x26a1, 0x26a1},+    {0x26aa, 0x26ab}, {0x26bd, 0x26be}, {0x26c4, 0x26c5},+    {0x26ce, 0x26ce}, {0x26d4, 0x26d4}, {0x26ea, 0x26ea},+    {0x26f2, 0x26f3}, {0x26f5, 0x26f5}, {0x26fa, 0x26fa},+    {0x26fd, 0x26fd}, {0x2705, 0x2705}, {0x270a, 0x270b},+    {0x2728, 0x2728}, {0x274c, 0x274c}, {0x274e, 0x274e},+    {0x2753, 0x2755}, {0x2757, 0x2757}, {0x2795, 0x2797},+    {0x27b0, 0x27b0}, {0x27bf, 0x27bf}, {0x2b1b, 0x2b1c},+    {0x2b50, 0x2b50}, {0x2b55, 0x2b55}, {0x2e80, 0x2fdf},+    {0x2ff0, 0x303e}, {0x3040, 0x3247}, {0x3250, 0x4dbf},+    {0x4e00, 0xa4cf}, {0xa960, 0xa97f}, {0xac00, 0xd7a3},+    {0xf900, 0xfaff}, {0xfe10, 0xfe19}, {0xfe30, 0xfe6f},+    {0xff01, 0xff60}, {0xffe0, 0xffe6}, {0x16fe0, 0x16fe1},+    {0x17000, 0x18aff}, {0x1b000, 0x1b12f}, {0x1b170, 0x1b2ff},+    {0x1f004, 0x1f004}, {0x1f0cf, 0x1f0cf}, {0x1f18e, 0x1f18e},+    {0x1f191, 0x1f19a}, {0x1f200, 0x1f202}, {0x1f210, 0x1f23b},+    {0x1f240, 0x1f248}, {0x1f250, 0x1f251}, {0x1f260, 0x1f265},+    {0x1f300, 0x1f320}, {0x1f32d, 0x1f335}, {0x1f337, 0x1f37c},+    {0x1f37e, 0x1f393}, {0x1f3a0, 0x1f3ca}, {0x1f3cf, 0x1f3d3},+    {0x1f3e0, 0x1f3f0}, {0x1f3f4, 0x1f3f4}, {0x1f3f8, 0x1f43e},+    {0x1f440, 0x1f440}, {0x1f442, 0x1f4fc}, {0x1f4ff, 0x1f53d},+    {0x1f54b, 0x1f54e}, {0x1f550, 0x1f567}, {0x1f57a, 0x1f57a},+    {0x1f595, 0x1f596}, {0x1f5a4, 0x1f5a4}, {0x1f5fb, 0x1f64f},+    {0x1f680, 0x1f6c5}, {0x1f6cc, 0x1f6cc}, {0x1f6d0, 0x1f6d2},+    {0x1f6eb, 0x1f6ec}, {0x1f6f4, 0x1f6f8}, {0x1f910, 0x1f93e},+    {0x1f940, 0x1f94c}, {0x1f950, 0x1f96b}, {0x1f980, 0x1f997},+    {0x1f9c0, 0x1f9c0}, {0x1f9d0, 0x1f9e6}, {0x20000, 0x2fffd},+    {0x30000, 0x3fffd},+  };++  if ( bisearch(ucs, wide, sizeof(wide) / sizeof(struct interval) - 1) ) {+    return 1;+	}++  return 0;+}++static int mk_wcwidth(int32_t ucs) {+	/* sorted list of non-overlapping intervals of non-spacing characters */+	/* generated by "uniset +cat=Me +cat=Mn +cat=Cf -00AD +1160-11FF +200B c" */+	static const struct interval combining[] = {+    {0x00ad, 0x00ad}, {0x0300, 0x036f}, {0x0483, 0x0489},+    {0x0591, 0x05bd}, {0x05bf, 0x05bf}, {0x05c1, 0x05c2},+    {0x05c4, 0x05c5}, {0x05c7, 0x05c7}, {0x0610, 0x061a},+    {0x061c, 0x061c}, {0x064b, 0x065f}, {0x0670, 0x0670},+    {0x06d6, 0x06dc}, {0x06df, 0x06e4}, {0x06e7, 0x06e8},+    {0x06ea, 0x06ed}, {0x0711, 0x0711}, {0x0730, 0x074a},+    {0x07a6, 0x07b0}, {0x07eb, 0x07f3}, {0x0816, 0x0819},+    {0x081b, 0x0823}, {0x0825, 0x0827}, {0x0829, 0x082d},+    {0x0859, 0x085b}, {0x08d4, 0x08e1}, {0x08e3, 0x0902},+    {0x093a, 0x093a}, {0x093c, 0x093c}, {0x0941, 0x0948},+    {0x094d, 0x094d}, {0x0951, 0x0957}, {0x0962, 0x0963},+    {0x0981, 0x0981}, {0x09bc, 0x09bc}, {0x09c1, 0x09c4},+    {0x09cd, 0x09cd}, {0x09e2, 0x09e3}, {0x0a01, 0x0a02},+    {0x0a3c, 0x0a3c}, {0x0a41, 0x0a42}, {0x0a47, 0x0a48},+    {0x0a4b, 0x0a4d}, {0x0a51, 0x0a51}, {0x0a70, 0x0a71},+    {0x0a75, 0x0a75}, {0x0a81, 0x0a82}, {0x0abc, 0x0abc},+    {0x0ac1, 0x0ac5}, {0x0ac7, 0x0ac8}, {0x0acd, 0x0acd},+    {0x0ae2, 0x0ae3}, {0x0afa, 0x0aff}, {0x0b01, 0x0b01},+    {0x0b3c, 0x0b3c}, {0x0b3f, 0x0b3f}, {0x0b41, 0x0b44},+    {0x0b4d, 0x0b4d}, {0x0b56, 0x0b56}, {0x0b62, 0x0b63},+    {0x0b82, 0x0b82}, {0x0bc0, 0x0bc0}, {0x0bcd, 0x0bcd},+    {0x0c00, 0x0c00}, {0x0c3e, 0x0c40}, {0x0c46, 0x0c48},+    {0x0c4a, 0x0c4d}, {0x0c55, 0x0c56}, {0x0c62, 0x0c63},+    {0x0c81, 0x0c81}, {0x0cbc, 0x0cbc}, {0x0cbf, 0x0cbf},+    {0x0cc6, 0x0cc6}, {0x0ccc, 0x0ccd}, {0x0ce2, 0x0ce3},+    {0x0d00, 0x0d01}, {0x0d3b, 0x0d3c}, {0x0d41, 0x0d44},+    {0x0d4d, 0x0d4d}, {0x0d62, 0x0d63}, {0x0dca, 0x0dca},+    {0x0dd2, 0x0dd4}, {0x0dd6, 0x0dd6}, {0x0e31, 0x0e31},+    {0x0e34, 0x0e3a}, {0x0e47, 0x0e4e}, {0x0eb1, 0x0eb1},+    {0x0eb4, 0x0eb9}, {0x0ebb, 0x0ebc}, {0x0ec8, 0x0ecd},+    {0x0f18, 0x0f19}, {0x0f35, 0x0f35}, {0x0f37, 0x0f37},+    {0x0f39, 0x0f39}, {0x0f71, 0x0f7e}, {0x0f80, 0x0f84},+    {0x0f86, 0x0f87}, {0x0f8d, 0x0f97}, {0x0f99, 0x0fbc},+    {0x0fc6, 0x0fc6}, {0x102d, 0x1030}, {0x1032, 0x1037},+    {0x1039, 0x103a}, {0x103d, 0x103e}, {0x1058, 0x1059},+    {0x105e, 0x1060}, {0x1071, 0x1074}, {0x1082, 0x1082},+    {0x1085, 0x1086}, {0x108d, 0x108d}, {0x109d, 0x109d},+    {0x1160, 0x11ff}, {0x135d, 0x135f}, {0x1712, 0x1714},+    {0x1732, 0x1734}, {0x1752, 0x1753}, {0x1772, 0x1773},+    {0x17b4, 0x17b5}, {0x17b7, 0x17bd}, {0x17c6, 0x17c6},+    {0x17c9, 0x17d3}, {0x17dd, 0x17dd}, {0x180b, 0x180e},+    {0x1885, 0x1886}, {0x18a9, 0x18a9}, {0x1920, 0x1922},+    {0x1927, 0x1928}, {0x1932, 0x1932}, {0x1939, 0x193b},+    {0x1a17, 0x1a18}, {0x1a1b, 0x1a1b}, {0x1a56, 0x1a56},+    {0x1a58, 0x1a5e}, {0x1a60, 0x1a60}, {0x1a62, 0x1a62},+    {0x1a65, 0x1a6c}, {0x1a73, 0x1a7c}, {0x1a7f, 0x1a7f},+    {0x1ab0, 0x1abe}, {0x1b00, 0x1b03}, {0x1b34, 0x1b34},+    {0x1b36, 0x1b3a}, {0x1b3c, 0x1b3c}, {0x1b42, 0x1b42},+    {0x1b6b, 0x1b73}, {0x1b80, 0x1b81}, {0x1ba2, 0x1ba5},+    {0x1ba8, 0x1ba9}, {0x1bab, 0x1bad}, {0x1be6, 0x1be6},+    {0x1be8, 0x1be9}, {0x1bed, 0x1bed}, {0x1bef, 0x1bf1},+    {0x1c2c, 0x1c33}, {0x1c36, 0x1c37}, {0x1cd0, 0x1cd2},+    {0x1cd4, 0x1ce0}, {0x1ce2, 0x1ce8}, {0x1ced, 0x1ced},+    {0x1cf4, 0x1cf4}, {0x1cf8, 0x1cf9}, {0x1dc0, 0x1df9},+    {0x1dfb, 0x1dff}, {0x200b, 0x200f}, {0x202a, 0x202e},+    {0x2060, 0x2064}, {0x2066, 0x206f}, {0x20d0, 0x20f0},+    {0x2cef, 0x2cf1}, {0x2d7f, 0x2d7f}, {0x2de0, 0x2dff},+    {0x302a, 0x302d}, {0x3099, 0x309a}, {0xa66f, 0xa672},+    {0xa674, 0xa67d}, {0xa69e, 0xa69f}, {0xa6f0, 0xa6f1},+    {0xa802, 0xa802}, {0xa806, 0xa806}, {0xa80b, 0xa80b},+    {0xa825, 0xa826}, {0xa8c4, 0xa8c5}, {0xa8e0, 0xa8f1},+    {0xa926, 0xa92d}, {0xa947, 0xa951}, {0xa980, 0xa982},+    {0xa9b3, 0xa9b3}, {0xa9b6, 0xa9b9}, {0xa9bc, 0xa9bc},+    {0xa9e5, 0xa9e5}, {0xaa29, 0xaa2e}, {0xaa31, 0xaa32},+    {0xaa35, 0xaa36}, {0xaa43, 0xaa43}, {0xaa4c, 0xaa4c},+    {0xaa7c, 0xaa7c}, {0xaab0, 0xaab0}, {0xaab2, 0xaab4},+    {0xaab7, 0xaab8}, {0xaabe, 0xaabf}, {0xaac1, 0xaac1},+    {0xaaec, 0xaaed}, {0xaaf6, 0xaaf6}, {0xabe5, 0xabe5},+    {0xabe8, 0xabe8}, {0xabed, 0xabed}, {0xfb1e, 0xfb1e},+    {0xfe00, 0xfe0f}, {0xfe20, 0xfe2f}, {0xfeff, 0xfeff},+    {0xfff9, 0xfffb}, {0x101fd, 0x101fd}, {0x102e0, 0x102e0},+    {0x10376, 0x1037a}, {0x10a01, 0x10a03}, {0x10a05, 0x10a06},+    {0x10a0c, 0x10a0f}, {0x10a38, 0x10a3a}, {0x10a3f, 0x10a3f},+    {0x10ae5, 0x10ae6}, {0x11001, 0x11001}, {0x11038, 0x11046},+    {0x1107f, 0x11081}, {0x110b3, 0x110b6}, {0x110b9, 0x110ba},+    {0x11100, 0x11102}, {0x11127, 0x1112b}, {0x1112d, 0x11134},+    {0x11173, 0x11173}, {0x11180, 0x11181}, {0x111b6, 0x111be},+    {0x111ca, 0x111cc}, {0x1122f, 0x11231}, {0x11234, 0x11234},+    {0x11236, 0x11237}, {0x1123e, 0x1123e}, {0x112df, 0x112df},+    {0x112e3, 0x112ea}, {0x11300, 0x11301}, {0x1133c, 0x1133c},+    {0x11340, 0x11340}, {0x11366, 0x1136c}, {0x11370, 0x11374},+    {0x11438, 0x1143f}, {0x11442, 0x11444}, {0x11446, 0x11446},+    {0x114b3, 0x114b8}, {0x114ba, 0x114ba}, {0x114bf, 0x114c0},+    {0x114c2, 0x114c3}, {0x115b2, 0x115b5}, {0x115bc, 0x115bd},+    {0x115bf, 0x115c0}, {0x115dc, 0x115dd}, {0x11633, 0x1163a},+    {0x1163d, 0x1163d}, {0x1163f, 0x11640}, {0x116ab, 0x116ab},+    {0x116ad, 0x116ad}, {0x116b0, 0x116b5}, {0x116b7, 0x116b7},+    {0x1171d, 0x1171f}, {0x11722, 0x11725}, {0x11727, 0x1172b},+    {0x11a01, 0x11a06}, {0x11a09, 0x11a0a}, {0x11a33, 0x11a38},+    {0x11a3b, 0x11a3e}, {0x11a47, 0x11a47}, {0x11a51, 0x11a56},+    {0x11a59, 0x11a5b}, {0x11a8a, 0x11a96}, {0x11a98, 0x11a99},+    {0x11c30, 0x11c36}, {0x11c38, 0x11c3d}, {0x11c3f, 0x11c3f},+    {0x11c92, 0x11ca7}, {0x11caa, 0x11cb0}, {0x11cb2, 0x11cb3},+    {0x11cb5, 0x11cb6}, {0x11d31, 0x11d36}, {0x11d3a, 0x11d3a},+    {0x11d3c, 0x11d3d}, {0x11d3f, 0x11d45}, {0x11d47, 0x11d47},+    {0x16af0, 0x16af4}, {0x16b30, 0x16b36}, {0x16f8f, 0x16f92},+    {0x1bc9d, 0x1bc9e}, {0x1bca0, 0x1bca3}, {0x1d167, 0x1d169},+    {0x1d173, 0x1d182}, {0x1d185, 0x1d18b}, {0x1d1aa, 0x1d1ad},+    {0x1d242, 0x1d244}, {0x1da00, 0x1da36}, {0x1da3b, 0x1da6c},+    {0x1da75, 0x1da75}, {0x1da84, 0x1da84}, {0x1da9b, 0x1da9f},+    {0x1daa1, 0x1daaf}, {0x1e000, 0x1e006}, {0x1e008, 0x1e018},+    {0x1e01b, 0x1e021}, {0x1e023, 0x1e024}, {0x1e026, 0x1e02a},+    {0x1e8d0, 0x1e8d6}, {0x1e944, 0x1e94a}, {0xe0001, 0xe0001},+    {0xe0020, 0xe007f}, {0xe0100, 0xe01ef},+	};++	/* test for 8-bit control characters */+	if ( ucs == 0 ) {+		return 0;+	}+	if ( ( ucs < 32 ) || ( ( ucs >= 0x7f ) && ( ucs < 0xa0 ) ) ) {+		return -1;+	}++	/* binary search in table of non-spacing characters */+	if ( bisearch( ucs, combining, sizeof( combining ) / sizeof( struct interval ) - 1 ) ) {+		return 0;+	}++	/* if we arrive here, ucs is not a combining or C0/C1 control character */+  return ( mk_is_wide_char( ucs ) ? 2 : 1 );+}+
+ test/Example.hs view
@@ -0,0 +1,96 @@+{- ----------------------------------------------------------------------------
+  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.
+---------------------------------------------------------------------------- -}
+
+import System.Console.Isocline
+import Data.List (isPrefixOf)
+import Data.Char 
+import Control.Monad( when )
+
+main :: IO ()
+main
+  = do styleDef "kbd" "gray underline"     -- define a style
+       styleDef "ic-prompt" "#00A060"      -- or redefine a system style
+       putFmtLn welcome                 
+       setHistory "history.txt" 200        -- history
+       enableAutoTab True                  -- complete as far as possible
+       interaction
+  where
+    welcome = "\n[b]Isocline[/b] sample program:\n" ++
+              "- Type 'exit' to quit. (or use [kbd]ctrl-d[/]).\n" ++
+              "- Press [kbd]F1[/] for help on editing commands.\n" ++
+              "- Use [kbd]shift-tab[/] for multiline input. (or [kbd]ctrl-enter[/], or [kbd]ctrl-j[/])\n" ++
+              "- Type 'p' (or 'id', 'f', or 'h') followed by tab for completion.\n" ++
+              "- Type 'fun' or 'int' to see syntax highlighting\n" ++
+              "- Use [kbd]ctrl-r[/] to search the history.\n"
+
+interaction :: IO ()
+interaction 
+  = do s <- readlineEx "hαskell" (Just completer) (Just highlighter)
+       putStrLn $ unlines ["--------",s,"--------"]
+       if (s == "" || s == "exit") 
+         then return ()
+         else interaction
+
+
+----------------------------------------------------------------------------
+-- Tab Completion
+----------------------------------------------------------------------------       
+
+completer :: CompletionEnv -> String -> IO () 
+completer compl input
+  = do completeFileName compl input Nothing [".","/usr/local"] [] {-any extension-}
+       completeWord compl input Nothing wordCompletions
+  
+wordCompletions :: String -> [Completion]
+wordCompletions input0
+  = let input = map toLower input0
+    in -- simple completion based on available words
+       (completionsFor input ["print","printer","println","printsln","prompt"])
+       ++
+       -- with display versus replacement
+       (if (input == "id") 
+         then map (\(d,r) -> Completion r d "") $    -- Completion replacement display help
+              [ ("D — (x) => x",       "(x) => x")
+              , ("Haskell — \\x -> x", "\\x -> x")
+              , ("Idris — \\x => x",   "\\x => x")
+              , ("Koka — fn(x){ x }",  "fn(x){ x }")
+              , ("Ocaml — fun x -> x", "fun x -> x")]
+         else []) 
+       ++
+       -- add many hello isocline completions; we should generate these lazily!
+       (if (not (null input) && input `isPrefixOf` "hello_isocline_") 
+         then map (\i -> completion ("hello_isocline_" ++ show i)) [1..100000]
+         else [])
+  
+
+----------------------------------------------------------------------------
+-- Syntax highlighting
+-- uses a simple tokenizer but a full fledged one probably needs 
+-- Parsec or regex's for syntax highlighting
+----------------------------------------------------------------------------       
+
+highlighter :: String -> Fmt
+highlighter input
+  = tokenize input
+  where
+    tokenize [] = []
+    tokenize s@('/':'/':_)  -- comment    
+      = let (t,ds) = span (/='\n') s in style "#408700" (plain t) ++ tokenize ds
+    tokenize s@(c:cs)
+      | isAlpha c   = let (t,ds) = span isAlpha s
+                      in (if (t `elem` ["fun","struct","var","val"]) 
+                            then style "keyword" t   -- builtin style
+                          else if (t `elem` ["return","if","then","else"]) 
+                            then style "control" t   -- builtin style
+                          else if (t `elem` ["int","double","char","void"])
+                            then style "#00AFAF" t   -- or use specific colors
+                            else plain t)            -- never lose input, all original characters must be present!
+                         ++ tokenize ds
+      | isDigit c   = let (t,ds) = span isDigit s 
+                      in style "number" t ++ tokenize ds
+      | otherwise   = plain [c] ++ tokenize cs      -- never lose input
+