diff options
author | bptato <nincsnevem662@gmail.com> | 2022-11-28 19:52:10 +0100 |
---|---|---|
committer | bptato <nincsnevem662@gmail.com> | 2022-11-28 23:00:06 +0100 |
commit | eb2e57c97eb67eec19f068e294a8f6d1375c82f5 (patch) | |
tree | 87156c515f6ee9a63f58dc080184bd3127ce6836 | |
parent | 8af10b8b74fd29fe4c9debcd5cbecfaddf53a7b5 (diff) | |
download | chawan-eb2e57c97eb67eec19f068e294a8f6d1375c82f5.tar.gz |
Add textarea
Editing is implemented using an external editor (like vi).
-rw-r--r-- | res/config.toml | 4 | ||||
-rw-r--r-- | res/ua.css | 5 | ||||
-rw-r--r-- | src/buffer/buffer.nim | 98 | ||||
-rw-r--r-- | src/buffer/container.nim | 49 | ||||
-rw-r--r-- | src/config/bufferconfig.nim | 7 | ||||
-rw-r--r-- | src/config/config.nim | 26 | ||||
-rw-r--r-- | src/css/cascade.nim | 23 | ||||
-rw-r--r-- | src/css/selectorparser.nim | 2 | ||||
-rw-r--r-- | src/display/client.nim | 2 | ||||
-rw-r--r-- | src/display/pager.nim | 11 | ||||
-rw-r--r-- | src/display/term.nim (renamed from src/io/term.nim) | 13 | ||||
-rw-r--r-- | src/html/dom.nim | 157 | ||||
-rw-r--r-- | src/html/htmlparser.nim | 74 | ||||
-rw-r--r-- | src/html/tags.nim | 2 | ||||
-rw-r--r-- | src/io/lineedit.nim | 2 | ||||
-rw-r--r-- | src/ips/editor.nim | 59 | ||||
-rw-r--r-- | src/ips/forkserver.nim | 16 | ||||
-rw-r--r-- | src/ips/serialize.nim | 17 | ||||
-rw-r--r-- | src/ips/serversocket.nim | 8 | ||||
-rw-r--r-- | src/ips/socketstream.nim | 2 | ||||
-rw-r--r-- | src/main.nim | 24 |
21 files changed, 411 insertions, 190 deletions
diff --git a/res/config.toml b/res/config.toml index b9a37d75..f4cbba2a 100644 --- a/res/config.toml +++ b/res/config.toml @@ -1,3 +1,7 @@ +[external] +tmpdir = "/tmp/cha" +editor = "vi %s +%d" + [display] color-mode = "auto" format-mode = "auto" diff --git a/res/ua.css b/res/ua.css index 6284f855..1e5acd84 100644 --- a/res/ua.css +++ b/res/ua.css @@ -125,6 +125,11 @@ button { color: red; } +textarea { + color: red; + white-space: pre; +} + li { display: list-item; } diff --git a/src/buffer/buffer.nim b/src/buffer/buffer.nim index 2bc67d7c..d7061ce8 100644 --- a/src/buffer/buffer.nim +++ b/src/buffer/buffer.nim @@ -17,7 +17,7 @@ import css/cssparser import css/mediaquery import css/sheet import css/stylednode -import config/bufferconfig +import config/config import html/dom import html/tags import html/htmlparser @@ -56,7 +56,6 @@ type lasttimeout: int timeout: int readbufsize: int - input: HTMLInputElement contenttype: string lines: FlexibleGrid rendered: bool @@ -239,7 +238,7 @@ func getLink(node: StyledNode): HTMLAnchorElement = #TODO ::before links? const ClickableElements = { - TAG_A, TAG_INPUT, TAG_OPTION, TAG_BUTTON + TAG_A, TAG_INPUT, TAG_OPTION, TAG_BUTTON, TAG_TEXTAREA } func getClickable(styledNode: StyledNode): Element = @@ -267,6 +266,7 @@ func cursorBytes(buffer: Buffer, y: int, cc: int): int = return i proc findPrevLink*(buffer: Buffer, cursorx, cursory: int): tuple[x, y: int] {.proxy.} = + if cursory >= buffer.lines.len: return (-1, -1) let line = buffer.lines[cursory] var i = line.findFormatN(cursorx) - 1 var link: Element = nil @@ -324,6 +324,7 @@ proc findPrevLink*(buffer: Buffer, cursorx, cursory: int): tuple[x, y: int] {.pr return (-1, -1) proc findNextLink*(buffer: Buffer, cursorx, cursory: int): tuple[x, y: int] {.proxy.} = + if cursory >= buffer.lines.len: return (-1, -1) let line = buffer.lines[cursory] var i = line.findFormatN(cursorx) - 1 var link: Element = nil @@ -350,6 +351,7 @@ proc findNextLink*(buffer: Buffer, cursorx, cursory: int): tuple[x, y: int] {.pr return (-1, -1) proc findPrevMatch*(buffer: Buffer, regex: Regex, cursorx, cursory: int, wrap: bool): BufferMatch {.proxy.} = + if cursory >= buffer.lines.len: return template return_if_match = if res.success and res.captures.len > 0: let cap = res.captures[^1] @@ -377,6 +379,7 @@ proc findPrevMatch*(buffer: Buffer, regex: Regex, cursorx, cursory: int, wrap: b dec y proc findNextMatch*(buffer: Buffer, regex: Regex, cursorx, cursory: int, wrap: bool): BufferMatch {.proxy.} = + if cursory >= buffer.lines.len: return template return_if_match = if res.success and res.captures.len > 0: let cap = res.captures[0] @@ -471,6 +474,7 @@ proc updateHover*(buffer: Buffer, cursorx, cursory: int): UpdateHoverResult {.pr buffer.prevnode = thisnode proc loadResource(buffer: Buffer, document: Document, elem: HTMLLinkElement) = + if elem.href == "": return let url = parseUrl(elem.href, document.location.some) if url.isSome: let url = url.get @@ -514,8 +518,7 @@ proc setupSource(buffer: Buffer): ConnectResult = buffer.location = source.location case source.t of CLONE: - buffer.istream = connectSocketStream(source.clonepid) - SocketStream(buffer.istream).source.getFd().setBlocking(false) + buffer.istream = connectSocketStream(source.clonepid, blocking = false) if buffer.istream == nil: result.code = -2 return @@ -574,7 +577,7 @@ proc load*(buffer: Buffer): tuple[atend: bool, lines, bytes: int] {.proxy.} = var s = newString(buffer.readbufsize) try: buffer.istream.readStr(buffer.readbufsize, s) - result = (s.len < buffer.readbufsize, buffer.lines.len, bytes) + result = (s.len == 0, buffer.lines.len, bytes) if buffer.readbufsize < BufferSize: buffer.readbufsize = min(BufferSize, buffer.readbufsize * 2) except IOError: @@ -671,12 +674,14 @@ proc constructEntryList(form: HTMLFormElement, submitter: Element = nil, encodin "UTF-8" entrylist.add((name, charset)) else: - if field.tagType == TAG_INPUT: + case field.tagType + of TAG_INPUT: entrylist.add((name, HTMLInputElement(field).value)) - elif field.tagType == TAG_BUTTON: + of TAG_BUTTON: entrylist.add((name, HTMLButtonElement(field).value)) - else: - assert false + of TAG_TEXTAREA: + entrylist.add((name, HTMLTextAreaElement(field).value)) + else: assert false, "Tag type " & $field.tagType & " not accounted for in constructEntryList" if field.tagType == TAG_TEXTAREA or field.tagType == TAG_INPUT and HTMLInputElement(field).inputType in {INPUT_TEXT, INPUT_SEARCH}: if field.attr("dirname") != "": @@ -803,39 +808,46 @@ type ReadSuccessResult* = object open*: Option[Request] repaint*: bool +proc implicitSubmit(buffer: Buffer, input: HTMLInputElement): Option[Request] = + if input.form != nil and input.form.canSubmitImplicitly(): + return submitForm(input.form, input.form) + proc readSuccess*(buffer: Buffer, s: string): ReadSuccessResult {.proxy.} = - if buffer.input != nil: - let input = buffer.input - case input.inputType - of INPUT_SEARCH: - input.value = s - input.invalid = true - buffer.do_reshape() - result.repaint = true - if input.form != nil: - let submitaction = submitForm(input.form, input) - if submitaction.isSome: - result.open = submitaction - of INPUT_TEXT, INPUT_PASSWORD: - input.value = s - input.invalid = true - buffer.do_reshape() - result.repaint = true - of INPUT_FILE: - let cdir = parseUrl("file://" & getCurrentDir() & DirSep) - let path = parseUrl(s, cdir) - if path.issome: - input.file = path + if buffer.document.focus != nil: + case buffer.document.focus.tagType + of TAG_INPUT: + let input = HTMLInputElement(buffer.document.focus) + case input.inputType + of INPUT_SEARCH, INPUT_TEXT, INPUT_PASSWORD: + input.value = s input.invalid = true buffer.do_reshape() result.repaint = true + result.open = buffer.implicitSubmit(input) + of INPUT_FILE: + let cdir = parseUrl("file://" & getCurrentDir() & DirSep) + let path = parseUrl(s, cdir) + if path.issome: + input.file = path + input.invalid = true + buffer.do_reshape() + result.repaint = true + result.open = buffer.implicitSubmit(input) + else: discard + of TAG_TEXTAREA: + let textarea = HTMLTextAreaElement(buffer.document.focus) + textarea.value = s + textarea.invalid = true + buffer.do_reshape() + result.repaint = true else: discard - buffer.input = nil + buffer.restore_focus type ReadLineResult* = object prompt*: string value*: string hide*: bool + area*: bool type ClickResult* = object open*: Option[Request] @@ -848,6 +860,7 @@ proc click*(buffer: Buffer, cursorx, cursory: int): ClickResult {.proxy.} = case clickable.tagType of TAG_SELECT: buffer.set_focus clickable + result.repaint = true of TAG_A: buffer.restore_focus let url = parseUrl(HTMLAnchorElement(clickable).href, clickable.document.baseUrl.some) @@ -877,24 +890,32 @@ proc click*(buffer: Buffer, cursorx, cursory: int): ClickResult {.proxy.} = result.repaint = true buffer.do_reshape() of BUTTON_BUTTON: discard + of TAG_TEXTAREA: + buffer.set_focus clickable + let textarea = HTMLTextAreaElement(clickable) + result.readline = some(ReadLineResult( + value: textarea.value, + area: true + )) of TAG_INPUT: buffer.restore_focus let input = HTMLInputElement(clickable) case input.inputType of INPUT_SEARCH: - buffer.input = input + buffer.set_focus input result.readline = some(ReadLineResult( prompt: "SEARCH: ", value: input.value )) of INPUT_TEXT, INPUT_PASSWORD: - buffer.input = input + buffer.set_focus input result.readline = some(ReadLineResult( prompt: "TEXT: ", value: input.value, hide: input.inputType == INPUT_PASSWORD )) of INPUT_FILE: + buffer.set_focus input var path = if input.file.issome: input.file.get.path.serialize_unicode() else: @@ -930,12 +951,12 @@ proc click*(buffer: Buffer, cursorx, cursory: int): ClickResult {.proxy.} = buffer.restore_focus proc readCanceled*(buffer: Buffer) {.proxy.} = - buffer.input = nil + buffer.restore_focus proc findAnchor*(buffer: Buffer, anchor: string): bool {.proxy.} = return buffer.document != nil and buffer.document.getElementById(anchor) != nil -proc getLines*(buffer: Buffer, w: Slice[int]): seq[SimpleFlexibleLine] {.proxy.} = +proc getLines*(buffer: Buffer, w: Slice[int]): tuple[numLines: int, lines: seq[SimpleFlexibleLine]] {.proxy.} = var w = w if w.b < 0 or w.b > buffer.lines.high: w.b = buffer.lines.high @@ -944,7 +965,8 @@ proc getLines*(buffer: Buffer, w: Slice[int]): seq[SimpleFlexibleLine] {.proxy.} var line = SimpleFlexibleLine(str: buffer.lines[y].str) for f in buffer.lines[y].formats: line.formats.add(SimpleFormatCell(format: f.format, pos: f.pos)) - result.add(line) + result.lines.add(line) + result.numLines = buffer.lines.len proc passFd*(buffer: Buffer) {.proxy.} = let fd = SocketStream(buffer.pistream).recvFileHandle() diff --git a/src/buffer/container.nim b/src/buffer/container.nim index 65b1ae59..9a0ff802 100644 --- a/src/buffer/container.nim +++ b/src/buffer/container.nim @@ -8,7 +8,6 @@ when defined(posix): import buffer/buffer import buffer/cell -import config/bufferconfig import config/config import io/request import io/window @@ -33,7 +32,7 @@ type ContainerEventType* = enum NO_EVENT, FAIL, SUCCESS, NEEDS_AUTH, REDIRECT, ANCHOR, NO_ANCHOR, UPDATE, - READ_LINE, OPEN, INVALID_COMMAND, STATUS, ALERT + READ_LINE, READ_AREA, OPEN, INVALID_COMMAND, STATUS, ALERT ContainerEvent* = object case t*: ContainerEventType @@ -41,6 +40,8 @@ type prompt*: string value*: string password*: bool + of READ_AREA: + tvalue*: string of OPEN: request*: Request of ANCHOR, NO_ANCHOR: @@ -71,7 +72,7 @@ type bpos: seq[CursorPosition] highlights: seq[Highlight] parent*: Container - process*: Pid + process* {.jsget.}: Pid loadinfo*: string lines: SimpleFlexibleGrid lineshift: int @@ -93,7 +94,7 @@ proc newBuffer*(dispatcher: Dispatcher, config: Config, source: BufferSource, ti let istream = dispatcher.forkserver.istream ostream.swrite(FORK_BUFFER) ostream.swrite(source) - ostream.swrite(config.loadBufferConfig()) + ostream.swrite(config.getBufferConfig()) ostream.swrite(attrs) ostream.swrite(dispatcher.mainproc) ostream.flush() @@ -252,13 +253,17 @@ proc triggerEvent(container: Container, t: ContainerEventType) = proc updateCursor(container: Container) +proc setNumLines(container: Container, lines: int) = + container.numLines = lines + container.updateCursor() + proc requestLines*(container: Container, w = container.lineWindow) = - container.iface.getLines(w).then(proc(res: seq[SimpleFlexibleLine]) = + container.iface.getLines(w).then(proc(res: tuple[numLines: int, lines: seq[SimpleFlexibleLine]]) = container.lines.setLen(w.len) container.lineshift = w.a - for y in 0 ..< min(res.len, w.len): - container.lines[y] = res[y] - container.updateCursor() + for y in 0 ..< min(res.lines.len, w.len): + container.lines[y] = res.lines[y] + container.setNumLines(res.numLines) container.redraw = true let cw = container.fromy ..< container.fromy + container.height if w.a in cw or w.b in cw or cw.a in w or cw.b in w: @@ -544,8 +549,8 @@ proc cursorNextMatch*(container: Container, regex: Regex, wrap: bool) {.jsfunc.} container.iface .findNextMatch(regex, container.cursorx, container.cursory, wrap) .then(proc(res: BufferMatch) = + container.setCursorXY(res.x, res.y) if container.hlon: - container.setCursorXY(res.x, res.y) container.clearSearchHighlights() let ex = res.x + res.str.width() - 1 let hl = Highlight(x: res.x, y: res.y, endx: ex, endy: res.y, clear: true) @@ -556,8 +561,8 @@ proc cursorPrevMatch*(container: Container, regex: Regex, wrap: bool) {.jsfunc.} container.iface .findPrevMatch(regex, container.cursorx, container.cursory, wrap) .then(proc(res: BufferMatch) = + container.setCursorXY(res.x, res.y) if container.hlon: - container.setCursorXY(res.x, res.y) container.clearSearchHighlights() let ex = res.x + res.str.width() - 1 let hl = Highlight(x: res.x, y: res.y, endx: ex, endy: res.y, clear: true) @@ -568,10 +573,6 @@ proc setLoadInfo(container: Container, msg: string) = container.loadinfo = msg container.triggerEvent(STATUS) -proc setNumLines(container: Container, lines: int) = - container.numLines = lines - container.updateCursor() - proc alert(container: Container, msg: string) = container.triggerEvent(ContainerEvent(t: ALERT, msg: msg)) @@ -677,12 +678,20 @@ proc click*(container: Container) {.jsfunc.} = container.triggerEvent(ContainerEvent(t: OPEN, request: res.open.get)) if res.readline.isSome: let rl = res.readline.get - container.triggerEvent( - ContainerEvent( - t: READ_LINE, - prompt: rl.prompt, - value: rl.value, - password: rl.hide))) + if rl.area: + container.triggerEvent( + ContainerEvent( + t: READ_AREA, + tvalue: rl.value + )) + else: + container.triggerEvent( + ContainerEvent( + t: READ_LINE, + prompt: rl.prompt, + value: rl.value, + password: rl.hide + ))) proc windowChange*(container: Container, attrs: WindowAttributes) = container.attrs = attrs diff --git a/src/config/bufferconfig.nim b/src/config/bufferconfig.nim deleted file mode 100644 index 78f3b87e..00000000 --- a/src/config/bufferconfig.nim +++ /dev/null @@ -1,7 +0,0 @@ -import config/config - -type BufferConfig* = object - userstyle*: string - -proc loadBufferConfig*(config: Config): BufferConfig = - result.userstyle = config.stylesheet diff --git a/src/config/config.nim b/src/config/config.nim index ff4f0b45..316e6d33 100644 --- a/src/config/config.nim +++ b/src/config/config.nim @@ -11,8 +11,11 @@ import utils/twtstr type ColorMode* = enum MONOCHROME, ANSI, EIGHT_BIT, TRUE_COLOR + FormatMode* = set[FormatFlags] + ActionMap = Table[string, string] + Config* = ref ConfigObj ConfigObj* = object nmap*: ActionMap @@ -26,6 +29,24 @@ type formatmode*: Option[FormatMode] altscreen*: Option[bool] mincontrast*: float + editor*: string + tmpdir*: string + + BufferConfig* = object + userstyle*: string + + ForkServerConfig* = object + tmpdir*: string + ambiguous_double*: bool + +func getForkServerConfig*(config: Config): ForkServerConfig = + return ForkServerConfig( + tmpdir: config.tmpdir, + ambiguous_double: config.ambiguous_double + ) + +func getBufferConfig*(config: Config): BufferConfig = + result.userstyle = config.stylesheet func getRealKey(key: string): string = var realk: string @@ -164,6 +185,11 @@ proc parseConfig(config: Config, dir: string, t: TomlValue) = config.mincontrast = float(v.i) else: config.mincontrast = float(v.f) + of "external": + for k, v in v: + case k + of "editor": config.editor = v.s + of "tmpdir": config.tmpdir = v.s proc parseConfig(config: Config, dir: string, stream: Stream) = config.parseConfig(dir, parseToml(stream)) diff --git a/src/css/cascade.nim b/src/css/cascade.nim index 851e6f83..273ba4cb 100644 --- a/src/css/cascade.nim +++ b/src/css/cascade.nim @@ -109,6 +109,10 @@ func calcPresentationalHints(element: Element): CSSComputedValues = map_width of TAG_BODY: map_bgcolor + of TAG_TEXTAREA: + let textarea = HTMLTextAreaElement(element) + set_cv(PROPERTY_WIDTH, length, CSSLength(unit: UNIT_CH, num: float64(textarea.cols))) + set_cv(PROPERTY_HEIGHT, length, CSSLength(unit: UNIT_EM, num: float64(textarea.rows))) else: discard proc applyDeclarations(styledNode: StyledNode, parent: CSSComputedValues, ua, user: DeclarationList, author: seq[DeclarationList]) = @@ -225,6 +229,12 @@ proc applyRules(document: Document, ua, user: CSSStylesheet, cachedTree: StyledN let styledText = styledParent.newStyledText(content) styledText.pseudo = pseudo styledParent.children.add(styledText) + of PSEUDO_TEXTAREA_TEXT: + let content = HTMLTextAreaElement(styledParent.node).textAreaString() + if content.len > 0: + let styledText = styledParent.newStyledText(content) + styledText.pseudo = pseudo + styledParent.children.add(styledText) of PSEUDO_NONE: discard else: assert child != nil @@ -299,12 +309,15 @@ proc applyRules(document: Document, ua, user: CSSStylesheet, cachedTree: StyledN stack_append styledChild, PSEUDO_AFTER - for i in countdown(elem.childNodes.high, 0): - if elem.childNodes[i].nodeType in {ELEMENT_NODE, TEXT_NODE}: - stack_append styledChild, elem.childNodes[i] + if elem.tagType != TAG_TEXTAREA: + for i in countdown(elem.childNodes.high, 0): + if elem.childNodes[i].nodeType in {ELEMENT_NODE, TEXT_NODE}: + stack_append styledChild, elem.childNodes[i] + if elem.tagType == TAG_INPUT: + stack_append styledChild, PSEUDO_INPUT_TEXT + else: + stack_append styledChild, PSEUDO_TEXTAREA_TEXT - if elem.tagType == TAG_INPUT: - stack_append styledChild, PSEUDO_INPUT_TEXT stack_append styledChild, PSEUDO_BEFORE proc applyStylesheets*(document: Document, uass, userss: CSSStylesheet, previousStyled: StyledNode): StyledNode = diff --git a/src/css/selectorparser.nim b/src/css/selectorparser.nim index f8abc7f7..97c6f029 100644 --- a/src/css/selectorparser.nim +++ b/src/css/selectorparser.nim @@ -19,7 +19,7 @@ type PseudoElem* = enum PSEUDO_NONE, PSEUDO_BEFORE, PSEUDO_AFTER, # internal - PSEUDO_INPUT_TEXT + PSEUDO_INPUT_TEXT, PSEUDO_TEXTAREA_TEXT PseudoClass* = enum PSEUDO_FIRST_CHILD, PSEUDO_LAST_CHILD, PSEUDO_ONLY_CHILD, PSEUDO_HOVER, diff --git a/src/display/client.nim b/src/display/client.nim index 3b56373e..34477c1a 100644 --- a/src/display/client.nim +++ b/src/display/client.nim @@ -17,12 +17,12 @@ import buffer/container import css/sheet import config/config import display/pager +import display/term import html/dom import html/htmlparser import io/lineedit import io/loader import io/request -import io/term import io/window import ips/forkserver import ips/serialize diff --git a/src/display/pager.nim b/src/display/pager.nim index b280d906..6d684b26 100644 --- a/src/display/pager.nim +++ b/src/display/pager.nim @@ -11,10 +11,11 @@ when defined(posix): import buffer/cell import buffer/container import config/config +import display/term import io/lineedit import io/request -import io/term import io/window +import ips/editor import ips/forkserver import ips/socketstream import js/javascript @@ -629,6 +630,14 @@ proc handleEvent0(pager: Pager, container: Container, event: ContainerEvent): bo of READ_LINE: if container == pager.container: pager.setLineEdit(readLine(event.prompt, pager.attrs.width, current = event.value, hide = event.password, term = pager.term), BUFFER) + of READ_AREA: + if container == pager.container: + var s = event.tvalue + if openInEditor(pager.term, pager.config, s): + pager.container.readSuccess(s) + else: + pager.container.readCanceled() + pager.redraw = true of OPEN: pager.gotoURL(event.request, some(container.source.location)) of INVALID_COMMAND: discard diff --git a/src/io/term.nim b/src/display/term.nim index 7d3786e0..ad9368d3 100644 --- a/src/io/term.nim +++ b/src/display/term.nim @@ -401,8 +401,9 @@ proc quit*(term: Terminal) = term.write(term.disableAltScreen()) else: term.write(term.cursorGoto(0, term.attrs.height - 1)) - term.outfile.showCursor() - term.outfile.flushFile() + term.showCursor() + term.cleared = false + term.flush() when termcap_found: proc loadTermcap(term: Terminal) = @@ -451,14 +452,18 @@ proc detectTermAttributes(term: Terminal) = proc start*(term: Terminal, infile: File) = term.infile = infile - assert term.outfile.getFileHandle().setInheritable(false) - assert term.infile.getFileHandle().setInheritable(false) if term.isatty(): enableRawMode(infile.getFileHandle()) term.detectTermAttributes() if term.smcup: term.write(term.enableAltScreen()) +proc restart*(term: Terminal) = + if term.isatty(): + enableRawMode(term.infile.getFileHandle()) + if term.smcup: + term.write(term.enableAltScreen()) + proc newTerminal*(outfile: File, config: Config, attrs: WindowAttributes): Terminal = let term = new Terminal term.outfile = outfile diff --git a/src/html/dom.nim b/src/html/dom.nim index be66dc51..ab3b95be 100644 --- a/src/html/dom.nim +++ b/src/html/dom.nim @@ -1,3 +1,4 @@ +import macros import options import streams import strutils @@ -91,10 +92,10 @@ type HTMLElement* = ref object of Element FormAssociatedElement* = ref object of HTMLElement - form*: HTMLFormElement parserInserted*: bool HTMLInputElement* = ref object of FormAssociatedElement + form* {.jsget.}: HTMLFormElement inputType*: InputType autofocus*: bool required*: bool @@ -108,6 +109,7 @@ type HTMLAnchorElement* = ref object of HTMLElement HTMLSelectElement* = ref object of FormAssociatedElement + form* {.jsget.}: HTMLFormElement size*: int HTMLSpanElement* = ref object of HTMLElement @@ -169,8 +171,15 @@ type HTMLAreaElement* = ref object of HTMLElement HTMLButtonElement* = ref object of FormAssociatedElement + form* {.jsget.}: HTMLFormElement ctype*: ButtonType - value*: string + value* {.jsget, jsset.}: string + + HTMLTextAreaElement* = ref object of FormAssociatedElement + form* {.jsget.}: HTMLFormElement + rows*: int + cols*: int + value* {.jsget.}: string proc tostr(ftype: enum): string = return ($ftype).split('_')[1..^1].join("-").tolower() @@ -312,6 +321,39 @@ iterator options*(select: HTMLSelectElement): HTMLOptionElement {.inline.} = if opt.tagType == TAG_OPTION: yield HTMLOptionElement(child) +func form*(element: FormAssociatedElement): HTMLFormElement = + case element.tagType + of TAG_INPUT: return HTMLInputElement(element).form + of TAG_SELECT: return HTMLSelectElement(element).form + of TAG_BUTTON: return HTMLButtonElement(element).form + of TAG_TEXTAREA: return HTMLTextAreaElement(element).form + else: assert false + +func `form=`*(element: FormAssociatedElement, form: HTMLFormElement) = + case element.tagType + of TAG_INPUT: HTMLInputElement(element).form = form + of TAG_SELECT: HTMLSelectElement(element).form = form + of TAG_BUTTON: HTMLButtonElement(element).form = form + of TAG_TEXTAREA: HTMLTextAreaElement(element).form = form + else: assert false + +func canSubmitImplicitly*(form: HTMLFormElement): bool = + const BlocksImplicitSubmission = { + INPUT_TEXT, INPUT_SEARCH, INPUT_URL, INPUT_TEL, INPUT_EMAIL, INPUT_PASSWORD, + INPUT_DATE, INPUT_MONTH, INPUT_WEEK, INPUT_TIME, INPUT_DATETIME_LOCAL, + INPUT_NUMBER + } + var found = false + for control in form.controls: + if control.tagType == TAG_INPUT: + let input = HTMLInputElement(control) + if input.inputType in BlocksImplicitSubmission: + if found: + return false + else: + found = true + return true + func qualifiedName*(element: Element): string = if element.namespacePrefix.issome: element.namespacePrefix.get & ':' & element.localName else: element.localName @@ -441,7 +483,7 @@ func nextElementSibling*(elem: Element): Element = inc i return nil -func attr*(element: Element, s: string): string = +func attr*(element: Element, s: string): string {.inline.} = return element.attributes.getOrDefault(s, "") func attri*(element: Element, s: string): Option[int] = @@ -492,7 +534,7 @@ proc sheets*(element: Element): seq[CSSStylesheet] = result.add(child.sheet) func inputString*(input: HTMLInputElement): string = - var text = case input.inputType + case input.inputType of INPUT_CHECKBOX, INPUT_RADIO: if input.checked: "*" else: " " @@ -510,7 +552,17 @@ func inputString*(input: HTMLInputElement): string = if input.file.isnone: "".padToWidth(input.size) else: input.file.get.path.serialize_unicode().padToWidth(input.size) else: input.value - return text + +func textAreaString*(textarea: HTMLTextAreaElement): string = + let split = textarea.value.split('\n') + for i in 0 ..< textarea.rows: + if textarea.cols > 2: + if i < split.len: + result &= '[' & split[i].padToWidth(textarea.cols - 2) & "]\n" + else: + result &= '[' & ' '.repeat(textarea.cols - 2) & "]\n" + else: + result &= "[]\n" func isButton*(element: Element): bool = if element.tagType == TAG_BUTTON: @@ -537,6 +589,8 @@ func action*(element: Element): string = if element.form != nil: if element.form.attrb("action"): return element.form.attr("action") + if element.tagType == TAG_FORM: + return element.attr("action") return "" func enctype*(element: Element): FormEncodingType = @@ -655,6 +709,8 @@ func newHTMLElement*(document: Document, tagType: TagType, namespace = Namespace result = new(HTMLBaseElement) of TAG_BUTTON: result = new(HTMLButtonElement) + of TAG_TEXTAREA: + result = new(HTMLTextAreaElement) else: result = new(HTMLElement) @@ -886,6 +942,10 @@ proc resetElement*(element: Element) = if option.selected: option.selected = false inc j + of TAG_TEXTAREA: + let textarea = HTMLTextAreaElement(element) + textarea.value = textarea.childTextContent() + textarea.invalid = true else: discard proc setForm*(element: FormAssociatedElement, form: HTMLFormElement) = @@ -902,7 +962,11 @@ proc setForm*(element: FormAssociatedElement, form: HTMLFormElement) = let button = HTMLButtonElement(element) button.form = form form.controls.add(button) - of TAG_FIELDSET, TAG_OBJECT, TAG_OUTPUT, TAG_TEXTAREA, TAG_IMG: + of TAG_TEXTAREA: + let textarea = HTMLTextAreaElement(element) + textarea.form = form + form.controls.add(textarea) + of TAG_FIELDSET, TAG_OBJECT, TAG_OUTPUT, TAG_IMG: discard #TODO else: assert false @@ -935,9 +999,7 @@ proc insertionSteps(insertedNode: Node) = if select != nil: select.resetElement() else: discard - if tagType in FormAssociatedElements: - if tagType notin SupportedFormAssociatedElements: - return #TODO TODO TODO implement others too + if tagType in SupportedFormAssociatedElements: let element = FormAssociatedElement(element) if element.parserInserted: return @@ -987,52 +1049,57 @@ proc reset*(form: HTMLFormElement) = control.resetElement() control.invalid = true -proc appendAttribute*(element: Element, k, v: string) = - case k - of "id": element.id = v - of "class": - let classes = v.split(' ') - for class in classes: - if class != "" and class notin element.classList: - element.classList.add(class) +proc appendAttributes*(element: Element, attrs: Table[string, string]) = + for k, v in attrs: + element.attributes[k] = v + template reflect_str(element: Element, name: static string, val: untyped) = + element.attributes.withValue(name, val): + element.val = val[] + template reflect_str(element: Element, name: static string, val, fun: untyped) = + element.attributes.withValue(name, val): + element.val = fun(val[]) + template reflect_nonzero_int(element: Element, name: static string, val: untyped, default: int) = + element.attributes.withValue(name, val): + if val[].isValidNonZeroInt(): + element.val = parseInt(val[]) + else: + element.val = default + do: + element.val = default + template reflect_bool(element: Element, name: static string, val: untyped) = + if name in element.attributes: + element.val = true + element.reflect_str "id", id + element.attributes.withValue("class", val): + let classList = val[].split(' ') + for x in classList: + if x != "" and x notin element.classList: + element.classList.add(x) case element.tagType of TAG_INPUT: let input = HTMLInputElement(element) - case k - of "value": input.value = v - of "type": input.inputType = inputType(v) - of "size": - if v.isValidNonZeroInt(): - input.size = parseInt(v) - else: - input.size = 20 - of "checked": input.checked = true + input.reflect_str "value", value + input.reflect_str "type", inputType, inputType + input.reflect_nonzero_int "size", size, 20 + input.reflect_bool "checked", checked of TAG_OPTION: let option = HTMLOptionElement(element) - if k == "selected": - option.selected = true + option.reflect_bool "selected", selected of TAG_SELECT: let select = HTMLSelectElement(element) - case k - of "multiple": - if not select.attributes["size"].isValidNonZeroInt(): - select.size = 4 - of "size": - if v.isValidNonZeroInt(): - select.size = parseInt(v) - elif "multiple" in select.attributes: - select.size = 4 + select.reflect_nonzero_int "size", size, (if "multiple" in element.attributes: 4 else: 1) of TAG_BUTTON: let button = HTMLButtonElement(element) - if k == "type": - case v - of "submit": button.ctype = BUTTON_SUBMIT - of "reset": button.ctype = BUTTON_RESET - of "button": button.ctype = BUTTON_BUTTON - elif k == "value": - button.value = v + button.reflect_str "type", ctype, (func(s: string): ButtonType = + case s + of "submit": return BUTTON_SUBMIT + of "reset": return BUTTON_RESET + of "button": return BUTTON_BUTTON) + of TAG_TEXTAREA: + let textarea = HTMLTextAreaElement(element) + textarea.reflect_nonzero_int "cols", cols, 20 + textarea.reflect_nonzero_int "rows", rows, 1 else: discard - element.attributes[k] = v # Forward definition hack (these are set in selectors.nim) var doqsa*: proc (node: Node, q: string): seq[Element] diff --git a/src/html/htmlparser.nim b/src/html/htmlparser.nim index 2b48c59d..e1359201 100644 --- a/src/html/htmlparser.nim +++ b/src/html/htmlparser.nim @@ -16,11 +16,13 @@ import utils/twtstr type DOMParser = ref object # JS interface + OpenElements = seq[Element] + HTML5Parser = object case fragment: bool of true: ctx: Element else: discard - openElements: seq[Element] + openElements: OpenElements insertionMode: InsertionMode oldInsertionMode: InsertionMode templateModes: seq[InsertionMode] @@ -196,8 +198,9 @@ func createElement(parser: HTML5Parser, token: Token, namespace: Namespace, inte let document = intendedParent.document let localName = token.tagname let element = document.newHTMLElement(localName, namespace, tagType = token.tagtype) - for k, v in token.attrs: - element.appendAttribute(k, v) + element.appendAttributes(token.attrs) + #for k, v in token.attrs: + # element.appendAttribute(k, v) if element.isResettable(): element.resetElement() @@ -450,18 +453,23 @@ proc genericRCDATAElementParsingAlgorithm(parser: var HTML5Parser, token: Token) parser.oldInsertionMode = parser.insertionMode parser.insertionMode = TEXT +proc popElement(parser: var HTML5Parser): Element = + result = parser.openElements.pop() + if result.tagType == TAG_TEXTAREA: + result.resetElement() + # 13.2.6.3 proc generateImpliedEndTags(parser: var HTML5Parser) = const tags = {TAG_DD, TAG_DT, TAG_LI, TAG_OPTGROUP, TAG_OPTION, TAG_P, TAG_RB, TAG_RP, TAG_RT, TAG_RTC} while parser.currentNode.tagType in tags: - discard parser.openElements.pop() + discard parser.popElement() proc generateImpliedEndTags(parser: var HTML5Parser, exclude: TagType) = let tags = {TAG_DD, TAG_DT, TAG_LI, TAG_OPTGROUP, TAG_OPTION, TAG_P, TAG_RB, TAG_RP, TAG_RT, TAG_RTC} - {exclude} while parser.currentNode.tagType in tags: - discard parser.openElements.pop() + discard parser.popElement() proc generateImpliedEndTagsThoroughly(parser: var HTML5Parser) = const tags = {TAG_CAPTION, TAG_COLGROUP, TAG_DD, TAG_DT, TAG_LI, @@ -469,7 +477,7 @@ proc generateImpliedEndTagsThoroughly(parser: var HTML5Parser) = TAG_RTC, TAG_TBODY, TAG_TD, TAG_TFOOT, TAG_TH, TAG_THEAD, TAG_TR} while parser.currentNode.tagType in tags: - discard parser.openElements.pop() + discard parser.popElement() # 13.2.4.3 proc pushOntoActiveFormatting(parser: var HTML5Parser, element: Element, token: Token) = @@ -535,7 +543,7 @@ proc reconstructActiveFormatting(parser: var HTML5Parser) = proc clearActiveFormattingTillMarker(parser: var HTML5Parser) = while parser.activeFormatting.len > 0 and parser.activeFormatting.pop()[0] != nil: discard -template pop_current_node = discard parser.openElements.pop() +template pop_current_node = discard parser.popElement() func isHTMLIntegrationPoint(node: Element): bool = return false #TODO SVG (NOTE MathML not implemented) @@ -849,7 +857,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode = parser.generateImpliedEndTagsThoroughly() if parser.currentNode.tagType != TAG_TEMPLATE: parse_error - while parser.openElements.pop().tagType != TAG_TEMPLATE: discard + while parser.popElement().tagType != TAG_TEMPLATE: discard parser.clearActiveFormattingTillMarker() discard parser.templateModes.pop() parser.resetInsertionMode() @@ -918,7 +926,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode = proc closeP(parser: var HTML5Parser) = parser.generateImpliedEndTags(TAG_P) if parser.currentNode.tagType != TAG_P: parse_error - while parser.openElements.pop().tagType != TAG_P: discard + while parser.popElement().tagType != TAG_P: discard proc adoptionAgencyAlgorithm(parser: var HTML5Parser, token: Token): bool = if parser.currentNode.tagType != TAG_UNKNOWN and parser.currentNode.tagtype == token.tagtype or parser.currentNode.localName == token.tagname: @@ -965,7 +973,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode = furthestBlockIndex = j break if furthestBlock == nil: - while parser.openElements.pop() != formatting: discard + while parser.popElement() != formatting: discard parser.activeFormatting.delete(formattingIndex) return false let commonAncestor = parser.openElements[stackIndex - 1] @@ -1031,7 +1039,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode = if node.tagType != TAG_UNKNOWN and node.tagType == token.tagtype or node.localName == token.tagname: parser.generateImpliedEndTags(token.tagtype) if node != parser.currentNode: parse_error - while parser.openElements.pop() != node: discard + while parser.popElement() != node: discard break elif node.tagType in SpecialElements: parse_error @@ -1149,7 +1157,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode = of TAG_LI: parser.generateImpliedEndTags(TAG_LI) if parser.currentNode.tagType != TAG_LI: parse_error - while parser.openElements.pop().tagType != TAG_LI: discard + while parser.popElement().tagType != TAG_LI: discard break of SpecialElements - {TAG_ADDRESS, TAG_DIV, TAG_P, TAG_LI}: break @@ -1166,12 +1174,12 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode = of TAG_DD: parser.generateImpliedEndTags(TAG_DD) if parser.currentNode.tagType != TAG_DD: parse_error - while parser.openElements.pop().tagType != TAG_DD: discard + while parser.popElement().tagType != TAG_DD: discard break of TAG_DT: parser.generateImpliedEndTags(TAG_DT) if parser.currentNode.tagType != TAG_DT: parse_error - while parser.openElements.pop().tagType != TAG_DT: discard + while parser.popElement().tagType != TAG_DT: discard break of SpecialElements - {TAG_ADDRESS, TAG_DIV, TAG_P, TAG_DD, TAG_DT}: break @@ -1190,7 +1198,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode = if parser.openElements.hasElementInScope(TAG_BUTTON): parse_error parser.generateImpliedEndTags() - while parser.openElements.pop().tagType != TAG_BUTTON: discard + while parser.popElement().tagType != TAG_BUTTON: discard parser.reconstructActiveFormatting() discard parser.insertHTMLElement(token) parser.framesetOk = false @@ -1205,7 +1213,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode = else: parser.generateImpliedEndTags() if parser.currentNode.tagType != token.tagtype: parse_error - while parser.openElements.pop().tagType != token.tagtype: discard + while parser.popElement().tagType != token.tagtype: discard ) "</form>" => (block: if not parser.openElements.hasElement(TAG_TEMPLATE): @@ -1223,7 +1231,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode = return parser.generateImpliedEndTags() if parser.currentNode.tagType != TAG_FORM: parse_error - while parser.openElements.pop().tagType != TAG_FORM: discard + while parser.popElement().tagType != TAG_FORM: discard ) "</p>" => (block: if not parser.openElements.hasElementInButtonScope(TAG_P): @@ -1237,7 +1245,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode = else: parser.generateImpliedEndTags(TAG_LI) if parser.currentNode.tagType != TAG_LI: parse_error - while parser.openElements.pop().tagType != TAG_LI: discard + while parser.popElement().tagType != TAG_LI: discard ) ("</dd>", "</dt>") => (block: if not parser.openElements.hasElementInScope(token.tagtype): @@ -1245,7 +1253,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode = else: parser.generateImpliedEndTags(token.tagtype) if parser.currentNode.tagType != token.tagtype: parse_error - while parser.openElements.pop().tagType != token.tagtype: discard + while parser.popElement().tagType != token.tagtype: discard ) ("</h1>", "</h2>", "</h3>", "</h4>", "</h5>", "</h6>") => (block: if not parser.openElements.hasElementInScope(HTagTypes): @@ -1253,7 +1261,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode = else: parser.generateImpliedEndTags() if parser.currentNode.tagType != token.tagtype: parse_error - while parser.openElements.pop().tagType notin HTagTypes: discard + while parser.popElement().tagType notin HTagTypes: discard ) "</sarcasm>" => (block: #*deep breath* @@ -1321,7 +1329,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode = else: parser.generateImpliedEndTags() if parser.currentNode.tagType != token.tagtype: parse_error - while parser.openElements.pop().tagType != token.tagtype: discard + while parser.popElement().tagType != token.tagtype: discard parser.clearActiveFormattingTillMarker() ) "<table>" => (block: @@ -1504,7 +1512,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode = if not parser.openElements.hasElementInScope(TAG_TABLE): discard else: - while parser.openElements.pop().tagType != TAG_TABLE: discard + while parser.popElement().tagType != TAG_TABLE: discard parser.resetInsertionMode() reprocess token ) @@ -1512,7 +1520,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode = if not parser.openElements.hasElementInScope(TAG_TABLE): parse_error else: - while parser.openElements.pop().tagType != TAG_TABLE: discard + while parser.popElement().tagType != TAG_TABLE: discard parser.resetInsertionMode() ) ("</body>", "</caption>", "</col>", "</colgroup>", "</html>", "</tbody>", @@ -1587,7 +1595,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode = else: parser.generateImpliedEndTags() if parser.currentNode.tagType != TAG_CAPTION: parse_error - while parser.openElements.pop().tagType != TAG_CAPTION: discard + while parser.popElement().tagType != TAG_CAPTION: discard parser.clearActiveFormattingTillMarker() parser.insertionMode = IN_TABLE ) @@ -1728,7 +1736,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode = template close_cell() = parser.generateImpliedEndTags() if parser.currentNode.tagType notin {TAG_TD, TAG_TH}: parse_error - while parser.openElements.pop().tagType notin {TAG_TD, TAG_TH}: discard + while parser.popElement().tagType notin {TAG_TD, TAG_TH}: discard parser.clearActiveFormattingTillMarker() parser.insertionMode = IN_ROW @@ -1739,7 +1747,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode = else: parser.generateImpliedEndTags() if parser.currentNode.tagType != token.tagtype: parse_error - while parser.openElements.pop().tagType != token.tagtype: discard + while parser.popElement().tagType != token.tagtype: discard parser.clearActiveFormattingTillMarker() parser.insertionMode = IN_ROW ) @@ -1799,13 +1807,13 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode = if not parser.openElements.hasElementInSelectScope(TAG_SELECT): parse_error else: - while parser.openElements.pop().tagType != TAG_SELECT: discard + while parser.popElement().tagType != TAG_SELECT: discard parser.resetInsertionMode() ) "<select>" => (block: parse_error if parser.openElements.hasElementInSelectScope(TAG_SELECT): - while parser.openElements.pop().tagType != TAG_SELECT: discard + while parser.popElement().tagType != TAG_SELECT: discard parser.resetInsertionMode() ) ("<input>", "<keygen>", "<textarea>") => (block: @@ -1813,7 +1821,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode = if not parser.openElements.hasElementInSelectScope(TAG_SELECT): discard else: - while parser.openElements.pop().tagType != TAG_SELECT: discard + while parser.popElement().tagType != TAG_SELECT: discard parser.resetInsertionMode() reprocess token ) @@ -1826,7 +1834,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode = ("<caption>", "<table>", "<tbody>", "<tfoot>", "<thead>", "<tr>", "<td>", "<th>") => (block: parse_error - while parser.openElements.pop().tagType != TAG_SELECT: discard + while parser.popElement().tagType != TAG_SELECT: discard parser.resetInsertionMode() reprocess token ) @@ -1836,7 +1844,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode = if not parser.openElements.hasElementInTableScope(token.tagtype): discard else: - while parser.openElements.pop().tagType != TAG_SELECT: discard + while parser.popElement().tagType != TAG_SELECT: discard parser.resetInsertionMode() reprocess token ) @@ -1887,7 +1895,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode = discard # stop else: parse_error - while parser.openElements.pop().tagType != TAG_TEMPLATE: discard + while parser.popElement().tagType != TAG_TEMPLATE: discard parser.clearActiveFormattingTillMarker() discard parser.templateModes.pop() parser.resetInsertionMode() @@ -1982,7 +1990,7 @@ proc processInForeignContent(parser: var HTML5Parser, token: Token) = for i in countdown(parser.openElements.high, 1): let node = parser.openElements[i] if node.localName == token.tagname: - while parser.openElements.pop() != node: discard + while parser.popElement() != node: discard break if node.namespace == Namespace.HTML: break parser.processInHTMLContent(token) diff --git a/src/html/tags.nim b/src/html/tags.nim index fbec9164..ff6a3a30 100644 --- a/src/html/tags.nim +++ b/src/html/tags.nim @@ -118,7 +118,7 @@ const FormAssociatedElements* = { #TODO support all the other ones const SupportedFormAssociatedElements* = { - TAG_SELECT, TAG_INPUT, TAG_BUTTON + TAG_BUTTON, TAG_INPUT, TAG_SELECT, TAG_TEXTAREA } const ListedElements* = { diff --git a/src/io/lineedit.nim b/src/io/lineedit.nim index 6673676d..2cd76fff 100644 --- a/src/io/lineedit.nim +++ b/src/io/lineedit.nim @@ -5,7 +5,7 @@ import sugar import bindings/quickjs import buffer/cell -import io/term +import display/term import js/javascript import types/color import utils/twtstr diff --git a/src/ips/editor.nim b/src/ips/editor.nim new file mode 100644 index 00000000..3bce0fa9 --- /dev/null +++ b/src/ips/editor.nim @@ -0,0 +1,59 @@ +import os + +import config/config +import display/term + +func formatEditorName(editor, file: string, line: int): string = + result = newStringOfCap(editor.len + file.len) + var i = 0 + var filefound = false + while i < editor.len: + if editor[i] == '%' and i < editor.high: + if editor[i + 1] == 's': + result &= file + filefound = true + i += 2 + continue + elif editor[i + 1] == 'd': + result &= $line + i += 2 + continue + elif editor[i + 1] == '%': + result &= '%' + i += 2 + continue + result &= editor[i] + inc i + if editor.len == 0: + result = "vi" + if not filefound: + if result[^1] != ' ': + result &= ' ' + result &= file + +proc openEditor*(term: Terminal, config: Config, file: string, line = 0): bool = + var editor = config.editor + if editor == "": + editor = getEnv("EDITOR") + if editor == "": + editor = "vi %s +%d" + let cmd = formatEditorName(editor, file, line) + term.quit() + result = execShellCmd(cmd) == 0 + term.restart() + +var tmpf_seq: int +proc openInEditor*(term: Terminal, config: Config, input: var string): bool = + try: + let tmpdir = config.tmpdir + if not dirExists(tmpdir): + createDir(tmpdir) + let tmpf = tmpdir / "chatmp" & $tmpf_seq + inc tmpf_seq + writeFile(tmpf, input) + if openEditor(term, config, tmpf): + input = readFile(tmpf) + removeFile(tmpf) + return true + except IOError: + discard diff --git a/src/ips/forkserver.nim b/src/ips/forkserver.nim index 1c29b120..6e95fc8e 100644 --- a/src/ips/forkserver.nim +++ b/src/ips/forkserver.nim @@ -4,16 +4,18 @@ when defined(posix): import posix import buffer/buffer -import config/bufferconfig +import config/config import io/loader import io/request import io/window import ips/serialize +import ips/serversocket import types/buffersource +import utils/twtstr type ForkCommand* = enum - FORK_BUFFER, FORK_LOADER, REMOVE_CHILD + FORK_BUFFER, FORK_LOADER, REMOVE_CHILD, LOAD_CONFIG ForkServer* = ref object process*: Pid @@ -31,6 +33,11 @@ proc newFileLoader*(forkserver: ForkServer, defaultHeaders: HeaderList = Default forkserver.ostream.flush() forkserver.istream.sread(result) +proc loadForkServerConfig*(forkserver: ForkServer, config: Config) = + forkserver.ostream.swrite(LOAD_CONFIG) + forkserver.ostream.swrite(config.getForkServerConfig()) + forkserver.ostream.flush() + proc removeChild*(forkserver: Forkserver, pid: Pid) = forkserver.ostream.swrite(REMOVE_CHILD) forkserver.ostream.swrite(pid) @@ -103,6 +110,11 @@ proc runForkServer() = let loader = ctx.forkLoader(defaultHeaders) ctx.ostream.swrite(loader) ctx.children.add((loader.process, Pid(-1))) + of LOAD_CONFIG: + var config: ForkServerConfig + ctx.istream.sread(config) + width_table = makewidthtable(config.ambiguous_double) + SocketDirectory = config.tmpdir ctx.ostream.flush() except IOError: # EOF diff --git a/src/ips/serialize.nim b/src/ips/serialize.nim index 2647db2f..6eeaaca9 100644 --- a/src/ips/serialize.nim +++ b/src/ips/serialize.nim @@ -5,7 +5,6 @@ import streams import tables import buffer/cell -import config/bufferconfig import io/request import js/regex import types/buffersource @@ -92,7 +91,7 @@ proc slen*[T](o: T): int = template swrite*[T](stream: Stream, o: T) = stream.write(o) -proc swrite*(stream: Stream, s: string, maxlen = 8192) = +proc swrite*(stream: Stream, s: string) = stream.swrite(s.len) stream.write(s) @@ -182,9 +181,6 @@ proc swrite*(stream: Stream, source: BufferSource) = stream.swrite(source.location) stream.swrite(source.contenttype) -proc swrite*(stream: Stream, bconfig: BufferConfig) = - stream.swrite(bconfig.userstyle) - proc swrite*(stream: Stream, tup: tuple) = for f in tup.fields: stream.swrite(f) @@ -196,11 +192,11 @@ proc swrite*(stream: Stream, obj: object) = template sread*[T](stream: Stream, o: T) = stream.read(o) -proc sread*(stream: Stream, s: var string, maxlen = 8192) = +proc sread*(stream: Stream, s: var string) = var len: int stream.sread(len) - if maxlen != -1: - len = min(maxlen, len) + #if maxlen != -1: + # len = min(maxlen, len) stream.readStr(len, s) proc sread*(stream: Stream, b: var bool) = @@ -214,7 +210,7 @@ proc sread*(stream: Stream, b: var bool) = proc sread*(stream: Stream, url: var Url) = var s: string - stream.sread(s, 2048) + stream.sread(s) url = newURL(s) proc sread*(stream: Stream, headers: var HeaderList) = @@ -323,9 +319,6 @@ proc sread*(stream: Stream, source: var BufferSource) = stream.sread(source.location) stream.sread(source.contenttype) -proc sread*(stream: Stream, bconfig: var BufferConfig) = - stream.sread(bconfig.userstyle) - proc sread*(stream: Stream, obj: var object) = for f in obj.fields: stream.sread(f) diff --git a/src/ips/serversocket.nim b/src/ips/serversocket.nim index 8ed79ab3..b476320e 100644 --- a/src/ips/serversocket.nim +++ b/src/ips/serversocket.nim @@ -7,10 +7,10 @@ type ServerSocket* = object sock*: Socket path*: string -const SocketDirectory = "/tmp/cha/" -const SocketPathPrefix = SocketDirectory & "cha_sock_" -func getSocketPath*(pid: Pid): string = - SocketPathPrefix & $pid +var SocketDirectory* = "/tmp/cha" +const SocketPathPrefix = "cha_sock_" +proc getSocketPath*(pid: Pid): string = + SocketDirectory / SocketPathPrefix & $pid proc initServerSocket*(buffered = true): ServerSocket = createDir(SocketDirectory) diff --git a/src/ips/socketstream.nim b/src/ips/socketstream.nim index df4bbf09..ff2d189e 100644 --- a/src/ips/socketstream.nim +++ b/src/ips/socketstream.nim @@ -18,7 +18,7 @@ proc sockReadData(s: Stream, buffer: pointer, len: int): int = result = s.source.recv(buffer, len) if result < 0: raise newException(IOError, "Failed to read data (code " & $osLastError() & ")") - elif result < len: + elif result == 0: s.isend = true proc sockWriteData(s: Stream, buffer: pointer, len: int) = diff --git a/src/main.nim b/src/main.nim index ad978fa6..fdec3081 100644 --- a/src/main.nim +++ b/src/main.nim @@ -1,13 +1,4 @@ -import config/config import types/dispatcher -import utils/twtstr - -# Inherited memory -var conf = readConfig() -width_table = makewidthtable(conf.ambiguous_double) -# We don't actually want to inherit the entire config, so zero it out here. -zeroMem(addr conf[], sizeof(conf[])) - let disp = newDispatcher() import options @@ -17,9 +8,14 @@ import terminal when defined(profile): import nimprof +import config/config import display/client +import ips/forkserver +import utils/twtstr -conf = readConfig() +let conf = readConfig() +widthtable = makewidthtable(conf.ambiguous_double) +disp.forkserver.loadForkServerConfig(conf) let params = commandLineParams() proc version(long: static bool = false): string = @@ -34,14 +30,14 @@ proc help(i: int) = Usage: cha [options] [URL(s) or file(s)...] Options: + -- Interpret all following arguments as URLs -d, --dump Print page to stdout -c, --css <stylesheet> Pass stylesheet (e.g. -c 'a{color: blue}') -o, --opt <config> Pass config options (e.g. -o 'page.q="QUIT"') -T, --type <type> Specify content mime type - -v, --version Print version information - -h, --help Print this page -r, --run <script/file> Run passed script or file - -- Interpret all following arguments as URLs""" + -h, --help Print this usage message + -v, --version Print version information""" if i == 0: echo s else: @@ -71,7 +67,7 @@ while i < params.len: help(1) of "-": discard # emulate programs that accept - as stdin - of "-d", "--dump": + of "-d", "-dump", "--dump": dump = true of "-c", "--css": inc i |