about summary refs log tree commit diff stats
path: root/src/display
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2024-02-17 19:28:40 +0100
committerbptato <nincsnevem662@gmail.com>2024-02-17 19:28:40 +0100
commite98d0ad1dc51050eb17120f835847d55950c2a0b (patch)
treedfe6848c19bffbad3f24159aa6fe0af0355c6048 /src/display
parent89d1bdc04343dac1d09282424f4b8538f1db5ad3 (diff)
downloadchawan-e98d0ad1dc51050eb17120f835847d55950c2a0b.tar.gz
term: fix coloring mess
Until now, the config file required manual adjustment for the output to
look bearable on terminals colored differently than {bgcolor: black,
fgcolor: white}. Also, it only detected RGB when COLORTERM was set, but
this is not done by most (any?) terminal emulators (sad).

To improve upon the situation, we now query the terminal for some
attributes on startup:

* OSC(10/11, ?) -> get the terminal's bg/fgcolor
* DCS(+, q, 524742) -> XTGETTCAP for the "RGB" capability (only
  supported by a few terminals, but better than nothing)
* Primary device attributes -> check if ANSI colors are supported, also
  make sure we don't block indefinitely even if the previous queries
  fail

If primary device attributes does not return anything, we hang until
the user types something, then notify the user that something went
wrong, and tell them how to fix it. Seems like an OK fallback.

(The DA1 idea comes from notcurses; since this is implemented by pretty
much every terminal emulator, we don't have to rely on slow timing hacks
to skip non-supported queries.)
Diffstat (limited to 'src/display')
-rw-r--r--src/display/term.nim200
1 files changed, 171 insertions, 29 deletions
diff --git a/src/display/term.nim b/src/display/term.nim
index a4c91e61..fd6271f3 100644
--- a/src/display/term.nim
+++ b/src/display/term.nim
@@ -36,11 +36,10 @@ type
     us # start underline mode
     mr # start reverse mode
     mb # start blink mode
+    ZH # start italic mode
     ue # end underline mode
     se # end standout mode
     me # end all formatting modes
-    LE # cursor left %1 characters
-    RI # cursor right %1 characters
     vs # enhance cursor
     vi # make cursor invisible
     ve # reset cursor to normal
@@ -70,11 +69,23 @@ type
     orig_flags: cint
     orig_flags2: cint
     orig_termios: Termios
+    defaultBackground: RGBColor
+    defaultForeground: RGBColor
 
 # control sequence introducer
 template CSI(s: varargs[string, `$`]): string =
   "\e[" & s.join(';')
 
+# primary device attributes
+const DA1 = CSI("c")
+
+# device control string
+template DCS(a, b: char, s: varargs[string]): string =
+  "\eP" & a & b & s.join(';') & "\e\\"
+
+template XTGETTCAP(s: varargs[string, `$`]): string =
+  DCS('+', 'q', s)
+
 # OS command
 template OSC(s: varargs[string, `$`]): string =
   "\e]" & s.join(';') & '\a'
@@ -82,6 +93,9 @@ template OSC(s: varargs[string, `$`]): string =
 template XTERM_TITLE(s: string): string =
   OSC(0, s)
 
+const XTGETFG = OSC(10, "?") # get foreground color
+const XTGETBG = OSC(11, "?") # get background color
+
 when not termcap_found:
   # DEC set
   template DECSET(s: varargs[string, `$`]): string =
@@ -174,6 +188,7 @@ proc startFormat(term: Terminal, flag: FormatFlags): string =
       of FLAG_UNDERLINE: return term.cap us
       of FLAG_REVERSE: return term.cap mr
       of FLAG_BLINK: return term.cap mb
+      of FLAG_ITALIC: return term.cap ZH
       else: discard
   return SGR(FormatCodes[flag].s)
 
@@ -200,12 +215,6 @@ proc disableAltScreen(term: Terminal): string =
   else:
     return RMCUP
 
-func defaultBackground(term: Terminal): RGBColor =
-  return term.config.display.default_background_color
-
-func defaultForeground(term: Terminal): RGBColor =
-  return term.config.display.default_foreground_color
-
 func mincontrast(term: Terminal): int32 =
   return term.config.display.minimum_contrast
 
@@ -490,10 +499,9 @@ proc applyConfig(term: Terminal) =
   if term.config.display.color_mode.isSome:
     term.colormode = term.config.display.color_mode.get
   elif term.isatty():
-    term.colormode = ANSI
     let colorterm = getEnv("COLORTERM")
-    case colorterm
-    of "24bit", "truecolor": term.colormode = TRUE_COLOR
+    if colorterm in ["24bit", "truecolor"]:
+      term.colormode = TRUE_COLOR
   if term.config.display.format_mode.isSome:
     term.formatmode = term.config.display.format_mode.get
   for fm in FormatFlags:
@@ -503,6 +511,10 @@ proc applyConfig(term: Terminal) =
     if term.config.display.alt_screen.isSome:
       term.smcup = term.config.display.alt_screen.get
     term.set_title = term.config.display.set_title
+  if term.config.display.default_background_color.isSome:
+    term.defaultBackground = term.config.display.default_background_color.get
+  if term.config.display.default_foreground_color.isSome:
+    term.defaultForeground = term.config.display.default_foreground_color.get
   if term.config.encoding.display_charset.isSome:
     term.cs = term.config.encoding.display_charset.get
   else:
@@ -596,35 +608,163 @@ when termcap_found:
     else:
       raise newException(Defect, "Failed to load termcap description for terminal " & term.tname)
 
-proc detectTermAttributes(term: Terminal) =
+type
+  QueryAttrs = enum
+    qaAnsiColor, qaRGB, qaSixel
+
+  QueryResult = object
+    success: bool
+    attrs: set[QueryAttrs]
+    fgcolor: Option[RGBColor]
+    bgcolor: Option[RGBColor]
+
+proc queryAttrs(term: Terminal): QueryResult =
+  const tcapRGB = 0x524742 # RGB supported?
+  const outs =
+    XTGETFG &
+    XTGETBG &
+    XTGETTCAP("524742") &
+    DA1
+  term.outfile.write(outs)
+  result = QueryResult(success: false, attrs: {})
+  while true:
+    template consume(term: Terminal): char = term.infile.readChar()
+    template fail = break
+    template expect(term: Terminal, c: char) =
+      if term.consume != c:
+        fail
+    template expect(term: Terminal, s: string) =
+      for c in s:
+        term.expect c
+    template skip_until(term: Terminal, c: char) =
+      while (let cc = term.consume; cc != c):
+        discard
+    term.expect '\e'
+    case term.consume
+    of '[':
+      # CSI
+      term.expect '?'
+      var n = 0
+      while (let c = term.consume; c != 'c'):
+        if c == ';':
+          case n
+          of 4: result.attrs.incl(qaSixel)
+          of 22: result.attrs.incl(qaAnsiColor)
+          else: discard
+          n = 0
+        else:
+          n *= 10
+          n += decValue(c)
+      result.success = true
+      break # DA1 returned; done
+    of ']':
+      # OSC
+      term.expect '1'
+      let c = term.consume
+      if c notin {'0', '1'}: fail
+      term.expect ';'
+      if term.consume == 'r' and term.consume == 'g' and term.consume == 'b':
+        term.expect ':'
+        template eat_color(tc: char): uint8 =
+          var val = 0u8
+          var i = 0
+          while (let c = term.consume; c != tc):
+            let v0 = hexValue(c)
+            if i > 4 or v0 == -1: fail # wat
+            let v = uint8(v0)
+            if i == 0: # 1st place
+              val = (v shl 4) or v
+            elif i == 1: # 2nd place
+              val = (val xor 0xF) or v
+            # all other places are irrelevant
+            inc i
+          val
+        let r = eat_color '/'
+        let g = eat_color '/'
+        let b = eat_color '\a'
+        if c == '0':
+          result.fgcolor = some(rgb(r, g, b))
+        else:
+          result.bgcolor = some(rgb(r, g, b))
+      else:
+        # not RGB, give up
+        term.skip_until '\a'
+    of 'P':
+      # DCS
+      let c = term.consume
+      if c notin {'0', '1'}:
+        fail
+      term.expect "+r"
+      if c == '1':
+        var id = 0
+        while (let c = term.consume; c != '='):
+          if c notin AsciiHexDigit:
+            fail
+          id *= 0x10
+          id += hexValue(c)
+        term.skip_until '\e' # ST (1)
+        if id == tcapRGB:
+          result.attrs.incl(qaRGB)
+      else: # 0
+        term.expect '\e' # ST (1)
+      term.expect '\\' # ST (2)
+    else:
+      fail
+
+type TermStartResult* = enum
+  tsrSuccess, tsrDA1Fail
+
+proc detectTermAttributes(term: Terminal): TermStartResult =
+  result = tsrSuccess
   term.tname = getEnv("TERM")
   if term.tname == "":
     term.tname = "dosansi"
-  when termcap_found:
-    if term.isatty():
+  if term.isatty():
+    if term.config.display.query_da1:
+      let r = term.queryAttrs()
+      if r.success: # DA1 success
+        if qaAnsiColor in r.attrs:
+          term.colormode = ANSI
+        if qaRGB in r.attrs:
+          term.colormode = TRUE_COLOR
+        # just assume the terminal doesn't choke on these.
+        term.formatmode = {FLAG_STRIKE, FLAG_OVERLINE}
+        if r.bgcolor.isSome:
+          term.defaultBackground = r.bgcolor.get
+        if r.fgcolor.isSome:
+          term.defaultForeground = r.fgcolor.get
+      else:
+        # something went horribly wrong. set result to DA1 fail, pager will
+        # alert the user
+        result = tsrDA1Fail
+    if term.colormode != TRUE_COLOR:
+      let colorterm = getEnv("COLORTERM")
+      if colorterm in ["24bit", "truecolor"]:
+        term.colormode = TRUE_COLOR
+    when termcap_found:
       term.loadTermcap()
       if term.tc != nil:
         term.smcup = term.hascap(ti)
-      term.formatmode = {FLAG_ITALIC, FLAG_OVERLINE, FLAG_STRIKE}
-      if term.hascap(us):
-        term.formatmode.incl(FLAG_UNDERLINE)
-      if term.hascap(md):
-        term.formatmode.incl(FLAG_BOLD)
-      if term.hascap(mr):
-        term.formatmode.incl(FLAG_REVERSE)
-      if term.hascap(mb):
-        term.formatmode.incl(FLAG_BLINK)
-  else:
-    if term.isatty():
+        if term.hascap(ZH):
+          term.formatmode.incl(FLAG_ITALIC)
+        if term.hascap(us):
+          term.formatmode.incl(FLAG_UNDERLINE)
+        if term.hascap(md):
+          term.formatmode.incl(FLAG_BOLD)
+        if term.hascap(mr):
+          term.formatmode.incl(FLAG_REVERSE)
+        if term.hascap(mb):
+          term.formatmode.incl(FLAG_BLINK)
+    else:
       term.smcup = true
       term.formatmode = {low(FormatFlags)..high(FormatFlags)}
-  term.applyConfig()
 
-proc start*(term: Terminal, infile: File) =
+proc start*(term: Terminal, infile: File): TermStartResult =
   term.infile = infile
   if term.isatty():
     term.enableRawMode()
-  term.detectTermAttributes()
+  result = term.detectTermAttributes()
+  term.applyConfig()
   if term.smcup:
     term.write(term.enableAltScreen())
 
@@ -643,7 +783,9 @@ proc newTerminal*(outfile: File, config: Config, attrs: WindowAttributes):
     Terminal =
   let term = Terminal(
     outfile: outfile,
-    config: config
+    config: config,
+    defaultBackground: ColorsRGB["black"],
+    defaultForeground: ColorsRGB["white"]
   )
   term.windowChange(attrs)
   return term