about summary refs log blame commit diff stats
path: root/src/buffer/buffer.nim
blob: 260fc8ea0e4427157c2fef949e4d7f804b9a3ae3 (plain) (tree)
1
2
3
4
5
             
          

              
                












                     
                          



                      
                 


                       
               
                


                            
                         





                       


                                                                            



                                                                           
                                       














                           
                           



                          
                 
                           



                                      
                

                        
                      
                        




































































































































































































                                                                                                  
                                                  
                                   




























































                                                                                                     











                                                                         
                                       
                             







                                                         
                                       





                                       
                                       
















                                                              
                                                                      
                      















                                                                  





                                                 




                              
                                                      
                                                                                                                        


                              






















                                                                            














































































































































































































































































                                                                                                                                  






















                                                                                

                                          

                                    


                                     
                   





















































                                                                                  
                                  








                                              
                                          
















                                             



                                    

                         
                      

         
                                                              

                                                               



                         
                        
                         
                                      








                                                    
import macros
import net
import options
import os
import selectors
import streams
import tables
import unicode

when defined(posix):
  import posix

import buffer/cell
import css/cascade
import css/cssparser
import css/mediaquery
import css/sheet
import css/stylednode
import config/bufferconfig
import html/dom
import html/tags
import html/htmlparser
import io/loader
import io/request
import ips/serialize
import ips/serversocket
import ips/socketstream
import js/regex
import io/window
import layout/box
import render/renderdocument
import render/rendertext
import types/buffersource
import types/color
import types/url
import utils/twtstr

type
  BufferCommand* = enum
    LOAD, RENDER, WINDOW_CHANGE, GOTO_ANCHOR, READ_SUCCESS, READ_CANCELED,
    CLICK, FIND_NEXT_LINK, FIND_PREV_LINK, FIND_NEXT_MATCH, FIND_PREV_MATCH,
    GET_SOURCE, GET_LINES, MOVE_CURSOR

  ContainerCommand* = enum
    SET_LINES, SET_NEEDS_AUTH, SET_CONTENT_TYPE, SET_REDIRECT, SET_TITLE,
    SET_HOVER, READ_LINE, LOAD_DONE, ANCHOR_FOUND, ANCHOR_FAIL, JUMP, OPEN,
    BUFFER_READY, SOURCE_READY, RESHAPE

  BufferMatch* = object
    success*: bool
    x*: int
    y*: int
    str*: string

  Buffer* = ref object
    input: HTMLInputElement
    contenttype: string
    lines: FlexibleGrid
    rendered: bool
    bsource: BufferSource
    width: int
    height: int
    attrs: WindowAttributes
    document: Document
    viewport: Viewport
    prevstyled: StyledNode
    reshape: bool
    location: Url
    selector: Selector[int]
    istream: Stream
    pistream: Stream # for input pipe
    postream: Stream # for output pipe
    streamclosed: bool
    loaded: bool
    source: string
    prevnode: StyledNode
    loader: FileLoader
    config: BufferConfig

macro writeCommand(buffer: Buffer, cmd: ContainerCommand, args: varargs[typed]) =
  result = newStmtList()
  result.add(quote do: `buffer`.postream.swrite(`cmd`))
  for arg in args:
    result.add(quote do: `buffer`.postream.swrite(`arg`))
  result.add(quote do: `buffer`.postream.flush())

func getLink(node: StyledNode): HTMLAnchorElement =
  if node == nil:
    return nil
  if node.t == STYLED_ELEMENT and node.node != nil and Element(node.node).tagType == TAG_A:
    return HTMLAnchorElement(node.node)
  if node.node != nil:
    return HTMLAnchorElement(node.node.findAncestor({TAG_A}))
  #TODO ::before links?

const ClickableElements = {
  TAG_A, TAG_INPUT, TAG_OPTION
}

func getClickable(styledNode: StyledNode): Element =
  if styledNode == nil or styledNode.node == nil:
    return nil
  if styledNode.t == STYLED_ELEMENT:
    let element = Element(styledNode.node)
    if element.tagType in ClickableElements:
      return element
  styledNode.node.findAncestor(ClickableElements)

func getCursorClickable(buffer: Buffer, cursorx, cursory: int): Element =
  let i = buffer.lines[cursory].findFormatN(cursorx) - 1
  if i >= 0:
    return buffer.lines[cursory].formats[i].node.getClickable()

func cursorBytes(buffer: Buffer, y: int, cc: int): int =
  let line = buffer.lines[y].str
  var w = 0
  var i = 0
  while i < line.len and w < cc:
    var r: Rune
    fastRuneAt(line, i, r)
    w += r.width()
  return i

func findNextLink(buffer: Buffer, cursorx, cursory: int): tuple[x, y: int] =
  let line = buffer.lines[cursory]
  var i = line.findFormatN(cursorx) - 1
  var link: Element = nil
  if i >= 0:
    link = line.formats[i].node.getClickable()
  inc i

  while i < line.formats.len:
    let format = line.formats[i]
    let fl = format.node.getClickable()
    if fl != nil and fl != link:
      return (format.pos, cursory)
    inc i

  for y in (cursory + 1)..(buffer.lines.len - 1):
    let line = buffer.lines[y]
    i = 0
    while i < line.formats.len:
      let format = line.formats[i]
      let fl = format.node.getClickable()
      if fl != nil and fl != link:
        return (format.pos, y)
      inc i
  return (-1, -1)

func findPrevLink(buffer: Buffer, cursorx, cursory: int): tuple[x, y: int] =
  let line = buffer.lines[cursory]
  var i = line.findFormatN(cursorx) - 1
  var link: Element = nil
  if i >= 0:
    link = line.formats[i].node.getClickable()
  dec i

  var ly = 0 #last y
  var lx = 0 #last x
  template link_beginning() =
    #go to beginning of link
    ly = y #last y
    lx = format.pos #last x

    #on the current line
    let line = buffer.lines[y]
    while i >= 0:
      let format = line.formats[i]
      let nl = format.node.getClickable()
      if nl == fl:
        lx = format.pos
      dec i

    #on previous lines
    for iy in countdown(ly - 1, 0):
      let line = buffer.lines[iy]
      i = line.formats.len - 1
      while i >= 0:
        let format = line.formats[i]
        let nl = format.node.getClickable()
        if nl == fl:
          ly = iy
          lx = format.pos
        dec i

  while i >= 0:
    let format = line.formats[i]
    let fl = format.node.getClickable()
    if fl != nil and fl != link:
      let y = cursory
      link_beginning
      return (lx, ly)
    dec i

  for y in countdown(cursory - 1, 0):
    let line = buffer.lines[y]
    i = line.formats.len - 1
    while i >= 0:
      let format = line.formats[i]
      let fl = format.node.getClickable()
      if fl != nil and fl != link:
        link_beginning
        return (lx, ly)
      dec i
  return (-1, -1)

proc findNextMatch(buffer: Buffer, regex: Regex, cursorx, cursory: int, wrap: bool): BufferMatch =
  template return_if_match =
    if res.success and res.captures.len > 0:
      let cap = res.captures[0]
      let x = buffer.lines[y].str.width(cap.s)
      let str = buffer.lines[y].str.substr(cap.s, cap.e - 1)
      return BufferMatch(success: true, x: x, y: y, str: str)
  var y = cursory
  let b = buffer.cursorBytes(y, cursorx)
  let b2 = if buffer.lines[y].str.len > b: b + buffer.lines[y].str.runeLenAt(b) else: b
  let res = regex.exec(buffer.lines[y].str, b2, buffer.lines[y].str.len)
  return_if_match
  inc y
  while true:
    if y > buffer.lines.high:
      if wrap:
        y = 0
      else:
        break
    if y == cursory:
      let res = regex.exec(buffer.lines[y].str, 0, b)
      return_if_match
      break
    let res = regex.exec(buffer.lines[y].str)
    return_if_match
    inc y

proc findPrevMatch(buffer: Buffer, regex: Regex, cursorx, cursory: int, wrap: bool): BufferMatch =
  template return_if_match =
    if res.success and res.captures.len > 0:
      let cap = res.captures[^1]
      let x = buffer.lines[y].str.width(cap.s)
      let str = buffer.lines[y].str.substr(cap.s, cap.e - 1)
      return BufferMatch(success: true, x: x, y: y, str: str)
  var y = cursory
  let b = buffer.cursorBytes(y, cursorx)
  let b2 = if b > 0: b - buffer.lines[y].str.lastRune(b)[1] else: 0
  let res = regex.exec(buffer.lines[y].str, 0, b2)
  return_if_match
  dec y
  while true:
    if y < 0:
      if wrap:
        y = buffer.lines.high
      else:
        break
    if y == cursory:
      let res = regex.exec(buffer.lines[y].str, b, buffer.lines[y].str.len)
      return_if_match
      break
    let res = regex.exec(buffer.lines[y].str)
    return_if_match
    dec y

proc gotoAnchor(buffer: Buffer) =
  if buffer.document == nil: return
  let anchor = buffer.document.getElementById(buffer.location.anchor)
  if anchor == nil: return
  for y in 0..<buffer.lines.len:
    let line = buffer.lines[y]
    var i = 0
    while i < line.formats.len:
      let format = line.formats[i]
      if format.node != nil and anchor in format.node.node:
        buffer.writeCommand(JUMP, format.pos, y)
        return
      inc i

proc windowChange(buffer: Buffer) =
  buffer.viewport = Viewport(window: buffer.attrs)
  buffer.width = buffer.attrs.width
  buffer.height = buffer.attrs.height - 1
  buffer.reshape = true

proc updateHover(buffer: Buffer, cursorx, cursory: int) =
  var thisnode: StyledNode
  let i = buffer.lines[cursory].findFormatN(cursorx) - 1
  if i >= 0:
    thisnode = buffer.lines[cursory].formats[i].node
  let prevnode = buffer.prevnode

  if thisnode != prevnode and (thisnode == nil or prevnode == nil or thisnode.node != prevnode.node):
    for styledNode in thisnode.branch:
      if styledNode.t == STYLED_ELEMENT and styledNode.node != nil:
        let elem = Element(styledNode.node)
        if not elem.hover:
          elem.hover = true
          buffer.reshape = true

    let link = thisnode.getLink()
    if link != nil:
      buffer.writeCommand(SET_HOVER, link.href)
    else:
      buffer.writeCommand(SET_HOVER, "")

    for styledNode in prevnode.branch:
      if styledNode.t == STYLED_ELEMENT and styledNode.node != nil:
        let elem = Element(styledNode.node)
        if elem.hover:
          elem.hover = false
          buffer.reshape = true

  buffer.prevnode = thisnode

proc loadResource(buffer: Buffer, document: Document, elem: HTMLLinkElement) =
  let url = parseUrl(elem.href, document.location.some)
  if url.isSome:
    let url = url.get
    if url.scheme == buffer.location.scheme:
      let media = elem.media
      if media != "":
        let media = parseMediaQueryList(parseListOfComponentValues(newStringStream(media)))
        if not media.applies(): return
      let fs = buffer.loader.doRequest(newRequest(url))
      if fs.body != nil and fs.contenttype == "text/css":
        elem.sheet = parseStylesheet(fs.body)

proc loadResources(buffer: Buffer, document: Document) =
  var stack: seq[Element]
  if document.html != nil:
    stack.add(document.html)
  while stack.len > 0:
    let elem = stack.pop()

    if elem.tagType == TAG_LINK:
      let elem = HTMLLinkElement(elem)
      if elem.rel == "stylesheet":
        buffer.loadResource(document, elem)

    for child in elem.children_rev:
      stack.add(child)

proc c_setvbuf(f: File, buf: pointer, mode: cint, size: csize_t): cint {.
  importc: "setvbuf", header: "<stdio.h>", tags: [].}

func getFd(buffer: Buffer): FileHandle =
  let source = buffer.bsource
  case source.t
  of CLONE, LOAD_REQUEST:
    let istream = SocketStream(buffer.istream)
    return cast[FileHandle](istream.source.getFd())
  of LOAD_PIPE:
    return buffer.bsource.fd

proc setupSource(buffer: Buffer): int =
  if buffer.loaded: return -2
  let source = buffer.bsource
  let setct = source.contenttype.isNone
  if not setct:
    buffer.contenttype = source.contenttype.get
  buffer.location = source.location
  case source.t
  of CLONE:
    buffer.istream = connectSocketStream(source.clonepid)
    if buffer.istream == nil: return -2
    if setct:
      buffer.contenttype = "text/plain"
  of LOAD_PIPE:
    var f: File
    if not open(f, source.fd, fmRead):
      return 1
    discard c_setvbuf(f, nil, IONBF, 0)
    buffer.istream = newFileStream(f)
    if setct:
      buffer.contenttype = "text/plain"
  of LOAD_REQUEST:
    let request = source.request
    let response = buffer.loader.doRequest(request)
    if response.body == nil:
      return response.res
    if setct:
      buffer.contenttype = response.contenttype
    buffer.istream = response.body
    if response.status == 401: # Unauthorized
      buffer.writeCommand(SET_NEEDS_AUTH)
    if response.redirect.isSome:
      buffer.writeCommand(SET_REDIRECT, response.redirect.get)
  if setct:
    buffer.writeCommand(SET_CONTENT_TYPE, buffer.contenttype)
  buffer.selector.registerHandle(cast[int](buffer.getFd()), {Read}, 1)
  buffer.loaded = true

proc load(buffer: Buffer) =
  case buffer.contenttype
  of "text/html":
    if not buffer.streamclosed:
      buffer.source = buffer.istream.readAll()
      buffer.istream.close()
      buffer.istream = newStringStream(buffer.source)
      buffer.document = parseHTML5(buffer.istream)
      buffer.streamclosed = true
    else:
      buffer.document = parseHTML5(newStringStream(buffer.source))
    buffer.writeCommand(SET_TITLE, buffer.document.title)
    buffer.document.location = buffer.location
    buffer.loadResources(buffer.document)
  else:
    discard
    #if not buffer.streamclosed:
    #  if buffer.bsource.t != LOAD_PIPE:
    #    buffer.source = buffer.istream.readAll()
    #    buffer.istream.close()
    #    buffer.streamclosed = true

proc render(buffer: Buffer) =
  case buffer.contenttype
  of "text/html":
    if buffer.viewport == nil:
      buffer.viewport = Viewport(window: buffer.attrs)
    let ret = renderDocument(buffer.document, buffer.attrs, buffer.config.userstyle, buffer.viewport, buffer.prevstyled)
    buffer.lines = ret[0]
    buffer.prevstyled = ret[1]
  else:
    buffer.lines = renderPlainText(buffer.source)

proc load2(buffer: Buffer) =
  case buffer.contenttype
  of "text/html":
    assert false, "Not implemented yet..."
  else:
    # This is incredibly stupid but it works so whatever.
    # (We're basically recv'ing single bytes, but nim std/net does buffering
    # for us so we should be ok?)
    if not buffer.streamclosed:
      let c = buffer.istream.readChar()
      buffer.source &= c
      buffer.reshape = true

proc finishLoad(buffer: Buffer) =
  if not buffer.streamclosed:
    if not buffer.istream.atEnd:
      buffer.source &= buffer.istream.readAll()
      buffer.reshape = true
    buffer.selector.unregister(int(buffer.getFd()))
    buffer.istream.close()
    buffer.streamclosed = true

# https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#constructing-the-form-data-set
proc constructEntryList(form: HTMLFormElement, submitter: Element = nil, encoding: string = ""): seq[tuple[name, value: string]] =
  if form.constructingentrylist:
    return
  form.constructingentrylist = true

  var entrylist: seq[tuple[name, value: string]]
  for field in form.controls:
    if field.findAncestor({TAG_DATALIST}) != nil or
        field.attrb("disabled") or
        field.isButton() and Element(field) != submitter:
      continue

    if field.tagType == TAG_INPUT:
      let field = HTMLInputElement(field)
      if field.inputType == INPUT_IMAGE:
        let name = if field.attr("name") != "":
          field.attr("name") & '.'
        else:
          ""
        entrylist.add((name & 'x', $field.xcoord))
        entrylist.add((name & 'y', $field.ycoord))
        continue

    #TODO custom elements

    let name = field.attr("name")

    if name == "":
      continue

    if field.tagType == TAG_SELECT:
      let field = HTMLSelectElement(field)
      for option in field.options:
        if option.selected or option.disabled:
          entrylist.add((name, option.value))
    elif field.tagType == TAG_INPUT and HTMLInputElement(field).inputType in {INPUT_CHECKBOX, INPUT_RADIO}:
      let value = if field.attr("value") != "":
        field.attr("value")
      else:
        "on"
      entrylist.add((name, value))
    elif field.tagType == TAG_INPUT and HTMLInputElement(field).inputType == INPUT_FILE:
      #TODO file
      discard
    elif field.tagType == TAG_INPUT and HTMLInputElement(field).inputType == INPUT_HIDDEN and name.equalsIgnoreCase("_charset_"):
      let charset = if encoding != "":
        encoding
      else:
        "UTF-8"
      entrylist.add((name, charset))
    else:
      if field.tagType == TAG_INPUT:
        entrylist.add((name, HTMLInputElement(field).value))
      else:
        assert false
    if field.tagType == TAG_TEXTAREA or
        field.tagType == TAG_INPUT and HTMLInputElement(field).inputType in {INPUT_TEXT, INPUT_SEARCH}:
      if field.attr("dirname") != "":
        let dirname = field.attr("dirname")
        let dir = "ltr" #TODO bidi
        entrylist.add((dirname, dir))

  form.constructingentrylist = false
  return entrylist

#https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#multipart/form-data-encoding-algorithm
proc makeCRLF(s: string): string =
  result = newStringOfCap(s.len)
  var i = 0
  while i < s.len - 1:
    if s[i] == '\r' and s[i + 1] != '\n':
      result &= '\r'
      result &= '\n'
    elif s[i] != '\r' and s[i + 1] == '\n':
      result &= s[i]
      result &= '\r'
      result &= '\n'
      inc i
    else:
      result &= s[i]
    inc i

proc serializeMultipartFormData(kvs: seq[(string, string)]): MimeData =
  for it in kvs:
    let name = makeCRLF(it[0])
    let value = makeCRLF(it[1])
    result[name] = value

proc serializePlainTextFormData(kvs: seq[(string, string)]): string =
  for it in kvs:
    let (name, value) = it
    result &= name
    result &= '='
    result &= value
    result &= "\r\n"

proc submitForm(form: HTMLFormElement, submitter: Element): Option[Request] =
  let entrylist = form.constructEntryList(submitter)

  let action = if submitter.action() == "":
    $form.document.location
  else:
    submitter.action()

  let url = parseUrl(action, submitter.document.baseUrl.some)
  if url.isnone:
    return none(Request)

  var parsedaction = url.get
  let scheme = parsedaction.scheme
  let enctype = submitter.enctype()
  let formmethod = submitter.formmethod()
  if formmethod == FORM_METHOD_DIALOG:
    #TODO
    return none(Request)
  let httpmethod = if formmethod == FORM_METHOD_GET:
    HTTP_GET
  else:
    assert formmethod == FORM_METHOD_POST
    HTTP_POST

  #let target = if submitter.isSubmitButton() and submitter.attrb("formtarget"):
  #  submitter.attr("formtarget")
  #else:
  #  submitter.target()
  #let noopener = true #TODO

  template mutateActionUrl() =
    let query = serializeApplicationXWWWFormUrlEncoded(entrylist)
    parsedaction.query = query.some
    return newRequest(parsedaction, httpmethod).some

  template submitAsEntityBody() =
    var mimetype: string
    var body = none(string)
    var multipart = none(MimeData)
    case enctype
    of FORM_ENCODING_TYPE_URLENCODED:
      body = serializeApplicationXWWWFormUrlEncoded(entrylist).some
      mimeType = $enctype
    of FORM_ENCODING_TYPE_MULTIPART:
      multipart = serializeMultipartFormData(entrylist).some
      mimetype = $enctype
    of FORM_ENCODING_TYPE_TEXT_PLAIN:
      body = serializePlainTextFormData(entrylist).some
      mimetype = $enctype
    return newRequest(parsedaction, httpmethod, {"Content-Type": mimetype}, body, multipart).some

  template getActionUrl() =
    return newRequest(parsedaction).some

  case scheme
  of "http", "https":
    if formmethod == FORM_METHOD_GET:
      mutateActionUrl
    else:
      assert formmethod == FORM_METHOD_POST
      submitAsEntityBody
  of "ftp":
    getActionUrl
  of "data":
    if formmethod == FORM_METHOD_GET:
      mutateActionUrl
    else:
      assert formmethod == FORM_METHOD_POST
      getActionUrl

template set_focus(e: Element) =
  if buffer.document.focus != e:
    buffer.document.focus = e
    buffer.reshape = true

template restore_focus =
  if buffer.document.focus != nil:
    buffer.document.focus = nil
    buffer.reshape = true

proc lineInput(buffer: Buffer, s: string) =
  if buffer.input != nil:
    let input = buffer.input
    case input.inputType
    of INPUT_SEARCH:
      input.value = s
      input.invalid = true
      buffer.reshape = true
      if input.form != nil:
        let submitaction = submitForm(input.form, input)
        if submitaction.isSome:
          buffer.writeCommand(OPEN, submitaction.get)
    of INPUT_TEXT, INPUT_PASSWORD:
      input.value = s
      input.invalid = true
      buffer.reshape = true
    of INPUT_FILE:
      let cdir = parseUrl("file://" & getCurrentDir() & DirSep)
      let path = parseUrl(s, cdir)
      if path.issome:
        input.file = path
        input.invalid = true
        buffer.reshape = true
    else: discard
    buffer.input = nil

proc click(buffer: Buffer, cursorx, cursory: int) =
  let clickable = buffer.getCursorClickable(cursorx, cursory)
  if clickable != nil:
    case clickable.tagType
    of TAG_SELECT:
      set_focus clickable
    of TAG_A:
      restore_focus
      let url = parseUrl(HTMLAnchorElement(clickable).href, clickable.document.baseUrl.some)
      if url.issome:
        buffer.writeCommand(OPEN, newRequest(url.get, HTTP_GET))
    of TAG_OPTION:
      let option = HTMLOptionElement(clickable)
      let select = option.select
      if select != nil:
        if buffer.document.focus == select:
          # select option
          if not select.attrb("multiple"):
            for option in select.options:
              option.selected = false
          option.selected = true
          restore_focus
        else:
          # focus on select
          set_focus select
    of TAG_INPUT:
      restore_focus
      let input = HTMLInputElement(clickable)
      case input.inputType
      of INPUT_SEARCH:
        buffer.input = input
        buffer.writeCommand(READ_LINE, "SEARCH: ", input.value, false)
      of INPUT_TEXT, INPUT_PASSWORD:
        buffer.input = input
        buffer.writeCommand(READ_LINE, "TEXT: ", input.value, input.inputType == INPUT_PASSWORD)
      of INPUT_FILE:
        var path = if input.file.issome:
          input.file.get.path.serialize_unicode()
        else:
          ""
        buffer.writeCommand(READ_LINE, "Filename: ", path, false)
      of INPUT_CHECKBOX:
        input.checked = not input.checked
        input.invalid = true
        buffer.reshape = true
      of INPUT_RADIO:
        for radio in input.radiogroup:
          radio.checked = false
          radio.invalid = true
        input.checked = true
        input.invalid = true
        buffer.reshape = true
      of INPUT_RESET:
        if input.form != nil:
          input.form.reset()
          buffer.reshape = true
      of INPUT_SUBMIT, INPUT_BUTTON:
        if input.form != nil:
          let submitaction = submitForm(input.form, input)
          if submitaction.isSome:
            buffer.writeCommand(OPEN, submitaction.get)
      else:
        restore_focus
    else:
      restore_focus

proc readCommand(buffer: Buffer) =
  let istream = buffer.pistream
  let ostream = buffer.postream
  var cmd: BufferCommand
  istream.sread(cmd)
  case cmd
  of LOAD:
    let code = buffer.setupSource()
    buffer.load()
    buffer.writeCommand(LOAD_DONE, code)
  of GOTO_ANCHOR:
    var anchor: string
    istream.sread(anchor)
    if buffer.document != nil and buffer.document.getElementById(anchor) != nil:
      buffer.writeCommand(ANCHOR_FOUND)
    else:
      buffer.writeCommand(ANCHOR_FAIL)
  of RENDER:
    buffer.render()
    buffer.gotoAnchor()
  of GET_LINES:
    var w: Slice[int]
    istream.sread(w)
    if w.b < 0 or w.b > buffer.lines.high:
      w.b = buffer.lines.high
    ostream.swrite(SET_LINES)
    ostream.swrite(buffer.lines.len)
    ostream.swrite(w)
    for y in w:
      ostream.swrite(buffer.lines[y])
    ostream.flush()
  of WINDOW_CHANGE:
    istream.sread(buffer.attrs)
    buffer.windowChange()
  of FIND_PREV_LINK:
    var cx, cy: int
    istream.sread(cx)
    istream.sread(cy)
    let pl = buffer.findPrevLink(cx, cy)
    buffer.writeCommand(JUMP, pl.x, pl.y, 0)
  of FIND_NEXT_LINK:
    var cx, cy: int
    istream.sread(cx)
    istream.sread(cy)
    let nl = buffer.findNextLink(cx, cy)
    buffer.writeCommand(JUMP, nl.x, nl.y, 0)
  of FIND_PREV_MATCH:
    var cx, cy: int
    var regex: Regex
    var wrap: bool
    istream.sread(cx)
    istream.sread(cy)
    istream.sread(regex)
    istream.sread(wrap)
    let match = buffer.findPrevMatch(regex, cx, cy, wrap)
    if match.success:
      buffer.writeCommand(JUMP, match.x, match.y, match.x + match.str.width() - 1)
  of FIND_NEXT_MATCH:
    var cx, cy: int
    var regex: Regex
    var wrap: bool
    istream.sread(cx)
    istream.sread(cy)
    istream.sread(regex)
    istream.sread(wrap)
    let match = buffer.findNextMatch(regex, cx, cy, wrap)
    if match.success:
      buffer.writeCommand(JUMP, match.x, match.y, match.x + match.str.width() - 1)
  of READ_SUCCESS:
    var s: string
    istream.sread(s)
    buffer.lineInput(s)
  of READ_CANCELED:
    buffer.input = nil
  of CLICK:
    var cx, cy: int
    istream.sread(cx)
    istream.sread(cy)
    buffer.click(cx, cy)
  of MOVE_CURSOR:
    var cx, cy: int
    istream.sread(cx)
    istream.sread(cy)
    buffer.updateHover(cx, cy)
  of GET_SOURCE:
    let ssock = initServerSocket()
    buffer.writeCommand(SOURCE_READY)
    let stream = ssock.acceptSocketStream()
    if not buffer.streamclosed:
      buffer.source = buffer.istream.readAll()
      buffer.streamclosed = true
    stream.write(buffer.source)
    stream.close()
    ssock.close()

proc runBuffer(buffer: Buffer, rfd: int) =
  block loop:
    while true:
      let events = buffer.selector.select(-1)
      for event in events:
        if Read in event.events:
          if event.fd == rfd:
            try:
              buffer.readCommand()
            except IOError:
              break loop
          else:
            buffer.load2()
        if Error in event.events:
          if event.fd == rfd:
            break loop
          elif event.fd == buffer.getFd():
            buffer.finishLoad()
      if buffer.reshape:
        buffer.reshape = false
        buffer.render()
        buffer.writeCommand(RESHAPE)
  buffer.pistream.close()
  buffer.postream.close()
  buffer.loader.quit()
  quit(0)

proc launchBuffer*(config: BufferConfig, source: BufferSource,
                   attrs: WindowAttributes, loader: FileLoader,
                   mainproc: Pid) =
  let buffer = new Buffer
  buffer.attrs = attrs
  buffer.windowChange()
  buffer.config = config
  buffer.loader = loader
  buffer.bsource = source
  buffer.selector = newSelector[int]()
  let sstream = connectSocketStream(mainproc, false)
  sstream.swrite(getpid())
  sstream.swrite(BUFFER_READY)
  sstream.flush()
  buffer.pistream = sstream
  buffer.postream = sstream
  let rfd = int(sstream.source.getFd())
  buffer.selector.registerHandle(rfd, {Read}, 0)
  buffer.runBuffer(rfd)