From d26766c4c4015990703e84e8136f96d222edbc97 Mon Sep 17 00:00:00 2001 From: bptato Date: Thu, 14 Mar 2024 20:57:45 +0100 Subject: Move around some modules * extern -> gone, runproc absorbed by pager, others moved into io/ * display -> local/ (where else would we display?) * xhr -> html/ * move out WindowAttributes from term, so we don't depend on local from server --- src/css/values.nim | 2 +- src/display/lineedit.nim | 328 --------------- src/display/term.nim | 906 ------------------------------------------ src/extern/runproc.nim | 58 --- src/extern/stdio.nim | 25 -- src/extern/tempfile.nim | 18 - src/html/dom.nim | 2 +- src/html/env.nim | 6 +- src/html/formdata.nim | 177 +++++++++ src/html/xmlhttprequest.nim | 118 ++++++ src/io/stdio.nim | 25 ++ src/io/tempfile.nim | 18 + src/layout/engine.nim | 2 +- src/layout/renderdocument.nim | 2 +- src/loader/cgi.nim | 2 +- src/loader/loader.nim | 2 +- src/local/client.nim | 8 +- src/local/container.nim | 2 +- src/local/lineedit.nim | 328 +++++++++++++++ src/local/pager.nim | 70 +++- src/local/term.nim | 899 +++++++++++++++++++++++++++++++++++++++++ src/main.nim | 2 +- src/server/buffer.nim | 4 +- src/server/forkserver.nim | 2 +- src/types/winattrs.nim | 7 + src/xhr/formdata.nim | 177 --------- src/xhr/xmlhttprequest.nim | 118 ------ 27 files changed, 1650 insertions(+), 1658 deletions(-) delete mode 100644 src/display/lineedit.nim delete mode 100644 src/display/term.nim delete mode 100644 src/extern/runproc.nim delete mode 100644 src/extern/stdio.nim delete mode 100644 src/extern/tempfile.nim create mode 100644 src/html/formdata.nim create mode 100644 src/html/xmlhttprequest.nim create mode 100644 src/io/stdio.nim create mode 100644 src/io/tempfile.nim create mode 100644 src/local/lineedit.nim create mode 100644 src/local/term.nim create mode 100644 src/types/winattrs.nim delete mode 100644 src/xhr/formdata.nim delete mode 100644 src/xhr/xmlhttprequest.nim diff --git a/src/css/values.nim b/src/css/values.nim index dd3c96ea..e1c54c04 100644 --- a/src/css/values.nim +++ b/src/css/values.nim @@ -6,11 +6,11 @@ import std/unicode import css/cssparser import css/selectorparser -import display/term import img/bitmap import layout/layoutunit import types/color import types/opt +import types/winattrs import utils/twtstr export selectorparser.PseudoElem diff --git a/src/display/lineedit.nim b/src/display/lineedit.nim deleted file mode 100644 index a69465a6..00000000 --- a/src/display/lineedit.nim +++ /dev/null @@ -1,328 +0,0 @@ -import std/strutils -import std/unicode - -import bindings/quickjs -import display/term -import js/javascript -import types/cell -import types/opt -import utils/strwidth -import utils/twtstr - -import chagashi/charset -import chagashi/validator -import chagashi/decoder - -type - LineEditState* = enum - EDIT, FINISH, CANCEL - - LineHistory* = ref object - lines: seq[string] - - LineEdit* = ref object - news*: string - prompt*: string - promptw: int - state*: LineEditState - escNext*: bool - cursorx: int # 0 ..< news.notwidth - cursori: int # 0 ..< news.len - shiftx: int # 0 ..< news.notwidth - shifti: int # 0 ..< news.len - padding: int # 0 or 1 - maxwidth: int - disallowed: set[char] - hide: bool - hist: LineHistory - histindex: int - histtmp: string - invalid*: bool - -jsDestructor(LineEdit) - -func newLineHistory*(): LineHistory = - return LineHistory() - -# Note: capped at edit.maxwidth. -func getDisplayWidth(edit: LineEdit): int = - var dispw = 0 - var i = edit.shifti - var r: Rune - while i < edit.news.len and dispw < edit.maxwidth: - fastRuneAt(edit.news, i, r) - dispw += r.width() - return dispw - -proc shiftView(edit: LineEdit) = - # Shift view so it contains the cursor. - if edit.cursorx < edit.shiftx: - edit.shiftx = edit.cursorx - edit.shifti = edit.cursori - # Shift view so it is completely filled. - if edit.shiftx > 0: - let dispw = edit.getDisplayWidth() - if dispw < edit.maxwidth: - let targetx = edit.shiftx - edit.maxwidth + dispw - if targetx <= 0: - edit.shiftx = 0 - edit.shifti = 0 - else: - while edit.shiftx > targetx: - let (r, len) = edit.news.lastRune(edit.shifti - 1) - edit.shiftx -= r.width() - edit.shifti -= len - edit.padding = 0 - # Shift view so it contains the cursor. (act 2) - if edit.shiftx < edit.cursorx - edit.maxwidth: - while edit.shiftx < edit.cursorx - edit.maxwidth and - edit.shifti < edit.news.len: - var r: Rune - fastRuneAt(edit.news, edit.shifti, r) - edit.shiftx += r.width() - if edit.shiftx > edit.cursorx - edit.maxwidth: - # skipped over a cell because of a double-width char - edit.padding = 1 - -proc generateOutput*(edit: LineEdit): FixedGrid = - edit.shiftView() - # Make the output grid +1 cell wide, so it covers the whole input area. - result = newFixedGrid(edit.promptw + edit.maxwidth + 1) - var x = 0 - for r in edit.prompt.runes: - result[x].str &= $r - x += r.width() - if x >= result.width: break - for i in 0 ..< edit.padding: - if x < result.width: - result[x].str = " " - inc x - var i = edit.shifti - while i < edit.news.len: - var r: Rune - fastRuneAt(edit.news, i, r) - if not edit.hide: - let w = r.width() - if x + w > result.width: break - if r.isControlChar(): - result[x].str &= '^' - inc x - result[x].str &= char(r).getControlLetter() - inc x - else: - result[x].str &= $r - x += w - else: - if x + 1 > result.width: break - result[x].str &= '*' - inc x - -proc getCursorX*(edit: LineEdit): int = - return edit.promptw + edit.cursorx + edit.padding - edit.shiftx - -proc insertCharseq(edit: LineEdit, s: string) = - let s = if edit.escNext: - s - else: - deleteChars(s, edit.disallowed) - edit.escNext = false - if s.len == 0: - return - let rem = edit.news.substr(edit.cursori) - edit.news.setLen(edit.cursori) - edit.news &= s - edit.news &= rem - edit.cursori += s.len - edit.cursorx += s.notwidth() - edit.invalid = true - -proc cancel(edit: LineEdit) {.jsfunc.} = - edit.state = CANCEL - -proc submit(edit: LineEdit) {.jsfunc.} = - if edit.hist.lines.len == 0 or edit.news != edit.hist.lines[^1]: - edit.hist.lines.add(edit.news) - edit.state = FINISH - -proc backspace(edit: LineEdit) {.jsfunc.} = - if edit.cursori > 0: - let (r, len) = edit.news.lastRune(edit.cursori - 1) - edit.news.delete(edit.cursori - len .. edit.cursori - 1) - edit.cursori -= len - edit.cursorx -= r.width() - edit.invalid = true - -proc write*(edit: LineEdit, s: string, cs: Charset): bool = - if cs == CHARSET_UTF_8: - if s.validateUTF8Surr() != -1: - return false - edit.insertCharseq(s) - else: - let td = newTextDecoder(cs) - var success = false - let s = td.decodeAll(s, success) - if not success: - return false - edit.insertCharseq(s) - return true - -proc write(edit: LineEdit, s: string): bool {.jsfunc.} = - edit.write(s, CHARSET_UTF_8) - -proc delete(edit: LineEdit) {.jsfunc.} = - if edit.cursori < edit.news.len: - let len = edit.news.runeLenAt(edit.cursori) - edit.news.delete(edit.cursori ..< edit.cursori + len) - edit.invalid = true - -proc escape(edit: LineEdit) {.jsfunc.} = - edit.escNext = true - -proc clear(edit: LineEdit) {.jsfunc.} = - if edit.cursori > 0: - edit.news.delete(0..edit.cursori - 1) - edit.cursori = 0 - edit.cursorx = 0 - edit.invalid = true - -proc kill(edit: LineEdit) {.jsfunc.} = - if edit.cursori < edit.news.len: - edit.news.setLen(edit.cursori) - edit.invalid = true - -proc backward(edit: LineEdit) {.jsfunc.} = - if edit.cursori > 0: - let (r, len) = edit.news.lastRune(edit.cursori - 1) - edit.cursori -= len - edit.cursorx -= r.width() - if edit.cursorx < edit.shiftx: - edit.invalid = true - -proc forward(edit: LineEdit) {.jsfunc.} = - if edit.cursori < edit.news.len: - var r: Rune - fastRuneAt(edit.news, edit.cursori, r) - edit.cursorx += r.width() - if edit.cursorx >= edit.shiftx + edit.maxwidth: - edit.invalid = true - -proc prevWord(edit: LineEdit) {.jsfunc.} = - if edit.cursori == 0: - return - let (r, len) = edit.news.lastRune(edit.cursori - 1) - if r.breaksWord(): - edit.cursori -= len - edit.cursorx -= r.width() - while edit.cursori > 0: - let (r, len) = edit.news.lastRune(edit.cursori - 1) - if r.breaksWord(): - break - edit.cursori -= len - edit.cursorx -= r.width() - if edit.cursorx < edit.shiftx: - edit.invalid = true - -proc nextWord(edit: LineEdit) {.jsfunc.} = - if edit.cursori >= edit.news.len: - return - let oc = edit.cursori - var r: Rune - fastRuneAt(edit.news, edit.cursori, r) - if r.breaksWord(): - edit.cursorx += r.width() - else: - edit.cursori = oc - while edit.cursori < edit.news.len: - let pc = edit.cursori - fastRuneAt(edit.news, edit.cursori, r) - if r.breaksWord(): - edit.cursori = pc - break - edit.cursorx += r.width() - if edit.cursorx >= edit.shiftx + edit.maxwidth: - edit.invalid = true - -proc clearWord(edit: LineEdit) {.jsfunc.} = - let oc = edit.cursori - edit.prevWord() - if oc != edit.cursori: - edit.news.delete(edit.cursori .. oc - 1) - edit.invalid = true - -proc killWord(edit: LineEdit) {.jsfunc.} = - if edit.cursori >= edit.news.len: - return - let oc = edit.cursori - let ox = edit.cursorx - edit.nextWord() - if edit.cursori != oc: - if edit.cursori < edit.news.len: - let len = edit.news.runeLenAt(edit.cursori) - edit.news.delete(oc ..< edit.cursori + len) - else: - edit.news.delete(oc ..< edit.cursori) - edit.cursori = oc - edit.cursorx = ox - edit.invalid = true - -proc begin(edit: LineEdit) {.jsfunc.} = - edit.cursori = 0 - edit.cursorx = 0 - if edit.shiftx > 0: - edit.invalid = true - -proc `end`(edit: LineEdit) {.jsfunc.} = - if edit.cursori < edit.news.len: - edit.cursori = edit.news.len - edit.cursorx = edit.news.notwidth() - if edit.cursorx >= edit.shiftx + edit.maxwidth: - edit.invalid = true - -proc prevHist(edit: LineEdit) {.jsfunc.} = - if edit.histindex > 0: - if edit.news.len > 0: - edit.histtmp = $edit.news - dec edit.histindex - edit.news = edit.hist.lines[edit.histindex] - # The begin call is needed so the cursor doesn't get lost outside - # the string. - edit.begin() - edit.end() - edit.invalid = true - -proc nextHist(edit: LineEdit) {.jsfunc.} = - if edit.histindex + 1 < edit.hist.lines.len: - inc edit.histindex - edit.news = edit.hist.lines[edit.histindex] - edit.begin() - edit.end() - edit.invalid = true - elif edit.histindex < edit.hist.lines.len: - inc edit.histindex - edit.news = edit.histtmp - edit.begin() - edit.end() - edit.histtmp = "" - -proc windowChange*(edit: LineEdit, attrs: WindowAttributes) = - edit.maxwidth = attrs.width - edit.promptw - 1 - -proc readLine*(prompt: string, termwidth: int, current = "", - disallowed: set[char] = {}, hide = false, hist: LineHistory): LineEdit = - result = LineEdit( - prompt: prompt, - promptw: prompt.width(), - news: current, - disallowed: disallowed, - hide: hide, - invalid: true - ) - result.cursori = result.news.len - result.cursorx = result.news.notwidth() - # - 1, so that the cursor always has place - result.maxwidth = termwidth - result.promptw - 1 - result.hist = hist - result.histindex = result.hist.lines.len - -proc addLineEditModule*(ctx: JSContext) = - ctx.registerType(LineEdit) diff --git a/src/display/term.nim b/src/display/term.nim deleted file mode 100644 index cb96a385..00000000 --- a/src/display/term.nim +++ /dev/null @@ -1,906 +0,0 @@ -import std/options -import std/os -import std/posix -import std/streams -import std/strutils -import std/tables -import std/termios -import std/unicode - -import bindings/termcap -import config/config -import types/cell -import types/color -import types/opt -import utils/strwidth -import utils/twtstr - -import chagashi/charset -import chagashi/encoder -import chagashi/validator - -#TODO switch from termcap... - -type - TermcapCap = enum - ce # clear till end of line - cd # clear display - cm # cursor move - ti # terminal init (=smcup) - te # terminal end (=rmcup) - so # start standout mode - md # start bold mode - 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 - vs # enhance cursor - vi # make cursor invisible - ve # reset cursor to normal - - Termcap = ref object - bp: array[1024, uint8] - funcstr: array[256, uint8] - caps: array[TermcapCap, cstring] - - WindowAttributes* = object - width*: int - height*: int - ppc*: int # cell width - ppl*: int # cell height - width_px*: int - height_px*: int - - Terminal* = ref TerminalObj - TerminalObj = object - cs*: Charset - config: Config - infile*: File - outfile: File - cleared: bool - canvas: FixedGrid - pcanvas: FixedGrid - attrs*: WindowAttributes - colormode: ColorMode - formatmode: FormatMode - smcup: bool - tc: Termcap - tname: string - set_title: bool - stdin_unblocked: bool - 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") - -# report xterm text area size in pixels -const GEOMPIXEL = CSI(14, "t") - -# report window size in chars -const GEOMCELL = CSI(18, "t") - -# allow shift-key to override mouse protocol -const XTSHIFTESCAPE = CSI(">0s") - -# 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' - -template XTERM_TITLE(s: string): string = - OSC(0, s) - -const XTGETFG = OSC(10, "?") # get foreground color -const XTGETBG = OSC(11, "?") # get background color - -# DEC set -template DECSET(s: varargs[string, `$`]): string = - "\e[?" & s.join(';') & 'h' - -# DEC reset -template DECRST(s: varargs[string, `$`]): string = - "\e[?" & s.join(';') & 'l' - -# alt screen -const SMCUP = DECSET(1049) -const RMCUP = DECRST(1049) - -# mouse tracking -const SGRMOUSEBTNON = DECSET(1002, 1006) -const SGRMOUSEBTNOFF = DECRST(1002, 1006) - -when not termcap_found: - const CNORM = DECSET(25) - const CIVIS = DECRST(25) - template HVP(s: varargs[string, `$`]): string = - CSI(s) & "f" - template EL(): string = - CSI() & "K" - template ED(): string = - CSI() & "J" - - proc write(term: Terminal, s: string) = - term.outfile.write(s) -else: - func hascap(term: Terminal, c: TermcapCap): bool = term.tc.caps[c] != nil - func cap(term: Terminal, c: TermcapCap): string = $term.tc.caps[c] - func ccap(term: Terminal, c: TermcapCap): cstring = term.tc.caps[c] - - var goutfile: File - proc putc(c: char): cint {.cdecl.} = - goutfile.write(c) - - proc write(term: Terminal, s: cstring) = - discard tputs(s, 1, putc) - - proc write(term: Terminal, s: string) = - term.write(cstring(s)) - -template SGR*(s: varargs[string, `$`]): string = - CSI(s) & "m" - -#TODO a) this should be customizable b) these defaults sucks -const ANSIColorMap = [ - rgb(0, 0, 0), - rgb(205, 0, 0), - rgb(0, 205, 0), - rgb(205, 205, 0), - rgb(0, 0, 238), - rgb(205, 0, 205), - rgb(0, 205, 205), - rgb(229, 229, 229), - rgb(127, 127, 127), - rgb(255, 0, 0), - rgb(0, 255, 0), - rgb(255, 255, 0), - rgb(92, 92, 255), - rgb(255, 0, 255), - rgb(0, 255, 255), - rgb(255, 255, 255) -] - -proc flush*(term: Terminal) = - term.outfile.flushFile() - -proc cursorGoto(term: Terminal, x, y: int): string = - when termcap_found: - return $tgoto(term.ccap cm, cint(x), cint(y)) - else: - return HVP(y + 1, x + 1) - -proc clearEnd(term: Terminal): string = - when termcap_found: - return term.cap ce - else: - return EL() - -proc clearDisplay(term: Terminal): string = - when termcap_found: - return term.cap cd - else: - return ED() - -proc isatty(fd: FileHandle): cint {.importc: "isatty", header: "".} -proc isatty*(f: File): bool = - return isatty(f.getFileHandle()) != 0 - -proc isatty*(term: Terminal): bool = - term.infile != nil and term.infile.isatty() and term.outfile.isatty() - -proc anyKey*(term: Terminal) = - if term.isatty(): - term.outfile.write("[Hit any key]") - discard term.infile.readChar() - -proc resetFormat(term: Terminal): string = - when termcap_found: - if term.isatty(): - return term.cap me - return SGR() - -proc startFormat(term: Terminal, flag: FormatFlags): string = - when termcap_found: - if term.isatty(): - case flag - of FLAG_BOLD: return term.cap md - 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) - -proc endFormat(term: Terminal, flag: FormatFlags): string = - when termcap_found: - if flag == FLAG_UNDERLINE and term.isatty(): - return term.cap ue - return SGR(FormatCodes[flag].e) - -proc setCursor*(term: Terminal, x, y: int) = - term.write(term.cursorGoto(x, y)) - -proc enableAltScreen(term: Terminal): string = - when termcap_found: - if term.hascap ti: - return term.cap ti - return SMCUP - -proc disableAltScreen(term: Terminal): string = - when termcap_found: - if term.hascap te: - return term.cap te - return RMCUP - -func mincontrast(term: Terminal): int32 = - return term.config.display.minimum_contrast - -proc getRGB(a: CellColor, termDefault: RGBColor): RGBColor = - case a.t - of ctNone: - return termDefault - of ctANSI: - if a.color >= 16: - return EightBitColor(a.color).toRGB() - return ANSIColorMap[a.color] - of ctRGB: - return a.rgbcolor - -# Use euclidian distance to quantize RGB colors. -proc approximateANSIColor(rgb, termDefault: RGBColor): CellColor = - var a = 0i32 - var n = -1 - for i in -1 .. ANSIColorMap.high: - let color = if i >= 0: - ANSIColorMap[i] - else: - termDefault - if color == rgb: - return if i == -1: defaultColor else: ANSIColor(i).cellColor() - let x = int32(color.r) - int32(rgb.r) - let y = int32(color.g) - int32(rgb.g) - let z = int32(color.b) - int32(rgb.b) - let xx = x * x - let yy = y * y - let zz = z * z - let b = xx + yy + zz - if i == -1 or b < a: - n = i - a = b - return if n == -1: defaultColor else: ANSIColor(n).cellColor() - -# Return a fgcolor contrasted to the background by term.mincontrast. -proc correctContrast(term: Terminal, bgcolor, fgcolor: CellColor): CellColor = - let contrast = term.mincontrast - let cfgcolor = fgcolor - let bgcolor = getRGB(bgcolor, term.defaultBackground) - let fgcolor = getRGB(fgcolor, term.defaultForeground) - let bgY = int(bgcolor.Y) - var fgY = int(fgcolor.Y) - let diff = abs(bgY - fgY) - if diff < contrast: - if bgY > fgY: - fgY = bgY - contrast - if fgY < 0: - fgY = bgY + contrast - if fgY > 255: - fgY = 0 - else: - fgY = bgY + contrast - if fgY > 255: - fgY = bgY - contrast - if fgY < 0: - fgY = 255 - let newrgb = YUV(cast[uint8](fgY), fgcolor.U, fgcolor.V) - case term.colormode - of TRUE_COLOR: - return cellColor(newrgb) - of ANSI: - return approximateANSIColor(newrgb, term.defaultForeground) - of EIGHT_BIT: - return cellColor(newrgb.toEightBit()) - of MONOCHROME: - doAssert false - return cfgcolor - -template ansiSGR(n: uint8, bgmod: int): string = - if n < 8: - SGR(30 + bgmod + n) - else: - SGR(82 + bgmod + n) - -template eightBitSGR(n: uint8, bgmod: int): string = - if n < 16: - ansiSGR(n, bgmod) - else: - SGR(38 + bgmod, 5, n) - -template rgbSGR(rgb: RGBColor, bgmod: int): string = - SGR(38 + bgmod, 2, rgb.r, rgb.g, rgb.b) - -proc processFormat*(term: Terminal, format: var Format, cellf: Format): string = - for flag in FormatFlags: - if flag in term.formatmode: - if flag in format.flags and flag notin cellf.flags: - result &= term.endFormat(flag) - if flag notin format.flags and flag in cellf.flags: - result &= term.startFormat(flag) - var cellf = cellf - case term.colormode - of ANSI: - # quantize - if cellf.bgcolor.t == ctANSI and cellf.bgcolor.color > 15: - cellf.bgcolor = cellf.fgcolor.eightbit.toRGB().cellColor() - if cellf.bgcolor.t == ctRGB: - cellf.bgcolor = approximateANSIColor(cellf.bgcolor.rgbcolor, - term.defaultBackground) - if cellf.fgcolor.t == ctANSI and cellf.fgcolor.color > 15: - cellf.fgcolor = cellf.fgcolor.eightbit.toRGB().cellColor() - if cellf.fgcolor.t == ctRGB: - if cellf.bgcolor.t == ctNone: - cellf.fgcolor = approximateANSIColor(cellf.fgcolor.rgbcolor, - term.defaultForeground) - else: - # ANSI fgcolor + bgcolor at the same time is broken - cellf.fgcolor = defaultColor - # correct - cellf.fgcolor = term.correctContrast(cellf.bgcolor, cellf.fgcolor) - # print - case cellf.fgcolor.t - of ctNone: result &= SGR(39) - of ctANSI: result &= ansiSGR(cellf.fgcolor.color, 0) - else: assert false - case cellf.bgcolor.t - of ctNone: result &= SGR(49) - of ctANSI: result &= ansiSGR(cellf.bgcolor.color, 10) - else: assert false - of EIGHT_BIT: - # quantize - if cellf.bgcolor.t == ctRGB: - cellf.bgcolor = cellf.bgcolor.rgbcolor.toEightBit().cellColor() - if cellf.fgcolor.t == ctRGB: - cellf.fgcolor = cellf.fgcolor.rgbcolor.toEightBit().cellColor() - # correct - cellf.fgcolor = term.correctContrast(cellf.bgcolor, cellf.fgcolor) - # print - case cellf.fgcolor.t - of ctNone: result &= SGR(39) - of ctANSI: result &= eightBitSGR(cellf.fgcolor.color, 0) - of ctRGB: assert false - case cellf.bgcolor.t - of ctNone: result &= SGR(49) - of ctANSI: result &= eightBitSGR(cellf.bgcolor.color, 10) - of ctRGB: assert false - of TRUE_COLOR: - # correct - cellf.fgcolor = term.correctContrast(cellf.bgcolor, cellf.fgcolor) - # print - if cellf.fgcolor != format.fgcolor: - case cellf.fgcolor.t - of ctNone: result &= SGR(39) - of ctANSI: result &= eightBitSGR(cellf.fgcolor.color, 0) - of ctRGB: result &= rgbSGR(cellf.fgcolor.rgbcolor, 0) - if cellf.bgcolor != format.bgcolor: - case cellf.bgcolor.t - of ctNone: result &= SGR(49) - of ctANSI: result &= eightBitSGR(cellf.bgcolor.color, 10) - of ctRGB: result &= rgbSGR(cellf.bgcolor.rgbcolor, 10) - of MONOCHROME: - discard # nothing to do - format = cellf - -proc setTitle*(term: Terminal, title: string) = - if term.set_title: - term.outfile.write(XTERM_TITLE(title.replaceControls())) - -proc enableMouse*(term: Terminal) = - term.write(XTSHIFTESCAPE & SGRMOUSEBTNON) - -proc disableMouse*(term: Terminal) = - term.write(SGRMOUSEBTNOFF) - -proc processOutputString*(term: Terminal, str: string, w: var int): string = - if str.validateUTF8Surr() != -1: - return "?" - # twidth wouldn't work here, the view may start at the nth character. - # pager must ensure tabs are converted beforehand. - w += str.notwidth() - let str = if Controls in str: - str.replaceControls() - else: - str - if term.cs == CHARSET_UTF_8: - # The output encoding matches the internal representation. - return str - else: - # Output is not utf-8, so we must encode it first. - var success = false - return newTextEncoder(term.cs).encodeAll(str, success) - -proc generateFullOutput(term: Terminal, grid: FixedGrid): string = - var format = Format() - result &= term.cursorGoto(0, 0) - result &= term.resetFormat() - result &= term.clearDisplay() - for y in 0 ..< grid.height: - if y != 0: - result &= "\r\n" - var w = 0 - for x in 0 ..< grid.width: - while w < x: - result &= " " - inc w - let cell = grid[y * grid.width + x] - result &= term.processFormat(format, cell.format) - result &= term.processOutputString(cell.str, w) - -proc generateSwapOutput(term: Terminal, grid, prev: FixedGrid): string = - var vy = -1 - for y in 0 ..< grid.height: - var w = 0 - var change = false - # scan for changes, and set cx to x of the first change - var cx = 0 - # if there is a change, we have to start from the last x with - # a string (otherwise we might overwrite a double-width char) - var lastx = 0 - for x in 0 ..< grid.width: - let i = y * grid.width + x - if grid[i].str != "": - lastx = x - if grid[i] != prev[i]: - change = true - cx = lastx - w = lastx - break - if change: - if cx == 0 and vy != -1: - while vy < y: - result &= "\r\n" - inc vy - else: - result &= term.cursorGoto(cx, y) - vy = y - result &= term.resetFormat() - var format = Format() - for x in cx ..< grid.width: - while w < x: # if previous cell had no width, catch up with x - result &= ' ' - inc w - let cell = grid[y * grid.width + x] - result &= term.processFormat(format, cell.format) - result &= term.processOutputString(cell.str, w) - if w < grid.width: - result &= term.clearEnd() - -proc hideCursor*(term: Terminal) = - when termcap_found: - term.write(term.ccap vi) - else: - term.write(CIVIS) - -proc showCursor*(term: Terminal) = - when termcap_found: - term.write(term.ccap ve) - else: - term.write(CNORM) - -func emulateOverline(term: Terminal): bool = - term.config.display.emulate_overline and - FLAG_OVERLINE notin term.formatmode and FLAG_UNDERLINE in term.formatmode - -proc writeGrid*(term: Terminal, grid: FixedGrid, x = 0, y = 0) = - for ly in y ..< y + grid.height: - for lx in x ..< x + grid.width: - let i = ly * term.canvas.width + lx - term.canvas[i] = grid[(ly - y) * grid.width + (lx - x)] - let isol = FLAG_OVERLINE in term.canvas[i].format.flags - if i >= term.canvas.width and isol and term.emulateOverline: - let w = grid[(ly - y) * grid.width + (lx - x)].width() - let s = i - term.canvas.width - var j = s - while j < term.canvas.len and j < s + w: - let cell = addr term.canvas[j] - cell.format.flags.incl(FLAG_UNDERLINE) - if cell.str == "": - cell.str = " " - if cell.str == " ": - let i = (ly - y) * grid.width + (lx - x) - cell.format.fgcolor = grid[i].format.fgcolor - j += cell[].width() - -proc applyConfigDimensions(term: Terminal) = - # screen dimensions - if term.attrs.width == 0 or term.config.display.force_columns: - term.attrs.width = int(term.config.display.columns) - if term.attrs.height == 0 or term.config.display.force_lines: - term.attrs.height = int(term.config.display.lines) - if term.attrs.ppc == 0 or term.config.display.force_pixels_per_column: - term.attrs.ppc = int(term.config.display.pixels_per_column) - if term.attrs.ppl == 0 or term.config.display.force_pixels_per_line: - term.attrs.ppl = int(term.config.display.pixels_per_line) - term.attrs.width_px = term.attrs.ppc * term.attrs.width - term.attrs.height_px = term.attrs.ppl * term.attrs.height - -proc applyConfig(term: Terminal) = - # colors, formatting - if term.config.display.color_mode.isSome: - term.colormode = term.config.display.color_mode.get - elif term.isatty(): - let colorterm = getEnv("COLORTERM") - 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: - if fm in term.config.display.no_format_mode: - term.formatmode.excl(fm) - if term.isatty(): - 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 - # charsets - if term.config.encoding.display_charset.isSome: - term.cs = term.config.encoding.display_charset.get - else: - term.cs = DefaultCharset - for s in ["LC_ALL", "LC_CTYPE", "LANG"]: - let env = getEnv(s) - if env == "": - continue - let cs = getLocaleCharset(env) - if cs != CHARSET_UNKNOWN: - term.cs = cs - break - term.applyConfigDimensions() - -proc outputGrid*(term: Terminal) = - term.outfile.write(term.resetFormat()) - let samesize = term.canvas.width == term.pcanvas.width and - term.canvas.height == term.pcanvas.height - if term.config.display.force_clear or not term.cleared or not samesize: - term.outfile.write(term.generateFullOutput(term.canvas)) - term.cleared = true - else: - term.outfile.write(term.generateSwapOutput(term.canvas, term.pcanvas)) - if not samesize: - term.pcanvas.width = term.canvas.width - term.pcanvas.height = term.canvas.height - term.pcanvas.cells.setLen(term.canvas.cells.len) - for i in 0 ..< term.canvas.cells.len: - term.pcanvas[i] = term.canvas[i] - -proc clearCanvas*(term: Terminal) = - term.cleared = false - -# see https://viewsourcecode.org/snaptoken/kilo/02.enteringRawMode.html -proc disableRawMode(term: Terminal) = - let fd = term.infile.getFileHandle() - discard tcSetAttr(fd, TCSAFLUSH, addr term.orig_termios) - -proc enableRawMode(term: Terminal) = - let fd = term.infile.getFileHandle() - discard tcGetAttr(fd, addr term.orig_termios) - var raw = term.orig_termios - raw.c_iflag = raw.c_iflag and not (BRKINT or ICRNL or INPCK or ISTRIP or IXON) - raw.c_oflag = raw.c_oflag and not (OPOST) - raw.c_cflag = raw.c_cflag or CS8 - raw.c_lflag = raw.c_lflag and not (ECHO or ICANON or ISIG or IEXTEN) - discard tcSetAttr(fd, TCSAFLUSH, addr raw) - -proc unblockStdin*(term: Terminal) = - if term.isatty(): - let fd = term.infile.getFileHandle() - term.orig_flags = fcntl(fd, F_GETFL, 0) - let flags = term.orig_flags or O_NONBLOCK - discard fcntl(fd, F_SETFL, flags) - term.stdin_unblocked = true - -proc restoreStdin*(term: Terminal) = - if term.stdin_unblocked: - let fd = term.infile.getFileHandle() - discard fcntl(fd, F_SETFL, term.orig_flags) - term.stdin_unblocked = false - -proc quit*(term: Terminal) = - if term.isatty(): - term.disableRawMode() - if term.smcup: - term.write(term.disableAltScreen()) - else: - term.write(term.cursorGoto(0, term.attrs.height - 1) & - term.resetFormat() & "\n") - if term.config.input.use_mouse: - term.disableMouse() - term.showCursor() - term.cleared = false - if term.stdin_unblocked: - let fd = term.infile.getFileHandle() - term.orig_flags2 = fcntl(fd, F_GETFL, 0) - discard fcntl(fd, F_SETFL, term.orig_flags2 and (not O_NONBLOCK)) - term.stdin_unblocked = false - else: - term.orig_flags2 = -1 - term.flush() - -when termcap_found: - proc loadTermcap(term: Terminal) = - assert goutfile == nil - goutfile = term.outfile - let tc = new Termcap - if tgetent(cast[cstring](addr tc.bp), cstring(term.tname)) == 1: - term.tc = tc - for id in TermcapCap: - tc.caps[id] = tgetstr(cstring($id), cast[ptr cstring](addr tc.funcstr)) - else: - raise newException(Defect, "Failed to load termcap description for terminal " & term.tname) - -type - QueryAttrs = enum - qaAnsiColor, qaRGB, qaSixel - - QueryResult = object - success: bool - attrs: set[QueryAttrs] - fgcolor: Option[RGBColor] - bgcolor: Option[RGBColor] - widthPx: int - heightPx: int - width: int - height: int - -proc queryAttrs(term: Terminal, windowOnly: bool): QueryResult = - const tcapRGB = 0x524742 # RGB supported? - if not windowOnly: - const outs = - XTGETFG & - XTGETBG & - GEOMPIXEL & - GEOMCELL & - XTGETTCAP("524742") & - DA1 - term.outfile.write(outs) - else: - const outs = - GEOMPIXEL & - GEOMCELL & - DA1 - term.outfile.write(outs) - result = QueryResult(success: false, attrs: {}) - while true: - template consume(term: Terminal): char = term.infile.readChar() - template fail = return - 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 - case (let c = term.consume; c) - of '?': # DA1 - 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 '4', '8': # GEOMPIXEL, GEOMCELL - term.expect ';' - var height = 0 - var width = 0 - while (let c = term.consume; c != ';'): - if (let x = decValue(c); x != -1): - height *= 10 - height += x - else: - fail - while (let c = term.consume; c != 't'): - if (let x = decValue(c); x != -1): - width *= 10 - width += x - else: - fail - if c == '4': # GEOMSIZE - result.widthPx = width - result.heightPx = height - if c == '8': # GEOMCELL - result.width = width - result.height = height - else: fail - 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 - -# when windowOnly, only refresh window size. -proc detectTermAttributes(term: Terminal, windowOnly: bool): TermStartResult = - result = tsrSuccess - term.tname = getEnv("TERM") - if term.tname == "": - term.tname = "dosansi" - if not term.isatty(): - return - let fd = term.infile.getFileHandle() - var win: IOctl_WinSize - if ioctl(cint(fd), TIOCGWINSZ, addr win) != -1: - term.attrs.width = int(win.ws_col) - term.attrs.height = int(win.ws_row) - term.attrs.ppc = int(win.ws_xpixel) div term.attrs.width - term.attrs.ppl = int(win.ws_ypixel) div term.attrs.height - if term.config.display.query_da1: - let r = term.queryAttrs(windowOnly) - if r.success: # DA1 success - if r.width != 0: - term.attrs.width = r.width - if r.widthPx != 0: - term.attrs.ppc = r.widthPx div r.width - if r.height != 0: - term.attrs.height = r.height - if r.heightPx != 0: - term.attrs.ppl = r.heightPx div r.height - if windowOnly: - return - 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 windowOnly: - return - 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 - 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)} - -proc windowChange*(term: Terminal) = - discard term.detectTermAttributes(windowOnly = true) - term.applyConfigDimensions() - term.canvas = newFixedGrid(term.attrs.width, term.attrs.height) - term.cleared = false - -proc start*(term: Terminal, infile: File): TermStartResult = - term.infile = infile - if term.isatty(): - term.enableRawMode() - result = term.detectTermAttributes(windowOnly = false) - if result == tsrDA1Fail: - term.config.display.query_da1 = false - if term.isatty() and term.config.input.use_mouse: - term.enableMouse() - term.applyConfig() - term.canvas = newFixedGrid(term.attrs.width, term.attrs.height) - if term.smcup: - term.write(term.enableAltScreen()) - -proc restart*(term: Terminal) = - if term.isatty(): - term.enableRawMode() - if term.orig_flags2 != -1: - let fd = term.infile.getFileHandle() - discard fcntl(fd, F_SETFL, term.orig_flags2) - term.orig_flags2 = 0 - term.stdin_unblocked = true - if term.config.input.use_mouse: - term.enableMouse() - if term.smcup: - term.write(term.enableAltScreen()) - -proc newTerminal*(outfile: File, config: Config): Terminal = - return Terminal( - outfile: outfile, - config: config, - defaultBackground: ColorsRGB["black"], - defaultForeground: ColorsRGB["white"] - ) diff --git a/src/extern/runproc.nim b/src/extern/runproc.nim deleted file mode 100644 index 9d189dff..00000000 --- a/src/extern/runproc.nim +++ /dev/null @@ -1,58 +0,0 @@ -import std/posix -import std/streams - -import display/term - -proc c_system(cmd: cstring): cint {. - importc: "system", header: "".} - -# Run process (without suspending the terminal controller). -proc runProcess*(cmd: string): bool = - let wstatus = c_system(cstring(cmd)) - if wstatus == -1: - result = false - else: - result = WIFEXITED(wstatus) and WEXITSTATUS(wstatus) == 0 - if not result: - # Hack. - #TODO this is a very bad idea, e.g. say the editor is writing into the - # file, then receives SIGINT, now the file is corrupted but Chawan will - # happily read it as if nothing happened. - # We should find a proper solution for this. - result = WIFSIGNALED(wstatus) and WTERMSIG(wstatus) == SIGINT - -# Run process (and suspend the terminal controller). -proc runProcess*(term: Terminal, cmd: string, wait = false): bool = - term.quit() - result = runProcess(cmd) - if wait: - term.anyKey() - term.restart() - -# Run process, and capture its output. -proc runProcessCapture*(cmd: string, outs: var string): bool = - let file = popen(cmd, "r") - if file == nil: - return false - let fs = newFileStream(file) - outs = fs.readAll() - let rv = pclose(file) - if rv == -1: - return false - return rv == 0 - -# Run process, and write an arbitrary string into its standard input. -proc runProcessInto*(cmd, ins: string): bool = - let file = popen(cmd, "w") - if file == nil: - return false - let fs = newFileStream(file) - fs.write(ins) - let rv = pclose(file) - if rv == -1: - return false - return rv == 0 - -proc myExec*(cmd: string) = - discard execl("/bin/sh", "sh", "-c", cmd, nil) - exitnow(127) diff --git a/src/extern/stdio.nim b/src/extern/stdio.nim deleted file mode 100644 index 729b50f6..00000000 --- a/src/extern/stdio.nim +++ /dev/null @@ -1,25 +0,0 @@ -import std/posix - -proc closeHandle(fd, flags: cint) = - let devnull = open("/dev/null", flags) - doAssert devnull != -1 - if devnull != fd: - discard dup2(devnull, fd) - discard close(devnull) - -proc closeStdin*() = - closeHandle(0, O_RDONLY) - -proc closeStdout*() = - closeHandle(1, O_WRONLY) - -proc closeStderr*() = - closeHandle(2, O_WRONLY) - -proc safeClose*(fd: cint) = - if fd == 0: - closeStdin() - elif fd == 1 or fd == 2: - closeHandle(fd, O_WRONLY) - else: - discard close(fd) diff --git a/src/extern/tempfile.nim b/src/extern/tempfile.nim deleted file mode 100644 index 5968270b..00000000 --- a/src/extern/tempfile.nim +++ /dev/null @@ -1,18 +0,0 @@ -import std/os - -var tmpf_seq: int -proc getTempFile*(tmpdir: string, ext = ""): string = - if not dirExists(tmpdir): - createDir(tmpdir) - var tmpf = tmpdir / "chatmp" & $getCurrentProcessId() & "-" & $tmpf_seq - if ext != "": - tmpf &= "." - tmpf &= ext - while fileExists(tmpf): - inc tmpf_seq - tmpf = tmpdir / "chatmp" & $tmpf_seq - if ext != "": - tmpf &= "." - tmpf &= ext - inc tmpf_seq - return tmpf diff --git a/src/html/dom.nim b/src/html/dom.nim index 2f36d81a..163e4a66 100644 --- a/src/html/dom.nim +++ b/src/html/dom.nim @@ -10,7 +10,6 @@ import css/cssparser import css/mediaquery import css/sheet import css/values -import display/term import html/catom import html/enums import html/event @@ -37,6 +36,7 @@ import types/matrix import types/referrer import types/url import types/vector +import types/winattrs import utils/mimeguess import utils/strwidth import utils/twtstr diff --git a/src/html/env.nim b/src/html/env.nim index 29104707..3ffff116 100644 --- a/src/html/env.nim +++ b/src/html/env.nim @@ -2,12 +2,13 @@ import std/selectors import std/streams import bindings/quickjs -import display/term import html/catom import html/chadombuilder import html/dom import html/event +import html/formdata import html/script +import html/xmlhttprequest import io/promise import js/base64 import js/console @@ -24,8 +25,7 @@ import loader/request import loader/response import types/blob import types/url -import xhr/formdata -import xhr/xmlhttprequest +import types/winattrs # NavigatorID proc appCodeName(navigator: ptr Navigator): string {.jsfget.} = "Mozilla" diff --git a/src/html/formdata.nim b/src/html/formdata.nim new file mode 100644 index 00000000..bbf9a843 --- /dev/null +++ b/src/html/formdata.nim @@ -0,0 +1,177 @@ +import std/base64 +import std/streams + +import html/catom +import html/dom +import html/enums +import js/domexception +import js/javascript +import js/tojs +import types/blob +import types/formdata +import utils/twtstr + +import chame/tags + +proc constructEntryList*(form: HTMLFormElement, submitter: Element = nil, + encoding = "UTF-8"): seq[FormDataEntry] + +proc generateBoundary(): string = + let urandom = newFileStream("/dev/urandom") + let s = urandom.readStr(32) + urandom.close() + # 32 * 4 / 3 (padded) = 44 + prefix string is 22 bytes = 66 bytes + return "----WebKitFormBoundary" & base64.encode(s) + +proc newFormData0*(): FormData = + return FormData(boundary: generateBoundary()) + +proc newFormData*(form: HTMLFormElement = nil, + submitter: HTMLElement = nil): DOMResult[FormData] {.jsctor.} = + let this = newFormData0() + if form != nil: + if submitter != nil: + if not submitter.isSubmitButton(): + return errDOMException("Submitter must be a submit button", + "InvalidStateError") + if FormAssociatedElement(submitter).form != form: + return errDOMException("Submitter's form owner is not form", + "InvalidStateError") + if not form.constructingEntryList: + this.entries = constructEntryList(form, submitter) + return ok(this) + +#TODO filename should not be allowed for string entries +# in other words, this should be an overloaded function, not just an or type +proc append*[T: string|Blob](this: FormData, name: string, value: T, + filename = opt(string)) {.jsfunc.} = + when T is Blob: + let filename = if filename.isSome: + filename.get + elif value of WebFile: + WebFile(value).name + else: + "blob" + this.entries.add(FormDataEntry( + name: name, + isstr: false, + value: value, + filename: filename + )) + else: # string + this.entries.add(FormDataEntry( + name: name, + isstr: true, + svalue: value + )) + +proc delete(this: FormData, name: string) {.jsfunc.} = + for i in countdown(this.entries.high, 0): + if this.entries[i].name == name: + this.entries.delete(i) + +proc get(ctx: JSContext, this: FormData, name: string): JSValue {.jsfunc.} = + for entry in this.entries: + if entry.name == name: + if entry.isstr: + return toJS(ctx, entry.svalue) + else: + return toJS(ctx, entry.value) + return JS_NULL + +proc getAll(ctx: JSContext, this: FormData, name: string): seq[JSValue] + {.jsfunc.} = + for entry in this.entries: + if entry.name == name: + if entry.isstr: + result.add(toJS(ctx, entry.svalue)) + else: + result.add(toJS(ctx, entry.value)) + +proc add(list: var seq[FormDataEntry], entry: tuple[name, value: string]) = + list.add(FormDataEntry( + name: entry.name, + isstr: true, + svalue: entry.value + )) + +func toNameValuePairs*(list: seq[FormDataEntry]): + seq[tuple[name, value: string]] = + for entry in list: + if entry.isstr: + result.add((entry.name, entry.svalue)) + else: + result.add((entry.name, entry.name)) + +# https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#constructing-the-form-data-set +# Warning: we skip the first "constructing entry list" check; the caller must +# do it. +proc constructEntryList*(form: HTMLFormElement, submitter: Element = nil, + encoding = "UTF-8"): seq[FormDataEntry] = + assert not form.constructingEntryList + form.constructingEntryList = true + var entrylist: seq[FormDataEntry] = @[] + for field in form.controls: + if field.findAncestor({TAG_DATALIST}) != nil or + field.attrb(atDisabled) or + field.isButton() and Element(field) != submitter: + continue + if field of HTMLInputElement: + let field = HTMLInputElement(field) + if field.inputType in {INPUT_CHECKBOX, INPUT_RADIO} and not field.checked: + continue + if field.inputType == INPUT_IMAGE: + var name = field.attr(atName) + if name != "": + name &= '.' + entrylist.add((name & 'x', $field.xcoord)) + entrylist.add((name & 'y', $field.ycoord)) + continue + #TODO custom elements + let name = field.attr(atName) + if name == "": + continue + if field of HTMLSelectElement: + let field = HTMLSelectElement(field) + for option in field.options: + if option.selected and not option.isDisabled: + entrylist.add((name, option.value)) + elif field of HTMLInputElement: + let field = HTMLInputElement(field) + case field.inputType + of INPUT_CHECKBOX, INPUT_RADIO: + let v = field.attr(atValue) + let value = if v != "": + v + else: + "on" + entrylist.add((name, value)) + of INPUT_FILE: + #TODO file + discard + of INPUT_HIDDEN: + if name.equalsIgnoreCase("_charset_"): + entrylist.add((name, encoding)) + else: + entrylist.add((name, field.value)) + else: + entrylist.add((name, field.value)) + elif field of HTMLButtonElement: + entrylist.add((name, HTMLButtonElement(field).value)) + elif field of HTMLTextAreaElement: + entrylist.add((name, HTMLTextAreaElement(field).value)) + else: + assert false, "Tag type " & $field.tagType & + " not accounted for in constructEntryList" + if field of HTMLTextAreaElement or + field of HTMLInputElement and + HTMLInputElement(field).inputType in AutoDirInput: + let dirname = field.attr(atDirname) + if dirname != "": + let dir = "ltr" #TODO bidi + entrylist.add((dirname, dir)) + form.constructingEntryList = false + return entrylist + +proc addFormDataModule*(ctx: JSContext) = + ctx.registerType(FormData) diff --git a/src/html/xmlhttprequest.nim b/src/html/xmlhttprequest.nim new file mode 100644 index 00000000..e9905760 --- /dev/null +++ b/src/html/xmlhttprequest.nim @@ -0,0 +1,118 @@ +import std/options +import std/strutils + +import bindings/quickjs +import html/dom +import html/event +import js/domexception +import js/fromjs +import js/javascript +import loader/headers +import loader/request +import loader/response +import types/url + +type + XMLHttpRequestResponseType = enum + TYPE_UNKNOWN = "" + TYPE_ARRAYBUFFER = "arraybuffer" + TYPE_BLOB = "blob" + TYPE_DOCUMENT = "document" + TYPE_JSON = "json" + TYPE_TEXT = "text" + + XMLHttpRequestState = enum + UNSENT = 0u16 + OPENED = 1u16 + HEADERS_RECEIVED = 2u16 + LOADING = 3u16 + DONE = 4u16 + + XMLHttpRequestFlag = enum + SEND_FLAG, UPLOAD_LISTENER_FLAG, SYNC_FLAG + + XMLHttpRequestEventTarget = ref object of EventTarget + onloadstart {.jsgetset.}: EventHandler + onprogress {.jsgetset.}: EventHandler + onabort {.jsgetset.}: EventHandler + onerror {.jsgetset.}: EventHandler + onload {.jsgetset.}: EventHandler + ontimeout {.jsgetset.}: EventHandler + onloadend {.jsgetset.}: EventHandler + + XMLHttpRequestUpload = ref object of XMLHttpRequestEventTarget + + XMLHttpRequest = ref object of XMLHttpRequestEventTarget + onreadystatechange {.jsgetset.}: EventHandler + readyState: XMLHttpRequestState + upload {.jsget.}: XMLHttpRequestUpload + flags: set[XMLHttpRequestFlag] + requestMethod: HttpMethod + requestURL: URL + authorRequestHeaders: Headers + response: Response + responseType {.jsgetset.}: XMLHttpRequestResponseType + +jsDestructor(XMLHttpRequestEventTarget) +jsDestructor(XMLHttpRequestUpload) +jsDestructor(XMLHttpRequest) + +func newXMLHttpRequest(): XMLHttpRequest {.jsctor.} = + let upload = XMLHttpRequestUpload() + return XMLHttpRequest( + upload: upload, + authorRequestHeaders: newHeaders() + ) + +func readyState(this: XMLHttpRequest): uint16 {.jsfget.} = + return uint16(this.readyState) + +proc parseMethod(s: string): DOMResult[HttpMethod] = + return case s.toLowerAscii() + of "get": ok(HTTP_GET) + of "delete": ok(HTTP_DELETE) + of "head": ok(HTTP_HEAD) + of "options": ok(HTTP_OPTIONS) + of "patch": ok(HTTP_PATCH) + of "post": ok(HTTP_POST) + of "put": ok(HTTP_PUT) + of "connect", "trace", "track": + errDOMException("Forbidden method", "SecurityError") + else: + errDOMException("Invalid method", "SyntaxError") + +proc open(ctx: JSContext, this: XMLHttpRequest, httpMethod, url: string): + Err[DOMException] {.jsfunc.} = + let httpMethod = ?parseMethod(httpMethod) + let global = JS_GetGlobalObject(ctx) + let window = fromJS[Window](ctx, global) + JS_FreeValue(ctx, global) + let x = if window.isSome: + parseURL(url, some(window.get.document.baseURL)) + else: + parseURL(url) + if x.isNone: + return errDOMException("Invalid URL", "SyntaxError") + let parsedURL = x.get + #TODO async, username, password arguments + let async = true + #TODO if async is false... probably just throw. + #TODO terminate fetch controller + this.flags.excl(SEND_FLAG) + this.flags.excl(UPLOAD_LISTENER_FLAG) + if async: + this.flags.excl(SYNC_FLAG) + else: + this.flags.incl(SYNC_FLAG) + this.requestMethod = httpMethod + this.authorRequestHeaders = newHeaders() + this.response = makeNetworkError() + this.requestURL = parsedURL + return ok() + +proc addXMLHttpRequestModule*(ctx: JSContext) = + let eventTargetCID = ctx.getClass("EventTarget") + let xhretCID = ctx.registerType(XMLHttpRequestEventTarget, eventTargetCID) + ctx.registerType(XMLHttpRequestUpload, xhretCID) + let xhrCID = ctx.registerType(XMLHttpRequest, xhretCID) + ctx.defineConsts(xhrCID, XMLHttpRequestState, uint16) diff --git a/src/io/stdio.nim b/src/io/stdio.nim new file mode 100644 index 00000000..729b50f6 --- /dev/null +++ b/src/io/stdio.nim @@ -0,0 +1,25 @@ +import std/posix + +proc closeHandle(fd, flags: cint) = + let devnull = open("/dev/null", flags) + doAssert devnull != -1 + if devnull != fd: + discard dup2(devnull, fd) + discard close(devnull) + +proc closeStdin*() = + closeHandle(0, O_RDONLY) + +proc closeStdout*() = + closeHandle(1, O_WRONLY) + +proc closeStderr*() = + closeHandle(2, O_WRONLY) + +proc safeClose*(fd: cint) = + if fd == 0: + closeStdin() + elif fd == 1 or fd == 2: + closeHandle(fd, O_WRONLY) + else: + discard close(fd) diff --git a/src/io/tempfile.nim b/src/io/tempfile.nim new file mode 100644 index 00000000..5968270b --- /dev/null +++ b/src/io/tempfile.nim @@ -0,0 +1,18 @@ +import std/os + +var tmpf_seq: int +proc getTempFile*(tmpdir: string, ext = ""): string = + if not dirExists(tmpdir): + createDir(tmpdir) + var tmpf = tmpdir / "chatmp" & $getCurrentProcessId() & "-" & $tmpf_seq + if ext != "": + tmpf &= "." + tmpf &= ext + while fileExists(tmpf): + inc tmpf_seq + tmpf = tmpdir / "chatmp" & $tmpf_seq + if ext != "": + tmpf &= "." + tmpf &= ext + inc tmpf_seq + return tmpf diff --git a/src/layout/engine.nim b/src/layout/engine.nim index 371121a3..e9c3ffde 100644 --- a/src/layout/engine.nim +++ b/src/layout/engine.nim @@ -5,9 +5,9 @@ import std/unicode import css/stylednode import css/values -import display/term import layout/box import layout/layoutunit +import types/winattrs import utils/luwrap import utils/strwidth import utils/twtstr diff --git a/src/layout/renderdocument.nim b/src/layout/renderdocument.nim index ef6504c8..ef82931d 100644 --- a/src/layout/renderdocument.nim +++ b/src/layout/renderdocument.nim @@ -3,10 +3,10 @@ import std/unicode import css/stylednode import css/values -import display/term import layout/box import layout/engine import layout/layoutunit +import types/winattrs import types/cell import types/color import utils/strwidth diff --git a/src/loader/cgi.nim b/src/loader/cgi.nim index f7922224..b91e3b1b 100644 --- a/src/loader/cgi.nim +++ b/src/loader/cgi.nim @@ -4,8 +4,8 @@ import std/posix import std/streams import std/strutils -import extern/stdio import io/posixstream +import io/stdio import loader/connecterror import loader/headers import loader/loaderhandle diff --git a/src/loader/loader.nim b/src/loader/loader.nim index b1bcce38..135a8c96 100644 --- a/src/loader/loader.nim +++ b/src/loader/loader.nim @@ -24,12 +24,12 @@ import std/streams import std/strutils import std/tables -import extern/tempfile import io/posixstream import io/promise import io/serialize import io/serversocket import io/socketstream +import io/tempfile import io/urlfilter import js/error import js/javascript diff --git a/src/local/client.nim b/src/local/client.nim index 4f7d9f2b..bc52f704 100644 --- a/src/local/client.nim +++ b/src/local/client.nim @@ -15,11 +15,11 @@ when defined(posix): import bindings/constcharp import bindings/quickjs import config/config -import display/lineedit -import display/term import html/chadombuilder import html/dom import html/event +import html/formdata +import html/xmlhttprequest import io/bufstream import io/posixstream import io/promise @@ -41,7 +41,9 @@ import loader/headers import loader/loader import loader/request import local/container +import local/lineedit import local/pager +import local/term import server/buffer import server/forkserver import types/blob @@ -49,8 +51,6 @@ import types/cookie import types/opt import types/url import utils/twtstr -import xhr/formdata -import xhr/xmlhttprequest import chagashi/charset diff --git a/src/local/container.nim b/src/local/container.nim index 7946200a..e2d84ac0 100644 --- a/src/local/container.nim +++ b/src/local/container.nim @@ -6,7 +6,6 @@ when defined(posix): import std/posix import config/config -import display/term import io/promise import io/serialize import io/socketstream @@ -23,6 +22,7 @@ import types/color import types/cookie import types/referrer import types/url +import types/winattrs import utils/luwrap import utils/mimeguess import utils/strwidth diff --git a/src/local/lineedit.nim b/src/local/lineedit.nim new file mode 100644 index 00000000..0f048586 --- /dev/null +++ b/src/local/lineedit.nim @@ -0,0 +1,328 @@ +import std/strutils +import std/unicode + +import bindings/quickjs +import js/javascript +import types/cell +import types/opt +import types/winattrs +import utils/strwidth +import utils/twtstr + +import chagashi/charset +import chagashi/validator +import chagashi/decoder + +type + LineEditState* = enum + EDIT, FINISH, CANCEL + + LineHistory* = ref object + lines: seq[string] + + LineEdit* = ref object + news*: string + prompt: string + promptw: int + state*: LineEditState + escNext*: bool + cursorx: int # 0 ..< news.notwidth + cursori: int # 0 ..< news.len + shiftx: int # 0 ..< news.notwidth + shifti: int # 0 ..< news.len + padding: int # 0 or 1 + maxwidth: int + disallowed: set[char] + hide: bool + hist: LineHistory + histindex: int + histtmp: string + invalid*: bool + +jsDestructor(LineEdit) + +func newLineHistory*(): LineHistory = + return LineHistory() + +# Note: capped at edit.maxwidth. +func getDisplayWidth(edit: LineEdit): int = + var dispw = 0 + var i = edit.shifti + var r: Rune + while i < edit.news.len and dispw < edit.maxwidth: + fastRuneAt(edit.news, i, r) + dispw += r.width() + return dispw + +proc shiftView(edit: LineEdit) = + # Shift view so it contains the cursor. + if edit.cursorx < edit.shiftx: + edit.shiftx = edit.cursorx + edit.shifti = edit.cursori + # Shift view so it is completely filled. + if edit.shiftx > 0: + let dispw = edit.getDisplayWidth() + if dispw < edit.maxwidth: + let targetx = edit.shiftx - edit.maxwidth + dispw + if targetx <= 0: + edit.shiftx = 0 + edit.shifti = 0 + else: + while edit.shiftx > targetx: + let (r, len) = edit.news.lastRune(edit.shifti - 1) + edit.shiftx -= r.width() + edit.shifti -= len + edit.padding = 0 + # Shift view so it contains the cursor. (act 2) + if edit.shiftx < edit.cursorx - edit.maxwidth: + while edit.shiftx < edit.cursorx - edit.maxwidth and + edit.shifti < edit.news.len: + var r: Rune + fastRuneAt(edit.news, edit.shifti, r) + edit.shiftx += r.width() + if edit.shiftx > edit.cursorx - edit.maxwidth: + # skipped over a cell because of a double-width char + edit.padding = 1 + +proc generateOutput*(edit: LineEdit): FixedGrid = + edit.shiftView() + # Make the output grid +1 cell wide, so it covers the whole input area. + result = newFixedGrid(edit.promptw + edit.maxwidth + 1) + var x = 0 + for r in edit.prompt.runes: + result[x].str &= $r + x += r.width() + if x >= result.width: break + for i in 0 ..< edit.padding: + if x < result.width: + result[x].str = " " + inc x + var i = edit.shifti + while i < edit.news.len: + var r: Rune + fastRuneAt(edit.news, i, r) + if not edit.hide: + let w = r.width() + if x + w > result.width: break + if r.isControlChar(): + result[x].str &= '^' + inc x + result[x].str &= char(r).getControlLetter() + inc x + else: + result[x].str &= $r + x += w + else: + if x + 1 > result.width: break + result[x].str &= '*' + inc x + +proc getCursorX*(edit: LineEdit): int = + return edit.promptw + edit.cursorx + edit.padding - edit.shiftx + +proc insertCharseq(edit: LineEdit, s: string) = + let s = if edit.escNext: + s + else: + deleteChars(s, edit.disallowed) + edit.escNext = false + if s.len == 0: + return + let rem = edit.news.substr(edit.cursori) + edit.news.setLen(edit.cursori) + edit.news &= s + edit.news &= rem + edit.cursori += s.len + edit.cursorx += s.notwidth() + edit.invalid = true + +proc cancel(edit: LineEdit) {.jsfunc.} = + edit.state = CANCEL + +proc submit(edit: LineEdit) {.jsfunc.} = + if edit.hist.lines.len == 0 or edit.news != edit.hist.lines[^1]: + edit.hist.lines.add(edit.news) + edit.state = FINISH + +proc backspace(edit: LineEdit) {.jsfunc.} = + if edit.cursori > 0: + let (r, len) = edit.news.lastRune(edit.cursori - 1) + edit.news.delete(edit.cursori - len .. edit.cursori - 1) + edit.cursori -= len + edit.cursorx -= r.width() + edit.invalid = true + +proc write*(edit: LineEdit, s: string, cs: Charset): bool = + if cs == CHARSET_UTF_8: + if s.validateUTF8Surr() != -1: + return false + edit.insertCharseq(s) + else: + let td = newTextDecoder(cs) + var success = false + let s = td.decodeAll(s, success) + if not success: + return false + edit.insertCharseq(s) + return true + +proc write(edit: LineEdit, s: string): bool {.jsfunc.} = + edit.write(s, CHARSET_UTF_8) + +proc delete(edit: LineEdit) {.jsfunc.} = + if edit.cursori < edit.news.len: + let len = edit.news.runeLenAt(edit.cursori) + edit.news.delete(edit.cursori ..< edit.cursori + len) + edit.invalid = true + +proc escape(edit: LineEdit) {.jsfunc.} = + edit.escNext = true + +proc clear(edit: LineEdit) {.jsfunc.} = + if edit.cursori > 0: + edit.news.delete(0..edit.cursori - 1) + edit.cursori = 0 + edit.cursorx = 0 + edit.invalid = true + +proc kill(edit: LineEdit) {.jsfunc.} = + if edit.cursori < edit.news.len: + edit.news.setLen(edit.cursori) + edit.invalid = true + +proc backward(edit: LineEdit) {.jsfunc.} = + if edit.cursori > 0: + let (r, len) = edit.news.lastRune(edit.cursori - 1) + edit.cursori -= len + edit.cursorx -= r.width() + if edit.cursorx < edit.shiftx: + edit.invalid = true + +proc forward(edit: LineEdit) {.jsfunc.} = + if edit.cursori < edit.news.len: + var r: Rune + fastRuneAt(edit.news, edit.cursori, r) + edit.cursorx += r.width() + if edit.cursorx >= edit.shiftx + edit.maxwidth: + edit.invalid = true + +proc prevWord(edit: LineEdit) {.jsfunc.} = + if edit.cursori == 0: + return + let (r, len) = edit.news.lastRune(edit.cursori - 1) + if r.breaksWord(): + edit.cursori -= len + edit.cursorx -= r.width() + while edit.cursori > 0: + let (r, len) = edit.news.lastRune(edit.cursori - 1) + if r.breaksWord(): + break + edit.cursori -= len + edit.cursorx -= r.width() + if edit.cursorx < edit.shiftx: + edit.invalid = true + +proc nextWord(edit: LineEdit) {.jsfunc.} = + if edit.cursori >= edit.news.len: + return + let oc = edit.cursori + var r: Rune + fastRuneAt(edit.news, edit.cursori, r) + if r.breaksWord(): + edit.cursorx += r.width() + else: + edit.cursori = oc + while edit.cursori < edit.news.len: + let pc = edit.cursori + fastRuneAt(edit.news, edit.cursori, r) + if r.breaksWord(): + edit.cursori = pc + break + edit.cursorx += r.width() + if edit.cursorx >= edit.shiftx + edit.maxwidth: + edit.invalid = true + +proc clearWord(edit: LineEdit) {.jsfunc.} = + let oc = edit.cursori + edit.prevWord() + if oc != edit.cursori: + edit.news.delete(edit.cursori .. oc - 1) + edit.invalid = true + +proc killWord(edit: LineEdit) {.jsfunc.} = + if edit.cursori >= edit.news.len: + return + let oc = edit.cursori + let ox = edit.cursorx + edit.nextWord() + if edit.cursori != oc: + if edit.cursori < edit.news.len: + let len = edit.news.runeLenAt(edit.cursori) + edit.news.delete(oc ..< edit.cursori + len) + else: + edit.news.delete(oc ..< edit.cursori) + edit.cursori = oc + edit.cursorx = ox + edit.invalid = true + +proc begin(edit: LineEdit) {.jsfunc.} = + edit.cursori = 0 + edit.cursorx = 0 + if edit.shiftx > 0: + edit.invalid = true + +proc `end`(edit: LineEdit) {.jsfunc.} = + if edit.cursori < edit.news.len: + edit.cursori = edit.news.len + edit.cursorx = edit.news.notwidth() + if edit.cursorx >= edit.shiftx + edit.maxwidth: + edit.invalid = true + +proc prevHist(edit: LineEdit) {.jsfunc.} = + if edit.histindex > 0: + if edit.news.len > 0: + edit.histtmp = $edit.news + dec edit.histindex + edit.news = edit.hist.lines[edit.histindex] + # The begin call is needed so the cursor doesn't get lost outside + # the string. + edit.begin() + edit.end() + edit.invalid = true + +proc nextHist(edit: LineEdit) {.jsfunc.} = + if edit.histindex + 1 < edit.hist.lines.len: + inc edit.histindex + edit.news = edit.hist.lines[edit.histindex] + edit.begin() + edit.end() + edit.invalid = true + elif edit.histindex < edit.hist.lines.len: + inc edit.histindex + edit.news = edit.histtmp + edit.begin() + edit.end() + edit.histtmp = "" + +proc windowChange*(edit: LineEdit, attrs: WindowAttributes) = + edit.maxwidth = attrs.width - edit.promptw - 1 + +proc readLine*(prompt: string, termwidth: int, current = "", + disallowed: set[char] = {}, hide = false, hist: LineHistory): LineEdit = + result = LineEdit( + prompt: prompt, + promptw: prompt.width(), + news: current, + disallowed: disallowed, + hide: hide, + invalid: true + ) + result.cursori = result.news.len + result.cursorx = result.news.notwidth() + # - 1, so that the cursor always has place + result.maxwidth = termwidth - result.promptw - 1 + result.hist = hist + result.histindex = result.hist.lines.len + +proc addLineEditModule*(ctx: JSContext) = + ctx.registerType(LineEdit) diff --git a/src/local/pager.nim b/src/local/pager.nim index bf9d3168..7502cb00 100644 --- a/src/local/pager.nim +++ b/src/local/pager.nim @@ -16,16 +16,13 @@ import config/chapath import config/config import config/mailcap import config/mimetypes -import display/lineedit -import display/term -import extern/runproc -import extern/stdio -import extern/tempfile import io/bufstream import io/posixstream import io/promise import io/serialize import io/socketstream +import io/stdio +import io/tempfile import io/urlfilter import js/error import js/javascript @@ -37,7 +34,9 @@ import loader/headers import loader/loader import loader/request import local/container +import local/lineedit import local/select +import local/term import server/buffer import server/forkserver import types/cell @@ -47,6 +46,7 @@ import types/opt import types/referrer import types/urimethodmap import types/url +import types/winattrs import utils/strwidth import utils/twtstr @@ -126,7 +126,8 @@ type jsDestructor(Pager) -func attrs(pager: Pager): WindowAttributes = pager.term.attrs +template attrs(pager: Pager): WindowAttributes = + pager.term.attrs func loaderPid(pager: Pager): int64 {.jsfget.} = int64(pager.loader.process) @@ -745,6 +746,59 @@ proc discardTree(pager: Pager, container = none(Container)) {.jsfunc.} = else: pager.alert("Buffer has no children!") +proc c_system(cmd: cstring): cint {.importc: "system", header: "".} + +# Run process (without suspending the terminal controller). +proc runProcess(cmd: string): bool = + let wstatus = c_system(cstring(cmd)) + if wstatus == -1: + result = false + else: + result = WIFEXITED(wstatus) and WEXITSTATUS(wstatus) == 0 + if not result: + # Hack. + #TODO this is a very bad idea, e.g. say the editor is writing into the + # file, then receives SIGINT, now the file is corrupted but Chawan will + # happily read it as if nothing happened. + # We should find a proper solution for this. + result = WIFSIGNALED(wstatus) and WTERMSIG(wstatus) == SIGINT + +# Run process (and suspend the terminal controller). +proc runProcess(term: Terminal, cmd: string, wait = false): bool = + term.quit() + result = runProcess(cmd) + if wait: + term.anyKey() + term.restart() + +# Run process, and capture its output. +proc runProcessCapture(cmd: string, outs: var string): bool = + let file = popen(cmd, "r") + if file == nil: + return false + let fs = newFileStream(file) + outs = fs.readAll() + let rv = pclose(file) + if rv == -1: + return false + return rv == 0 + +# Run process, and write an arbitrary string into its standard input. +proc runProcessInto(cmd, ins: string): bool = + let file = popen(cmd, "w") + if file == nil: + return false + let fs = newFileStream(file) + fs.write(ins) + let rv = pclose(file) + if rv == -1: + return false + return rv == 0 + +template myExec(cmd: string) = + discard execl("/bin/sh", "sh", "-c", cstring(cmd), nil) + exitnow(127) + proc toggleSource(pager: Pager) {.jsfunc.} = if not pager.container.canreinterpret: return @@ -1182,7 +1236,6 @@ proc ansiDecode(pager: Pager; url: URL; ishtml: var bool; fdin: cint): cint = discard close(pipefdOutAnsi[1]) closeStderr() myExec(cmd) - assert false else: discard close(pipefdOutAnsi[1]) discard close(fdin) @@ -1206,7 +1259,6 @@ proc runMailcapReadPipe(pager: Pager; stream: SocketStream; cmd: string; closeStderr() discard close(pipefdOut[1]) myExec(cmd) - doAssert false # parent pid @@ -1227,7 +1279,6 @@ proc runMailcapWritePipe(pager: Pager; stream: SocketStream; closeStdout() closeStderr() myExec(cmd) - doAssert false else: # parent stream.close() @@ -1326,7 +1377,6 @@ proc filterBuffer(pager: Pager; stream: SocketStream; cmd: string; closeStderr() discard close(pipefd_out[1]) myExec(cmd) - doAssert false # parent discard close(pipefd_out[1]) let fdout = pipefd_out[0] diff --git a/src/local/term.nim b/src/local/term.nim new file mode 100644 index 00000000..050edf3b --- /dev/null +++ b/src/local/term.nim @@ -0,0 +1,899 @@ +import std/options +import std/os +import std/posix +import std/streams +import std/strutils +import std/tables +import std/termios +import std/unicode + +import bindings/termcap +import config/config +import types/cell +import types/color +import types/opt +import types/winattrs +import utils/strwidth +import utils/twtstr + +import chagashi/charset +import chagashi/encoder +import chagashi/validator + +#TODO switch from termcap... + +type + TermcapCap = enum + ce # clear till end of line + cd # clear display + cm # cursor move + ti # terminal init (=smcup) + te # terminal end (=rmcup) + so # start standout mode + md # start bold mode + 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 + vs # enhance cursor + vi # make cursor invisible + ve # reset cursor to normal + + Termcap = ref object + bp: array[1024, uint8] + funcstr: array[256, uint8] + caps: array[TermcapCap, cstring] + + Terminal* = ref TerminalObj + TerminalObj = object + cs*: Charset + config: Config + infile*: File + outfile: File + cleared: bool + canvas: FixedGrid + pcanvas: FixedGrid + attrs*: WindowAttributes + colormode: ColorMode + formatmode: FormatMode + smcup: bool + tc: Termcap + tname: string + set_title: bool + stdin_unblocked: bool + 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") + +# report xterm text area size in pixels +const GEOMPIXEL = CSI(14, "t") + +# report window size in chars +const GEOMCELL = CSI(18, "t") + +# allow shift-key to override mouse protocol +const XTSHIFTESCAPE = CSI(">0s") + +# 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' + +template XTERM_TITLE(s: string): string = + OSC(0, s) + +const XTGETFG = OSC(10, "?") # get foreground color +const XTGETBG = OSC(11, "?") # get background color + +# DEC set +template DECSET(s: varargs[string, `$`]): string = + "\e[?" & s.join(';') & 'h' + +# DEC reset +template DECRST(s: varargs[string, `$`]): string = + "\e[?" & s.join(';') & 'l' + +# alt screen +const SMCUP = DECSET(1049) +const RMCUP = DECRST(1049) + +# mouse tracking +const SGRMOUSEBTNON = DECSET(1002, 1006) +const SGRMOUSEBTNOFF = DECRST(1002, 1006) + +when not termcap_found: + const CNORM = DECSET(25) + const CIVIS = DECRST(25) + template HVP(s: varargs[string, `$`]): string = + CSI(s) & "f" + template EL(): string = + CSI() & "K" + template ED(): string = + CSI() & "J" + + proc write(term: Terminal, s: string) = + term.outfile.write(s) +else: + func hascap(term: Terminal, c: TermcapCap): bool = term.tc.caps[c] != nil + func cap(term: Terminal, c: TermcapCap): string = $term.tc.caps[c] + func ccap(term: Terminal, c: TermcapCap): cstring = term.tc.caps[c] + + var goutfile: File + proc putc(c: char): cint {.cdecl.} = + goutfile.write(c) + + proc write(term: Terminal, s: cstring) = + discard tputs(s, 1, putc) + + proc write(term: Terminal, s: string) = + term.write(cstring(s)) + +template SGR*(s: varargs[string, `$`]): string = + CSI(s) & "m" + +#TODO a) this should be customizable b) these defaults sucks +const ANSIColorMap = [ + rgb(0, 0, 0), + rgb(205, 0, 0), + rgb(0, 205, 0), + rgb(205, 205, 0), + rgb(0, 0, 238), + rgb(205, 0, 205), + rgb(0, 205, 205), + rgb(229, 229, 229), + rgb(127, 127, 127), + rgb(255, 0, 0), + rgb(0, 255, 0), + rgb(255, 255, 0), + rgb(92, 92, 255), + rgb(255, 0, 255), + rgb(0, 255, 255), + rgb(255, 255, 255) +] + +proc flush*(term: Terminal) = + term.outfile.flushFile() + +proc cursorGoto(term: Terminal, x, y: int): string = + when termcap_found: + return $tgoto(term.ccap cm, cint(x), cint(y)) + else: + return HVP(y + 1, x + 1) + +proc clearEnd(term: Terminal): string = + when termcap_found: + return term.cap ce + else: + return EL() + +proc clearDisplay(term: Terminal): string = + when termcap_found: + return term.cap cd + else: + return ED() + +proc isatty(fd: FileHandle): cint {.importc: "isatty", header: "".} +proc isatty*(f: File): bool = + return isatty(f.getFileHandle()) != 0 + +proc isatty*(term: Terminal): bool = + term.infile != nil and term.infile.isatty() and term.outfile.isatty() + +proc anyKey*(term: Terminal) = + if term.isatty(): + term.outfile.write("[Hit any key]") + discard term.infile.readChar() + +proc resetFormat(term: Terminal): string = + when termcap_found: + if term.isatty(): + return term.cap me + return SGR() + +proc startFormat(term: Terminal, flag: FormatFlags): string = + when termcap_found: + if term.isatty(): + case flag + of FLAG_BOLD: return term.cap md + 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) + +proc endFormat(term: Terminal, flag: FormatFlags): string = + when termcap_found: + if flag == FLAG_UNDERLINE and term.isatty(): + return term.cap ue + return SGR(FormatCodes[flag].e) + +proc setCursor*(term: Terminal, x, y: int) = + term.write(term.cursorGoto(x, y)) + +proc enableAltScreen(term: Terminal): string = + when termcap_found: + if term.hascap ti: + return term.cap ti + return SMCUP + +proc disableAltScreen(term: Terminal): string = + when termcap_found: + if term.hascap te: + return term.cap te + return RMCUP + +func mincontrast(term: Terminal): int32 = + return term.config.display.minimum_contrast + +proc getRGB(a: CellColor, termDefault: RGBColor): RGBColor = + case a.t + of ctNone: + return termDefault + of ctANSI: + if a.color >= 16: + return EightBitColor(a.color).toRGB() + return ANSIColorMap[a.color] + of ctRGB: + return a.rgbcolor + +# Use euclidian distance to quantize RGB colors. +proc approximateANSIColor(rgb, termDefault: RGBColor): CellColor = + var a = 0i32 + var n = -1 + for i in -1 .. ANSIColorMap.high: + let color = if i >= 0: + ANSIColorMap[i] + else: + termDefault + if color == rgb: + return if i == -1: defaultColor else: ANSIColor(i).cellColor() + let x = int32(color.r) - int32(rgb.r) + let y = int32(color.g) - int32(rgb.g) + let z = int32(color.b) - int32(rgb.b) + let xx = x * x + let yy = y * y + let zz = z * z + let b = xx + yy + zz + if i == -1 or b < a: + n = i + a = b + return if n == -1: defaultColor else: ANSIColor(n).cellColor() + +# Return a fgcolor contrasted to the background by term.mincontrast. +proc correctContrast(term: Terminal, bgcolor, fgcolor: CellColor): CellColor = + let contrast = term.mincontrast + let cfgcolor = fgcolor + let bgcolor = getRGB(bgcolor, term.defaultBackground) + let fgcolor = getRGB(fgcolor, term.defaultForeground) + let bgY = int(bgcolor.Y) + var fgY = int(fgcolor.Y) + let diff = abs(bgY - fgY) + if diff < contrast: + if bgY > fgY: + fgY = bgY - contrast + if fgY < 0: + fgY = bgY + contrast + if fgY > 255: + fgY = 0 + else: + fgY = bgY + contrast + if fgY > 255: + fgY = bgY - contrast + if fgY < 0: + fgY = 255 + let newrgb = YUV(cast[uint8](fgY), fgcolor.U, fgcolor.V) + case term.colormode + of TRUE_COLOR: + return cellColor(newrgb) + of ANSI: + return approximateANSIColor(newrgb, term.defaultForeground) + of EIGHT_BIT: + return cellColor(newrgb.toEightBit()) + of MONOCHROME: + doAssert false + return cfgcolor + +template ansiSGR(n: uint8, bgmod: int): string = + if n < 8: + SGR(30 + bgmod + n) + else: + SGR(82 + bgmod + n) + +template eightBitSGR(n: uint8, bgmod: int): string = + if n < 16: + ansiSGR(n, bgmod) + else: + SGR(38 + bgmod, 5, n) + +template rgbSGR(rgb: RGBColor, bgmod: int): string = + SGR(38 + bgmod, 2, rgb.r, rgb.g, rgb.b) + +proc processFormat*(term: Terminal, format: var Format, cellf: Format): string = + for flag in FormatFlags: + if flag in term.formatmode: + if flag in format.flags and flag notin cellf.flags: + result &= term.endFormat(flag) + if flag notin format.flags and flag in cellf.flags: + result &= term.startFormat(flag) + var cellf = cellf + case term.colormode + of ANSI: + # quantize + if cellf.bgcolor.t == ctANSI and cellf.bgcolor.color > 15: + cellf.bgcolor = cellf.fgcolor.eightbit.toRGB().cellColor() + if cellf.bgcolor.t == ctRGB: + cellf.bgcolor = approximateANSIColor(cellf.bgcolor.rgbcolor, + term.defaultBackground) + if cellf.fgcolor.t == ctANSI and cellf.fgcolor.color > 15: + cellf.fgcolor = cellf.fgcolor.eightbit.toRGB().cellColor() + if cellf.fgcolor.t == ctRGB: + if cellf.bgcolor.t == ctNone: + cellf.fgcolor = approximateANSIColor(cellf.fgcolor.rgbcolor, + term.defaultForeground) + else: + # ANSI fgcolor + bgcolor at the same time is broken + cellf.fgcolor = defaultColor + # correct + cellf.fgcolor = term.correctContrast(cellf.bgcolor, cellf.fgcolor) + # print + case cellf.fgcolor.t + of ctNone: result &= SGR(39) + of ctANSI: result &= ansiSGR(cellf.fgcolor.color, 0) + else: assert false + case cellf.bgcolor.t + of ctNone: result &= SGR(49) + of ctANSI: result &= ansiSGR(cellf.bgcolor.color, 10) + else: assert false + of EIGHT_BIT: + # quantize + if cellf.bgcolor.t == ctRGB: + cellf.bgcolor = cellf.bgcolor.rgbcolor.toEightBit().cellColor() + if cellf.fgcolor.t == ctRGB: + cellf.fgcolor = cellf.fgcolor.rgbcolor.toEightBit().cellColor() + # correct + cellf.fgcolor = term.correctContrast(cellf.bgcolor, cellf.fgcolor) + # print + case cellf.fgcolor.t + of ctNone: result &= SGR(39) + of ctANSI: result &= eightBitSGR(cellf.fgcolor.color, 0) + of ctRGB: assert false + case cellf.bgcolor.t + of ctNone: result &= SGR(49) + of ctANSI: result &= eightBitSGR(cellf.bgcolor.color, 10) + of ctRGB: assert false + of TRUE_COLOR: + # correct + cellf.fgcolor = term.correctContrast(cellf.bgcolor, cellf.fgcolor) + # print + if cellf.fgcolor != format.fgcolor: + case cellf.fgcolor.t + of ctNone: result &= SGR(39) + of ctANSI: result &= eightBitSGR(cellf.fgcolor.color, 0) + of ctRGB: result &= rgbSGR(cellf.fgcolor.rgbcolor, 0) + if cellf.bgcolor != format.bgcolor: + case cellf.bgcolor.t + of ctNone: result &= SGR(49) + of ctANSI: result &= eightBitSGR(cellf.bgcolor.color, 10) + of ctRGB: result &= rgbSGR(cellf.bgcolor.rgbcolor, 10) + of MONOCHROME: + discard # nothing to do + format = cellf + +proc setTitle*(term: Terminal, title: string) = + if term.set_title: + term.outfile.write(XTERM_TITLE(title.replaceControls())) + +proc enableMouse*(term: Terminal) = + term.write(XTSHIFTESCAPE & SGRMOUSEBTNON) + +proc disableMouse*(term: Terminal) = + term.write(SGRMOUSEBTNOFF) + +proc processOutputString*(term: Terminal, str: string, w: var int): string = + if str.validateUTF8Surr() != -1: + return "?" + # twidth wouldn't work here, the view may start at the nth character. + # pager must ensure tabs are converted beforehand. + w += str.notwidth() + let str = if Controls in str: + str.replaceControls() + else: + str + if term.cs == CHARSET_UTF_8: + # The output encoding matches the internal representation. + return str + else: + # Output is not utf-8, so we must encode it first. + var success = false + return newTextEncoder(term.cs).encodeAll(str, success) + +proc generateFullOutput(term: Terminal, grid: FixedGrid): string = + var format = Format() + result &= term.cursorGoto(0, 0) + result &= term.resetFormat() + result &= term.clearDisplay() + for y in 0 ..< grid.height: + if y != 0: + result &= "\r\n" + var w = 0 + for x in 0 ..< grid.width: + while w < x: + result &= " " + inc w + let cell = grid[y * grid.width + x] + result &= term.processFormat(format, cell.format) + result &= term.processOutputString(cell.str, w) + +proc generateSwapOutput(term: Terminal, grid, prev: FixedGrid): string = + var vy = -1 + for y in 0 ..< grid.height: + var w = 0 + var change = false + # scan for changes, and set cx to x of the first change + var cx = 0 + # if there is a change, we have to start from the last x with + # a string (otherwise we might overwrite a double-width char) + var lastx = 0 + for x in 0 ..< grid.width: + let i = y * grid.width + x + if grid[i].str != "": + lastx = x + if grid[i] != prev[i]: + change = true + cx = lastx + w = lastx + break + if change: + if cx == 0 and vy != -1: + while vy < y: + result &= "\r\n" + inc vy + else: + result &= term.cursorGoto(cx, y) + vy = y + result &= term.resetFormat() + var format = Format() + for x in cx ..< grid.width: + while w < x: # if previous cell had no width, catch up with x + result &= ' ' + inc w + let cell = grid[y * grid.width + x] + result &= term.processFormat(format, cell.format) + result &= term.processOutputString(cell.str, w) + if w < grid.width: + result &= term.clearEnd() + +proc hideCursor*(term: Terminal) = + when termcap_found: + term.write(term.ccap vi) + else: + term.write(CIVIS) + +proc showCursor*(term: Terminal) = + when termcap_found: + term.write(term.ccap ve) + else: + term.write(CNORM) + +func emulateOverline(term: Terminal): bool = + term.config.display.emulate_overline and + FLAG_OVERLINE notin term.formatmode and FLAG_UNDERLINE in term.formatmode + +proc writeGrid*(term: Terminal, grid: FixedGrid, x = 0, y = 0) = + for ly in y ..< y + grid.height: + for lx in x ..< x + grid.width: + let i = ly * term.canvas.width + lx + term.canvas[i] = grid[(ly - y) * grid.width + (lx - x)] + let isol = FLAG_OVERLINE in term.canvas[i].format.flags + if i >= term.canvas.width and isol and term.emulateOverline: + let w = grid[(ly - y) * grid.width + (lx - x)].width() + let s = i - term.canvas.width + var j = s + while j < term.canvas.len and j < s + w: + let cell = addr term.canvas[j] + cell.format.flags.incl(FLAG_UNDERLINE) + if cell.str == "": + cell.str = " " + if cell.str == " ": + let i = (ly - y) * grid.width + (lx - x) + cell.format.fgcolor = grid[i].format.fgcolor + j += cell[].width() + +proc applyConfigDimensions(term: Terminal) = + # screen dimensions + if term.attrs.width == 0 or term.config.display.force_columns: + term.attrs.width = int(term.config.display.columns) + if term.attrs.height == 0 or term.config.display.force_lines: + term.attrs.height = int(term.config.display.lines) + if term.attrs.ppc == 0 or term.config.display.force_pixels_per_column: + term.attrs.ppc = int(term.config.display.pixels_per_column) + if term.attrs.ppl == 0 or term.config.display.force_pixels_per_line: + term.attrs.ppl = int(term.config.display.pixels_per_line) + term.attrs.width_px = term.attrs.ppc * term.attrs.width + term.attrs.height_px = term.attrs.ppl * term.attrs.height + +proc applyConfig(term: Terminal) = + # colors, formatting + if term.config.display.color_mode.isSome: + term.colormode = term.config.display.color_mode.get + elif term.isatty(): + let colorterm = getEnv("COLORTERM") + 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: + if fm in term.config.display.no_format_mode: + term.formatmode.excl(fm) + if term.isatty(): + 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 + # charsets + if term.config.encoding.display_charset.isSome: + term.cs = term.config.encoding.display_charset.get + else: + term.cs = DefaultCharset + for s in ["LC_ALL", "LC_CTYPE", "LANG"]: + let env = getEnv(s) + if env == "": + continue + let cs = getLocaleCharset(env) + if cs != CHARSET_UNKNOWN: + term.cs = cs + break + term.applyConfigDimensions() + +proc outputGrid*(term: Terminal) = + term.outfile.write(term.resetFormat()) + let samesize = term.canvas.width == term.pcanvas.width and + term.canvas.height == term.pcanvas.height + if term.config.display.force_clear or not term.cleared or not samesize: + term.outfile.write(term.generateFullOutput(term.canvas)) + term.cleared = true + else: + term.outfile.write(term.generateSwapOutput(term.canvas, term.pcanvas)) + if not samesize: + term.pcanvas.width = term.canvas.width + term.pcanvas.height = term.canvas.height + term.pcanvas.cells.setLen(term.canvas.cells.len) + for i in 0 ..< term.canvas.cells.len: + term.pcanvas[i] = term.canvas[i] + +proc clearCanvas*(term: Terminal) = + term.cleared = false + +# see https://viewsourcecode.org/snaptoken/kilo/02.enteringRawMode.html +proc disableRawMode(term: Terminal) = + let fd = term.infile.getFileHandle() + discard tcSetAttr(fd, TCSAFLUSH, addr term.orig_termios) + +proc enableRawMode(term: Terminal) = + let fd = term.infile.getFileHandle() + discard tcGetAttr(fd, addr term.orig_termios) + var raw = term.orig_termios + raw.c_iflag = raw.c_iflag and not (BRKINT or ICRNL or INPCK or ISTRIP or IXON) + raw.c_oflag = raw.c_oflag and not (OPOST) + raw.c_cflag = raw.c_cflag or CS8 + raw.c_lflag = raw.c_lflag and not (ECHO or ICANON or ISIG or IEXTEN) + discard tcSetAttr(fd, TCSAFLUSH, addr raw) + +proc unblockStdin*(term: Terminal) = + if term.isatty(): + let fd = term.infile.getFileHandle() + term.orig_flags = fcntl(fd, F_GETFL, 0) + let flags = term.orig_flags or O_NONBLOCK + discard fcntl(fd, F_SETFL, flags) + term.stdin_unblocked = true + +proc restoreStdin*(term: Terminal) = + if term.stdin_unblocked: + let fd = term.infile.getFileHandle() + discard fcntl(fd, F_SETFL, term.orig_flags) + term.stdin_unblocked = false + +proc quit*(term: Terminal) = + if term.isatty(): + term.disableRawMode() + if term.smcup: + term.write(term.disableAltScreen()) + else: + term.write(term.cursorGoto(0, term.attrs.height - 1) & + term.resetFormat() & "\n") + if term.config.input.use_mouse: + term.disableMouse() + term.showCursor() + term.cleared = false + if term.stdin_unblocked: + let fd = term.infile.getFileHandle() + term.orig_flags2 = fcntl(fd, F_GETFL, 0) + discard fcntl(fd, F_SETFL, term.orig_flags2 and (not O_NONBLOCK)) + term.stdin_unblocked = false + else: + term.orig_flags2 = -1 + term.flush() + +when termcap_found: + proc loadTermcap(term: Terminal) = + assert goutfile == nil + goutfile = term.outfile + let tc = new Termcap + if tgetent(cast[cstring](addr tc.bp), cstring(term.tname)) == 1: + term.tc = tc + for id in TermcapCap: + tc.caps[id] = tgetstr(cstring($id), cast[ptr cstring](addr tc.funcstr)) + else: + raise newException(Defect, "Failed to load termcap description for terminal " & term.tname) + +type + QueryAttrs = enum + qaAnsiColor, qaRGB, qaSixel + + QueryResult = object + success: bool + attrs: set[QueryAttrs] + fgcolor: Option[RGBColor] + bgcolor: Option[RGBColor] + widthPx: int + heightPx: int + width: int + height: int + +proc queryAttrs(term: Terminal, windowOnly: bool): QueryResult = + const tcapRGB = 0x524742 # RGB supported? + if not windowOnly: + const outs = + XTGETFG & + XTGETBG & + GEOMPIXEL & + GEOMCELL & + XTGETTCAP("524742") & + DA1 + term.outfile.write(outs) + else: + const outs = + GEOMPIXEL & + GEOMCELL & + DA1 + term.outfile.write(outs) + result = QueryResult(success: false, attrs: {}) + while true: + template consume(term: Terminal): char = term.infile.readChar() + template fail = return + 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 + case (let c = term.consume; c) + of '?': # DA1 + 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 '4', '8': # GEOMPIXEL, GEOMCELL + term.expect ';' + var height = 0 + var width = 0 + while (let c = term.consume; c != ';'): + if (let x = decValue(c); x != -1): + height *= 10 + height += x + else: + fail + while (let c = term.consume; c != 't'): + if (let x = decValue(c); x != -1): + width *= 10 + width += x + else: + fail + if c == '4': # GEOMSIZE + result.widthPx = width + result.heightPx = height + if c == '8': # GEOMCELL + result.width = width + result.height = height + else: fail + 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 + +# when windowOnly, only refresh window size. +proc detectTermAttributes(term: Terminal, windowOnly: bool): TermStartResult = + result = tsrSuccess + term.tname = getEnv("TERM") + if term.tname == "": + term.tname = "dosansi" + if not term.isatty(): + return + let fd = term.infile.getFileHandle() + var win: IOctl_WinSize + if ioctl(cint(fd), TIOCGWINSZ, addr win) != -1: + term.attrs.width = int(win.ws_col) + term.attrs.height = int(win.ws_row) + term.attrs.ppc = int(win.ws_xpixel) div term.attrs.width + term.attrs.ppl = int(win.ws_ypixel) div term.attrs.height + if term.config.display.query_da1: + let r = term.queryAttrs(windowOnly) + if r.success: # DA1 success + if r.width != 0: + term.attrs.width = r.width + if r.widthPx != 0: + term.attrs.ppc = r.widthPx div r.width + if r.height != 0: + term.attrs.height = r.height + if r.heightPx != 0: + term.attrs.ppl = r.heightPx div r.height + if windowOnly: + return + 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 windowOnly: + return + 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 + 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)} + +proc windowChange*(term: Terminal) = + discard term.detectTermAttributes(windowOnly = true) + term.applyConfigDimensions() + term.canvas = newFixedGrid(term.attrs.width, term.attrs.height) + term.cleared = false + +proc start*(term: Terminal, infile: File): TermStartResult = + term.infile = infile + if term.isatty(): + term.enableRawMode() + result = term.detectTermAttributes(windowOnly = false) + if result == tsrDA1Fail: + term.config.display.query_da1 = false + if term.isatty() and term.config.input.use_mouse: + term.enableMouse() + term.applyConfig() + term.canvas = newFixedGrid(term.attrs.width, term.attrs.height) + if term.smcup: + term.write(term.enableAltScreen()) + +proc restart*(term: Terminal) = + if term.isatty(): + term.enableRawMode() + if term.orig_flags2 != -1: + let fd = term.infile.getFileHandle() + discard fcntl(fd, F_SETFL, term.orig_flags2) + term.orig_flags2 = 0 + term.stdin_unblocked = true + if term.config.input.use_mouse: + term.enableMouse() + if term.smcup: + term.write(term.enableAltScreen()) + +proc newTerminal*(outfile: File, config: Config): Terminal = + return Terminal( + outfile: outfile, + config: config, + defaultBackground: ColorsRGB["black"], + defaultForeground: ColorsRGB["white"] + ) diff --git a/src/main.nim b/src/main.nim index 9d782a90..ea5a92dd 100644 --- a/src/main.nim +++ b/src/main.nim @@ -8,9 +8,9 @@ import std/os import config/chapath import config/config -import display/term import io/serversocket import local/client +import local/term import types/opt import utils/strwidth import utils/twtstr diff --git a/src/server/buffer.nim b/src/server/buffer.nim index e4af4232..7e4b7190 100644 --- a/src/server/buffer.nim +++ b/src/server/buffer.nim @@ -19,13 +19,13 @@ import css/mediaquery import css/sheet import css/stylednode import css/values -import display/term import html/catom import html/chadombuilder import html/dom import html/enums import html/env import html/event +import html/formdata as formdata_impl import io/bufstream import io/posixstream import io/promise @@ -46,9 +46,9 @@ import types/cookie import types/formdata import types/opt import types/url +import types/winattrs import utils/strwidth import utils/twtstr -import xhr/formdata as formdata_impl from chagashi/decoder import newTextDecoder import chagashi/charset diff --git a/src/server/forkserver.nim b/src/server/forkserver.nim index 41784e8f..2c00dd4e 100644 --- a/src/server/forkserver.nim +++ b/src/server/forkserver.nim @@ -5,7 +5,6 @@ import std/streams import std/tables import config/config -import display/term import io/posixstream import io/serialize import io/serversocket @@ -13,6 +12,7 @@ import loader/loader import server/buffer import types/urimethodmap import types/url +import types/winattrs import utils/strwidth import chagashi/charset diff --git a/src/types/winattrs.nim b/src/types/winattrs.nim new file mode 100644 index 00000000..7d9e0218 --- /dev/null +++ b/src/types/winattrs.nim @@ -0,0 +1,7 @@ +type WindowAttributes* = object + width*: int + height*: int + ppc*: int # cell width + ppl*: int # cell height + width_px*: int + height_px*: int diff --git a/src/xhr/formdata.nim b/src/xhr/formdata.nim deleted file mode 100644 index bbf9a843..00000000 --- a/src/xhr/formdata.nim +++ /dev/null @@ -1,177 +0,0 @@ -import std/base64 -import std/streams - -import html/catom -import html/dom -import html/enums -import js/domexception -import js/javascript -import js/tojs -import types/blob -import types/formdata -import utils/twtstr - -import chame/tags - -proc constructEntryList*(form: HTMLFormElement, submitter: Element = nil, - encoding = "UTF-8"): seq[FormDataEntry] - -proc generateBoundary(): string = - let urandom = newFileStream("/dev/urandom") - let s = urandom.readStr(32) - urandom.close() - # 32 * 4 / 3 (padded) = 44 + prefix string is 22 bytes = 66 bytes - return "----WebKitFormBoundary" & base64.encode(s) - -proc newFormData0*(): FormData = - return FormData(boundary: generateBoundary()) - -proc newFormData*(form: HTMLFormElement = nil, - submitter: HTMLElement = nil): DOMResult[FormData] {.jsctor.} = - let this = newFormData0() - if form != nil: - if submitter != nil: - if not submitter.isSubmitButton(): - return errDOMException("Submitter must be a submit button", - "InvalidStateError") - if FormAssociatedElement(submitter).form != form: - return errDOMException("Submitter's form owner is not form", - "InvalidStateError") - if not form.constructingEntryList: - this.entries = constructEntryList(form, submitter) - return ok(this) - -#TODO filename should not be allowed for string entries -# in other words, this should be an overloaded function, not just an or type -proc append*[T: string|Blob](this: FormData, name: string, value: T, - filename = opt(string)) {.jsfunc.} = - when T is Blob: - let filename = if filename.isSome: - filename.get - elif value of WebFile: - WebFile(value).name - else: - "blob" - this.entries.add(FormDataEntry( - name: name, - isstr: false, - value: value, - filename: filename - )) - else: # string - this.entries.add(FormDataEntry( - name: name, - isstr: true, - svalue: value - )) - -proc delete(this: FormData, name: string) {.jsfunc.} = - for i in countdown(this.entries.high, 0): - if this.entries[i].name == name: - this.entries.delete(i) - -proc get(ctx: JSContext, this: FormData, name: string): JSValue {.jsfunc.} = - for entry in this.entries: - if entry.name == name: - if entry.isstr: - return toJS(ctx, entry.svalue) - else: - return toJS(ctx, entry.value) - return JS_NULL - -proc getAll(ctx: JSContext, this: FormData, name: string): seq[JSValue] - {.jsfunc.} = - for entry in this.entries: - if entry.name == name: - if entry.isstr: - result.add(toJS(ctx, entry.svalue)) - else: - result.add(toJS(ctx, entry.value)) - -proc add(list: var seq[FormDataEntry], entry: tuple[name, value: string]) = - list.add(FormDataEntry( - name: entry.name, - isstr: true, - svalue: entry.value - )) - -func toNameValuePairs*(list: seq[FormDataEntry]): - seq[tuple[name, value: string]] = - for entry in list: - if entry.isstr: - result.add((entry.name, entry.svalue)) - else: - result.add((entry.name, entry.name)) - -# https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#constructing-the-form-data-set -# Warning: we skip the first "constructing entry list" check; the caller must -# do it. -proc constructEntryList*(form: HTMLFormElement, submitter: Element = nil, - encoding = "UTF-8"): seq[FormDataEntry] = - assert not form.constructingEntryList - form.constructingEntryList = true - var entrylist: seq[FormDataEntry] = @[] - for field in form.controls: - if field.findAncestor({TAG_DATALIST}) != nil or - field.attrb(atDisabled) or - field.isButton() and Element(field) != submitter: - continue - if field of HTMLInputElement: - let field = HTMLInputElement(field) - if field.inputType in {INPUT_CHECKBOX, INPUT_RADIO} and not field.checked: - continue - if field.inputType == INPUT_IMAGE: - var name = field.attr(atName) - if name != "": - name &= '.' - entrylist.add((name & 'x', $field.xcoord)) - entrylist.add((name & 'y', $field.ycoord)) - continue - #TODO custom elements - let name = field.attr(atName) - if name == "": - continue - if field of HTMLSelectElement: - let field = HTMLSelectElement(field) - for option in field.options: - if option.selected and not option.isDisabled: - entrylist.add((name, option.value)) - elif field of HTMLInputElement: - let field = HTMLInputElement(field) - case field.inputType - of INPUT_CHECKBOX, INPUT_RADIO: - let v = field.attr(atValue) - let value = if v != "": - v - else: - "on" - entrylist.add((name, value)) - of INPUT_FILE: - #TODO file - discard - of INPUT_HIDDEN: - if name.equalsIgnoreCase("_charset_"): - entrylist.add((name, encoding)) - else: - entrylist.add((name, field.value)) - else: - entrylist.add((name, field.value)) - elif field of HTMLButtonElement: - entrylist.add((name, HTMLButtonElement(field).value)) - elif field of HTMLTextAreaElement: - entrylist.add((name, HTMLTextAreaElement(field).value)) - else: - assert false, "Tag type " & $field.tagType & - " not accounted for in constructEntryList" - if field of HTMLTextAreaElement or - field of HTMLInputElement and - HTMLInputElement(field).inputType in AutoDirInput: - let dirname = field.attr(atDirname) - if dirname != "": - let dir = "ltr" #TODO bidi - entrylist.add((dirname, dir)) - form.constructingEntryList = false - return entrylist - -proc addFormDataModule*(ctx: JSContext) = - ctx.registerType(FormData) diff --git a/src/xhr/xmlhttprequest.nim b/src/xhr/xmlhttprequest.nim deleted file mode 100644 index e9905760..00000000 --- a/src/xhr/xmlhttprequest.nim +++ /dev/null @@ -1,118 +0,0 @@ -import std/options -import std/strutils - -import bindings/quickjs -import html/dom -import html/event -import js/domexception -import js/fromjs -import js/javascript -import loader/headers -import loader/request -import loader/response -import types/url - -type - XMLHttpRequestResponseType = enum - TYPE_UNKNOWN = "" - TYPE_ARRAYBUFFER = "arraybuffer" - TYPE_BLOB = "blob" - TYPE_DOCUMENT = "document" - TYPE_JSON = "json" - TYPE_TEXT = "text" - - XMLHttpRequestState = enum - UNSENT = 0u16 - OPENED = 1u16 - HEADERS_RECEIVED = 2u16 - LOADING = 3u16 - DONE = 4u16 - - XMLHttpRequestFlag = enum - SEND_FLAG, UPLOAD_LISTENER_FLAG, SYNC_FLAG - - XMLHttpRequestEventTarget = ref object of EventTarget - onloadstart {.jsgetset.}: EventHandler - onprogress {.jsgetset.}: EventHandler - onabort {.jsgetset.}: EventHandler - onerror {.jsgetset.}: EventHandler - onload {.jsgetset.}: EventHandler - ontimeout {.jsgetset.}: EventHandler - onloadend {.jsgetset.}: EventHandler - - XMLHttpRequestUpload = ref object of XMLHttpRequestEventTarget - - XMLHttpRequest = ref object of XMLHttpRequestEventTarget - onreadystatechange {.jsgetset.}: EventHandler - readyState: XMLHttpRequestState - upload {.jsget.}: XMLHttpRequestUpload - flags: set[XMLHttpRequestFlag] - requestMethod: HttpMethod - requestURL: URL - authorRequestHeaders: Headers - response: Response - responseType {.jsgetset.}: XMLHttpRequestResponseType - -jsDestructor(XMLHttpRequestEventTarget) -jsDestructor(XMLHttpRequestUpload) -jsDestructor(XMLHttpRequest) - -func newXMLHttpRequest(): XMLHttpRequest {.jsctor.} = - let upload = XMLHttpRequestUpload() - return XMLHttpRequest( - upload: upload, - authorRequestHeaders: newHeaders() - ) - -func readyState(this: XMLHttpRequest): uint16 {.jsfget.} = - return uint16(this.readyState) - -proc parseMethod(s: string): DOMResult[HttpMethod] = - return case s.toLowerAscii() - of "get": ok(HTTP_GET) - of "delete": ok(HTTP_DELETE) - of "head": ok(HTTP_HEAD) - of "options": ok(HTTP_OPTIONS) - of "patch": ok(HTTP_PATCH) - of "post": ok(HTTP_POST) - of "put": ok(HTTP_PUT) - of "connect", "trace", "track": - errDOMException("Forbidden method", "SecurityError") - else: - errDOMException("Invalid method", "SyntaxError") - -proc open(ctx: JSContext, this: XMLHttpRequest, httpMethod, url: string): - Err[DOMException] {.jsfunc.} = - let httpMethod = ?parseMethod(httpMethod) - let global = JS_GetGlobalObject(ctx) - let window = fromJS[Window](ctx, global) - JS_FreeValue(ctx, global) - let x = if window.isSome: - parseURL(url, some(window.get.document.baseURL)) - else: - parseURL(url) - if x.isNone: - return errDOMException("Invalid URL", "SyntaxError") - let parsedURL = x.get - #TODO async, username, password arguments - let async = true - #TODO if async is false... probably just throw. - #TODO terminate fetch controller - this.flags.excl(SEND_FLAG) - this.flags.excl(UPLOAD_LISTENER_FLAG) - if async: - this.flags.excl(SYNC_FLAG) - else: - this.flags.incl(SYNC_FLAG) - this.requestMethod = httpMethod - this.authorRequestHeaders = newHeaders() - this.response = makeNetworkError() - this.requestURL = parsedURL - return ok() - -proc addXMLHttpRequestModule*(ctx: JSContext) = - let eventTargetCID = ctx.getClass("EventTarget") - let xhretCID = ctx.registerType(XMLHttpRequestEventTarget, eventTargetCID) - ctx.registerType(XMLHttpRequestUpload, xhretCID) - let xhrCID = ctx.registerType(XMLHttpRequest, xhretCID) - ctx.defineConsts(xhrCID, XMLHttpRequestState, uint16) -- cgit 1.4.1-2-gfad0