Skip to content

Commit 4b4b4e0

Browse files
committed
Build smaller unicode table by default
1 parent b7db450 commit 4b4b4e0

3 files changed

Lines changed: 96 additions & 13 deletions

File tree

app/Main.hs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ import Sauron.UI.Modals.NewIssueModal (renderNewIssueModal)
4646
import Sauron.UI.Modals.ZoomModal (renderZoomModal)
4747
import Sauron.UI.TimelineBorder (borderWithAttr)
4848
import Sauron.UI.TopBox (topBox)
49-
import Sauron.UnicodeWidthTable (buildAndSaveWidthTable, loadWidthTable)
49+
import Sauron.UnicodeWidthTable (WidthTableMode(..), buildAndSaveWidthTable, loadWidthTable)
5050
import System.IO.Error (userError)
5151
import UnliftIO.Async
5252
import UnliftIO.Concurrent
@@ -108,11 +108,13 @@ main = parseCommand >>= \case
108108
RunApp cliArgs -> runApp cliArgs
109109

110110
runApp :: CliArgs -> IO ()
111-
runApp (CliArgs {cliRefreshCharacterWidths=True}) = buildAndSaveWidthTable
111+
runApp (CliArgs {cliRebuildCharacterWidths=True}) =
112+
buildAndSaveWidthTable EmojiOnly
113+
runApp (CliArgs {cliRebuildCharacterWidthsFull=True}) =
114+
buildAndSaveWidthTable FullUnicode
112115
runApp cliArgs@(CliArgs {cliConfigFile, cliShowAllRepos, cliColorMode, cliSplitLogs}) = do
113-
-- Build or load Unicode width table before starting TUI
114-
unlessM (loadWidthTable) $ do
115-
buildAndSaveWidthTable
116+
unlessM loadWidthTable $ do
117+
buildAndSaveWidthTable EmojiOnly
116118
void loadWidthTable
117119

118120
eventChan <- newBChan 10

app/Sauron/Options.hs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ data CliArgs = CliArgs {
4040
, cliShowAllRepos :: Bool
4141
, cliColorMode :: Maybe V.ColorMode
4242
, cliSplitLogs :: Bool
43-
, cliRefreshCharacterWidths :: Bool
43+
, cliRebuildCharacterWidths :: Bool
44+
, cliRebuildCharacterWidthsFull :: Bool
4445
} deriving (Show)
4546

4647
cliArgsParser :: Parser CliArgs
@@ -53,7 +54,8 @@ cliArgsParser = CliArgs
5354
<*> switch (long "all" <> help "Show all repositories for the authenticated user")
5455
<*> optional (option (maybeReader parseColorMode) (long "color-mode" <> help "Force a specific color mode (full, 240, 16, 8, none)" <> metavar "MODE"))
5556
<*> switch (long "split-logs" <> help "Split terminal view: app on left, logs on right")
56-
<*> switch (long "refresh-character-widths" <> help "Rebuild the Unicode character width table for the current terminal")
57+
<*> switch (long "rebuild-widths" <> help "Rebuild the Unicode character width table to improve display (emoji/symbol ranges)")
58+
<*> switch (long "rebuild-widths-full" <> help "Rebuild the Unicode character width table to improve display (full Unicode scan, slower)")
5759

5860
initConfigParser :: Parser Command
5961
initConfigParser = InitConfig

app/Sauron/UnicodeWidthTable.hs

Lines changed: 85 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
{-# LANGUAGE LambdaCase #-}
22

33
module Sauron.UnicodeWidthTable (
4-
buildAndSaveWidthTable
4+
WidthTableMode(..)
5+
, buildAndSaveWidthTable
56
, loadWidthTable
67
, widthTablePath
78
) where
89

10+
import Data.Char (generalCategory, GeneralCategory(..))
911
import Data.String.Interpolate
1012
import Graphics.Vty.UnicodeWidthTable.IO (readUnicodeWidthTable, writeUnicodeWidthTable)
1113
import Graphics.Vty.UnicodeWidthTable.Install (installUnicodeWidthTable)
12-
import Graphics.Vty.UnicodeWidthTable.Query (buildUnicodeWidthTable, defaultUnicodeTableUpperBound)
14+
import Graphics.Vty.UnicodeWidthTable.Types (UnicodeWidthTable(..), WidthTableRange(..))
1315
import Relude
1416
import Sauron.OAuth (getConfigDir)
1517
import System.Console.ANSI (getCursorPosition)
@@ -19,6 +21,47 @@ import UnliftIO.Directory (createDirectoryIfMissing, doesFileExist)
1921
import 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+
2265
widthTablePath :: MonadIO m => m FilePath
2366
widthTablePath = 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

Comments
 (0)