diff options
author | bptato <nincsnevem662@gmail.com> | 2024-04-25 01:06:43 +0200 |
---|---|---|
committer | bptato <nincsnevem662@gmail.com> | 2024-04-25 01:20:58 +0200 |
commit | 14b871eb7eaf329b67b71385597f114f8782318a (patch) | |
tree | ac7b4655124b4579ad27d56116d9a2dee63cd31e /src | |
parent | 62944ac7abc6e37475739a1667ed5a0240fedf66 (diff) | |
download | chawan-14b871eb7eaf329b67b71385597f114f8782318a.tar.gz |
Initial image support
* png: add missing filters, various decoder fixes * term: fix kitty response interpretation, add support for kitty image detection * buffer, pager: initial image display support Emphasis on "initial"; it only "works" with kitty output and PNG input. Also, it's excruciatingly slow, and repaints images way too often. Left undocumented intentionally it for now, until it actually becomes useful. In the meantime, adventurous users can find out themselves why: [[siteconf]] url = "https://.*" images = true
Diffstat (limited to 'src')
-rw-r--r-- | src/config/config.nim | 4 | ||||
-rw-r--r-- | src/css/cascade.nim | 6 | ||||
-rw-r--r-- | src/html/dom.nim | 2 | ||||
-rw-r--r-- | src/img/png.nim | 67 | ||||
-rw-r--r-- | src/io/bufreader.nim | 5 | ||||
-rw-r--r-- | src/io/bufwriter.nim | 8 | ||||
-rw-r--r-- | src/layout/box.nim | 5 | ||||
-rw-r--r-- | src/layout/engine.nim | 26 | ||||
-rw-r--r-- | src/layout/renderdocument.nim | 39 | ||||
-rw-r--r-- | src/local/container.nim | 9 | ||||
-rw-r--r-- | src/local/pager.nim | 5 | ||||
-rw-r--r-- | src/local/term.nim | 97 | ||||
-rw-r--r-- | src/server/buffer.nim | 22 |
13 files changed, 233 insertions, 62 deletions
diff --git a/src/config/config.nim b/src/config/config.nim index 1ac0393e..6611b4ef 100644 --- a/src/config/config.nim +++ b/src/config/config.nim @@ -33,6 +33,9 @@ type FormatMode* = set[FormatFlags] + ImageMode* = enum + imNone = "none", imSixel = "sixel", imKitty = "kitty" + ChaPathResolved* = distinct string ActionMap = object @@ -104,6 +107,7 @@ type color_mode* {.jsgetset.}: Option[ColorMode] format_mode* {.jsgetset.}: Option[FormatMode] no_format_mode* {.jsgetset.}: FormatMode + image_mode* {.jsgetset.}: Option[ImageMode] emulate_overline* {.jsgetset.}: bool alt_screen* {.jsgetset.}: Option[bool] highlight_color* {.jsgetset.}: RGBAColor diff --git a/src/css/cascade.nim b/src/css/cascade.nim index 9c3e5803..b2d007f2 100644 --- a/src/css/cascade.nim +++ b/src/css/cascade.nim @@ -380,7 +380,11 @@ proc applyRulesFrameInvalid(frame: CascadeFrame; ua, user: CSSStylesheet; styledParent.children.add(styledText) of peImage: let src = Element(styledParent.node).attr(satSrc) - let content = CSSContent(t: ContentImage, s: src) + let content = CSSContent( + t: ContentImage, + s: src, + bmp: HTMLImageElement(styledParent.node).bitmap + ) let styledText = styledParent.newStyledReplacement(content) styledText.pseudo = pseudo styledParent.children.add(styledText) diff --git a/src/html/dom.nim b/src/html/dom.nim index 32e98824..76ed9b70 100644 --- a/src/html/dom.nim +++ b/src/html/dom.nim @@ -2849,7 +2849,7 @@ proc loadResource(window: Window; image: HTMLImageElement) = return let pngData = pngData.get let buffer = cast[ptr UncheckedArray[uint8]](pngData.buffer) - let high = int(pngData.size - 1) + let high = int(pngData.size) - 1 image.bitmap = fromPNG(toOpenArray(buffer, 0, high)) ) window.loadingResourcePromises.add(p) diff --git a/src/img/png.nim b/src/img/png.nim index 7f4d8493..bd7e6206 100644 --- a/src/img/png.nim +++ b/src/img/png.nim @@ -132,8 +132,7 @@ func spp(reader: PNGReader): int = of pcTrueColorWithAlpha: return 4 func scanlen(reader: PNGReader): int {.inline.} = - let w = reader.width + 1 - return (w * reader.spp * int(reader.bitDepth) + 7) div 8 + return 1 + (reader.width * reader.spp * int(reader.bitDepth) + 7) div 8 proc handleError(reader: var PNGReader; msg: string) = #TODO proper error handling? @@ -258,28 +257,55 @@ proc readtRNS(reader: var PNGReader) = for i in 0 ..< reader.palette.len: reader.palette[i].a = reader.readU8() +func paethPredictor(a, b, c: uint16): uint16 = + let pa0 = int(b) - int(c) + let pb0 = int(a) - int(c) + let pa = abs(pa0) + let pb = abs(pb0) + let pc = abs(pa0 + pb0) + return if pa <= pb and pa <= pc: a elif pb <= pc: b else: c + proc unfilter(reader: var PNGReader; irow: openArray[uint8]; bpp: int) = # none, sub, up -> replace uprow directly # average, paeth -> copy to temp array, then replace uprow - let fil = irow[0] - let w = reader.width - case fil + case irow[0] of 0u8: # none - copyMem(addr reader.uprow[0], unsafeAddr irow[1], w) + copyMem(addr reader.uprow[0], unsafeAddr irow[1], irow.len) of 1u8: # sub for i in 1 ..< irow.len: let j = i - 1 # skip filter byte - reader.uprow[j] = irow[i] - if j - bpp >= 0: - reader.uprow[j] += irow[j - bpp] + let aidx = j - bpp + let x = uint16(irow[i]) + let a = if aidx >= 0: uint16(reader.uprow[aidx]) else: 0u16 + reader.uprow[j] = uint8((x + a) and 0xFF) of 2u8: # up for i in 1 ..< irow.len: let j = i - 1 # skip filter byte - reader.uprow[j] += irow[i] + let x = uint16(irow[i]) + let b = uint16(reader.uprow[j]) + reader.uprow[j] = uint8((x + b) and 0xFF) of 3u8: # average - reader.err "average not implemented yet" + for i in 1 ..< irow.len: + let j = i - 1 # skip filter byte + let aidx = j - bpp + let x = uint16(irow[i]) + let a = if aidx >= 0: uint16(reader.uprow[aidx]) else: 0u16 + let b = uint16(reader.uprow[j]) + reader.uprow[j] = uint8((x + (a + b) div 2) and 0xFF) of 4u8: # paeth - reader.err "paeth not implemented yet" + var cmap: array[8, uint16] # max bpp is 16 bit with true color = 4 * 2 + var k = 0 + for i in 1 ..< irow.len: + let j = i - 1 # skip filter byte + let aidx = j - bpp + let x = uint16(irow[i]) + let a = if aidx >= 0: uint16(reader.uprow[aidx]) else: 0u16 + let b = uint16(reader.uprow[j]) + let kk = k mod bpp + let c = cmap[kk] + cmap[kk] = b + reader.uprow[j] = uint8((x + paethPredictor(a, b, c)) and 0xFF) + inc k else: reader.err "got invalid filter" @@ -340,11 +366,16 @@ proc writepxs(reader: var PNGReader; crow: var openArray[RGBAColor]) = crow[x] = rgba(n, n, n, a) of pcTrueColorWithAlpha: let step = int(reader.bitDepth) div 8 + var i = 0 for x in 0 ..< crow.len: - let r = reader.uprow[x * step] - let g = reader.uprow[(x + 1) * step] - let b = reader.uprow[(x + 2) * step] - let a = reader.uprow[(x + 3) * step] + let r = reader.uprow[i] + i += step + let g = reader.uprow[i] + i += step + let b = reader.uprow[i] + i += step + let a = reader.uprow[i] + i += step crow[x] = rgba(r, g, b, a) proc readPLTE(reader: var PNGReader) = @@ -404,10 +435,10 @@ proc readIDAT(reader: var PNGReader) = for y in reader.atline ..< maxline: let yi = y * sl assert yi + sl - 1 < reader.idatAt - reader.unfilter(toOpenArray(reader.idatBuf, yi, yi + sl - 1), bpp) + reader.unfilter(reader.idatBuf.toOpenArray(yi, yi + sl - 1), bpp) if unlikely(reader.bmp == nil): return let yj = y * reader.width - reader.writepxs(toOpenArray(bmp.px, yj, yj + reader.width - 1)) + reader.writepxs(bmp.px.toOpenArray(yj, yj + reader.width - 1)) proc readIEND(reader: var PNGReader) = if reader.i < reader.limit: diff --git a/src/io/bufreader.nim b/src/io/bufreader.nim index 6de269ac..3ee36208 100644 --- a/src/io/bufreader.nim +++ b/src/io/bufreader.nim @@ -6,6 +6,7 @@ import std/tables import io/dynstream import types/blob +import types/color import types/formdata import types/opt import types/url @@ -53,6 +54,7 @@ proc sread*(reader: var BufferedReader; part: var FormDataEntry) proc sread*(reader: var BufferedReader; blob: var Blob) proc sread*[T](reader: var BufferedReader; o: var Option[T]) proc sread*[T, E](reader: var BufferedReader; o: var Result[T, E]) +proc sread*(reader: var BufferedReader; c: var RGBAColor) {.inline.} proc readData(reader: var BufferedReader; buffer: pointer; len: int) = assert reader.bufIdx + len <= reader.buffer.len @@ -198,3 +200,6 @@ proc sread*[T, E](reader: var BufferedReader; o: var Result[T, E]) = o.err(e) else: o.err() + +proc sread*(reader: var BufferedReader; c: var RGBAColor) = + reader.sread(uint32(c)) diff --git a/src/io/bufwriter.nim b/src/io/bufwriter.nim index 75da4190..0957d3e8 100644 --- a/src/io/bufwriter.nim +++ b/src/io/bufwriter.nim @@ -6,11 +6,11 @@ import std/sets import std/tables import io/dynstream - import types/blob +import types/color import types/formdata -import types/url import types/opt +import types/url type BufferedWriter* = object stream: DynStream @@ -77,6 +77,7 @@ proc swrite*(writer: var BufferedWriter; part: FormDataEntry) proc swrite*(writer: var BufferedWriter; blob: Blob) proc swrite*[T](writer: var BufferedWriter; o: Option[T]) proc swrite*[T, E](writer: var BufferedWriter; o: Result[T, E]) +proc swrite*(writer: var BufferedWriter; c: RGBAColor) {.inline.} proc writeData(writer: var BufferedWriter; buffer: pointer; len: int) = let targetLen = writer.bufLen + len @@ -181,3 +182,6 @@ proc swrite*[T, E](writer: var BufferedWriter; o: Result[T, E]) = else: when not (E is void): writer.swrite(o.error) + +proc swrite*(writer: var BufferedWriter; c: RGBAColor) = + writer.swrite(uint32(c)) diff --git a/src/layout/box.nim b/src/layout/box.nim index 79f927ef..40628204 100644 --- a/src/layout/box.nim +++ b/src/layout/box.nim @@ -1,5 +1,6 @@ import css/stylednode import css/values +import img/bitmap import layout/layoutunit type @@ -12,7 +13,7 @@ type h*: LayoutUnit InlineAtomType* = enum - iatSpacing, iatWord, iatInlineBlock + iatSpacing, iatWord, iatInlineBlock, iatImage InlineAtom* = ref object offset*: Offset @@ -24,6 +25,8 @@ type str*: string of iatInlineBlock: innerbox*: BlockBox + of iatImage: + bmp*: Bitmap RootInlineFragment* = ref object # offset relative to parent diff --git a/src/layout/engine.nim b/src/layout/engine.nim index 99d205f5..30afb066 100644 --- a/src/layout/engine.nim +++ b/src/layout/engine.nim @@ -5,6 +5,7 @@ import std/unicode import css/stylednode import css/values +import img/bitmap import layout/box import layout/layoutunit import types/winattrs @@ -68,6 +69,7 @@ type text: seq[string] newline: bool splitType: set[SplitType] + bmp: Bitmap BlockBoxBuilder = ref object of BoxBuilder inlinelayout: bool @@ -1500,8 +1502,20 @@ proc layoutInline(ictx: var InlineContext; box: InlineBoxBuilder): if ictx.firstTextFragment == nil: ictx.firstTextFragment = fragment ictx.lastTextFragment = fragment - ictx.layoutText(state, box.text) - ictx.layoutChildren(state, box.children) + if box.bmp != nil: + let iastate = InlineAtomState( + vertalign: state.computed{"vertical-align"}, + baseline: ictx.cellheight + ) + let atom = InlineAtom( + t: iatImage, + bmp: box.bmp, + size: Size(w: int(box.bmp.width), h: int(box.bmp.height)) #TODO overflow + ) + discard ictx.addAtom(state, iastate, atom) + else: + ictx.layoutText(state, box.text) + ictx.layoutChildren(state, box.children) if stSplitEnd in box.splitType: let paddingRight = box.computed{"padding-right"}.px(lctx, ictx.space.w) ictx.currentLine.size.w += paddingRight @@ -2856,10 +2870,10 @@ proc generateFromElem(ctx: var InnerBlockContext; styledNode: StyledNode) = of DisplayNone: discard proc generateAnonymousInlineText(ctx: var InnerBlockContext; text: string; - styledNode: StyledNode) = + styledNode: StyledNode; bmp: Bitmap = nil) = if ctx.iroot == nil: let computed = styledNode.computed.inheritProperties() - ctx.ibox = InlineBoxBuilder(computed: computed, node: styledNode) + ctx.ibox = InlineBoxBuilder(computed: computed, node: styledNode, bmp: bmp) if ctx.inlineStack.len > 0: let iparent = ctx.reconstructInlineParents() iparent.children.add(ctx.ibox) @@ -2900,7 +2914,7 @@ proc generateReplacement(ctx: var InnerBlockContext; ctx.generateAnonymousInlineText(child.content.s, parent) of ContentImage: #TODO idk - ctx.generateAnonymousInlineText("[img]", parent) + ctx.generateAnonymousInlineText("[img]", parent, child.content.bmp) of ContentVideo: ctx.generateAnonymousInlineText("[video]", parent) of ContentAudio: @@ -3116,7 +3130,7 @@ proc generateTableBox(styledNode: StyledNode; lctx: LayoutState; box.generateTableChildWrappers() return box -proc renderLayout*(root: StyledNode; attrsp: ptr WindowAttributes): BlockBox = +proc layout*(root: StyledNode; attrsp: ptr WindowAttributes): BlockBox = let space = AvailableSpace( w: stretch(attrsp[].width_px), h: stretch(attrsp[].height_px) diff --git a/src/layout/renderdocument.nim b/src/layout/renderdocument.nim index fe7b92cb..e57a6d2c 100644 --- a/src/layout/renderdocument.nim +++ b/src/layout/renderdocument.nim @@ -3,12 +3,13 @@ import std/unicode import css/stylednode import css/values +import img/bitmap import layout/box import layout/engine import layout/layoutunit -import types/winattrs import types/cell import types/color +import types/winattrs import utils/strwidth type @@ -208,12 +209,19 @@ proc setText(grid: var FlexibleGrid; linestr: string; x, y: int; format: Format; assert grid[y].formats[fi].pos <= nx # That's it! -type RenderState = object - # Position of the absolute positioning containing block: - # https://drafts.csswg.org/css-position/#absolute-positioning-containing-block - absolutePos: seq[Offset] - bgcolor: CellColor - attrsp: ptr WindowAttributes +type + PosBitmap* = ref object + x*: int + y*: int + bmp*: Bitmap + + RenderState = object + # Position of the absolute positioning containing block: + # https://drafts.csswg.org/css-position/#absolute-positioning-containing-block + absolutePos: seq[Offset] + bgcolor: CellColor + attrsp: ptr WindowAttributes + images: seq[PosBitmap] template attrs(state: RenderState): WindowAttributes = state.attrsp[] @@ -361,6 +369,15 @@ proc renderInlineFragment(grid: var FlexibleGrid; state: var RenderState; grid.setRowWord(state, atom, offset, format, fragment.node) of iatSpacing: grid.setSpacing(state, atom, offset, format, fragment.node) + of iatImage: + state.images.add(PosBitmap( + x: (offset.x div state.attrs.ppc).toInt, + y: (offset.y div state.attrs.ppl).toInt, + bmp: atom.bmp + )) + else: + for child in fragment.children: + grid.renderInlineFragment(state, child, offset) if fragment.computed{"position"} != PositionStatic: if fragment.splitType != {stSplitStart, stSplitEnd}: if stSplitStart in fragment.splitType: @@ -370,8 +387,6 @@ proc renderInlineFragment(grid: var FlexibleGrid; state: var RenderState; )) if stSplitEnd in fragment.splitType: discard state.absolutePos.pop() - for child in fragment.children: - grid.renderInlineFragment(state, child, offset) proc renderRootInlineFragment(grid: var FlexibleGrid; state: var RenderState; root: RootInlineFragment; offset: Offset) = @@ -440,7 +455,8 @@ proc renderBlockBox(grid: var FlexibleGrid; state: var RenderState; stack.add((box.nested[i], offset)) proc renderDocument*(grid: var FlexibleGrid; bgcolor: var CellColor; - styledRoot: StyledNode; attrsp: ptr WindowAttributes) = + styledRoot: StyledNode; attrsp: ptr WindowAttributes; + images: var seq[PosBitmap]) = grid.setLen(0) if styledRoot == nil: # no HTML element when we run cascade; just clear all lines. @@ -449,8 +465,9 @@ proc renderDocument*(grid: var FlexibleGrid; bgcolor: var CellColor; absolutePos: @[Offset(x: 0, y: 0)], attrsp: attrsp ) - let rootBox = renderLayout(styledRoot, attrsp) + let rootBox = styledRoot.layout(attrsp) grid.renderBlockBox(state, rootBox, Offset(x: 0, y: 0)) if grid.len == 0: grid.addLines(1) bgcolor = state.bgcolor + images = state.images diff --git a/src/local/container.nim b/src/local/container.nim index b88161b8..3a0f1daf 100644 --- a/src/local/container.nim +++ b/src/local/container.nim @@ -14,6 +14,7 @@ import io/socketstream import js/javascript import js/jstypes import js/regex +import layout/renderdocument import loader/headers import loader/loader import loader/request @@ -25,7 +26,6 @@ import types/cookie import types/referrer import types/url import types/winattrs -import utils/luwrap import utils/mimeguess import utils/strwidth import utils/twtstr @@ -152,6 +152,7 @@ type cacheFile* {.jsget.}: string mainConfig*: Config flags*: set[ContainerFlag] + images*: seq[PosBitmap] jsDestructor(Highlight) jsDestructor(Container) @@ -458,7 +459,6 @@ proc requestLines(container: Container): EmptyPromise {.discardable.} = container.lineshift = w.a for y in 0 ..< min(res.lines.len, w.len): container.lines[y] = res.lines[y] - container.lines[y].str.mnormalize() var isBgNew = container.bgcolor != res.bgcolor if isBgNew: container.bgcolor = res.bgcolor @@ -474,6 +474,7 @@ proc requestLines(container: Container): EmptyPromise {.discardable.} = let cw = container.fromy ..< container.fromy + container.height if w.a in cw or w.b in cw or cw.a in w or cw.b in w or isBgNew: container.triggerEvent(cetUpdate) + container.images = res.images ) proc redraw(container: Container) {.jsfunc.} = @@ -1387,9 +1388,9 @@ proc onload(container: Container; res: int) = container.triggerEvent(cetStatus) container.triggerEvent(cetLoaded) if cfHasStart notin container.flags and container.url.anchor != "": - container.requestLines().then(proc(): Promise[Opt[tuple[x, y: int]]] = + container.requestLines().then(proc(): Promise[GotoAnchorResult] = return container.iface.gotoAnchor() - ).then(proc(res: Opt[tuple[x, y: int]]) = + ).then(proc(res: GotoAnchorResult) = if res.isSome: let res = res.get container.setCursorXYCenter(res.x, res.y) diff --git a/src/local/pager.nim b/src/local/pager.nim index 28c1face..8e828190 100644 --- a/src/local/pager.nim +++ b/src/local/pager.nim @@ -449,6 +449,11 @@ proc draw*(pager: Pager) = else: pager.term.writeGrid(pager.statusgrid, 0, pager.attrs.height - 1) pager.term.outputGrid() + if container != nil: + pager.term.clearImages() + for image in container.images: + pager.term.outputImage(image.bmp, image.x - container.fromx, + image.y - container.fromy, pager.attrs.width, pager.attrs.height - 1) if pager.askpromise != nil: pager.term.setCursor(pager.askcursor, pager.attrs.height - 1) elif pager.lineedit.isSome: diff --git a/src/local/term.nim b/src/local/term.nim index eee53039..0da700f5 100644 --- a/src/local/term.nim +++ b/src/local/term.nim @@ -8,7 +8,9 @@ import std/unicode import bindings/termcap import config/config +import img/bitmap import io/posixstream +import js/base64 import types/cell import types/color import types/opt @@ -64,6 +66,7 @@ type attrs*: WindowAttributes colormode: ColorMode formatmode: FormatMode + imagemode: ImageMode smcup: bool tc: Termcap tname: string @@ -128,6 +131,15 @@ const RMCUP = DECRST(1049) const SGRMOUSEBTNON = DECSET(1002, 1006) const SGRMOUSEBTNOFF = DECRST(1002, 1006) +# application program command + +# This is only used in kitty images, and join()'ing kilobytes of base64 +# is rather inefficient so we don't use a template. +const APC = "\e_" +const ST = "\e\\" + +const KITTYQUERY = APC & "Gi=1,a=q;" & ST + when not termcap_found: const CNORM = DECSET(25) const CIVIS = DECRST(25) @@ -555,15 +567,13 @@ proc applyConfig(term: Terminal) = # colors, formatting if term.config.display.color_mode.isSome: term.colormode = term.config.display.color_mode.get - elif term.isatty(): - let colorterm = getEnv("COLORTERM") - if colorterm in ["24bit", "truecolor"]: - term.colormode = cmTrueColor if term.config.display.format_mode.isSome: term.formatmode = term.config.display.format_mode.get for fm in FormatFlags: if fm in term.config.display.no_format_mode: term.formatmode.excl(fm) + if term.config.display.image_mode.isSome: + term.imagemode = term.config.display.image_mode.get if term.isatty(): if term.config.display.alt_screen.isSome: term.smcup = term.config.display.alt_screen.get @@ -603,6 +613,50 @@ proc outputGrid*(term: Terminal) = for i in 0 ..< term.canvas.cells.len: term.pcanvas[i] = term.canvas[i] +proc clearImages*(term: Terminal) = + if term.imagemode == imKitty: + term.write(APC & "Ga=d" & ST) + +proc outputImage*(term: Terminal; bmp: Bitmap; x, y, maxw, maxh: int) = + case term.imagemode + of imNone: discard + of imSixel: + discard #TODO + of imKitty: + # max 4096 bytes, base encoded + const MaxPixels = ((4096 div 4) * 3) div 3 + let offx = if x < 0: -(x * term.attrs.ppc) else: 0 + let offy = if y < 0: -(y * term.attrs.ppl) else: 0 + let w = int(bmp.width) + let h = int(bmp.height) + var dispw = w + if x + dispw div term.attrs.ppc > maxw: + dispw = (maxw - x) * term.attrs.ppc + var disph = h + if y + disph div term.attrs.ppl > maxh: + disph = (maxh - y) * term.attrs.ppl + var outs = term.cursorGoto(max(x, 0), max(y, 0)) + outs &= APC & "Gf=24,m=1,a=T,C=1,s=" & $w & ",v=" & $h & + ",x=" & $offx & ",y=" & $offy & ",w=" & $dispw & ",h=" & $disph & ';' + var buf = newStringOfCap(MaxPixels * 4) + var i = 0 + # transcode to RGB + while i < bmp.px.len: # max is 4096 + if i > 0 and i mod MaxPixels == 0: + outs &= btoa(buf) + outs &= ST + term.write(outs) + buf.setLen(0) + outs = APC & "Gm=1;" + buf &= char(bmp.px[i].r) + buf &= char(bmp.px[i].g) + buf &= char(bmp.px[i].b) + inc i + outs = APC & "Gm=0;" + outs &= btoa(buf) + outs &= ST + term.write(outs) + proc clearCanvas*(term: Terminal) = term.cleared = false @@ -665,7 +719,7 @@ when termcap_found: type QueryAttrs = enum - qaAnsiColor, qaRGB, qaSixel + qaAnsiColor, qaRGB, qaSixel, qaKittyImage QueryResult = object success: bool @@ -683,6 +737,7 @@ proc queryAttrs(term: Terminal; windowOnly: bool): QueryResult = const outs = XTGETFG & XTGETBG & + KITTYQUERY & GEOMPIXEL & GEOMCELL & XTGETTCAP("524742") & @@ -764,12 +819,15 @@ proc queryAttrs(term: Terminal; windowOnly: bool): QueryResult = term.expect ';' if term.consume == 'r' and term.consume == 'g' and term.consume == 'b': term.expect ':' - template eat_color(tc: char): uint8 = + var was_esc = false + template eat_color(tc: set[char]): uint8 = var val = 0u8 var i = 0 - while (let c = term.consume; c != tc): + var c = char(0) + while (c = term.consume; c notin tc): let v0 = hexValue(c) - if i > 4 or v0 == -1: fail # wat + if i > 4 or v0 == -1: + fail # wat let v = uint8(v0) if i == 0: # 1st place val = (v shl 4) or v @@ -777,10 +835,14 @@ proc queryAttrs(term: Terminal; windowOnly: bool): QueryResult = val = (val xor 0xF) or v # all other places are irrelevant inc i + was_esc = c == '\e' val - let r = eat_color '/' - let g = eat_color '/' - let b = eat_color '\a' + let r = eat_color {'/'} + let g = eat_color {'/'} + let b = eat_color {'\a', '\e'} + if was_esc: + # we got ST, not BEL; at least kitty does this + term.expect '\\' if c == '0': result.fgcolor = some(rgb(r, g, b)) else: @@ -805,7 +867,14 @@ proc queryAttrs(term: Terminal; windowOnly: bool): QueryResult = if id == tcapRGB: result.attrs.incl(qaRGB) else: # 0 - term.expect '\e' # ST (1) + # pure insanity: kitty returns P0, but also +r524742 after. please + # make up your mind! + term.skip_until '\e' # ST (1) + term.expect '\\' # ST (2) + of '_': # APC + term.expect 'G' + result.attrs.incl(qaKittyImage) + term.skip_until '\e' # ST (1) term.expect '\\' # ST (2) else: fail @@ -844,6 +913,10 @@ proc detectTermAttributes(term: Terminal; windowOnly: bool): TermStartResult = term.colormode = cmANSI if qaRGB in r.attrs: term.colormode = cmTrueColor + if qaSixel in r.attrs: + term.imagemode = imSixel + if qaKittyImage in r.attrs: + term.imagemode = imKitty # just assume the terminal doesn't choke on these. term.formatmode = {ffStrike, ffOverline} if r.bgcolor.isSome: diff --git a/src/server/buffer.nim b/src/server/buffer.nim index 10d1cce0..56ad946f 100644 --- a/src/server/buffer.nim +++ b/src/server/buffer.nim @@ -26,6 +26,7 @@ import html/enums import html/env import html/event import html/formdata as formdata_impl +import img/bitmap import io/bufreader import io/bufstream import io/bufwriter @@ -91,6 +92,7 @@ type ishtml: bool firstBufferRead: bool lines: FlexibleGrid + images: seq[PosBitmap] request: Request # source request attrs: WindowAttributes window: Window @@ -648,7 +650,9 @@ proc findNextMatch*(buffer: Buffer; regex: Regex; cursorx, cursory: int; break inc y -proc gotoAnchor*(buffer: Buffer): Opt[tuple[x, y: int]] {.proxy.} = +type GotoAnchorResult* = Opt[tuple[x, y: int]] + +proc gotoAnchor*(buffer: Buffer): GotoAnchorResult {.proxy.} = if buffer.document == nil: return err() let anchor = buffer.document.findAnchor(buffer.url.anchor) @@ -673,7 +677,8 @@ proc do_reshape(buffer: Buffer) = buffer.prevStyled = nil let styledRoot = buffer.document.applyStylesheets(uastyle, buffer.userstyle, buffer.prevStyled) - buffer.lines.renderDocument(buffer.bgcolor, styledRoot, addr buffer.attrs) + buffer.lines.renderDocument(buffer.bgcolor, styledRoot, addr buffer.attrs, + buffer.images) buffer.prevStyled = styledRoot proc processData0(buffer: Buffer; data: openArray[char]): bool = @@ -1718,11 +1723,11 @@ proc readCanceled*(buffer: Buffer): bool {.proxy.} = proc findAnchor*(buffer: Buffer; anchor: string): bool {.proxy.} = return buffer.document != nil and buffer.document.findAnchor(anchor) != nil -type GetLinesResult* = tuple[ - numLines: int, - lines: seq[SimpleFlexibleLine], +type GetLinesResult* = tuple + numLines: int + lines: seq[SimpleFlexibleLine] bgcolor: CellColor -] + images: seq[PosBitmap] proc getLines*(buffer: Buffer; w: Slice[int]): GetLinesResult {.proxy.} = var w = w @@ -1736,6 +1741,11 @@ proc getLines*(buffer: Buffer; w: Slice[int]): GetLinesResult {.proxy.} = result.lines.add(line) result.numLines = buffer.lines.len result.bgcolor = buffer.bgcolor + if buffer.config.images: + for image in buffer.images: + if image.y <= w.b and + image.y + int(image.bmp.height) div buffer.attrs.ppl >= w.a: + result.images.add(image) proc markURL*(buffer: Buffer; schemes: seq[string]) {.proxy.} = if buffer.document == nil or buffer.document.body == nil: |