Files
bare-operating-system/packages/bare-os-coreutils/lib/summon/summon-tui.js
T
2026-08-18 18:11:28 -04:00

465 lines
14 KiB
JavaScript

/** TEA summon TUI. Cell buffer; engine lives in summon-*.js. */
function bareSummonTuiRunOpts() {
return { buffer: 'cell' }
}
function bareSummonCreateTuiApp(ctx, session) {
var tui = ctx.tui
var size0 = tui && typeof tui.size === 'function' ? tui.size() : {}
return {
session: session,
width: size0.width || 80,
height: size0.height || 24,
mode: 'browse',
help: false,
quitConfirm: false,
status: '',
num: '',
linkFocus: 0,
input: tui.textinput.create({ prompt: '', focused: true, charLimit: 800 }),
viewport: tui.viewport.create({
width: size0.width || 80,
height: Math.max(6, (size0.height || 24) - 5)
}),
init: function () {
var self = this
self.session.cols = Math.max(20, (self.width || 80) - 2)
var tab = bareSummonCurrent(self.session)
if (tab && tab.url) self._go(tab.url, { replace: true })
else self._sync()
return null
},
_tab: function () {
return bareSummonCurrent(this.session)
},
_sync: function () {
var tab = this._tab()
var cols = Math.max(20, (this.width || 80) - 2)
this.session.cols = cols
if (tab && tab.doc && (!tab.layout || tab.layout.cols !== cols)) {
bareSummonRelayout(this.session)
tab = this._tab()
}
this.viewport.width = cols
this.viewport.height = Math.max(4, (this.height || 24) - 5)
var lines = []
if (this.mode === 'source') {
lines = String((tab && tab.source) || '').split('\n')
} else if (tab && tab.reader && tab.doc) {
lines = String(bareSummonReaderText(tab.doc) || '').split('\n')
} else if (tab && tab.layout && tab.layout.plain) {
lines = tab.layout.plain.slice()
}
if (tab && tab.error) lines.unshift('error: ' + tab.error)
if (tab && tab.loading) lines.unshift('loading…')
this.viewport.setContent(lines.join('\n'))
var nlinks =
tab && tab.layout && tab.layout.links ? tab.layout.links.length : 0
if (this.linkFocus >= nlinks) this.linkFocus = Math.max(0, nlinks - 1)
},
_go: function (url, opts) {
var self = this
var tab = this._tab()
if (tab) tab.loading = true
this.status = 'loading…'
this.mode = 'browse'
this.num = ''
bareSummonNavigate(this.session, url, opts || {})
.then(function () {
self.status = ''
self._sync()
if (tui && typeof tui.send === 'function') {
tui.send({ type: 'summon.loaded' })
}
})
.catch(function (err) {
var cur = self._tab()
if (cur) {
cur.error = (err && err.message) || String(err)
cur.loading = false
}
self.status = 'error'
self._sync()
if (tui && typeof tui.send === 'function') {
tui.send({ type: 'summon.loaded' })
}
})
},
_follow: function (id) {
var tab = this._tab()
var links = (tab && tab.layout && tab.layout.links) || []
var hit = null
var i
for (i = 0; i < links.length; i++) {
if (links[i].id === id) hit = links[i]
}
if (!hit) {
this.status = 'no link ' + (id + 1)
return
}
this._go(hit.href)
},
_promptSubmit: function () {
var line = String(this.input.value || '').trim()
this.input.reset()
var mode = this.mode
this.mode = 'browse'
if (!line) return
if (mode === 'goto') this._go(line)
else if (mode === 'find') {
var tab = this._tab()
if (tab) tab.find = line
this._find(1)
}
},
_find: function (dir) {
var tab = this._tab()
var q = tab && tab.find ? String(tab.find) : ''
if (!q) {
this.status = 'no find'
return
}
var lines = this.viewport.lines || []
var start = this.viewport.yOffset + (dir > 0 ? 1 : -1)
var i
if (dir > 0) {
for (i = start; i < lines.length; i++) {
if (
String(lines[i] || '')
.toLowerCase()
.indexOf(q.toLowerCase()) >= 0
) {
this.viewport.setYOffset(i)
this.status = 'find ' + q
return
}
}
} else {
for (i = start; i >= 0; i--) {
if (
String(lines[i] || '')
.toLowerCase()
.indexOf(q.toLowerCase()) >= 0
) {
this.viewport.setYOffset(i)
this.status = 'find ' + q
return
}
}
}
this.status = 'not found: ' + q
},
update: function (msg) {
if (msg && msg.type === 'resize') {
this.width = msg.width || this.width
this.height = msg.height || this.height
this._sync()
return [this, null]
}
if (msg && msg.type === 'summon.loaded') {
this._sync()
return [this, null]
}
if (this.help) {
if (msg && msg.type === 'key') this.help = false
return [this, null]
}
if (this.quitConfirm) {
if (tui.key.matches(msg, 'y', 'Y')) return [this, tui.quit]
if (msg && msg.type === 'key') this.quitConfirm = false
return [this, null]
}
if (this.mode === 'goto' || this.mode === 'find') {
if (tui.key.matches(msg, 'escape', 'ctrl+c')) {
this.mode = 'browse'
this.input.reset()
return [this, null]
}
if (tui.key.matches(msg, 'enter')) {
this._promptSubmit()
return [this, null]
}
var pair = this.input.update(msg)
this.input = pair[0]
return [this, pair[1]]
}
if (tui.key.matches(msg, 'ctrl+c', 'q')) {
this.quitConfirm = true
return [this, null]
}
if (tui.key.matches(msg, 'f10')) return [this, tui.quit]
if (tui.key.matches(msg, '?')) {
this.help = true
return [this, null]
}
if (tui.key.matches(msg, 'g')) {
this.mode = 'goto'
this.input.reset()
this.input.setValue && this.input.setValue('')
return [this, null]
}
if (tui.key.matches(msg, '/')) {
this.mode = 'find'
this.input.reset()
return [this, null]
}
if (tui.key.matches(msg, 'n')) {
this._find(1)
return [this, null]
}
if (tui.key.matches(msg, 'N')) {
this._find(-1)
return [this, null]
}
if (tui.key.matches(msg, 'r')) {
this._go(this._tab().url, { replace: true })
return [this, null]
}
if (tui.key.matches(msg, 'R')) {
var tabR = this._tab()
tabR.reader = !tabR.reader
this._sync()
return [this, null]
}
if (tui.key.matches(msg, 'J')) {
this.session.config.javascript =
this.session.config.javascript === false
this.status =
this.session.config.javascript === false ? 'js off' : 'js on'
if (this._tab().url) this._go(this._tab().url, { replace: true })
return [this, null]
}
if (tui.key.matches(msg, 's')) {
this.mode = this.mode === 'source' ? 'browse' : 'source'
this._sync()
return [this, null]
}
if (tui.key.matches(msg, 'H')) {
this._go('about:summon')
return [this, null]
}
if (tui.key.matches(msg, 'o')) {
this._go('about:bookmarks')
return [this, null]
}
if (tui.key.matches(msg, 'a')) {
var tabA = this._tab()
bareSummonAddBookmark(this.session, tabA.url, tabA.title)
this.status = 'bookmarked'
if (typeof bareSummonSavePersisted === 'function') {
bareSummonSavePersisted(this.session)
}
return [this, null]
}
if (tui.key.matches(msg, 'left', 'h')) {
var selfB = this
bareSummonBack(this.session).then(function () {
selfB._sync()
if (tui && typeof tui.send === 'function')
tui.send({ type: 'summon.loaded' })
})
return [this, null]
}
if (tui.key.matches(msg, 'u')) {
var selfF = this
bareSummonForward(this.session).then(function () {
selfF._sync()
if (tui && typeof tui.send === 'function')
tui.send({ type: 'summon.loaded' })
})
return [this, null]
}
if (tui.key.matches(msg, 't')) {
bareSummonNewTab(this.session, 'about:summon')
this._go('about:summon', { replace: true })
return [this, null]
}
if (tui.key.matches(msg, 'w')) {
bareSummonCloseTab(this.session)
this._sync()
return [this, null]
}
if (tui.key.matches(msg, ']')) {
bareSummonCycleTab(this.session, 1)
this._sync()
return [this, null]
}
if (tui.key.matches(msg, '[')) {
bareSummonCycleTab(this.session, -1)
this._sync()
return [this, null]
}
if (tui.key.matches(msg, 'tab')) {
var tabT = this._tab()
var n =
tabT && tabT.layout && tabT.layout.links
? tabT.layout.links.length
: 0
if (n) this.linkFocus = (this.linkFocus + 1) % n
return [this, null]
}
if (tui.key.matches(msg, 'enter')) {
if (this.num) {
var id = parseInt(this.num, 10) - 1
this.num = ''
if (id >= 0) this._follow(id)
return [this, null]
}
this._follow(this.linkFocus)
return [this, null]
}
if (
msg &&
msg.type === 'key' &&
msg.name &&
msg.name.length === 1 &&
msg.name >= '0' &&
msg.name <= '9'
) {
this.num += msg.name
this.status = '#' + this.num
return [this, null]
}
if (tui.key.matches(msg, 'j', 'down')) {
this.viewport.scrollDown(1)
return [this, null]
}
if (tui.key.matches(msg, 'k', 'up')) {
this.viewport.scrollUp(1)
return [this, null]
}
if (tui.key.matches(msg, ' ', 'pagedown')) {
this.viewport.scrollDown(this.viewport.height || 10)
return [this, null]
}
if (tui.key.matches(msg, 'pageup')) {
this.viewport.scrollUp(this.viewport.height || 10)
return [this, null]
}
if (tui.key.matches(msg, 'home')) {
this.viewport.gotoTop()
return [this, null]
}
if (tui.key.matches(msg, 'end')) {
this.viewport.gotoBottom()
return [this, null]
}
return [this, null]
},
view: function () {
var cols = Math.max(40, this.width || 80)
var rows = Math.max(12, this.height || 24)
var st = tui.style
var tab = this._tab()
var ntab = this.session.tabs.length
var idx = this.session.current + 1
var url = (tab && tab.url) || ''
var title = (tab && tab.title) || ''
var jsOn = this.session.config && this.session.config.javascript !== false
var scripts =
' js:' +
(jsOn ? 'on' : 'off') +
(tab && tab.js && tab.js.ran ? '+' + tab.js.ran : '') +
(tab && tab.scriptsBlocked ? '/' + tab.scriptsBlocked : '')
var titleRaw =
' summon [' +
idx +
'/' +
ntab +
'] ' +
(title || url) +
(tab && tab.reader ? ' reader' : '') +
(this.mode === 'source' ? ' source' : '') +
scripts +
' ' +
cols +
'x' +
rows +
' '
var bar = st
? st()
.foreground('brightwhite')
.background('blue')
.width(cols)
.render(titleRaw)
: titleRaw
var locRaw =
' ' + url + ' ' + (tab && tab.status ? tab.status : '') + ' '
var loc = st ? st().dim().width(cols).render(locRaw) : locRaw
var body = this.viewport.view()
var prompt = ''
if (this.mode === 'goto') {
prompt =
'Go: ' +
(this.input.view ? this.input.view() : this.input.value || '')
} else if (this.mode === 'find') {
prompt =
'Find: ' +
(this.input.view ? this.input.view() : this.input.value || '')
} else {
prompt =
(this.status || '') +
(this.num ? ' #' + this.num : '') +
(this.linkFocus >= 0 &&
tab &&
tab.layout &&
tab.layout.links[this.linkFocus]
? ' → ' + tab.layout.links[this.linkFocus].href
: '')
}
var footRaw =
'g go Enter follow 12+Enter link h back j/k /find a mark o marks q quit ?'
var foot = st ? st().dim().width(cols).render(footRaw) : footRaw
var promptLine = st
? st()
.width(cols)
.render(prompt || ' ')
: prompt || ' '
var lines = [bar, loc]
.concat(String(body).split('\n'))
.concat([promptLine, foot])
while (lines.length < rows) lines.push('')
var out = []
var i
for (i = 0; i < rows; i++) {
out.push(
st
? st.truncate(lines[i] || '', cols)
: String(lines[i] || '').slice(0, cols)
)
}
return out.join('\n')
},
overlay: function (size) {
if (!this.help && !this.quitConfirm) return null
var cols = (size && size.width) || this.width || 80
var rows = (size && size.height) || this.height || 24
var st = tui.style
var body = this.quitConfirm
? 'Leave summon?\n\nY yes any other key cancel'
: 'summon — text web browser\n\n' +
'g URL go Enter follow focused / numbered link\n' +
'h/left back u forward r reload R reader s source\n' +
'j/k scroll / find n/N next/prev a bookmark o marks\n' +
't new tab w close [ ] tabs H home digits+Enter link #\n' +
'J toggle page JS (Bare VM / sandbox) HTTP via ctx.httpFetch\n\n' +
'Press any key to close.'
var boxed = st
? st()
.border(st.borders.rounded)
.padding(1, 2)
.background('black')
.render(body)
: body
var h = st ? st.height(boxed) : boxed.split('\n').length
var w = st ? st.width(boxed) : 56
return {
row: Math.max(0, Math.floor((rows - h) / 2)),
col: Math.max(0, Math.floor((cols - w) / 2)),
text: boxed
}
}
}
}