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 +21/−0
- haskell/System/Console/Isocline.hs +1046/−0
- haskell/readme.md +28/−0
- include/isocline.h +627/−0
- isocline.cabal +99/−0
- src/attr.c +294/−0
- src/attr.h +70/−0
- src/bbcode.c +841/−0
- src/bbcode.h +37/−0
- src/bbcode_colors.c +194/−0
- src/common.c +347/−0
- src/common.h +187/−0
- src/completers.c +675/−0
- src/completions.c +312/−0
- src/completions.h +52/−0
- src/editline.c +1132/−0
- src/editline_completion.c +277/−0
- src/editline_help.c +140/−0
- src/editline_history.c +260/−0
- src/env.h +60/−0
- src/highlight.c +259/−0
- src/highlight.h +24/−0
- src/history.c +269/−0
- src/history.h +38/−0
- src/isocline.c +589/−0
- src/stringbuf.c +1034/−0
- src/stringbuf.h +121/−0
- src/term.c +1113/−0
- src/term.h +85/−0
- src/term_color.c +371/−0
- src/tty.c +881/−0
- src/tty.h +160/−0
- src/tty_esc.c +375/−0
- src/undo.c +67/−0
- src/undo.h +24/−0
- src/wcwidth.c +292/−0
- test/Example.hs +96/−0
+ 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 + + +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:  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 +