import net
import options
import os
import streams
import tables
import unicode
when defined(posix):
import posix
import buffer/buffer
import buffer/cell
import buffer/container
import config/config
import io/lineedit
import io/request
import io/term
import io/window
import ips/forkserver
import ips/socketstream
import js/javascript
import js/regex
import types/buffersource
import types/color
import types/dispatcher
import types/url
import utils/twtstr
type
LineMode* = enum
NO_LINEMODE, LOCATION, USERNAME, PASSWORD, COMMAND, BUFFER, SEARCH_F,
SEARCH_B, ISEARCH_F, ISEARCH_B
Pager* = ref object
alerts: seq[string]
commandMode*: bool
container*: Container
dispatcher*: Dispatcher
lineedit*: Option[LineEdit]
linemode*: LineMode
username: string
scommand*: string
config: Config
regex: Option[Regex]
iregex: Option[Regex]
reverseSearch: bool
statusgrid*: FixedGrid
tty: File
procmap*: Table[Pid, Container]
unreg*: seq[(Pid, SocketStream)]
icpos: CursorPosition
display: FixedGrid
redraw*: bool
term*: Terminal
iterator containers*(pager: Pager): Container =
if pager.container != nil:
var c = pager.container
while c.parent != nil: c = c.parent
var stack: seq[Container]
stack.add(c)
while stack.len > 0:
yield stack.pop()
for i in countdown(c.children.high, 0):
stack.add(c.children[i])
proc setContainer*(pager: Pager, c: Container) =
pager.container = c
pager.redraw = true
proc cursorDown(pager: Pager) {.jsfunc.} = pager.container.cursorDown()
proc cursorUp(pager: Pager) {.jsfunc.} = pager.container.cursorUp()
proc cursorLeft(pager: Pager) {.jsfunc.} = pager.container.cursorLeft()
proc cursorRight(pager: Pager) {.jsfunc.} = pager.container.cursorRight()
proc cursorLineBegin(pager: Pager) {.jsfunc.} = pager.container.cursorLineBegin()
proc cursorLineEnd(pager: Pager) {.jsfunc.} = pager.container.cursorLineEnd()
proc cursorNextWord(pager: Pager) {.jsfunc.} = pager.container.cursorNextWord()
proc cursorPrevWord(pager: Pager) {.jsfunc.} = pager.container.cursorPrevWord()
proc cursorNextLink(pager: Pager) {.jsfunc.} = pager.container.cursorNextLink()
proc cursorPrevLink(pager: Pager) {.jsfunc.} = pager.container.cursorPrevLink()
proc pageUp(pager: Pager) {.jsfunc.} = pager.container.pageUp()
proc pageDown(pager: Pager) {.jsfunc.} = pager.container.pageDown()
proc pageRight(pager: Pager) {.jsfunc.} = pager.container.pageRight()
proc pageLeft(pager: Pager) {.jsfunc.} = pager.container.pageLeft()
proc halfPageDown(pager: Pager) {.jsfunc.} = pager.container.halfPageDown()
proc halfPageUp(pager: Pager) {.jsfunc.} = pager.container.halfPageUp()
proc cursorFirstLine(pager: Pager) {.jsfunc.} = pager.container.cursorFirstLine()
proc cursorLastLine(pager: Pager) {.jsfunc.} = pager.container.cursorLastLine()
proc cursorTop(pager: Pager) {.jsfunc.} = pager.container.cursorTop()
proc cursorMiddle(pager: Pager) {.jsfunc.} = pager.container.cursorMiddle()
proc cursorBottom(pager: Pager) {.jsfunc.} = pager.container.cursorBottom()
proc cursorLeftEdge(pager: Pager) {.jsfunc.} = pager.container.cursorLeftEdge()
proc cursorVertMiddle(pager: Pager) {.jsfunc.} = pager.container.cursorVertMiddle()
proc cursorRightEdge(pager: Pager) {.jsfunc.} = pager.container.cursorRightEdge()
proc centerLine(pager: Pager) {.jsfunc.} = pager.container.centerLine()
proc scrollDown(pager: Pager) {.jsfunc.} = pager.container.scrollDown()
proc scrollUp(pager: Pager) {.jsfunc.} = pager.container.scrollUp()
proc scrollLeft(pager: Pager) {.jsfunc.} = pager.container.scrollLeft()
proc scrollRight(pager: Pager) {.jsfunc.} = pager.container.scrollRight()
proc reshape(pager: Pager) {.jsfunc.} = pager.container.reshape()
proc searchNext(pager: Pager) {.jsfunc.} =
if pager.regex.issome:
if not pager.reverseSearch:
pager.container.cursorNextMatch(pager.regex.get, true)
else:
pager.container.cursorPrevMatch(pager.regex.get, true)
proc searchPrev(pager: Pager) {.jsfunc.} =
if pager.regex.issome:
if not pager.reverseSearch:
pager.container.cursorPrevMatch(pager.regex.get, true)
else:
pager.container.cursorNextMatch(pager.regex.get, true)
proc setLineEdit(pager: Pager, edit: LineEdit, mode: LineMode) =
pager.lineedit = some(edit)
pager.linemode = mode
proc clearLineEdit(pager: Pager) =
pager.lineedit = none(LineEdit)
func attrs(pager: Pager): WindowAttributes = pager.term.attrs
proc searchForward(pager: Pager) {.jsfunc.} =
pager.setLineEdit(readLine("/", pager.attrs.width, term = pager.term), SEARCH_F)
proc searchBackward(pager: Pager) {.jsfunc.} =
pager.setLineEdit(readLine("?", pager.attrs.width, term = pager.term), SEARCH_B)
proc isearchForward(pager: Pager) {.jsfunc.} =
pager.container.pushCursorPos()
pager.setLineEdit(readLine("/", pager.attrs.width, term = pager.term), ISEARCH_F)
proc isearchBackward(pager: Pager) {.jsfunc.} =
pager.container.pushCursorPos()
pager.setLineEdit(readLine("?", pager.attrs.width, term = pager.term), ISEARCH_B)
proc newPager*(config: Config, attrs: WindowAttributes, dispatcher: Dispatcher): Pager =
let pager = Pager(
dispatcher: dispatcher,
config: config,
display: newFixedGrid(attrs.width, attrs.height - 1),
statusgrid: newFixedGrid(attrs.width),
term: newTerminal(stdout, config, attrs)
)
return pager
proc launchPager*(pager: Pager, tty: File) =
pager.tty = tty
if tty != nil:
pager.term.start(tty)
proc dumpAlerts*(pager: Pager) =
for msg in pager.alerts:
eprint msg
proc quit*(pager: Pager, code = 0) =
pager.term.quit()
pager.dumpAlerts()
proc clearDisplay(pager: Pager) =
pager.display = newFixedGrid(pager.display.width, pager.display.height)
proc buffer(pager: Pager): Container {.jsfget, inline.} = pager.container
proc refreshDisplay(pager: Pager, container = pager.container) =
var r: Rune
var by = 0
pager.clearDisplay()
var hlformat = newFormat()
hlformat.bgcolor = CellColor(rgb: true, rgbcolor: pager.config.hlcolor)
for line in container.ilines(container.fromy ..< min(container.fromy + pager.display.height, container.numLines)):
var w = 0 # width of the row so far
var i = 0 # byte in line.str
# Skip cells till fromx.
while w < container.fromx and i < line.str.len:
fastRuneAt(line.str, i, r)
w += r.width()
let dls = by * pager.display.width # starting position of row in display
# Fill in the gap in case we skipped more cells than fromx mandates (i.e.
# we encountered a double-width character.)
var k = 0
if w > container.fromx:
while k < w - container.fromx:
pager.display[dls + k].str &= ' '
inc k
var cf = line.findFormat(w)
var nf = line.findNextFormat(w)
let startw = w # save this for later
var lan = ""
# Now fill in the visible part of the row.
while i < line.str.len:
let pw = w
fastRuneAt(line.str, i, r)
w += r.width()
if w > container.fromx + pager.display.width:
break # die on exceeding the width limit
if nf.pos != -1 and nf.pos <= pw:
cf = nf
nf = line.findNextFormat(pw)
pager.display[dls + k].str &= r
lan &= r
if cf.pos != -1:
pager.display[dls + k].format = cf.format
let tk = k + r.width()
while k < tk and k < pager.display.width - 1:
inc k
# Finally, override cell formatting for highlighted cells.
let hls = container.findHighlights(container.fromy + by)
let aw = container.width - (startw - container.fromx) # actual width
for hl in hls:
let area = hl.colorArea(container.fromy + by, startw .. startw + aw)
for i in area:
pager.display[dls + i - startw].format = hlformat
inc by
proc clearStatusMessage(pager: Pager) =
pager.statusgrid = newFixedGrid(pager.statusgrid.width)
proc writeStatusMessage(pager: Pager, str: string, format: Format = Format()) =
pager.clearStatusMessage()
var i = 0
for r in str.runes:
i += r.width()
if i >= pager.statusgrid.len:
pager.statusgrid[^1].str = "$"
break
pager.statusgrid[i].str &= r
pager.statusgrid[i].format = format
proc refreshStatusMsg*(pager: Pager) =
let container = pager.container
if container == nil: return
if container.loadinfo != "":
pager.writeStatusMessage(container.loadinfo)
elif pager.alerts.len > 0:
pager.writeStatusMessage(pager.alerts[0])
pager.alerts.delete(0)
else:
var msg = $(container.cursory + 1) & "/" & $container.numLines & " (" &
$container.atPercentOf() & "%) " & "<" & container.getTitle() & ">"
if container.hovertext.len > 0:
msg &= " " & container.hovertext
var format: Format
format.reverse = true
pager.writeStatusMessage(msg, format)
proc drawBuffer*(pager: Pager, container: Container, ostream: Stream) =
var format = newFormat()
for line in container.readLines:
if line.formats.len == 0:
ostream.write(line.str & "\n")
else:
var x = 0
var i = 0
var s = ""
for f in line.formats:
var outstr = ""
#assert f.pos < line.str.width(), "fpos " & $f.pos & "\nstr" & line.str & "\n"
while x < f.pos:
var r: Rune
fastRuneAt(line.str, i, r)
outstr &= r
x += r.width()
s &= outstr
s &= pager.term.processFormat(format, f.format)
s &= line.str.substr(i) & pager.term.processFormat(format, newFormat()) & "\n"
ostream.write(s)
ostream.flush()
proc redraw(pager: Pager) {.jsfunc.} =
pager.redraw = true
proc draw*(pager: Pager) =
pager.term.hideCursor()
if pager.redraw or pager.container != nil and pager.container.redraw:
pager.refreshDisplay()
pager.term.writeGrid(pager.display)
if pager.lineedit.isSome:
pager.term.writeGrid(pager.lineedit.get.generateOutput(), 0, pager.attrs.height - 1)
else:
pager.term.writeGrid(pager.statusgrid, 0, pager.attrs.height - 1)
pager.term.outputGrid()
if pager.lineedit.isSome:
pager.term.setCursor(pager.lineedit.get.getCursorX(), pager.container.attrs.height - 1)
else:
pager.term.setCursor(pager.container.acursorx, pager.container.acursory)
pager.term.showCursor()
pager.term.flush()
pager.redraw = false
pager.container.redraw = false
proc registerContainer*(pager: Pager, container: Container) =
pager.procmap[container.process] = container
proc addContainer*(pager: Pager, container: Container) =
container.parent = pager.container
if pager.container != nil:
pager.container.children.add(container)
pager.registerContainer(container)
pager.setContainer(container)
proc dupeContainer(pager: Pager, container: Container, location: Option[URL]): Container =
return pager.dispatcher.dupeBuffer(container, pager.config, location)
proc dupeBuffer*(pager: Pager, location = none(URL)) {.jsfunc.} =
pager.addContainer(pager.dupeContainer(pager.container, location))
# The prevBuffer and nextBuffer procedures emulate w3m's PREV and NEXT
# commands by traversing the container tree in a depth-first order.
proc prevBuffer*(pager: Pager): bool {.jsfunc.} =
if pager.container == nil:
return false
if pager.container.parent == nil:
return false
let n = pager.container.parent.children.find(pager.container)
assert n != -1, "Container not a child of its parent"
if n > 0:
pager.setContainer(pager.container.parent.children[n - 1])
else:
pager.setContainer(pager.container.parent)
return true
proc nextBuffer*(pager: Pager): bool {.jsfunc.} =
if pager.container == nil:
return false
if pager.container.children.len > 0:
pager.setContainer(pager.container.children[0])
return true
if pager.container.parent == nil:
return false
let n = pager.container.parent.children.find(pager.container)
assert n != -1, "Container not a child of its parent"
if n < pager.container.parent.children.high:
pager.setContainer(pager.container.parent.children[n + 1])
return true
return false
proc alert*(pager: Pager, msg: string) {.jsfunc.} =
pager.alerts.add(msg)
proc lineInfo(pager: Pager) {.jsfunc.} =
pager.alert(pager.container.lineInfo())
proc deleteContainer(pager: Pager, container: Container) =
if container.parent == nil and
container.children.len == 0 and
container != pager.container:
return
if container.sourcepair != nil:
container.sourcepair.sourcepair = nil
container.sourcepair = nil
if container.parent != nil:
let parent = container.parent
let n = parent.children.find(container)
assert n != -1, "Container not a child of its parent"
for i in countdown(container.children.high, 0):
let child = container.children[i]
child.parent = container.parent
parent.children.insert(child, n + 1)
parent.children.delete(n)
if container == pager.container:
pager.setContainer(parent)
elif container.children.len > 0:
let parent = container.children[0]
parent.parent = nil
for i in 1..container.children.high:
container.children[i].parent = parent
parent.children.add(container.children[i])
if container == pager.container:
pager.setContainer(parent)
else:
for child in container.children:
child.parent = nil
if container == pager.container:
pager.setContainer(nil)
container.parent = nil
container.children.setLen(0)
pager.unreg.add((container.process, SocketStream(container.istream)))
pager.dispatcher.forkserver.removeChild(container.process)
proc discardBuffer*(pager: Pager) {.jsfunc.} =
if pager.container == nil or pager.container.parent == nil and
pager.container.children.len == 0:
pager.alert("Cannot discard last buffer!")
else:
pager.deleteContainer(pager.container)
proc toggleSource*(pager: Pager) {.jsfunc.} =
if pager.container.sourcepair != nil:
pager.setContainer(pager.container.sourcepair)
else:
let contenttype = if pager.container.contenttype.get("") == "text/html":
some("text/plain")
else:
some("text/html")
let container = pager.dispatcher.dupeBuffer(pager.container, pager.config, contenttype = contenttype)
container.sourcepair = pager.container
pager.container.sourcepair = container
pager.addContainer(container)
proc windowChange*(pager: Pager, attrs: WindowAttributes) =
pager.term.windowChange(attrs)
pager.display = newFixedGrid(attrs.width, attrs.height - 1)
pager.statusgrid = newFixedGrid(attrs.width)
for container in pager.containers:
container.windowChange(attrs)
# Load request in a new buffer.
proc gotoURL*(pager: Pager, request: Request, prevurl = none(URL), ctype = none(string), replace: Container = nil) =
if prevurl.isnone or not prevurl.get.equals(request.url, true) or
request.url.hash == "" or request.httpmethod != HTTP_GET:
# Basically, we want to reload the page *only* when
# a) we force a reload (by setting prevurl to none)
# b) or the new URL isn't just the old URL + an anchor
# I think this makes navigation pretty natural, or at least very close to
# what other browsers do. Still, it would be nice if we got some visual
# feedback on what is actually going to happen when typing a URL; TODO.
let source = BufferSource(
t: LOAD_REQUEST,
request: request,
contenttype: ctype,
location: request.url
)
let container = pager.dispatcher.newBuffer(pager.config, source)
container.replace = replace
pager.addContainer(container)
else:
pager.container.redirect = some(request.url)
pager.container.gotoAnchor(request.url.anchor)
# When the user has passed a partial URL as an argument, they might've meant
# either:
# * file://$PWD/<file>
# * https://<url>
# So we attempt to load both, and see what works.
# (TODO: make this optional)
proc loadURL*(pager: Pager, url: string, ctype = none(string)) =
let firstparse = parseURL(url)
if firstparse.issome:
let prev = if pager.container != nil:
some(pager.container.source.location)
else:
none(URL)
pager.gotoURL(newRequest(firstparse.get), prev, ctype)
return
var urls: seq[URL]
let pageurl = parseURL("https://" & url)
if pageurl.isSome: # attempt to load remote page
urls.add(pageurl.get)
let cdir = parseURL("file://" & getCurrentDir() & DirSep)
let purl = percentEncode(url, LocalPathPercentEncodeSet)
if purl != url:
let newurl = parseURL(purl, cdir)
if newurl.isSome:
urls.add(newurl.get)
let localurl = parseURL(url, cdir)
if localurl.isSome: # attempt to load local file
urls.add(localurl.get)
if urls.len == 0:
pager.alert("Invalid URL " & url)
else:
let prevc = pager.container
pager.gotoURL(newRequest(urls.pop()), ctype = ctype)
if pager.container != prevc:
pager.container.retry = urls
proc readPipe0*(pager: Pager, ctype: Option[string], fd: FileHandle, location: Option[URL], title: string): Container =
let source = BufferSource(
t: LOAD_PIPE,
fd: fd,
contenttype: some(ctype.get("text/plain")),
location: location.get(newURL("file://-"))
)
let container = pager.dispatcher.newBuffer(pager.config, source, title)
return container
proc readPipe*(pager: Pager, ctype: Option[string], fd: FileHandle) =
let container = pager.readPipe0(ctype, fd, none(URL), "*pipe*")
pager.addContainer(container)
proc command(pager: Pager) {.jsfunc.} =
pager.setLineEdit(readLine("COMMAND: ", pager.attrs.width, term = pager.term), COMMAND)
proc commandMode(pager: Pager) {.jsfunc.} =
pager.commandmode = true
pager.command()
proc updateReadLineISearch(pager: Pager, linemode: LineMode) =
let lineedit = pager.lineedit.get
case lineedit.state
of CANCEL:
pager.iregex = none(Regex)
pager.container.popCursorPos()
pager.container.clearSearchHighlights()
of EDIT:
let x = $lineedit.news
if x != "": pager.iregex = compileSearchRegex(x)
pager.container.popCursorPos(true)
if pager.iregex.isSome:
if linemode == ISEARCH_F:
pager.container.cursorNextMatch(pager.iregex.get, true)
else:
pager.container.cursorPrevMatch(pager.iregex.get, true)
pager.container.hlon = true
if not pager.container.redraw:
#TODO this is dumb
pager.container.requestLines()
pager.container.pushCursorPos()
pager.redraw = true
of FINISH:
if pager.iregex.isSome:
pager.regex = pager.iregex
pager.reverseSearch = linemode == ISEARCH_B
pager.container.clearSearchHighlights()
pager.redraw = true
proc updateReadLine*(pager: Pager) =
let lineedit = pager.lineedit.get
template s: string = $lineedit.news
if pager.linemode in {ISEARCH_F, ISEARCH_B}:
pager.updateReadLineISearch(pager.linemode)
else:
case lineedit.state
of EDIT: return
of FINISH:
case pager.linemode
of LOCATION: pager.loadURL(s)
of USERNAME:
pager.username = s
pager.setLineEdit(readLine("Password: ", pager.attrs.width, hide = true, term = pager.term), PASSWORD)
of PASSWORD:
let url = newURL(pager.container.source.location)
url.username = pager.username
url.password = s
pager.username = ""
pager.gotoURL(newRequest(url), some(pager.container.source.location), replace = pager.container)
of COMMAND:
pager.scommand = s
if pager.commandmode:
pager.command()
of BUFFER: pager.container.readSuccess(s)
of SEARCH_F:
let x = s
if x != "": pager.regex = compileSearchRegex(x)
pager.reverseSearch = false
pager.searchNext()
of SEARCH_B:
let x = s
if x != "": pager.regex = compileSearchRegex(x)
pager.reverseSearch = true
pager.searchPrev()
else: discard
of CANCEL:
case pager.linemode
of USERNAME: pager.discardBuffer()
of PASSWORD:
pager.username = ""
pager.discardBuffer()
of BUFFER: pager.container.readCanceled()
of COMMAND: pager.commandmode = false
else: discard
if lineedit.state in {CANCEL, FINISH}:
if pager.lineedit.get == lineedit:
pager.clearLineEdit()
# Open a URL prompt and visit the specified URL.
proc changeLocation(pager: Pager) {.jsfunc.} =
var url = pager.container.source.location.serialize()
pager.setLineEdit(readLine("URL: ", pager.attrs.width, current = url, term = pager.term), LOCATION)
# Reload the page in a new buffer, then kill the previous buffer.
proc reload(pager: Pager) {.jsfunc.} =
pager.gotoURL(newRequest(pager.container.source.location), none(URL), pager.container.contenttype, pager.container)
proc click(pager: Pager) {.jsfunc.} =
pager.container.click()
proc authorize*(pager: Pager) =
pager.setLineEdit(readLine("Username: ", pager.attrs.width, term = pager.term), USERNAME)
proc handleEvent*(pager: Pager, container: Container): bool =
var event: ContainerEvent
try:
event = container.handleEvent()
except IOError:
return false
case event.t
of FAIL:
pager.deleteContainer(container)
if container.retry.len > 0:
pager.gotoURL(newRequest(container.retry.pop()), ctype = container.contenttype)
else:
pager.alert("Couldn't load " & $container.source.location & " (error code " & $container.code & ")")
pager.refreshStatusMsg()
if pager.container == nil:
return false
of SUCCESS:
container.reshape()
pager.container.loadinfo = ""
if container.replace != nil:
container.children.add(container.replace.children)
for child in container.children:
child.parent = container
container.replace.children.setLen(0)
if container.replace.parent != nil:
container.parent = container.replace.parent
let n = container.replace.parent.children.find(container.replace)
assert n != -1, "Container not a child of its parent"
container.parent.children[n] = container
if pager.container == container.replace:
pager.setContainer(container)
of NEEDS_AUTH:
if pager.container == container:
pager.authorize()
of REDIRECT:
let redirect = container.redirect.get
pager.alert("Redirecting to " & $redirect)
pager.gotoURL(newRequest(redirect), some(pager.container.source.location), replace = pager.container)
of ANCHOR:
pager.addContainer(pager.dupeContainer(container, container.redirect))
of NO_ANCHOR:
pager.alert("Couldn't find anchor " & container.redirect.get.anchor)
of UPDATE:
if container == pager.container:
pager.redraw = true
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 OPEN:
pager.gotoURL(event.request, some(container.source.location))
of INVALID_COMMAND: discard
of STATUS:
if pager.container == container:
pager.refreshStatusMsg()
of NO_EVENT: discard
return true
proc addPagerModule*(ctx: JSContext) =
ctx.registerType(Pager)