import std/deques import std/net import std/options import std/os import std/osproc import std/posix import std/selectors import std/sets import std/tables import chagashi/charset import config/chapath import config/config import config/mailcap import img/bitmap import io/bufreader import io/dynstream import io/promise import io/stdio import io/tempfile import io/urlfilter import js/timeout import layout/renderdocument import loader/connecterror import loader/headers import loader/loader import loader/request import local/container import local/lineedit import local/term import monoucha/fromjs import monoucha/javascript import monoucha/jserror import monoucha/jsregex import monoucha/jstypes import monoucha/jsutils import monoucha/libregexp import monoucha/quickjs import monoucha/tojs import server/buffer import server/forkserver import types/blob import types/cell import types/color import types/cookie import types/opt import types/url import types/winattrs import utils/luwrap import utils/mimeguess import utils/regexutils import utils/strwidth import utils/twtstr import utils/twtuni type LineMode* = enum lmLocation = "URL: " lmUsername = "Username: " lmPassword = "Password: " lmCommand = "COMMAND: " lmBuffer = "(BUFFER) " lmSearchF = "/" lmSearchB = "?" lmISearchF = "/" lmISearchB = "?" lmGotoLine = "Goto line: " lmDownload = "(Download)Save file to: " lmBufferFile = "(Upload)Filename: " # fdin is the original fd; fdout may be the same, or different if mailcap # is used. ProcMapItem = object container*: Container fdin*: FileHandle fdout*: FileHandle istreamOutputId*: int ostreamOutputId*: int PagerAlertState = enum pasNormal, pasAlertOn, pasLoadInfo ContainerConnectionState = enum ccsBeforeResult, ccsBeforeStatus, ccsBeforeHeaders ConnectingContainerItem = ref object state: ContainerConnectionState container: Container stream*: SocketStream res: int outputId: int status: uint16 LineData = ref object of RootObj LineDataDownload = ref object of LineData outputId: int stream: DynStream LineDataAuth = ref object of LineData url: URL NavDirection = enum ndPrev = "prev" ndNext = "next" ndPrevSibling = "prev-sibling" ndNextSibling = "next-sibling" ndParent = "parent" ndFirstChild ndAny = "any" Surface = object redraw: bool grid: FixedGrid Pager* = ref object alertState: PagerAlertState alerts*: seq[string] askcharpromise*: Promise[string] askcursor: int askpromise*: Promise[bool] askprompt: string commandMode {.jsget.}: bool config*: Config connectingContainers*: seq[ConnectingContainerItem] container*: Container cookiejars: Table[string, CookieJar] devRandom: PosixStream display: Surface forkserver*: ForkServer formRequestMap*: Table[string, FormRequestType] hasload*: bool # has a page been successfully loaded since startup? inputBuffer*: string # currently uninterpreted characters iregex: Result[Regex, string] isearchpromise: EmptyPromise jsctx: JSContext lineData: LineData lineedit*: LineEdit linehist: array[LineMode, LineHistory] linemode: LineMode loader*: FileLoader luctx: LUContext navDirection {.jsget.}: NavDirection notnum*: bool # has a non-numeric character been input already? numload*: int # number of pages currently being loaded precnum*: int32 # current number prefix (when vi-numeric-prefix is true) procmap*: seq[ProcMapItem] refreshAllowed: HashSet[string] regex: Opt[Regex] reverseSearch: bool scommand*: string selector*: Selector[int] status: Surface term*: Terminal timeouts*: TimeoutState unreg*: seq[Container] jsDestructor(Pager) # Forward declarations proc alert*(pager: Pager; msg: string) template attrs(pager: Pager): WindowAttributes = pager.term.attrs func loaderPid(pager: Pager): int64 {.jsfget.} = int64(pager.loader.process) func getRoot(container: Container): Container = var c = container while c.parent != nil: c = c.parent return c func bufWidth(pager: Pager): int = return pager.attrs.width func bufHeight(pager: Pager): int = return pager.attrs.height - 1 # depth-first descendant iterator iterator descendants(parent: Container): Container {.inline.} = var stack = newSeqOfCap[Container](parent.children.len) for i in countdown(parent.children.high, 0): stack.add(parent.children[i]) while stack.len > 0: let c = stack.pop() # add children first, so that deleteContainer works on c for i in countdown(c.children.high, 0): stack.add(c.children[i]) yield c iterator containers*(pager: Pager): Container {.inline.} = if pager.container != nil: let root = getRoot(pager.container) yield root for c in root.descendants: yield c proc clearDisplay(pager: Pager) = pager.display = Surface( grid: newFixedGrid(pager.bufWidth, pager.bufHeight), redraw: true ) proc clearStatus(pager: Pager) = pager.status = Surface( grid: newFixedGrid(pager.attrs.width), redraw: true ) proc setContainer*(pager: Pager; c: Container) {.jsfunc.} = if pager.term.imageMode != imNone and pager.container != nil: pager.container.cachedImages.setLen(0) pager.container = c if c != nil: c.queueDraw() pager.term.setTitle(c.getTitle()) proc hasprop(ctx: JSContext; pager: Pager; s: string): bool {.jshasprop.} = result = false if pager.container != nil: let cval = toJS(ctx, pager.container) let val = JS_GetPropertyStr(ctx, cval, s) if val != JS_UNDEFINED: result = true JS_FreeValue(ctx, val) proc reflect(ctx: JSContext; this_val: JSValue; argc: cint; argv: ptr UncheckedArray[JSValue]; magic: cint; func_data: ptr UncheckedArray[JSValue]): JSValue {.cdecl.} = let obj = func_data[0] let fun = func_data[1] return JS_Call(ctx, fun, obj, argc, argv) proc getter(ctx: JSContext; pager: Pager; a: JSAtom): JSValue {.jsgetprop.} = if pager.container != nil: let cval = ctx.toJS(pager.container) let val = JS_GetProperty(ctx, cval, a) if JS_IsFunction(ctx, val): let func_data = @[cval, val] let fun = JS_NewCFunctionData(ctx, reflect, 1, 0, 2, func_data.toJSValueArray()) JS_FreeValue(ctx, cval) JS_FreeValue(ctx, val) return fun JS_FreeValue(ctx, cval) if not JS_IsUndefined(val): return val return JS_NULL proc searchNext(pager: Pager; n = 1) {.jsfunc.} = if pager.regex.isSome: let wrap = pager.config.search.wrap pager.container.markPos0() if not pager.reverseSearch: pager.container.cursorNextMatch(pager.regex.get, wrap, true, n) else: pager.container.cursorPrevMatch(pager.regex.get, wrap, true, n) pager.container.markPos() else: pager.alert("No previous regular expression") proc searchPrev(pager: Pager; n = 1) {.jsfunc.} = if pager.regex.isSome: let wrap = pager.config.search.wrap pager.container.markPos0() if not pager.reverseSearch: pager.container.cursorPrevMatch(pager.regex.get, wrap, true, n) else: pager.container.cursorNextMatch(pager.regex.get, wrap, true, n) pager.container.markPos() else: pager.alert("No previous regular expression") proc getLineHist(pager: Pager; mode: LineMode): LineHistory = if pager.linehist[mode] == nil: pager.linehist[mode] = newLineHistory() return pager.linehist[mode] proc setLineEdit(pager: Pager; mode: LineMode; current = ""; hide = false; extraPrompt = "") = let hist = pager.getLineHist(mode) if pager.term.isatty() and pager.config.input.use_mouse: pager.term.disableMouse() pager.lineedit = readLine($mode & extraPrompt, current, pager.attrs.width, {}, hide, hist, pager.luctx) pager.linemode = mode proc clearLineEdit(pager: Pager) = pager.lineedit = nil if pager.term.isatty() and pager.config.input.use_mouse: pager.term.enableMouse() proc searchForward(pager: Pager) {.jsfunc.} = pager.setLineEdit(lmSearchF) proc searchBackward(pager: Pager) {.jsfunc.} = pager.setLineEdit(lmSearchB) proc isearchForward(pager: Pager) {.jsfunc.} = pager.container.pushCursorPos() pager.isearchpromise = newResolvedPromise() pager.container.markPos0() pager.setLineEdit(lmISearchF) proc isearchBackward(pager: Pager) {.jsfunc.} = pager.container.pushCursorPos() pager.isearchpromise = newResolvedPromise() pager.container.markPos0() pager.setLineEdit(lmISearchB) proc gotoLine(ctx: JSContext; pager: Pager; val = JS_UNDEFINED): Opt[void] {.jsfunc.} = var n: int if ctx.fromJS(val, n).isSome: pager.container.gotoLine(n) elif JS_IsUndefined(val): pager.setLineEdit(lmGotoLine) else: var s: string ?ctx.fromJS(val, s) pager.container.gotoLine(s) return ok() proc dumpAlerts*(pager: Pager) = for msg in pager.alerts: stderr.write("cha: " & msg & '\n') proc quit*(pager: Pager) = pager.term.quit() pager.dumpAlerts() proc newPager*(config: Config; forkserver: ForkServer; ctx: JSContext; alerts: seq[string]): Pager = return Pager( config: config, forkserver: forkserver, term: newTerminal(stdout, config), alerts: alerts, jsctx: ctx, luctx: LUContext() ) proc genClientKey(pager: Pager): ClientKey = var key: ClientKey pager.devRandom.recvDataLoop(key) return key proc addLoaderClient*(pager: Pager; pid: int; config: LoaderClientConfig; clonedFrom = -1): ClientKey = var key = pager.genClientKey() while unlikely(not pager.loader.addClient(key, pid, config, clonedFrom)): key = pager.genClientKey() return key proc setLoader*(pager: Pager; loader: FileLoader) = pager.devRandom = newPosixStream("/dev/urandom", O_RDONLY, 0) pager.loader = loader let config = LoaderClientConfig( defaultHeaders: newHeaders(pager.config.network.default_headers), proxy: pager.config.network.proxy, filter: newURLFilter(default = true), ) loader.key = pager.addLoaderClient(pager.loader.clientPid, config) proc launchPager*(pager: Pager; istream: PosixStream; selector: Selector[int]) = pager.selector = selector case pager.term.start(istream) of tsrSuccess: discard of tsrDA1Fail: pager.alert("Failed to query DA1, please set display.query-da1 = false") pager.clearDisplay() pager.clearStatus() proc buffer(pager: Pager): Container {.jsfget, inline.} = return pager.container # Note: this function does not work correctly if start < i of last written char proc writeStatusMessage(pager: Pager; str: string; format = Format(); start = 0; maxwidth = -1; clip = '$'): int {.discardable.} = var maxwidth = maxwidth if maxwidth == -1: maxwidth = pager.status.grid.len var i = start let e = min(start + maxwidth, pager.status.grid.width) if i >= e: return i pager.status.redraw = true for u in str.points: let w = u.width() if i + w >= e: pager.status.grid[i].format = format pager.status.grid[i].str = $clip inc i # Note: we assume `clip' is 1 cell wide break if u.isControlChar(): pager.status.grid[i].str = "^" pager.status.grid[i + 1].str = $getControlLetter(char(u)) pager.status.grid[i + 1].format = format else: pager.status.grid[i].str = u.toUTF8() pager.status.grid[i].format = format i += w result = i var def = Format() while i < e: pager.status.grid[i].str = "" pager.status.grid[i].format = def inc i # Note: should only be called directly after user interaction. proc refreshStatusMsg*(pager: Pager) = let container = pager.container if container == nil: return if pager.askpromise != nil: return if pager.precnum != 0: pager.writeStatusMessage($pager.precnum & pager.inputBuffer) elif pager.inputBuffer != "": pager.writeStatusMessage(pager.inputBuffer) elif pager.alerts.len > 0: pager.alertState = pasAlertOn pager.writeStatusMessage(pager.alerts[0]) pager.alerts.delete(0) else: var format = Format(flags: {ffReverse}) pager.alertState = pasNormal container.clearHover() var msg = $(container.cursory + 1) & "/" & $container.numLines & " (" & $container.atPercentOf() & "%)" let mw = pager.writeStatusMessage(msg, format) let title = " <" & container.getTitle() & ">" let hover = container.getHoverText() if hover.len == 0: pager.writeStatusMessage(title, format, mw) else: let hover2 = " " & hover let maxwidth = pager.status.grid.width - hover2.width() - mw let tw = pager.writeStatusMessage(title, format, mw, maxwidth, '>') pager.writeStatusMessage(hover2, format, tw) # Call refreshStatusMsg if no alert is being displayed on the screen. # Alerts take precedence over load info, but load info is preserved when no # pending alerts exist. proc showAlerts*(pager: Pager) = if (pager.alertState == pasNormal or pager.alertState == pasLoadInfo and pager.alerts.len > 0) and pager.inputBuffer == "" and pager.precnum == 0: pager.refreshStatusMsg() proc drawBuffer*(pager: Pager; container: Container; ofile: File) = var format = Format() container.readLines(proc(line: SimpleFlexibleLine) = if line.formats.len == 0: ofile.write(line.str & "\n") else: var x = 0 var w = 0 var i = 0 var s = "" for f in line.formats: let si = i while x < f.pos: let u = line.str.nextUTF8(i) x += u.width() let outstr = line.str.substr(si, i - 1) s &= pager.term.processOutputString(outstr, w) s &= pager.term.processFormat(format, f.format) if i < line.str.len: s &= pager.term.processOutputString(line.str.substr(i), w) s &= pager.term.processFormat(format, Format()) & "\n" ofile.write(s)) ofile.flushFile() proc redraw(pager: Pager) {.jsfunc.} = pager.term.clearCanvas() pager.display.redraw = true pager.status.redraw = true if pager.container != nil: pager.container.redraw = true if pager.container.select != nil: pager.container.select.redraw = true proc loadCachedImage(pager: Pager; container: Container; image: PosBitmap; offx, erry, dispw: int) = let bmp = image.bmp let request = newRequest(newURL("cache:" & $bmp.cacheId).get) let cachedImage = CachedImage( bmp: bmp, width: image.width, height: image.height, offx: offx, erry: erry, dispw: dispw ) pager.loader.shareCachedItem(bmp.cacheId, pager.loader.clientPid, container.process) let imageMode = pager.term.imageMode pager.loader.fetch(request).then(proc(res: JSResult[Response]): Promise[JSResult[Response]] = if res.isNone: pager.loader.removeCachedItem(bmp.cacheId) return let response = res.get let headers = newHeaders() if image.width != bmp.width or image.height != bmp.height: headers.add("Cha-Image-Target-Dimensions", $image.width & 'x' & $image.height) let request = newRequest( newURL("img-codec+" & bmp.contentType.after('/') & ":decode").get, httpMethod = hmPost, headers = headers, body = RequestBody(t: rbtOutput, outputId: response.outputId), ) let r = pager.loader.fetch(request) response.resume() response.close() return r ).then(proc(res: JSResult[Response]) = if res.isNone: pager.loader.removeCachedItem(bmp.cacheId) return let response = res.get let headers = newHeaders({ "Cha-Image-Dimensions": $image.width & 'x' & $image.height }) var url: URL = nil case imageMode of imSixel: url = newURL("img-codec+x-sixel:encode").get headers.add("Cha-Image-Sixel-Halfdump", "1") headers.add("Cha-Image-Sixel-Palette", $pager.term.sixelRegisterNum) headers.add("Cha-Image-Background-Color", $pager.term.defaultBackground) headers.add("Cha-Image-Offset", $offx & 'x' & $erry) headers.add("Cha-Image-Crop-Width", $dispw) of imKitty: url = newURL("img-codec+png:encode").get of imNone: assert false let request = newRequest( url, httpMethod = hmPost, headers = headers, body = RequestBody(t: rbtOutput, outputId: response.outputId), ) let r = pager.loader.fetch(request) response.resume() response.close() r.then(proc(res: JSResult[Response]): Promise[JSResult[Blob]] = if res.isNone: let p = newPromise[JSResult[Blob]]() p.resolve(JSResult[Blob].err(res.error)) return p return res.get.blob() ).then(proc(res: JSResult[Blob]) = if res.isSome: container.redraw = true cachedImage.data = res.get cachedImage.loaded = true pager.loader.removeCachedItem(bmp.cacheId) ) ) container.cachedImages.add(cachedImage) proc initImages(pager: Pager; container: Container) = var newImages: seq[CanvasImage] = @[] for image in container.images: var erry = 0 var offx = 0 var dispw = 0 if pager.term.imageMode == imSixel: let xpx = (image.x - container.fromx) * pager.attrs.ppc offx = -min(xpx, 0) let maxwpx = pager.bufWidth * pager.attrs.ppc let width = min(image.width - offx, pager.term.sixelMaxWidth) + offx dispw = min(width + xpx, maxwpx) - xpx let ypx = (image.y - container.fromy) * pager.attrs.ppl erry = -min(ypx, 0) mod 6 if dispw <= offx: continue let cached = container.findCachedImage(image, offx, erry, dispw) let imageId = image.bmp.imageId if cached == nil: pager.loadCachedImage(container, image, offx, erry, dispw) continue if not cached.loaded: continue # loading let canvasImage = pager.term.loadImage(cached.data, container.process, imageId, image.x - container.fromx, image.y - container.fromy, image.width, image.height, image.x, image.y, pager.bufWidth, pager.bufHeight, erry, offx, dispw) if canvasImage != nil: newImages.add(canvasImage) pager.term.clearImages(pager.bufHeight) pager.term.canvasImages = newImages proc draw*(pager: Pager) = var redraw = false var imageRedraw = false let container = pager.container if container != nil: if container.redraw: pager.clearDisplay() let hlcolor = cellColor(pager.config.display.highlight_color) container.drawLines(pager.display.grid, hlcolor) if pager.config.display.highlight_marks: container.highlightMarks(pager.display.grid, hlcolor) container.redraw = false pager.display.redraw = true imageRedraw = true if container.select != nil: container.select.redraw = true if (let select = container.select; select != nil and select.redraw): select.drawSelect(pager.display.grid) select.redraw = false pager.display.redraw = true if pager.display.redraw: pager.term.writeGrid(pager.display.grid) pager.display.redraw = false redraw = true if pager.askpromise != nil or pager.askcharpromise != nil: pager.term.writeGrid(pager.status.grid, 0, pager.attrs.height - 1) pager.status.redraw = false redraw = true elif pager.lineedit != nil: if pager.lineedit.redraw: let x = pager.lineedit.generateOutput() pager.term.writeGrid(x, 0, pager.attrs.height - 1) pager.lineedit.redraw = false redraw = true elif pager.status.redraw: pager.term.writeGrid(pager.status.grid, 0, pager.attrs.height - 1) pager.status.redraw = false redraw = true if imageRedraw and pager.term.imageMode != imNone: # init images only after term canvas has been finalized pager.initImages(container) if redraw: pager.term.hideCursor() pager.term.outputGrid() if pager.term.imageMode != imNone: pager.term.outputImages() if pager.askpromise != nil: pager.term.setCursor(pager.askcursor, pager.attrs.height - 1) elif pager.lineedit != nil: pager.term.setCursor(pager.lineedit.getCursorX(), pager.attrs.height - 1) elif container != nil: if (let select = container.select; select != nil): pager.term.setCursor(select.getCursorX(), select.getCursorY()) else: pager.term.setCursor(container.acursorx, container.acursory) if redraw: pager.term.showCursor() pager.term.flush() proc writeAskPrompt(pager: Pager; s = "") = let maxwidth = pager.status.grid.width - s.len let i = pager.writeStatusMessage(pager.askprompt, maxwidth = maxwidth) pager.askcursor = pager.writeStatusMessage(s, start = i) proc ask(pager: Pager; prompt: string): Promise[bool] {.jsfunc.} = pager.askprompt = prompt pager.writeAskPrompt(" (y/n)") pager.askpromise = Promise[bool]() return pager.askpromise proc askChar(pager: Pager; prompt: string): Promise[string] {.jsfunc.} = pager.askprompt = prompt pager.writeAskPrompt() pager.askcharpromise = Promise[string]() return pager.askcharpromise proc fulfillAsk*(pager: Pager; y: bool) = pager.askpromise.resolve(y) pager.askpromise = nil pager.askprompt = "" proc fulfillCharAsk*(pager: Pager; s: string) = pager.askcharpromise.resolve(s) pager.askcharpromise = nil pager.askprompt = "" proc addContainer*(pager: Pager; container: Container) = container.parent = pager.container if pager.container != nil: pager.container.children.insert(container, 0) pager.setContainer(container) proc onSetLoadInfo(pager: Pager; container: Container) = if pager.alertState != pasAlertOn: if container.loadinfo == "": pager.alertState = pasNormal else: pager.writeStatusMessage(container.loadinfo) pager.alertState = pasLoadInfo proc newContainer(pager: Pager; bufferConfig: BufferConfig; loaderConfig: LoaderClientConfig; request: Request; title = ""; redirectDepth = 0; flags = {cfCanReinterpret, cfUserRequested}; contentType = none(string); charsetStack: seq[Charset] = @[]; url = request.url): Container = let stream = pager.loader.startRequest(request, loaderConfig) pager.loader.registerFun(stream.fd) let cacheId = if request.url.scheme == "cache": parseInt32(request.url.pathname).get(-1) else: -1 let container = newContainer( bufferConfig, loaderConfig, url, request, pager.luctx, pager.term.attrs, title, redirectDepth, flags, contentType, charsetStack, cacheId, pager.config ) pager.connectingContainers.add(ConnectingContainerItem( state: ccsBeforeResult, container: container, stream: stream )) pager.onSetLoadInfo(container) return container proc newContainerFrom(pager: Pager; container: Container; contentType: string): Container = let url = newURL("cache:" & $container.cacheId).get return pager.newContainer( container.config, container.loaderConfig, newRequest(url), contentType = some(contentType), charsetStack = container.charsetStack, url = container.url ) func findConnectingContainer*(pager: Pager; fd: int): int = for i, item in pager.connectingContainers: if item.stream.fd == fd: return i -1 func findConnectingContainer*(pager: Pager; container: Container): int = for i, item in pager.connectingContainers: if item.container == container: return i -1 func findProcMapItem*(pager: Pager; pid: int): int = for i, item in pager.procmap: if item.container.process == pid: return i -1 proc dupeBuffer(pager: Pager; container: Container; url: URL) = let p = container.clone(url, pager.loader) if p == nil: pager.alert("Failed to duplicate buffer.") else: p.then(proc(container: Container) = if container == nil: pager.alert("Failed to duplicate buffer.") else: pager.addContainer(container) pager.procmap.add(ProcMapItem( container: container, fdin: -1, fdout: -1, istreamOutputId: -1, ostreamOutputId: -1 )) ) proc dupeBuffer(pager: Pager) {.jsfunc.} = pager.dupeBuffer(pager.container, pager.container.url) func findPrev(container: Container): Container = if container.parent == nil: return nil let n = container.parent.children.find(container) assert n != -1, "Container not a child of its parent" if n == 0: return container.parent var container = container.parent.children[n - 1] while container.children.len > 0: container = container.children[^1] return container func findNext(container: Container): Container = if container.children.len > 0: return container.children[0] var container = container while container.parent != nil: let n = container.parent.children.find(container) assert n != -1, "Container not a child of its parent" if n < container.parent.children.high: return container.parent.children[n + 1] container = container.parent return nil func findPrevSibling(container: Container): Container = if container.parent == nil: return nil var n = container.parent.children.find(container) assert n != -1, "Container not a child of its parent" if n == 0: n = container.parent.children.len return container.parent.children[n - 1] func findNextSibling(container: Container): Container = if container.parent == nil: return nil var n = container.parent.children.find(container) assert n != -1, "Container not a child of its parent" if n == container.parent.children.high: n = -1 return container.parent.children[n + 1] func findParent(container: Container): Container = return container.parent func findFirstChild(container: Container): Container = if container.children.len == 0: return nil return container.children[0] func findAny(container: Container): Container = let prev = container.findPrev() if prev != nil: return prev return container.findNext() func opposite(dir: NavDirection): NavDirection = const Map = [ ndPrev: ndNext, ndNext: ndPrev, ndPrevSibling: ndNextSibling, ndNextSibling: ndPrevSibling, ndParent: ndFirstChild, ndFirstChild: ndParent, ndAny: ndAny ] return Map[dir] func find(container: Container; dir: NavDirection): Container = return case dir of ndPrev: container.findPrev() of ndNext: container.findNext() of ndPrevSibling: container.findPrevSibling() of ndNextSibling: container.findNextSibling() of ndParent: container.findParent() of ndFirstChild: container.findFirstChild() of ndAny: container.findAny() # 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.} = pager.navDirection = ndPrev if pager.container == nil: return false let prev = pager.container.findPrev() if prev == nil: return false pager.setContainer(prev) return true proc nextBuffer*(pager: Pager): bool {.jsfunc.} = pager.navDirection = ndNext if pager.container == nil: return false let next = pager.container.findNext() if next == nil: return false pager.setContainer(next) return true proc parentBuffer(pager: Pager): bool {.jsfunc.} = pager.navDirection = ndParent if pager.container == nil: return false let parent = pager.container.findParent() if parent == nil: return false pager.setContainer(parent) return true proc prevSiblingBuffer(pager: Pager): bool {.jsfunc.} = pager.navDirection = ndPrevSibling if pager.container == nil: return false if pager.container.parent == nil: return false var n = pager.container.parent.children.find(pager.container) assert n != -1, "Container not a child of its parent" if n == 0: n = pager.container.parent.children.len pager.setContainer(pager.container.parent.children[n - 1]) return true proc nextSiblingBuffer(pager: Pager): bool {.jsfunc.} = pager.navDirection = ndNextSibling if pager.container == nil: return false if pager.container.parent == nil: return false var 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: n = -1 pager.setContainer(pager.container.parent.children[n + 1]) return true proc alert*(pager: Pager; msg: string) {.jsfunc.} = pager.alerts.add(msg) # replace target with container in the tree proc replace*(pager: Pager; target, container: Container) = let n = target.children.find(container) if n != -1: target.children.delete(n) container.parent = nil let n2 = container.children.find(target) if n2 != -1: container.children.delete(n2) target.parent = nil container.children.add(target.children) for child in container.children: child.parent = container target.children.setLen(0) if target.parent != nil: container.parent = target.parent let n = target.parent.children.find(target) assert n != -1, "Container not a child of its parent" container.parent.children[n] = container target.parent = nil if pager.container == target: pager.setContainer(container) proc deleteContainer(pager: Pager; container, setTarget: Container) = if container.loadState == lsLoading: container.cancel() if container.replaceBackup != nil: pager.setContainer(container.replaceBackup) elif container.replace != nil: pager.replace(container, container.replace) if container.sourcepair != nil: container.sourcepair.sourcepair = nil container.sourcepair = nil if container.replaceRef != nil: container.replaceRef.replace = nil container.replaceRef.replaceBackup = nil container.replaceRef = 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) 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]) container.parent = nil container.children.setLen(0) if container.replace != nil: container.replace = nil elif container.replaceBackup != nil: container.replaceBackup = nil elif pager.container == container: pager.setContainer(setTarget) pager.unreg.add(container) if container.process != -1: pager.loader.removeCachedItem(container.cacheId) pager.forkserver.removeChild(container.process) pager.loader.removeClient(container.process) proc discardBuffer*(pager: Pager; container = none(Container); dir = none(NavDirection)) {.jsfunc.} = if dir.isSome: pager.navDirection = dir.get.opposite() let container = container.get(pager.container) let dir = pager.navDirection.opposite() let setTarget = container.find(dir) if container == nil or setTarget == nil: pager.alert("No buffer in direction: " & $dir) else: pager.deleteContainer(container, setTarget) proc discardTree(pager: Pager; container = none(Container)) {.jsfunc.} = let container = container.get(pager.container) if container != nil: for c in container.descendants: pager.deleteContainer(container, nil) else: pager.alert("Buffer has no children!") proc c_system(cmd: cstring): cint {.importc: "system", header: "".} # Run process (without suspending the terminal controller). proc runProcess(cmd: string): bool = let wstatus = c_system(cstring(cmd)) if wstatus == -1: result = false else: result = WIFEXITED(wstatus) and WEXITSTATUS(wstatus) == 0 if not result: # Hack. #TODO this is a very bad idea, e.g. say the editor is writing into the # file, then receives SIGINT, now the file is corrupted but Chawan will # happily read it as if nothing happened. # We should find a proper solution for this. result = WIFSIGNALED(wstatus) and WTERMSIG(wstatus) == SIGINT # Run process (and suspend the terminal controller). proc runProcess(term: Terminal; cmd: string; wait = false): bool = term.quit() result = runProcess(cmd) if wait: term.anyKey() term.restart() # Run process, and capture its output. proc runProcessCapture(cmd: string; outs: var string): bool = let file = popen(cmd, "r") if file == nil: return false outs = file.readAll() let rv = pclose(file) if rv == -1: return false return rv == 0 # Run process, and write an arbitrary string into its standard input. proc runProcessInto(cmd, ins: string): bool = let file = popen(cmd, "w") if file == nil: return false file.write(ins) let rv = pclose(file) if rv == -1: return false return rv == 0 template myExec(cmd: string) = discard execl("/bin/sh", "sh", "-c", cstring(cmd), nil) exitnow(127) proc toggleSource(pager: Pager) {.jsfunc.} = if cfCanReinterpret notin pager.container.flags: return if pager.container.sourcepair != nil: pager.setContainer(pager.container.sourcepair) else: let ishtml = cfIsHTML notin pager.container.flags #TODO I wish I could set the contentType to whatever I wanted, not just HTML let contentType = if ishtml: "text/html" else: "text/plain" let container = pager.newContainerFrom(pager.container, contentType) if container != nil: container.sourcepair = pager.container pager.navDirection = ndNext pager.container.sourcepair = container pager.addContainer(container) proc getCacheFile(pager: Pager; cacheId: int): string {.jsfunc.} = return pager.loader.getCacheFile(cacheId) proc cacheFile(pager: Pager): string {.jsfget.} = if pager.container != nil: return pager.getCacheFile(pager.container.cacheId) return "" proc getEditorCommand(pager: Pager; file: string; line = 1): string {.jsfunc.} = var editor = pager.config.external.editor if (let uqEditor = ChaPath(editor).unquote(); uqEditor.isSome): if uqEditor.get in ["vi", "nvi", "vim", "nvim"]: editor = uqEditor.get & " +%d" var canpipe = true var s = unquoteCommand(editor, "", file, nil, canpipe, line) if canpipe: # %s not in command; add file name ourselves if s[^1] != ' ': s &= ' ' s &= quoteFile(file, qsNormal) return s proc openInEditor(pager: Pager; input: var string): bool = try: let tmpf = getTempFile(pager.config.external.tmpdir) if input != "": writeFile(tmpf, input) let cmd = pager.getEditorCommand(tmpf) if pager.term.runProcess(cmd): if fileExists(tmpf): input = readFile(tmpf) removeFile(tmpf) return true except IOError: discard return false proc windowChange*(pager: Pager) = let oldAttrs = pager.attrs pager.term.windowChange() if pager.attrs == oldAttrs: #TODO maybe it's more efficient to let false positives through? return if pager.lineedit != nil: pager.lineedit.windowChange(pager.attrs) pager.clearDisplay() pager.clearStatus() for container in pager.containers: container.windowChange(pager.attrs) if pager.askprompt != "": pager.writeAskPrompt() pager.showAlerts() # Apply siteconf settings to a request. # Note that this may modify the URL passed. proc applySiteconf(pager: Pager; url: var URL; charsetOverride: Charset; loaderConfig: var LoaderClientConfig): BufferConfig = let host = url.host let ctx = pager.jsctx var res = BufferConfig( userstyle: pager.config.css.stylesheet, refererFrom: pager.config.buffer.referer_from, scripting: pager.config.buffer.scripting, charsets: pager.config.encoding.document_charset, images: pager.config.buffer.images, styling: pager.config.buffer.styling, autofocus: pager.config.buffer.autofocus, isdump: pager.config.start.headless, charsetOverride: charsetOverride, protocol: pager.config.protocol, metaRefresh: pager.config.buffer.meta_refresh ) loaderConfig = LoaderClientConfig( defaultHeaders: newHeaders(pager.config.network.default_headers), cookiejar: nil, proxy: pager.config.network.proxy, filter: newURLFilter( scheme = some(url.scheme), allowschemes = @["data", "cache", "stream"], default = true ), insecureSSLNoVerify: false ) for sc in pager.config.siteconf: if sc.url.isSome and not sc.url.get.match($url): continue elif sc.host.isSome and not sc.host.get.match(host): continue if sc.rewrite_url.isSome: let fun = sc.rewrite_url.get var arg0 = ctx.toJS(url) let ret = JS_Call(ctx, fun, JS_UNDEFINED, 1, arg0.toJSValueArray()) if not JS_IsException(ret): # Warning: we must only print exceptions if the *call* returned one. # Conversion may simply error out because the function didn't return a # new URL, and that's fine. var nu: URL if ctx.fromJS(ret, nu).isSome and nu != nil: url = nu else: #TODO should writeException the message to console pager.alert("Error rewriting URL: " & ctx.getExceptionMsg()) JS_FreeValue(ctx, arg0) JS_FreeValue(ctx, ret) if sc.cookie.isSome: if sc.cookie.get: # host/url might have changed by now let jarid = sc.share_cookie_jar.get(url.host) if jarid notin pager.cookiejars: pager.cookiejars[jarid] = newCookieJar(url, sc.third_party_cookie) loaderConfig.cookieJar = pager.cookiejars[jarid] else: loaderConfig.cookieJar = nil # override if sc.scripting.isSome: res.scripting = sc.scripting.get if sc.referer_from.isSome: res.refererFrom = sc.referer_from.get if sc.document_charset.len > 0: res.charsets = sc.document_charset if sc.images.isSome: res.images = sc.images.get if sc.stylesheet.isSome: res.userstyle &= "\n" res.userstyle &= sc.stylesheet.get if sc.proxy.isSome: loaderConfig.proxy = sc.proxy.get if sc.default_headers != nil: loaderConfig.defaultHeaders = newHeaders(sc.default_headers[]) if sc.insecure_ssl_no_verify.isSome: loaderConfig.insecureSSLNoVerify = sc.insecure_ssl_no_verify.get if sc.autofocus.isSome: res.autofocus = sc.autofocus.get if sc.meta_refresh.isSome: res.metaRefresh = sc.meta_refresh.get if res.images: loaderConfig.filter.allowschemes .add(pager.config.external.urimethodmap.imageProtos) return res # Load request in a new buffer. proc gotoURL(pager: Pager; request: Request; prevurl = none(URL); contentType = none(string); cs = CHARSET_UNKNOWN; replace: Container = nil; replaceBackup: Container = nil; redirectDepth = 0; referrer: Container = nil; save = false; url: URL = nil): Container = pager.navDirection = ndNext if referrer != nil and referrer.config.refererFrom: request.referrer = referrer.url let url = if url != nil: url else: request.url var loaderConfig: LoaderClientConfig var bufferConfig = pager.applySiteconf(request.url, cs, loaderConfig) if prevurl.isNone or not prevurl.get.equals(request.url, true) or request.url.hash == "" or request.httpMethod != hmGet: # 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. if referrer != nil: loaderConfig.referrerPolicy = referrer.loaderConfig.referrerPolicy var flags = {cfCanReinterpret, cfUserRequested} if save: flags.incl(cfSave) let container = pager.newContainer( bufferConfig, loaderConfig, request, redirectDepth = redirectDepth, contentType = contentType, flags = flags, url = url ) if replace != nil: pager.replace(replace, container) if replaceBackup == nil: container.replace = replace replace.replaceRef = container else: container.replaceBackup = replaceBackup replaceBackup.replaceRef = container container.copyCursorPos(replace) else: pager.addContainer(container) inc pager.numload return container else: pager.container.findAnchor(request.url.anchor) return nil proc omniRewrite(pager: Pager; s: string): string = for rule in pager.config.omnirule: if rule.match.match(s): let fun = rule.substitute_url.get let ctx = pager.jsctx var arg0 = ctx.toJS(s) let jsRet = JS_Call(ctx, fun, JS_UNDEFINED, 1, arg0.toJSValueArray()) defer: JS_FreeValue(ctx, jsRet) defer: JS_FreeValue(ctx, arg0) var res: string if ctx.fromJS(jsRet, res).isSome: return res pager.alert("Error in substitution of " & $rule.match & " for " & s & ": " & ctx.getExceptionMsg()) return s # When the user has passed a partial URL as an argument, they might've meant # either: # * file://$PWD/ # * https:// # So we attempt to load both, and see what works. proc loadURL*(pager: Pager; url: string; ctype = none(string); cs = CHARSET_UNKNOWN) = let url0 = pager.omniRewrite(url) let url = if url[0] == '~': expandPath(url0) else: url0 let firstparse = parseURL(url) if firstparse.isSome: let prev = if pager.container != nil: some(pager.container.url) else: none(URL) discard pager.gotoURL(newRequest(firstparse.get), prev, ctype, cs) return var urls: seq[URL] if pager.config.network.prepend_https and pager.config.network.prepend_scheme != "" and url[0] != '/': let pageurl = parseURL(pager.config.network.prepend_scheme & url) if pageurl.isSome: # attempt to load remote page urls.add(pageurl.get) let cdir = parseURL("file://" & percentEncode(getCurrentDir(), LocalPathPercentEncodeSet) & DirSep) let localurl = percentEncode(url, LocalPathPercentEncodeSet) let newurl = parseURL(localurl, cdir) if newurl.isSome: urls.add(newurl.get) # attempt to load local file if urls.len == 0: pager.alert("Invalid URL " & url) else: let container = pager.gotoURL(newRequest(urls.pop()), contentType = ctype, cs = cs) if container != nil: container.retry = urls proc readPipe0*(pager: Pager; contentType: string; cs: Charset; fd: FileHandle; url: URL; title: string; flags: set[ContainerFlag]): Container = var url = url pager.loader.passFd(url.pathname, fd) safeClose(fd) var loaderConfig: LoaderClientConfig let bufferConfig = pager.applySiteconf(url, cs, loaderConfig) return pager.newContainer( bufferConfig, loaderConfig, newRequest(url), title = title, flags = flags, contentType = some(contentType) ) proc readPipe*(pager: Pager; contentType: string; cs: Charset; fd: FileHandle; title: string) = let url = newURL("stream:-").get let container = pager.readPipe0(contentType, cs, fd, url, title, {cfCanReinterpret, cfUserRequested}) inc pager.numload pager.addContainer(container) proc command(pager: Pager) {.jsfunc.} = pager.setLineEdit(lmCommand) proc commandMode(pager: Pager; val: bool) {.jsfset.} = pager.commandMode = val if val: pager.command() proc checkRegex(pager: Pager; regex: Result[Regex, string]): Opt[Regex] = if regex.isNone: pager.alert("Invalid regex: " & regex.error) return err() return ok(regex.get) proc compileSearchRegex(pager: Pager; s: string): Result[Regex, string] = return compileSearchRegex(s, pager.config.search.ignore_case) proc updateReadLineISearch(pager: Pager; linemode: LineMode) = let lineedit = pager.lineedit pager.isearchpromise = pager.isearchpromise.then(proc(): EmptyPromise = case lineedit.state of lesCancel: pager.iregex.err() pager.container.popCursorPos() pager.container.clearSearchHighlights() pager.container.redraw = true pager.isearchpromise = nil of lesEdit: if lineedit.news != "": pager.iregex = pager.compileSearchRegex(lineedit.news) pager.container.popCursorPos(true) pager.container.pushCursorPos() if pager.iregex.isSome: pager.container.hlon = true let wrap = pager.config.search.wrap return if linemode == lmISearchF: pager.container.cursorNextMatch(pager.iregex.get, wrap, false, 1) else: pager.container.cursorPrevMatch(pager.iregex.get, wrap, false, 1) of lesFinish: if lineedit.news != "": pager.regex = pager.checkRegex(pager.iregex) else: pager.searchNext() pager.reverseSearch = linemode == lmISearchB pager.container.markPos() pager.container.clearSearchHighlights() pager.container.sendCursorPosition() pager.container.redraw = true pager.isearchpromise = nil ) proc saveTo(pager: Pager; data: LineDataDownload; path: string) = if pager.loader.redirectToFile(data.outputId, path): pager.alert("Saving file to " & path) pager.loader.resume(data.outputId) data.stream.sclose() pager.lineData = nil else: pager.ask("Failed to save to " & path & ". Retry?").then( proc(x: bool) = if x: pager.setLineEdit(lmDownload, path) else: data.stream.sclose() pager.lineData = nil ) proc updateReadLine*(pager: Pager) = let lineedit = pager.lineedit if pager.linemode in {lmISearchF, lmISearchB}: pager.updateReadLineISearch(pager.linemode) else: case lineedit.state of lesEdit: discard of lesFinish: case pager.linemode of lmLocation: pager.loadURL(lineedit.news) of lmUsername: LineDataAuth(pager.lineData).url.username = lineedit.news pager.setLineEdit(lmPassword, hide = true) of lmPassword: let url = LineDataAuth(pager.lineData).url url.password = lineedit.news discard pager.gotoURL(newRequest(url), some(pager.container.url), replace = pager.container, referrer = pager.container) pager.lineData = nil of lmCommand: pager.scommand = lineedit.news if pager.commandMode: pager.command() of lmBuffer: pager.container.readSuccess(lineedit.news) of lmBufferFile: let ps = newPosixStream(lineedit.news, O_RDONLY, 0) if ps == nil: pager.alert("File not found") pager.container.readCanceled() else: var stats: Stat if fstat(ps.fd, stats) < 0 or S_ISDIR(stats.st_mode): pager.alert("Not a file: " & lineedit.news) else: let name = lineedit.news.afterLast('/') pager.container.readSuccess(name, ps.fd) ps.sclose() of lmSearchF, lmSearchB: if lineedit.news != "": let regex = pager.compileSearchRegex(lineedit.news) pager.regex = pager.checkRegex(regex) pager.reverseSearch = pager.linemode == lmSearchB pager.searchNext() of lmGotoLine: pager.container.gotoLine(lineedit.news) of lmDownload: let data = LineDataDownload(pager.lineData) if fileExists(lineedit.news): pager.ask("Override file " & lineedit.news & "?").then( proc(x: bool) = if x: pager.saveTo(data, lineedit.news) else: pager.setLineEdit(lmDownload, lineedit.news) ) else: pager.saveTo(data, lineedit.news) of lmISearchF, lmISearchB: discard of lesCancel: case pager.linemode of lmUsername, lmPassword: pager.discardBuffer() of lmBuffer: pager.container.readCanceled() of lmCommand: pager.commandMode = false of lmDownload: let data = LineDataDownload(pager.lineData) data.stream.sclose() else: discard pager.lineData = nil if lineedit.state in {lesCancel, lesFinish} and pager.lineedit == lineedit: pager.clearLineEdit() # Same as load(s + '\n') proc loadSubmit(pager: Pager; s: string) {.jsfunc.} = pager.loadURL(s) # Open a URL prompt and visit the specified URL. proc load(pager: Pager; s = "") {.jsfunc.} = if s.len > 0 and s[^1] == '\n': if s.len > 1: pager.loadURL(s[0..^2]) elif s == "": pager.setLineEdit(lmLocation, $pager.container.url) else: pager.setLineEdit(lmLocation, s) # Go to specific URL (for JS) type GotoURLDict = object of JSDict contentType {.jsdefault.}: Option[string] replace {.jsdefault.}: Container proc jsGotoURL(pager: Pager; v: JSValue; t = GotoURLDict()): JSResult[void] {.jsfunc: "gotoURL".} = var request: Request = nil var jsRequest: JSRequest = nil if pager.jsctx.fromJS(v, jsRequest).isSome: request = jsRequest.request else: var url: URL = nil if pager.jsctx.fromJS(v, url).isNone: var s: string ?pager.jsctx.fromJS(v, s) url = ?newURL(s) request = newRequest(url) discard pager.gotoURL(request, contentType = t.contentType, replace = t.replace) return ok() # Reload the page in a new buffer, then kill the previous buffer. proc reload(pager: Pager) {.jsfunc.} = discard pager.gotoURL(newRequest(pager.container.url), none(URL), pager.container.contentType, replace = pager.container) proc setEnvVars(pager: Pager) {.jsfunc.} = try: putEnv("CHA_URL", $pager.container.url) putEnv("CHA_CHARSET", $pager.container.charset) except OSError: pager.alert("Warning: failed to set some environment variables") type ExternDict = object of JSDict setenv {.jsdefault: true.}: bool suspend {.jsdefault: true.}: bool wait {.jsdefault: false.}: bool #TODO we should have versions with retval as int? proc extern(pager: Pager; cmd: string; t = ExternDict(setenv: true, suspend: true)): bool {.jsfunc.} = if t.setenv: pager.setEnvVars() if t.suspend: return runProcess(pager.term, cmd, t.wait) else: return runProcess(cmd) proc externCapture(pager: Pager; cmd: string): Option[string] {.jsfunc.} = pager.setEnvVars() var s: string if not runProcessCapture(cmd, s): return none(string) return some(s) proc externInto(pager: Pager; cmd, ins: string): bool {.jsfunc.} = pager.setEnvVars() return runProcessInto(cmd, ins) proc externFilterSource(pager: Pager; cmd: string; c: Container = nil; contentType = none(string)) {.jsfunc.} = let fromc = if c != nil: c else: pager.container let fallback = pager.container.contentType.get("text/plain") let contentType = contentType.get(fallback) let container = pager.newContainerFrom(fromc, contentType) if contentType == "text/html": container.flags.incl(cfIsHTML) else: container.flags.excl(cfIsHTML) pager.addContainer(container) container.filter = BufferFilter(cmd: cmd) type CheckMailcapResult = object fdout: int ostreamOutputId: int connect: bool ishtml: bool found: bool template myFork(): cint = stdout.flushFile() stderr.flushFile() fork() # Pipe output of an x-ansioutput mailcap command to the text/x-ansi handler. proc ansiDecode(pager: Pager; url: URL; ishtml: var bool; fdin: cint): cint = let entry = pager.config.external.mailcap.getMailcapEntry("text/x-ansi", "", url) var canpipe = true let cmd = unquoteCommand(entry.cmd, "text/x-ansi", "", url, canpipe) if not canpipe: pager.alert("Error: could not pipe to text/x-ansi, decoding as text/plain") return -1 var pipefdOutAnsi: array[2, cint] if pipe(pipefdOutAnsi) == -1: pager.alert("Error: failed to open pipe") return case myFork() of -1: pager.alert("Error: failed to fork ANSI decoder process") discard close(pipefdOutAnsi[0]) discard close(pipefdOutAnsi[1]) return -1 of 0: # child process discard close(pipefdOutAnsi[0]) discard dup2(fdin, stdin.getFileHandle()) discard close(fdin) discard dup2(pipefdOutAnsi[1], stdout.getFileHandle()) discard close(pipefdOutAnsi[1]) closeStderr() myExec(cmd) else: discard close(pipefdOutAnsi[1]) discard close(fdin) ishtml = mfHtmloutput in entry.flags return pipefdOutAnsi[0] # Pipe input into the mailcap command, then read its output into a buffer. # needsterminal is ignored. proc runMailcapReadPipe(pager: Pager; stream: SocketStream; cmd: string; pipefdOut: array[2, cint]): int = let pid = myFork() if pid == -1: pager.alert("Error: failed to fork mailcap read process") return -1 elif pid == 0: # child process discard close(pipefdOut[0]) discard dup2(stream.fd, stdin.getFileHandle()) stream.sclose() discard dup2(pipefdOut[1], stdout.getFileHandle()) closeStderr() discard close(pipefdOut[1]) myExec(cmd) # parent pid # Pipe input into the mailcap command, and discard its output. # If needsterminal, leave stderr and stdout open and wait for the process. proc runMailcapWritePipe(pager: Pager; stream: SocketStream; needsterminal: bool; cmd: string) = if needsterminal: pager.term.quit() let pid = myFork() if pid == -1: pager.alert("Error: failed to fork mailcap write process") elif pid == 0: # child process discard dup2(stream.fd, stdin.getFileHandle()) stream.sclose() if not needsterminal: closeStdout() closeStderr() myExec(cmd) else: # parent stream.sclose() if needsterminal: var x: cint discard waitpid(pid, x, 0) pager.term.restart() proc writeToFile(istream: SocketStream; outpath: string): bool = let ps = newPosixStream(outpath, O_WRONLY or O_CREAT, 0o600) if ps == nil: return false var buffer: array[4096, uint8] while true: let n = istream.recvData(buffer) if n == 0: break ps.sendDataLoop(buffer.toOpenArray(0, n - 1)) ps.sclose() true # Save input in a file, run the command, and redirect its output to a # new buffer. # needsterminal is ignored. proc runMailcapReadFile(pager: Pager; stream: SocketStream; cmd, outpath: string; pipefdOut: array[2, cint]): int = let pid = myFork() if pid == 0: # child process discard close(pipefdOut[0]) discard dup2(pipefdOut[1], stdout.getFileHandle()) discard close(pipefdOut[1]) closeStderr() if not stream.writeToFile(outpath): #TODO print error message quit(1) stream.sclose() let ret = execCmd(cmd) discard tryRemoveFile(outpath) quit(ret) # parent pid # Save input in a file, run the command, and discard its output. # If needsterminal, leave stderr and stdout open and wait for the process. proc runMailcapWriteFile(pager: Pager; stream: SocketStream; needsterminal: bool; cmd, outpath: string) = if needsterminal: pager.term.quit() if not stream.writeToFile(outpath): pager.term.restart() pager.alert("Error: failed to write file for mailcap process") else: discard execCmd(cmd) discard tryRemoveFile(outpath) pager.term.restart() else: # don't block let pid = myFork() if pid == 0: # child process closeStdin() closeStdout() closeStderr() if not stream.writeToFile(outpath): #TODO print error message (maybe in parent?) quit(1) stream.sclose() let ret = execCmd(cmd) discard tryRemoveFile(outpath) quit(ret) # parent stream.sclose() proc filterBuffer(pager: Pager; stream: SocketStream; cmd: string; ishtml: bool): CheckMailcapResult = pager.setEnvVars() var pipefd_out: array[2, cint] if pipe(pipefd_out) == -1: pager.alert("Error: failed to open pipe") return CheckMailcapResult(connect: false, fdout: -1) let pid = myFork() if pid == -1: pager.alert("Error: failed to fork buffer filter process") return CheckMailcapResult(connect: false, fdout: -1) elif pid == 0: # child discard close(pipefd_out[0]) discard dup2(stream.fd, stdin.getFileHandle()) stream.sclose() discard dup2(pipefd_out[1], stdout.getFileHandle()) closeStderr() discard close(pipefd_out[1]) myExec(cmd) # parent discard close(pipefd_out[1]) let fdout = pipefd_out[0] let url = parseURL("stream:" & $pid).get pager.loader.passFd(url.pathname, FileHandle(fdout)) safeClose(fdout) let response = pager.loader.doRequest(newRequest(url)) return CheckMailcapResult( connect: true, fdout: response.body.fd, ostreamOutputId: response.outputId, ishtml: ishtml, found: true ) # Search for a mailcap entry, and if found, execute the specified command # and pipeline the input and output appropriately. # There are four possible outcomes: # * pipe stdin, discard stdout # * pipe stdin, read stdout # * write to file, run, discard stdout # * write to file, run, read stdout # If needsterminal is specified, and stdout is not being read, then the # pager is suspended until the command exits. #TODO add support for edit/compose, better error handling proc checkMailcap(pager: Pager; container: Container; stream: SocketStream; istreamOutputId: int; contentType: string): CheckMailcapResult = if container.filter != nil: return pager.filterBuffer( stream, container.filter.cmd, cfIsHTML in container.flags ) # contentType must exist, because we set it in applyResponse let shortContentType = container.contentType.get if shortContentType == "text/html": # We support text/html natively, so it would make little sense to execute # mailcap filters for it. return CheckMailcapResult( connect: true, fdout: stream.fd, ishtml: true, found: true ) if shortContentType == "text/plain": # text/plain could potentially be useful. Unfortunately, many mailcaps # include a text/plain entry with less by default, so it's probably better # to ignore this. return CheckMailcapResult(connect: true, fdout: stream.fd, found: true) #TODO callback for outpath or something let url = container.url let entry = pager.config.external.mailcap.getMailcapEntry(contentType, "", url) if entry == nil: return CheckMailcapResult(connect: true, fdout: stream.fd, found: false) let ext = url.pathname.afterLast('.') let tempfile = getTempFile(pager.config.external.tmpdir, ext) let outpath = if entry.nametemplate != "": unquoteCommand(entry.nametemplate, contentType, tempfile, url) else: tempfile var canpipe = true let cmd = unquoteCommand(entry.cmd, contentType, outpath, url, canpipe) var ishtml = mfHtmloutput in entry.flags let needsterminal = mfNeedsterminal in entry.flags putEnv("MAILCAP_URL", $url) block needsConnect: if entry.flags * {mfCopiousoutput, mfHtmloutput, mfAnsioutput} == {}: # No output. Resume here, so that blocking needsterminal filters work. pager.loader.resume(istreamOutputId) if canpipe: pager.runMailcapWritePipe(stream, needsterminal, cmd) else: pager.runMailcapWriteFile(stream, needsterminal, cmd, outpath) # stream is already closed break needsConnect # never connect here, since there's no output var pipefdOut: array[2, cint] if pipe(pipefdOut) == -1: pager.alert("Error: failed to open pipe") stream.sclose() # connect: false implies that we consumed the stream break needsConnect let pid = if canpipe: pager.runMailcapReadPipe(stream, cmd, pipefdOut) else: pager.runMailcapReadFile(stream, cmd, outpath, pipefdOut) discard close(pipefdOut[1]) # close write let fdout = if not ishtml and mfAnsioutput in entry.flags: pager.ansiDecode(url, ishtml, pipefdOut[0]) else: pipefdOut[0] delEnv("MAILCAP_URL") let url = parseURL("stream:" & $pid).get pager.loader.passFd(url.pathname, FileHandle(fdout)) safeClose(cint(fdout)) let response = pager.loader.doRequest(newRequest(url)) return CheckMailcapResult( connect: true, fdout: response.body.fd, ostreamOutputId: response.outputId, ishtml: ishtml, found: true ) delEnv("MAILCAP_URL") return CheckMailcapResult(connect: false, fdout: -1, found: true) proc redirectTo(pager: Pager; container: Container; request: Request) = let replaceBackup = if container.replaceBackup != nil: container.replaceBackup else: container.find(ndAny) let nc = pager.gotoURL(request, some(container.url), replace = container, replaceBackup = replaceBackup, redirectDepth = container.redirectDepth + 1, referrer = container) nc.loadinfo = "Redirecting to " & $request.url pager.onSetLoadInfo(nc) dec pager.numload proc fail(pager: Pager; container: Container; errorMessage: string) = dec pager.numload pager.deleteContainer(container, container.find(ndAny)) if container.retry.len > 0: discard pager.gotoURL(newRequest(container.retry.pop()), contentType = container.contentType) else: pager.alert("Can't load " & $container.url & " (" & errorMessage & ")") proc redirect(pager: Pager; container: Container; response: Response; request: Request) = # if redirection fails, then we need some other container to move to... let failTarget = container.find(ndAny) # still need to apply response, or we lose cookie jars. container.applyResponse(response, pager.config.external.mime_types) if container.redirectDepth < pager.config.network.max_redirect: if container.url.scheme == request.url.scheme or container.url.scheme == "cgi-bin" or container.url.scheme == "http" and request.url.scheme == "https" or container.url.scheme == "https" and request.url.scheme == "http": pager.redirectTo(container, request) #TODO perhaps make following behavior configurable? elif request.url.scheme == "cgi-bin": pager.alert("Blocked redirection attempt to " & $request.url) else: let url = request.url pager.ask("Warning: switch protocols? " & $url).then(proc(x: bool) = if x: pager.redirectTo(container, request) ) else: pager.alert("Error: maximum redirection depth reached") pager.deleteContainer(container, failTarget) proc askDownloadPath(pager: Pager; container: Container; response: Response) = var buf = pager.config.external.download_dir let pathname = container.url.pathname if pathname[^1] == '/': buf &= "index.html" else: buf &= container.url.pathname.afterLast('/').percentDecode() pager.setLineEdit(lmDownload, buf) pager.lineData = LineDataDownload( outputId: response.outputId, stream: response.body ) pager.deleteContainer(container, container.find(ndAny)) pager.refreshStatusMsg() dec pager.numload proc connected(pager: Pager; container: Container; response: Response) = let istream = response.body container.applyResponse(response, pager.config.external.mime_types) if response.status == 401: # unauthorized pager.setLineEdit(lmUsername, container.url.username) pager.lineData = LineDataAuth(url: newURL(container.url)) istream.sclose() return # This forces client to ask for confirmation before quitting. # (It checks a flag on container, because console buffers must not affect this # variable.) if cfUserRequested in container.flags: pager.hasload = true if cfSave in container.flags: # download queried by user pager.askDownloadPath(container, response) return let realContentType = if "Content-Type" in response.headers: response.headers["Content-Type"] else: # both contentType and charset must be set by applyResponse. container.contentType.get & ";charset=" & $container.charset let mailcapRes = pager.checkMailcap(container, istream, response.outputId, realContentType) let shortContentType = container.contentType.get if not mailcapRes.found and not shortContentType.startsWithIgnoreCase("text/") and not shortContentType.isJavaScriptType(): pager.askDownloadPath(container, response) return if mailcapRes.connect: if mailcapRes.ishtml: container.flags.incl(cfIsHTML) else: container.flags.excl(cfIsHTML) # buffer now actually exists; create a process for it var attrs = pager.attrs # subtract status line height attrs.height -= 1 attrs.heightPx -= attrs.ppl container.process = pager.forkserver.forkBuffer( container.config, container.url, attrs, mailcapRes.ishtml, container.charsetStack ) if mailcapRes.fdout != istream.fd: # istream has been redirected into a filter istream.sclose() pager.procmap.add(ProcMapItem( container: container, fdout: FileHandle(mailcapRes.fdout), fdin: FileHandle(istream.fd), ostreamOutputId: mailcapRes.ostreamOutputId, istreamOutputId: response.outputId )) if container.replace != nil: pager.deleteContainer(container.replace, container.find(ndAny)) container.replace = nil else: dec pager.numload pager.deleteContainer(container, container.find(ndAny)) pager.refreshStatusMsg() proc unregisterFd(pager: Pager; fd: int) = pager.selector.unregister(fd) pager.loader.unregistered.add(fd) # true if done, false if keep proc handleConnectingContainer*(pager: Pager; i: int) = let item = pager.connectingContainers[i] let container = item.container let stream = item.stream case item.state of ccsBeforeResult: var r = stream.initPacketReader() var res: int r.sread(res) if res == 0: r.sread(item.outputId) inc item.state container.loadinfo = "Connected to " & $container.url & ". Downloading..." pager.onSetLoadInfo(container) # continue else: var msg: string r.sread(msg) if msg == "": msg = getLoaderErrorMessage(res) pager.fail(container, msg) # done pager.connectingContainers.del(i) pager.unregisterFd(int(item.stream.fd)) stream.sclose() of ccsBeforeStatus: var r = stream.initPacketReader() r.sread(item.status) inc item.state # continue of ccsBeforeHeaders: let response = newResponse(item.res, container.request, stream, item.outputId, item.status) var r = stream.initPacketReader() r.sread(response.headers) # done pager.connectingContainers.del(i) pager.unregisterFd(int(item.stream.fd)) let redirect = response.getRedirect(container.request) if redirect != nil: stream.sclose() pager.redirect(container, response, redirect) else: pager.connected(container, response) proc handleConnectingContainerError*(pager: Pager; i: int) = let item = pager.connectingContainers[i] pager.fail(item.container, "loader died while loading") pager.unregisterFd(int(item.stream.fd)) item.stream.sclose() pager.connectingContainers.del(i) proc metaRefresh(pager: Pager; container: Container; n: int; url: URL) = let ctx = pager.jsctx let fun = ctx.newFunction(["url", "replace"], "pager.gotoURL(url, {replace: replace})") let args = [ctx.toJS(url), ctx.toJS(container)] discard pager.timeouts.setTimeout(ttTimeout, fun, int32(n), args) JS_FreeValue(ctx, fun) for arg in args: JS_FreeValue(ctx, arg) proc handleEvent0(pager: Pager; container: Container; event: ContainerEvent): bool = case event.t of cetLoaded: dec pager.numload of cetAnchor: let url2 = newURL(container.url) url2.setHash(event.anchor) pager.dupeBuffer(container, url2) of cetNoAnchor: pager.alert("Couldn't find anchor " & event.anchor) of cetReadLine: if container == pager.container: pager.setLineEdit(lmBuffer, event.value, hide = event.password, extraPrompt = event.prompt) of cetReadArea: if container == pager.container: var s = event.tvalue if pager.openInEditor(s): pager.container.readSuccess(s) else: pager.container.readCanceled() of cetReadFile: if container == pager.container: pager.setLineEdit(lmBufferFile, "") of cetOpen: let url = event.request.url let sameScheme = container.url.scheme == url.scheme if event.request.httpMethod != hmGet and not sameScheme and not (container.url.scheme in ["http", "https"] and url.scheme in ["http", "https"]): pager.alert("Blocked cross-scheme POST: " & $url) return #TODO this is horrible UX, async actions shouldn't block input if pager.container != container or not event.save and not container.isHoverURL(url): pager.ask("Open pop-up? " & $url).then(proc(x: bool) = if x: discard pager.gotoURL(event.request, some(container.url), referrer = pager.container, save = event.save) ) else: let url = if event.url != nil: event.url else: event.request.url discard pager.gotoURL(event.request, some(container.url), referrer = pager.container, save = event.save, url = url) of cetStatus: if pager.container == container: pager.showAlerts() of cetSetLoadInfo: if pager.container == container: pager.onSetLoadInfo(container) of cetTitle: if pager.container == container: pager.showAlerts() pager.term.setTitle(container.getTitle()) of cetAlert: if pager.container == container: pager.alert(event.msg) of cetCancel: let i = pager.findConnectingContainer(container) if i == -1: # whoops. we tried to cancel, but the event loop did not favor us... # at least cancel it in the buffer container.remoteCancel() else: let item = pager.connectingContainers[i] dec pager.numload pager.deleteContainer(container, container.find(ndAny)) pager.connectingContainers.del(i) pager.unregisterFd(int(item.stream.fd)) item.stream.sclose() of cetMetaRefresh: let url = event.refreshURL let n = event.refreshIn case container.config.metaRefresh of mrNever: assert false of mrAlways: pager.metaRefresh(container, n, url) of mrAsk: let surl = $url if surl in pager.refreshAllowed: pager.metaRefresh(container, n, url) else: pager.ask("Redirect to " & $url & " (in " & $n & "ms)?") .then(proc(x: bool) = if x: pager.refreshAllowed.incl($url) pager.metaRefresh(container, n, url) ) return true proc handleEvents*(pager: Pager; container: Container) = while container.events.len > 0: let event = container.events.popFirst() if not pager.handleEvent0(container, event): break proc handleEvents*(pager: Pager) = if pager.container != nil: pager.handleEvents(pager.container) proc handleEvent*(pager: Pager; container: Container) = try: container.handleEvent() pager.handleEvents(container) except IOError: discard proc addPagerModule*(ctx: JSContext) = ctx.registerType(Pager)