on master help sign in
add an option to configure 24bit output

create a datatype `ColorMode`, which is stored in `Config` and `Output`. It is passed to all of the functions that handle color processing/output. Currently this only has one possible value, which gives us the default vty color handling behavior. Factor out color drawing code in TerminfoBased.hs Add datatype for 24-bit colors Add 24-bit color support we just write the colors straight to the terminal without using terminfo. Move all color-handling info to `ColorMode` - Add terms for 1/8/16/240 colors to `ColorMode` - Add function `detectColorMode` to detect color mode This removes all color-handling information outside of the `ColorMode` datatype, giving us a single source for color-support information. This emulates the old color-handling code perfectly pretty much perfectly, with one exception; For users with >256 colors listed, we don't do anything special when outputting `Color240`s. The old behavior was absolutely baffling to me, and seemed like it might cause future issues, so I just removed it. Add functions for creating 24-bit colors - linearColor is just a polymorphic synonym for `RGBColor` - srgbColor converts inputs from to linear before making a `RGBColor` - color240 is the same as rgbColor, but with a less confusing name This is all based of of the assumption that outputs will be linear, which is true for gnome-terminal, but I haven't tested it with anything else

commit43c6b3ee4ea73e53c0a01a91317a691e0e81f1ff authorColby Jenn <colbymjenn@gmail.com> dateTue 4 Jun 2019, 23:47 (7 years ago) parent3d32645
7 files changed+168−52
src/Graphics/Vty/Attributes/Color.hs+68−2 src/Graphics/Vty/Config.hs+10−1 src/Graphics/Vty/Output.hs+4−3 src/Graphics/Vty/Output/Interface.hs+22−17 src/Graphics/Vty/Output/Mock.hs+2−1 src/Graphics/Vty/Output/TerminfoBased.hs+58−25 src/Graphics/Vty/Output/XTermColor.hs+4−3
src/Graphics/Vty/Attributes/Color.hs+68−2
@@ -1,8 +1,14 @@
11{-# LANGUAGE DeriveGeneric #-}
22{-# LANGUAGE DeriveAnyClass #-}
3+{-# LANGUAGE MultiWayIf #-}
4+{-# LANGUAGE ScopedTypeVariables #-}
35
46module Graphics.Vty.Attributes.Color
57 ( Color(..)
8+ , ColorMode(..)
9+
10+ -- * Detecting Terminal Color Support
11+ , detectColorMode
612
713 -- ** Fixed Colors
814 -- | Standard 8-color ANSI terminal color codes.
@@ -32,7 +38,10 @@ module Graphics.Vty.Attributes.Color
3238 , brightCyan
3339 , brightWhite
3440 -- ** Creating Colors From RGB
41+ , linearColor
42+ , srgbColor
3543 , rgbColor
44+ , color240
3645 , module Graphics.Vty.Attributes.Color240
3746 )
3847where
@@ -40,6 +49,11 @@ where
4049import Data.Word
4150import GHC.Generics
4251import Control.DeepSeq
52+import Text.Printf (printf)
53+
54+import qualified System.Console.Terminfo as Terminfo
55+import Control.Exception (catch)
56+import Data.Maybe
4357
4458import Graphics.Vty.Attributes.Color240
4559
@@ -95,9 +109,30 @@ import Graphics.Vty.Attributes.Color240
95109-- terminfo but I don't know it.
96110--
97111-- Seriously, terminal color support is INSANE.
98−data Color = ISOColor !Word8 | Color240 !Word8
112+data Color = ISOColor !Word8 | Color240 !Word8 | RGBColor !Word8 !Word8 !Word8
99113 deriving ( Eq, Show, Read, Generic, NFData )
100114
115+data ColorMode
116+ = NoColor
117+ | ColorMode8
118+ | ColorMode16
119+ | ColorMode240 !Word8
120+ | FullColor
121+ deriving ( Eq, Show )
122+
123+detectColorMode :: String -> IO ColorMode
124+detectColorMode termName' = do
125+ term <- catch (Just <$> Terminfo.setupTerm termName')
126+ (\(_ :: Terminfo.SetupTermError) -> return Nothing)
127+ let getCap cap = term >>= \t -> Terminfo.getCapability t cap
128+ termColors = fromMaybe 0 $ getCap (Terminfo.tiGetNum "colors")
129+ return $ if
130+ | termColors < 8 -> NoColor
131+ | termColors < 16 -> ColorMode8
132+ | termColors == 16 -> ColorMode16
133+ | termColors < 256 -> ColorMode240 (fromIntegral termColors - 16)
134+ | otherwise -> ColorMode240 240
135+
101136black, red, green, yellow, blue, magenta, cyan, white :: Color
102137black = ISOColor 0
103138red = ISOColor 1
@@ -119,9 +154,40 @@ brightMagenta= ISOColor 13
119154brightCyan = ISOColor 14
120155brightWhite = ISOColor 15
121156
157+-- TODO: test this with other terminals
158+linearColor :: Integral i => i -> i -> i -> Color
159+linearColor r g b
160+ | r < 0 || r > 255 = makeError
161+ | g < 0 || g > 255 = makeError
162+ | b < 0 || b > 255 = makeError
163+ | otherwise = RGBColor r' g' b'
164+ where
165+ r' = fromIntegral r :: Word8
166+ g' = fromIntegral g :: Word8
167+ b' = fromIntegral b :: Word8
168+ makeError = error $ printf "invalid value in rgbColor: (%d, %d, %d)" r' g' b'
169+
170+srgbColor :: Integral i => i -> i -> i -> Color
171+srgbColor r g b =
172+ -- srgb to rgb transformation as described at
173+ -- https://en.wikipedia.org/wiki/SRGB#The_reverse_transformation
174+ --
175+ -- TODO: it may be worth translating this to a lookup table, as with color240
176+ let shrink n = fromIntegral n / 255 :: Double
177+ -- called gamma^-1 in wiki
178+ gamma u
179+ | u <= 0.04045 = u/12.92
180+ | otherwise = ((u + 0.055) / 1.055) ** 2.4
181+ -- TODO: this is a slightly inaccurate conversion. is it worth doing proterly?
182+ expand n = round (255 * n)
183+ convert = expand . gamma . shrink
184+ in RGBColor (convert r) (convert g) (convert b)
185+
186+color240 :: Integral i => i -> i -> i -> Color
187+color240 r g b = Color240 (rgbColorToColor240 r g b)
122188
123189-- | Create a Vty 'Color' (in the 240 color set) from an RGB triple.
124190-- This function is lossy in the sense that we only internally support 240 colors but the
125191-- #RRGGBB format supports 16^3 colors.
126192rgbColor :: Integral i => i -> i -> i -> Color
127−rgbColor r g b = Color240 (rgbColorToColor240 r g b)
193+rgbColor = color240
src/Graphics/Vty/Config.hs+10−1
@@ -3,6 +3,7 @@
33{-# LANGUAGE FlexibleContexts, FlexibleInstances #-}
44{-# LANGUAGE TypeOperators #-}
55{-# LANGUAGE ScopedTypeVariables #-}
6+{-# LANGUAGE LambdaCase, MultiWayIf #-}
67-- | Vty supports a configuration file format and associated 'Config'
78-- data type. The 'Config' can be provided to 'mkVty' to customize the
89-- application's use of Vty.
@@ -122,6 +123,7 @@ import Data.Semigroup (Semigroup(..))
122123import Data.Typeable (Typeable)
123124
124125import Graphics.Vty.Input.Events
126+import Graphics.Vty.Attributes.Color (ColorMode(..), detectColorMode)
125127
126128import GHC.Generics
127129
@@ -193,6 +195,9 @@ data Config =
193195 -- configuration specifies one. If no custom table is loaded
194196 -- (or if a load fails), the built-in character width table
195197 -- will be used.
198+ , colorMode :: Maybe ColorMode
199+ -- ^ The color mode used to know how many colors the terminal
200+ -- supports.
196201 }
197202 deriving (Show, Eq)
198203
@@ -214,6 +219,7 @@ instance Semigroup Config where
214219 , termWidthMaps = termWidthMaps c1 <|> termWidthMaps c0
215220 , allowCustomUnicodeWidthTables =
216221 allowCustomUnicodeWidthTables c1 <|> allowCustomUnicodeWidthTables c0
222+ , colorMode = colorMode c1 <|> colorMode c0
217223 }
218224
219225instance Monoid Config where
@@ -229,6 +235,7 @@ instance Monoid Config where
229235 , termName = Nothing
230236 , termWidthMaps = []
231237 , allowCustomUnicodeWidthTables = Nothing
238+ , colorMode = Nothing
232239 }
233240#if !(MIN_VERSION_base(4,11,0))
234241 mappend = (<>)
@@ -284,7 +291,8 @@ standardIOConfig = do
284291 mb <- lookupEnv termVariable
285292 case mb of
286293 Nothing -> throwIO VtyMissingTermEnvVar
287− Just t ->
294+ Just t -> do
295+ mcolorMode <- detectColorMode t
288296 return defaultConfig
289297 { vmin = Just 1
290298 , mouseMode = Just False
@@ -293,6 +301,7 @@ standardIOConfig = do
293301 , inputFd = Just stdInput
294302 , outputFd = Just stdOutput
295303 , termName = Just t
304+ , colorMode = Just mcolorMode
296305 }
297306
298307parseConfigFile :: FilePath -> IO Config
src/Graphics/Vty/Output.hs+4−3
@@ -58,11 +58,12 @@ import Data.Monoid ((<>))
5858-- * If TERM contains "xterm" or "screen", use XTermColor.
5959-- * otherwise use the TerminfoBased driver.
6060outputForConfig :: Config -> IO Output
61−outputForConfig Config{ outputFd = Just fd, termName = Just termName, .. } = do
61+outputForConfig Config{ outputFd = Just fd, termName = Just termName
62+ , colorMode = Just colorMode, .. } = do
6263 t <- if "xterm" `isPrefixOf` termName || "screen" `isPrefixOf` termName
63− then XTermColor.reserveTerminal termName fd
64+ then XTermColor.reserveTerminal termName fd colorMode
6465 -- Not an xterm-like terminal. try for generic terminfo.
65− else TerminfoBased.reserveTerminal termName fd
66+ else TerminfoBased.reserveTerminal termName fd colorMode
6667
6768 case mouseMode of
6869 Just s -> setMode t Mouse s
src/Graphics/Vty/Output/Interface.hs+22−17
@@ -79,9 +79,6 @@ data Output = Output
7979 , displayBounds :: IO DisplayRegion
8080 -- | Output the bytestring to the terminal device.
8181 , outputByteBuffer :: BS.ByteString -> IO ()
82− -- | Specifies the maximum number of colors supported by the
83− -- context.
84− , contextColorCount :: Int
8582 -- | Specifies whether the cursor can be shown / hidden.
8683 , supportsCursorVisibility :: Bool
8784 -- | Indicates support for terminal modes for this output device.
@@ -117,6 +114,8 @@ data Output = Output
117114 -- be a reliable indicator of whether the feature will work as
118115 -- desired.
119116 , supportsStrikethrough :: IO Bool
117+ -- | Returns how many colors the terminal supports.
118+ , outputColorMode :: ColorMode
120119 }
121120
122121displayContext :: Output -> DisplayRegion -> IO DisplayContext
@@ -310,17 +309,23 @@ limitAttrForDisplay t attr
310309 where
311310 clampColor Default = Default
312311 clampColor KeepCurrent = KeepCurrent
313− clampColor (SetTo c) = clampColor' c
314− clampColor' (ISOColor v)
315− | contextColorCount t < 8 = Default
316− | contextColorCount t < 16 && v >= 8 = SetTo $ ISOColor (v - 8)
317− | otherwise = SetTo $ ISOColor v
318− clampColor' (Color240 v)
319− -- Should we choose closest ISO color?
320− | contextColorCount t < 8 = Default
321− | contextColorCount t < 16 = Default
322− | contextColorCount t <= 256 = SetTo $ Color240 v
323− | otherwise
324− = let p :: Double = fromIntegral v / 240.0
325− v' = floor $ p * (fromIntegral $ contextColorCount t)
326− in SetTo $ Color240 v'
312+ clampColor (SetTo c) = clampColor' (outputColorMode t) c
313+
314+ clampColor' NoColor _ = Default
315+
316+ clampColor' ColorMode8 (ISOColor v)
317+ | v >= 8 = SetTo $ ISOColor (v - 8)
318+ | otherwise = SetTo $ ISOColor v
319+ clampColor' ColorMode8 _ = Default
320+
321+ clampColor' ColorMode16 c@(ISOColor _) = SetTo c
322+ clampColor' ColorMode16 _ = Default
323+
324+ clampColor' (ColorMode240 _) c@(ISOColor _) = SetTo c
325+ clampColor' (ColorMode240 colorCount) c@(Color240 n)
326+ | n <= colorCount = SetTo c
327+ | otherwise = Default
328+ clampColor' colorMode@(ColorMode240 _) (RGBColor r g b) =
329+ clampColor' colorMode (color240 r g b)
330+
331+ clampColor' FullColor c = SetTo c
src/Graphics/Vty/Output/Mock.hs+2−1
@@ -11,6 +11,7 @@ module Graphics.Vty.Output.Mock
1111where
1212
1313import Graphics.Vty.Image (DisplayRegion)
14+import Graphics.Vty.Attributes.Color (ColorMode(ColorMode16))
1415import Graphics.Vty.Output.Interface
1516
1617import Blaze.ByteString.Builder.Word (writeWord8)
@@ -55,12 +56,12 @@ mockTerminal r = liftIO $ do
5556 , outputByteBuffer = \bytes -> do
5657 putStrLn $ "mock outputByteBuffer of " ++ show (BS.length bytes) ++ " bytes"
5758 writeIORef outRef $ UTF8.fromRep bytes
58− , contextColorCount = 16
5959 , supportsCursorVisibility = True
6060 , supportsMode = const False
6161 , setMode = const $ const $ return ()
6262 , getModeStatus = const $ return False
6363 , assumedStateRef = newAssumedStateRef
64+ , outputColorMode = ColorMode16
6465 , mkDisplayContext = \tActual rActual -> return $ DisplayContext
6566 { contextRegion = rActual
6667 , contextDevice = tActual
src/Graphics/Vty/Output/TerminfoBased.hs+58−25
@@ -25,7 +25,7 @@ import Graphics.Vty.Image (DisplayRegion)
2525import Graphics.Vty.DisplayAttributes
2626import Graphics.Vty.Output.Interface
2727
28−import Blaze.ByteString.Builder (Write, writeToByteString, writeStorable)
28+import Blaze.ByteString.Builder (Write, writeToByteString, writeStorable, writeWord8)
2929
3030import Data.IORef
3131import Data.Maybe (isJust, isNothing, fromJust)
@@ -49,7 +49,6 @@ data TerminfoCaps = TerminfoCaps
4949 , cup :: CapExpression
5050 , cnorm :: Maybe CapExpression
5151 , civis :: Maybe CapExpression
52− , supportsNoColors :: Bool
5352 , useAltColorMap :: Bool
5453 , setForeColor :: CapExpression
5554 , setBackColor :: CapExpression
@@ -104,20 +103,20 @@ sendCapToTerminal t cap capParams = do
104103--
105104-- * Providing independent string capabilities for all display
106105-- attributes.
107−reserveTerminal :: String -> Fd -> IO Output
108−reserveTerminal termName outFd = do
106+reserveTerminal :: String -> Fd -> ColorMode -> IO Output
107+reserveTerminal termName outFd colorMode = do
109108 ti <- Terminfo.setupTerm termName
110109 -- assumes set foreground always implies set background exists.
111110 -- if set foreground is not set then all color changing style
112111 -- attributes are filtered.
113112 msetaf <- probeCap ti "setaf"
114113 msetf <- probeCap ti "setf"
115− let (noColors, useAlt, setForeCap)
114+ let (useAlt, setForeCap)
116115 = case msetaf of
117− Just setaf -> (False, False, setaf)
116+ Just setaf -> (False, setaf)
118117 Nothing -> case msetf of
119− Just setf -> (False, True, setf)
120− Nothing -> (True, True, error $ "no fore color support for terminal " ++ termName)
118+ Just setf -> (True, setf)
119+ Nothing -> (True, error $ "no fore color support for terminal " ++ termName)
121120 msetab <- probeCap ti "setab"
122121 msetb <- probeCap ti "setb"
123122 let setBackCap
@@ -151,7 +150,6 @@ reserveTerminal termName outFd = do
151150 <*> requireCap ti "cup"
152151 <*> probeCap ti "cnorm"
153152 <*> probeCap ti "civis"
154− <*> pure noColors
155153 <*> pure useAlt
156154 <*> pure setForeCap
157155 <*> pure setBackCap
@@ -194,17 +192,12 @@ reserveTerminal termName outFd = do
194192 when (toEnum len /= actualLen) $ fail $ "Graphics.Vty.Output: outputByteBuffer "
195193 ++ "length mismatch. " ++ show len ++ " /= " ++ show actualLen
196194 ++ " Please report this bug to vty project."
197− , contextColorCount
198− = case supportsNoColors terminfoCaps of
199− False -> case Terminfo.getCapability ti (Terminfo.tiGetNum "colors" ) of
200− Nothing -> 8
201− Just v -> toEnum v
202− True -> 1
203195 , supportsCursorVisibility = isJust $ civis terminfoCaps
204196 , supportsMode = terminfoModeSupported
205197 , setMode = terminfoSetMode
206198 , getModeStatus = terminfoModeStatus
207199 , assumedStateRef = newAssumedStateRef
200+ , outputColorMode = colorMode
208201 -- I think fix would help assure tActual is the only
209202 -- reference. I was having issues tho.
210203 , mkDisplayContext = (`terminfoDisplayContext` terminfoCaps)
@@ -376,9 +369,9 @@ terminfoWriteSetAttr dc terminfoCaps urlsEnabled prevAttr reqAttr diffs =
376369 -- applied states.
377370 EnterExitSeq caps -> foldMap (\cap -> writeCapExpr cap []) caps
378371 `mappend`
379− writeColorDiff setForeColor (foreColorDiff diffs)
372+ writeColorDiff Foreground (foreColorDiff diffs)
380373 `mappend`
381− writeColorDiff setBackColor (backColorDiff diffs)
374+ writeColorDiff Background (backColorDiff diffs)
382375 -- implicitly resets the colors to the defaults
383376 SetState state -> writeCapExpr (fromJust $ setAttrStates
384377 $ displayAttrCaps terminfoCaps
@@ -410,20 +403,52 @@ terminfoWriteSetAttr dc terminfoCaps urlsEnabled prevAttr reqAttr diffs =
410403 | otherwise = mempty
411404 setColors =
412405 (case fixedForeColor attr of
413− Just c -> writeCapExpr (setForeColor terminfoCaps)
414− [toEnum $ colorMap c]
406+ Just c -> writeColor Foreground c
415407 Nothing -> mempty)
416408 `mappend`
417409 (case fixedBackColor attr of
418− Just c -> writeCapExpr (setBackColor terminfoCaps)
419− [toEnum $ colorMap c]
410+ Just c -> writeColor Background c
420411 Nothing -> mempty)
421− writeColorDiff _f NoColorChange
412+ writeColorDiff _side NoColorChange
422413 = mempty
423− writeColorDiff _f ColorToDefault
414+ writeColorDiff _side ColorToDefault
424415 = error "ColorToDefault is not a possible case for applyColorDiffs"
425− writeColorDiff f (SetColor c)
426− = writeCapExpr (f terminfoCaps) [toEnum $ colorMap c]
416+ writeColorDiff side (SetColor c)
417+ = writeColor side c
418+
419+ writeColor side (RGBColor r g b) =
420+ case outputColorMode (contextDevice dc) of
421+ FullColor ->
422+ hardcodeColor side (r, g, b)
423+ _ ->
424+ error "clampColor should remove rgb colors in standard mode"
425+ writeColor side c =
426+ writeCapExpr (setSideColor side terminfoCaps) [toEnum $ colorMap c]
427+
428+-- a color can either be in the foreground or the background
429+data ColorSide = Foreground | Background
430+
431+-- get the capability for drawing a color on a specific side
432+setSideColor :: ColorSide -> TerminfoCaps -> CapExpression
433+setSideColor Foreground = setForeColor
434+setSideColor Background = setBackColor
435+
436+hardcodeColor :: ColorSide -> (Word8, Word8, Word8) -> Write
437+hardcodeColor side (r, g, b) =
438+ -- hardcoded color codes are formatted as "\x1b[{side};2;{r};{g};{b}m"
439+ mconcat [ writeStr "\x1b[", sideCode, delimiter, writeChar '2', delimiter
440+ , writeColor r, delimiter, writeColor g, delimiter, writeColor b
441+ , writeChar 'm']
442+ where
443+ writeChar = writeWord8 . fromIntegral . fromEnum
444+ writeStr = mconcat . map writeChar
445+ writeColor = writeStr . show
446+ delimiter = writeChar ';'
447+ -- 38/48 are used to set whether we should write to the
448+ -- foreground/background. I really don't want to know why.
449+ sideCode = case side of
450+ Foreground -> writeStr "38"
451+ Background -> writeStr "48"
427452
428453-- | The color table used by a terminal is a 16 color set followed by a
429454-- 240 color set that might not be supported by the terminal.
@@ -433,6 +458,10 @@ terminfoWriteSetAttr dc terminfoCaps urlsEnabled prevAttr reqAttr diffs =
433458ansiColorIndex :: Color -> Int
434459ansiColorIndex (ISOColor v) = fromEnum v
435460ansiColorIndex (Color240 v) = 16 + fromEnum v
461+ansiColorIndex (RGBColor _ _ _) =
462+ error $ unlines [ "Attempted to create color index from rgb color."
463+ , "This is currently unsupported, and shouldn't ever happen"
464+ ]
436465
437466-- | For terminals without setaf/setab
438467--
@@ -449,6 +478,10 @@ altColorIndex (ISOColor 6) = 3
449478altColorIndex (ISOColor 7) = 7
450479altColorIndex (ISOColor v) = fromEnum v
451480altColorIndex (Color240 v) = 16 + fromEnum v
481+altColorIndex (RGBColor _ _ _) =
482+ error $ unlines [ "Attempted to create color index from rgb color."
483+ , "This is currently unsupported, and shouldn't ever happen"
484+ ]
452485
453486{- | The sequence of terminfo caps to apply a given style are determined
454487 - according to these rules.
src/Graphics/Vty/Output/XTermColor.hs+4−3
@@ -10,6 +10,7 @@ where
1010import Graphics.Vty.Output.Interface
1111import Graphics.Vty.Input.Mouse
1212import Graphics.Vty.Input.Focus
13+import Graphics.Vty.Attributes.Color (ColorMode)
1314import qualified Graphics.Vty.Output.TerminfoBased as TerminfoBased
1415
1516import Blaze.ByteString.Builder (writeToByteString)
@@ -42,8 +43,8 @@ fdWrite fd s =
4243 fdWriteBuf fd (castPtr buf) (fromIntegral len)
4344
4445-- | Construct an Xterm output driver. Initialize the display to UTF-8.
45−reserveTerminal :: ( Applicative m, MonadIO m ) => String -> Fd -> m Output
46−reserveTerminal variant outFd = liftIO $ do
46+reserveTerminal :: ( Applicative m, MonadIO m ) => String -> Fd -> ColorMode -> m Output
47+reserveTerminal variant outFd colorMode = liftIO $ do
4748 let flushedPut = void . fdWrite outFd
4849 -- If the terminal variant is xterm-color use xterm instead since,
4950 -- more often than not, xterm-color is broken.
@@ -51,7 +52,7 @@ reserveTerminal variant outFd = liftIO $ do
5152
5253 utf8a <- utf8Active
5354 when (not utf8a) $ flushedPut setUtf8CharSet
54− t <- TerminfoBased.reserveTerminal variant' outFd
55+ t <- TerminfoBased.reserveTerminal variant' outFd colorMode
5556
5657 mouseModeStatus <- newIORef False
5758 focusModeStatus <- newIORef False