diff options
Diffstat (limited to 'src/io/term.nim')
-rw-r--r-- | src/io/term.nim | 294 |
1 files changed, 261 insertions, 33 deletions
diff --git a/src/io/term.nim b/src/io/term.nim index 1eacd237..d39273d0 100644 --- a/src/io/term.nim +++ b/src/io/term.nim @@ -1,16 +1,243 @@ +import os +import streams +import strutils import terminal -import std/exitprocs +import bindings/notcurses +import buffer/cell +import io/window +import types/color +import utils/twtstr type - TermAttributes* = object - width*: int - height*: int - ppc*: int # cell width - ppl*: int # cell height - cell_ratio*: float64 # ppl / ppc - width_px*: int - height_px*: int + ColorMode = enum + MONOCHROME, ANSI, EIGHT_BIT, TRUE_COLOR + + FormatMode = set[FormatFlags] + + Terminal* = ref TerminalObj + TerminalObj = object + infile: File + outfile: File + nc*: ncdirect + cleared: bool + prevgrid: FixedGrid + attrs*: WindowAttributes + colormode: ColorMode + formatmode: FormatMode + smcup: bool + + TermInfo = ref object + +proc `=destroy`(term: var TerminalObj) = + if term.nc != nil: + #discard ncdirect_stop(term.nc) + term.nc = nil + +template CSI*(s: varargs[string, `$`]): string = + var r = "\e[" + var first = true + for x in s: + if not first: + r &= ";" + first = false + r &= x + r + +template DECSET(s: varargs[string, `$`]): string = + var r = "\e[?" + var first = true + for x in s: + if not first: + r &= ";" + first = false + r &= x + r & "h" + +template DECRST(s: varargs[string, `$`]): string = + var r = "\e[?" + var first = true + for x in s: + if not first: + r &= ";" + first = false + r &= x + r & "l" + +template SMCUP(): string = DECSET(1049) +template RMCUP(): string = DECRST(1049) + +template SGR*(s: varargs[string, `$`]): string = + CSI(s) & "m" + +template HVP*(s: varargs[string, `$`]): string = + CSI(s) & "f" + +template EL*(s: varargs[string, `$`]): string = + CSI(s) & "K" + +proc processFormat*(term: Terminal, format: var Format, cellf: Format): string = + for flag in FormatFlags: + if flag in format.flags and flag notin cellf.flags: + result &= SGR(FormatCodes[flag].e) + + if cellf.fgcolor != format.fgcolor: + var color = cellf.fgcolor + if color.rgb: + let rgb = color.rgbcolor + result &= SGR(38, 2, rgb.r, rgb.g, rgb.b) + elif color == defaultColor: + result &= SGR() + format = newFormat() + else: + result &= SGR(color.color) + + if cellf.bgcolor != format.bgcolor: + var color = cellf.bgcolor + if color.rgb: + let rgb = color.rgbcolor + result &= SGR(48, 2, rgb.r, rgb.g, rgb.b) + elif color == defaultColor: + result &= SGR() + format = newFormat() + else: + result &= SGR(color.color) + + for flag in FormatFlags: + if flag notin format.flags and flag in cellf.flags: + result &= SGR(FormatCodes[flag].s) + + format = cellf + +proc updateWindow*(term: Terminal) = + term.attrs = getWindowAttributes(term.outfile) + +proc findTermInfoDirs(termenv: string): seq[string] = + let tienv = getEnv("TERMINFO") + if tienv != "": + if dirExists(tienv): + return @[tienv] + else: + let home = getEnv("HOME") + if home != "": + result.add(home & '/' & ".terminfo") + let tidirsenv = getEnv("TERMINFO_DIRS") + if tidirsenv != "": + for s in tidirsenv.split({':'}): + if s == "": + result.add("/usr/share/terminfo") + else: + result.add(s) + return result + result.add("/usr/share/terminfo") + +proc findFile(dir: string, file: string): string = + var stack = dir + for f in walkDirRec(dir, followFilter = {pcDir, pcLinkToDir}): + if f == file: + return f + +proc parseTermInfo(s: Stream): TermInfo = + let magic = s.readInt16() + #TODO do we really want this? + s.close() + +proc getTermInfo(termenv: string): TermInfo = + let tipaths = findTermInfoDirs(termenv) + for tipath in tipaths: + let f = findFile(tipath, termenv) + if f != "": + return parseTermInfo(newFileStream(f)) + +proc getCursorPos(term: Terminal): (int, int) = + term.outfile.write(CSI("6n")) + term.outfile.flushFile() + var c = term.infile.readChar() + while true: + while c != '\e': + c = term.infile.readChar() + c = term.infile.readChar() + if c == '[': break + var tmp = "" + while (let c = term.infile.readChar(); c != ';'): + tmp &= c + result[1] = parseInt32(tmp) + tmp = "" + while (let c = term.infile.readChar(); c != 'R'): + tmp &= c + result[0] = parseInt32(tmp) + +proc detectTermAttributes*(term: Terminal) = + term.colormode = ANSI + let colorterm = getEnv("COLORTERM") + case colorterm + of "24bit", "truecolor": term.colormode = TRUE_COLOR + #TODO terminfo/termcap? + +func generateFullOutput(term: Terminal, grid: FixedGrid): string = + var x = 0 + var format = newFormat() + result &= HVP(1, 1) + for cell in grid.cells: + if x >= grid.width - 1: + result &= EL() + result &= "\r\n" + x = 0 + result &= term.processFormat(format, cell.format) + result &= cell.str + inc x + result &= EL() + +func generateSwapOutput(term: Terminal, grid: FixedGrid): string = + var format = newFormat() + let curr = grid.cells + let prev = term.prevgrid.cells + var i = 0 + var x = 0 + var y = 0 + var line = "" + var lr = false + while i < curr.len: + if x >= grid.width - 1: + if lr: + result &= HVP(y + 1, 1) + result &= EL() + result &= line + lr = false + x = 0 + inc y + line = "" + lr = lr or (curr[i] != prev[i]) + line &= term.processFormat(format, curr[i].format) + line &= curr[i].str + inc i + inc x + if lr: + result &= HVP(y + 1, 1) + result &= EL() + result &= line + lr = false + +proc hideCursor*(term: Terminal) = + term.outfile.hideCursor() + +proc showCursor*(term: Terminal) = + term.outfile.showCursor() + +proc flush*(term: Terminal) = + term.outfile.flushFile() + +proc outputGrid*(term: Terminal, grid: FixedGrid) = + term.outfile.write(SGR()) + if not term.cleared: + term.outfile.write(term.generateFullOutput(grid)) + term.cleared = true + else: + term.outfile.write(term.generateSwapOutput(grid)) + term.prevgrid = grid + +proc setCursor*(term: Terminal, x, y: int) = + term.outfile.write(HVP(y + 1, x + 1)) when defined(posix): import posix @@ -24,7 +251,6 @@ when defined(posix): proc enableRawMode*(fileno: FileHandle) = stdin_fileno = fileno - addExitProc(disableRawMode) discard tcGetAttr(fileno, addr orig_termios) var raw = orig_termios raw.c_iflag = raw.c_iflag and not (BRKINT or ICRNL or INPCK or ISTRIP or IXON) @@ -52,27 +278,29 @@ else: proc restoreStdin*(flags: cint) = discard -proc getTermAttributes*(tty: File): TermAttributes = - if tty.isatty(): +proc isatty*(term: Terminal): bool = + term.infile.isatty() and term.outfile.isatty() + +proc quit*(term: Terminal) = + if term.isatty(): when defined(posix): - var win: IOctl_WinSize - if ioctl(cint(getOsFileHandle(tty)), TIOCGWINSZ, addr win) != -1: - result.ppc = int(win.ws_xpixel) div int(win.ws_col) - result.ppl = int(win.ws_ypixel) div int(win.ws_row) - # some terminals don't like it when we fill the last cell. #TODO make this optional - result.width = int(win.ws_col) - 1 - result.height = int(win.ws_row) - result.width_px = result.width * result.ppc - result.height_px = result.height * result.ppl - result.cell_ratio = result.ppl / result.ppc - return - #fail - result.width = terminalWidth() - 1 - result.height = terminalHeight() - if result.height == 0: - result.height = 24 - result.ppc = 9 - result.ppl = 18 - result.cell_ratio = result.ppl / result.ppc - result.width_px = result.ppc * result.width - result.height_px = result.ppl * result.height + disableRawMode() + if term.smcup: + term.outfile.write(RMCUP()) + else: + term.outfile.write(HVP(term.attrs.height, 1) & '\n') + term.outfile.showCursor() + term.outfile.flushFile() + +proc newTerminal*(infile, outfile: File, force_minimal = false): Terminal = + let term = new Terminal + term.infile = infile + term.outfile = outfile + when defined(posix): + if term.isatty(): + enableRawMode(infile.getFileHandle()) + if not force_minimal: + term.detectTermAttributes() + term.smcup = true + term.outfile.write(SMCUP()) + return term |