kopher

A simple Gopher client for KaiOS
git clone git://git.luxferre.top/kopher.git
Log | Files | Refs | README | LICENSE

commit 1bbe1ffd8172cc3678ddf14355f77a0a6aaee42d
parent 53f9dc3a46af138fc2f302cf0bb6ee87de7a60ff
Author: Luxferre <lux@ferre>
Date:   Mon,  3 Apr 2023 12:37:10 +0300

Added basic terminal coloring to the infolines

Diffstat:
MREADME.md | 13+++++++++++--
Mapp.css | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Mjs/hi01379.js | 68+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mmanifest.webapp | 2+-
4 files changed, 130 insertions(+), 4 deletions(-)

diff --git a/README.md b/README.md @@ -65,6 +65,7 @@ Note that currently, Kopher and hi01379.js assume that the search request from a - Unlimited navigation history (doesn't persist between sessions) - Up to 10 numbered bookmarks plus customizable homepage - Binary blob downloads (see "Downloads") +- Limited support of ANSI terminal escape codes in Gophermap infolines (see "Does it have terminal emulation support for character styling?") ## Controls @@ -105,9 +106,17 @@ For your convenience, only the current hole hostname is always displayed in the Yes. In fact, UTF-8 is the only supported encoding. As ASCII is a subset of UTF-8, old ASCII-only resources will be displayed just fine as well. -## Does it have terminal emulation support for character coloring purposes? +## Does it have terminal emulation support for character styling? -No, but that feature is considered to be implemented if this client gains more traction and if there still is enough amount of the resources that actually use VT escape characters. +As of April 2023, Kopher implements a subset of ANSI terminal escape codes for text styling purposes with the following limitations: + +- they are applied to the Gophermap information lines **only** (and detected and deleted everywhere else); +- they only work within the single infoline or a block of sequential infolines and all styling gets reset when another entry type is encountered, even if the reset sequence isn't there; +- only the basic 8/16-color palette is implemented (because these codes physically translate into corresponding data attributes); +- beside the colors, the following text attributes are implemented: bold, italic, underline, blinking, inverse, hidden, strikethrough; +- any unsupported escape sequences are automatically deleted from the rendered output. + +All these limitations are intended to keep Kopher's codebase simple, and not likely to change in the foreseeable future. ## Can I use the hi01379.js library in non-KaiOS environments? diff --git a/app.css b/app.css @@ -64,3 +64,54 @@ footer .status {width: 100%; text-overflow: ellipsis; overflow: hidden; white-sp } .content a[data-focused="1"] {text-decoration: underline} +/* Terminal infoline styling */ + +/* modes = ['', 'BB', 'FN', 'IT', 'UL', 'BL', '', 'IN', 'HI', 'ST'] */ + +/* colors (FC/BC): 0 black, 1 red, 2 green, 3 yellow, 4 blue, 5 magenta, 6 cyan, 7 white */ + +.content span[data-styling] { + --ds-fg-color: var(--fg-color); + --ds-bg-color: var(--bg-color); + background: var(--ds-bg-color); + color: var(--ds-fg-color); +} + +@keyframes strict_blink { + 0% { + visibility: hidden + } + 50% { + visibility: hidden + } + 100% { + visibility: visible + } +} + +.content span[data-styling~="BB"] {font-weight: bolder} +.content span[data-styling~="IT"] {font-style: italic} +.content span[data-styling~="UL"] {text-decoration: underline} +.content span[data-styling~="BL"] {animation: 2s linear infinite strict_blink} +.content span[data-styling~="IN"] {background:var(--ds-fg-color);color:var(--ds-bg-color)} +.content span[data-styling~="HI"] {visibility: hidden} +.content span[data-styling~="ST"] {text-decoration: line-through} + +.content span[data-styling~="FC0"] {--ds-fg-color: black} +.content span[data-styling~="FC1"] {--ds-fg-color: red} +.content span[data-styling~="FC2"] {--ds-fg-color: green} +.content span[data-styling~="FC3"] {--ds-fg-color: yellow} +.content span[data-styling~="FC4"] {--ds-fg-color: blue} +.content span[data-styling~="FC5"] {--ds-fg-color: magenta} +.content span[data-styling~="FC6"] {--ds-fg-color: cyan} +.content span[data-styling~="FC7"] {--ds-fg-color: white} + +.content span[data-styling~="BC0"] {--ds-bg-color: black} +.content span[data-styling~="BC1"] {--ds-bg-color: red} +.content span[data-styling~="BC2"] {--ds-bg-color: green} +.content span[data-styling~="BC3"] {--ds-bg-color: yellow} +.content span[data-styling~="BC4"] {--ds-bg-color: blue} +.content span[data-styling~="BC5"] {--ds-bg-color: magenta} +.content span[data-styling~="BC6"] {--ds-bg-color: cyan} +.content span[data-styling~="BC7"] {--ds-bg-color: white} + diff --git a/js/hi01379.js b/js/hi01379.js @@ -28,11 +28,74 @@ Hi01379 = (function(psGopherRequest) { }).join('\n') } + function terminalize(str, attrs) { // partial terminal styling support with ANSI codes + if(!attrs) attrs = [] // accept previous attributes array if present + var prevattrs = attrs.join(' ') // preserve stringified copy + var out = '', c, styleseq, sl, l = str.length, i, j, att, attindex, strattrs, + visualOpenTag='<span data-styling="%s">', visualCloseTag='</span>', + modes = ['', 'BB', 'FN', 'IT', 'UL', 'BL', '', 'IN', 'HI', 'ST'] + for(i=0;i<l;i++) { + c = str[i] + if(c === '\x1b') { // escape character encountered + c = str[i+1] // get the next character, don't increase the counter yet + if(c === '[') { // ANSI sequence starting + i++ // increase the counter, now it's pointing to [ + styleseq='' // init style sequence buffer + do { // read the style sequence until m character or any whitespace + c = str[++i] // increase the counter once more, [ is ignored + styleseq += c + } while(c !== 'm' && c !== ' ' && c !== '\t' && c !== '\r' && c !== '\n') + styleseq = styleseq.slice(0, -1).split(';') // attributes are delimited by ; + for(sl=styleseq.length,j=0;j<sl;j++) { // iterate over attributes in their order + att = parseInt(styleseq[j]) // in this case parseInt is more reliable than |0 + if(att === 0) attrs = [] // reset all styling + else if(att >= 30 && att <= 37) attrs.push('FC'+(att-30)) // foreground color + else if(att >= 40 && att <= 47) attrs.push('BC'+(att-40)) // background color + else if(att < 10) attrs.push(modes[att]) // activate special modes + else if(att > 20 && att < 30) { // deactivate special modes + if(att === 22) { // also deactivate bold mode 1 + attindex = attrs.indexOf('BB') + if(attindex > -1) + attrs.splice(attindex, 1) + } + attindex = attrs.indexOf(modes[att - 20]) + if(attindex > -1) + attrs.splice(attindex, 1) + } + else if(att === 39) { // reset only the foreground color + attrs = attrs.map(function(v) { + return v.startsWith('FC') ? '' : v + }).filter(String) + } + else if(att === 49) { // reset only the background color + attrs = attrs.map(function(v) { + return v.startsWith('BC') ? '' : v + }).filter(String) + } + } + // now we have our attrs array with all styling applied, output the tags + strattrs = attrs.join(' ') + if(strattrs != prevattrs) { // there are some changes + if(prevattrs != '') // previous attributes were not empty + out += visualCloseTag + if(strattrs) // current attributes are not empty + out += visualOpenTag.replace('%s', strattrs) + prevattrs = strattrs // update the previous attribute cache + } + } // don't add any processed characters "as is" to the output + else continue // skip the escape and proceed with the loop + } + else out += c // just append it to the output + } + return {output: out, attrs: attrs} // return both output and remaining attributes + } + function gophermapToHTML(gmap, chost, cport) { if(!cport) cport = 70 if(!chost) chost = 'localhost' var lines = gmap.split(/\r?\n/), line, l = lines.length, i, lbr = '<br>', type, rest, desc, sel, host, port, + iattrs = [], istyled, // attribute cache and styled infomessage placeholder output = '', pretag = 'pre', preflag = false for(i=0;i<l;i++) { line = lines[i] @@ -55,11 +118,14 @@ Hi01379 = (function(psGopherRequest) { preflag = true output += '<' + pretag + '>' } - output += desc + '\n' + istyled = terminalize(desc, iattrs) + iattrs = istyled.attrs + output += istyled.output + '\n' } else { //information messages ended, stop preformatting if(preflag) { preflag = false + iattrs = [] // reset info styling attributes output += '</' + pretag + '>' } if(desc = desc.trim()) { // ignore empty descriptions diff --git a/manifest.webapp b/manifest.webapp @@ -1,5 +1,5 @@ { - "version": "0.0.4", + "version": "0.0.5", "name": "Kopher", "description": "A Gopher client for KaiOS", "launch_path": "/index.html",