11{-# LANGUAGE LambdaCase #-}
22
33module Sauron.UnicodeWidthTable (
4- buildAndSaveWidthTable
4+ WidthTableMode (.. )
5+ , buildAndSaveWidthTable
56 , loadWidthTable
67 , widthTablePath
78 ) where
89
10+ import Data.Char (generalCategory , GeneralCategory (.. ))
911import Data.String.Interpolate
1012import Graphics.Vty.UnicodeWidthTable.IO (readUnicodeWidthTable , writeUnicodeWidthTable )
1113import Graphics.Vty.UnicodeWidthTable.Install (installUnicodeWidthTable )
12- import Graphics.Vty.UnicodeWidthTable.Query ( buildUnicodeWidthTable , defaultUnicodeTableUpperBound )
14+ import Graphics.Vty.UnicodeWidthTable.Types ( UnicodeWidthTable ( .. ), WidthTableRange ( .. ) )
1315import Relude
1416import Sauron.OAuth (getConfigDir )
1517import System.Console.ANSI (getCursorPosition )
@@ -19,6 +21,47 @@ import UnliftIO.Directory (createDirectoryIfMissing, doesFileExist)
1921import UnliftIO.Exception (bracket_ )
2022
2123
24+ -- | Character ranges where terminals commonly disagree with the built-in
25+ -- wcwidth table. Characters outside these ranges fall back to vty's built-in.
26+ emojiAndSymbolRanges :: [(Char , Char )]
27+ emojiAndSymbolRanges =
28+ [ (' \x2000 ' , ' \x206F ' ) -- General Punctuation
29+ , (' \x2190 ' , ' \x21FF ' ) -- Arrows
30+ , (' \x2300 ' , ' \x23FF ' ) -- Miscellaneous Technical (⌛, ⌚, etc.)
31+ , (' \x2460 ' , ' \x24FF ' ) -- Enclosed Alphanumerics
32+ , (' \x2500 ' , ' \x257F ' ) -- Box Drawing
33+ , (' \x2580 ' , ' \x259F ' ) -- Block Elements
34+ , (' \x25A0 ' , ' \x25FF ' ) -- Geometric Shapes
35+ , (' \x2600 ' , ' \x27BF ' ) -- Misc Symbols + Dingbats (☀, ✓, ✗, etc.)
36+ , (' \x2900 ' , ' \x297F ' ) -- Supplemental Arrows-B
37+ , (' \x2B00 ' , ' \x2BFF ' ) -- Misc Symbols and Arrows (⭐, etc.)
38+ , (' \x3000 ' , ' \x303F ' ) -- CJK Symbols and Punctuation
39+ , (' \xFE00 ' , ' \xFE0F ' ) -- Variation Selectors
40+ , (' \x1F000 ' , ' \x1F02F ' ) -- Mahjong Tiles
41+ , (' \x1F0A0 ' , ' \x1F0FF ' ) -- Playing Cards
42+ , (' \x1F100 ' , ' \x1F1FF ' ) -- Enclosed Alphanumeric Supplement + Regional Indicators
43+ , (' \x1F200 ' , ' \x1F2FF ' ) -- Enclosed Ideographic Supplement
44+ , (' \x1F300 ' , ' \x1F5FF ' ) -- Misc Symbols and Pictographs
45+ , (' \x1F600 ' , ' \x1F64F ' ) -- Emoticons
46+ , (' \x1F680 ' , ' \x1F6FF ' ) -- Transport and Map Symbols
47+ , (' \x1F700 ' , ' \x1F77F ' ) -- Alchemical Symbols
48+ , (' \x1F780 ' , ' \x1F7FF ' ) -- Geometric Shapes Extended
49+ , (' \x1F800 ' , ' \x1F8FF ' ) -- Supplemental Arrows-C
50+ , (' \x1F900 ' , ' \x1F9FF ' ) -- Supplemental Symbols and Pictographs
51+ , (' \x1FA00 ' , ' \x1FA6F ' ) -- Chess Symbols
52+ , (' \x1FA70 ' , ' \x1FAFF ' ) -- Symbols and Pictographs Extended-A
53+ , (' \x1FB00 ' , ' \x1FBFF ' ) -- Symbols for Legacy Computing
54+ ]
55+
56+ charsToQuery :: [Char ]
57+ charsToQuery = filter shouldConsider $ concatMap (\ (lo, hi) -> [lo.. hi]) emojiAndSymbolRanges
58+ where
59+ shouldConsider c = case generalCategory c of
60+ Control -> False
61+ NotAssigned -> False
62+ Surrogate -> False
63+ _ -> True
64+
2265widthTablePath :: MonadIO m => m FilePath
2366widthTablePath = do
2467 configDir <- getConfigDir
@@ -34,8 +77,40 @@ charWidth c = do
3477 Just (_, col) -> pure col
3578 Nothing -> pure 0
3679
37- buildAndSaveWidthTable :: IO ()
38- buildAndSaveWidthTable = do
80+ -- | Run-length encode a sorted list of (codepoint, width) pairs into ranges.
81+ mkRanges :: [(Word32 , Word8 )] -> [WidthTableRange ]
82+ mkRanges = go Nothing []
83+ where
84+ go Nothing finished [] = finished
85+ go (Just r) finished [] = r : finished
86+ go Nothing finished ((c, w): rest) =
87+ go (Just $ WidthTableRange c 1 w) finished rest
88+ go (Just r@ (WidthTableRange start sz prevW)) finished ((c, w): rest)
89+ | c == start + sz && w == prevW =
90+ go (Just $ WidthTableRange start (sz + 1 ) prevW) finished rest
91+ | otherwise =
92+ go (Just $ WidthTableRange c 1 w) (r : finished) rest
93+
94+ data WidthTableMode = EmojiOnly | FullUnicode
95+
96+ buildWidthTable :: WidthTableMode -> IO UnicodeWidthTable
97+ buildWidthTable mode = do
98+ let chars = case mode of
99+ EmojiOnly -> charsToQuery
100+ FullUnicode -> filter shouldConsider ['\ 0 '.. ' \xe0000 ' ]
101+ pairs <- forM chars $ \ c -> do
102+ w <- charWidth c
103+ pure (fromIntegral (fromEnum c) :: Word32 , fromIntegral w :: Word8 )
104+ pure $ UnicodeWidthTable { unicodeWidthTableRanges = reverse $ mkRanges pairs }
105+ where
106+ shouldConsider c = case generalCategory c of
107+ Control -> False
108+ NotAssigned -> False
109+ Surrogate -> False
110+ _ -> True
111+
112+ buildAndSaveWidthTable :: WidthTableMode -> IO ()
113+ buildAndSaveWidthTable mode = do
39114 isTTY <- hIsTerminalDevice stdout
40115 unless isTTY $
41116 fail " Must be run in an interactive terminal"
@@ -47,9 +122,12 @@ buildAndSaveWidthTable = do
47122 let setRaw = hSetBuffering stdout NoBuffering >> hSetBuffering stdin NoBuffering
48123 let restore = hSetBuffering stdout oldBuffering
49124
50- hPutStrLn stderr [i |Building Unicode width table...|]
125+ let label = case mode of
126+ EmojiOnly -> [i |emoji/symbols|]
127+ FullUnicode -> [i |full Unicode (this may take a while)|] :: String
128+ hPutStrLn stderr [i |Building #{label} width table...|]
51129 bracket_ setRaw restore $ do
52- table <- buildUnicodeWidthTable charWidth defaultUnicodeTableUpperBound
130+ table <- buildWidthTable mode
53131 writeUnicodeWidthTable path table
54132 hPutStrLn stderr [i |\nWritten to #{path}|]
55133
@@ -62,5 +140,6 @@ loadWidthTable = do
62140 readUnicodeWidthTable path >>= \ case
63141 Left _err -> pure False
64142 Right table -> do
143+ hPutStrLn stderr [i |Loaded character width table from #{path}|]
65144 installUnicodeWidthTable table
66145 pure True
0 commit comments