2024-10-02 21:03:04 +02:00
|
|
|
/*
|
|
|
|
* MIT License
|
|
|
|
*
|
|
|
|
* Copyright (c) 2019
|
|
|
|
*
|
|
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
|
|
* of this software and associated documentation files (the "Software"), to deal
|
|
|
|
* in the Software without restriction, including without limitation the rights
|
|
|
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
|
|
* copies of the Software, and to permit persons to whom the Software is
|
|
|
|
* furnished to do so, subject to the following conditions:
|
|
|
|
*
|
|
|
|
* The above copyright notice and this permission notice shall be included in all
|
|
|
|
* copies or substantial portions of the Software.
|
|
|
|
*
|
|
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
|
|
* SOFTWARE.
|
|
|
|
*
|
|
|
|
* 2019 Mauve Ranger
|
|
|
|
* 2024 Leon van Kammen
|
|
|
|
*/
|
|
|
|
|
2024-10-01 19:07:03 +02:00
|
|
|
let terminalInstance = 0
|
|
|
|
|
|
|
|
const TERMINAL_THEME = {
|
|
|
|
theme_foreground: {
|
|
|
|
// 'default': '#ffffff'
|
|
|
|
},
|
|
|
|
theme_background: {
|
|
|
|
// 'default': '#000'
|
|
|
|
},
|
|
|
|
theme_cursor: {
|
|
|
|
// 'default': '#ffffff'
|
|
|
|
},
|
|
|
|
theme_selection: {
|
|
|
|
// 'default': 'rgba(255, 255, 255, 0.3)'
|
|
|
|
},
|
|
|
|
theme_black: {
|
|
|
|
// 'default': '#000000'
|
|
|
|
},
|
|
|
|
theme_red: {
|
|
|
|
// 'default': '#e06c75'
|
|
|
|
},
|
|
|
|
theme_brightRed: {
|
|
|
|
// 'default': '#e06c75'
|
|
|
|
},
|
|
|
|
theme_green: {
|
|
|
|
// 'default': '#A4EFA1'
|
|
|
|
},
|
|
|
|
theme_brightGreen: {
|
|
|
|
// 'default': '#A4EFA1'
|
|
|
|
},
|
|
|
|
theme_brightYellow: {
|
|
|
|
// 'default': '#EDDC96'
|
|
|
|
},
|
|
|
|
theme_yellow: {
|
|
|
|
// 'default': '#EDDC96'
|
|
|
|
},
|
|
|
|
theme_magenta: {
|
|
|
|
// 'default': '#e39ef7'
|
|
|
|
},
|
|
|
|
theme_brightMagenta: {
|
|
|
|
// 'default': '#e39ef7'
|
|
|
|
},
|
|
|
|
theme_cyan: {
|
|
|
|
// 'default': '#5fcbd8'
|
|
|
|
},
|
|
|
|
theme_brightBlue: {
|
|
|
|
// 'default': '#5fcbd8'
|
|
|
|
},
|
|
|
|
theme_brightCyan: {
|
|
|
|
// 'default': '#5fcbd8'
|
|
|
|
},
|
|
|
|
theme_blue: {
|
|
|
|
// 'default': '#5fcbd8'
|
|
|
|
},
|
|
|
|
theme_white: {
|
|
|
|
// 'default': '#d0d0d0'
|
|
|
|
},
|
|
|
|
theme_brightBlack: {
|
|
|
|
// 'default': '#808080'
|
|
|
|
},
|
|
|
|
theme_brightWhite: {
|
|
|
|
// 'default': '#ffffff'
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
AFRAME.registerComponent('xterm', {
|
|
|
|
schema: Object.assign({
|
2024-10-15 12:32:16 +02:00
|
|
|
XRrenderer: { type: 'string', default: 'canvas', },
|
2024-10-04 11:08:39 +02:00
|
|
|
cols: { type: 'number', default: 110, },
|
|
|
|
rows: { type: 'number', default: Math.floor( (window.innerHeight * 0.7 ) * 0.054 ) },
|
|
|
|
canvasLatency:{ type:'number', default: 200 }
|
2024-10-01 19:07:03 +02:00
|
|
|
}, TERMINAL_THEME),
|
|
|
|
|
|
|
|
write: function(message) {
|
|
|
|
this.term.write(message)
|
|
|
|
},
|
|
|
|
init: function () {
|
|
|
|
const terminalElement = document.createElement('div')
|
|
|
|
terminalElement.setAttribute('style', `
|
2024-10-04 17:49:15 +02:00
|
|
|
width: 800px;
|
|
|
|
height: ${Math.floor( 800 * 0.527 )}px;
|
2024-10-01 19:07:03 +02:00
|
|
|
overflow: hidden;
|
|
|
|
`)
|
|
|
|
|
|
|
|
this.el.terminalElement = terminalElement
|
|
|
|
|
2024-10-15 12:32:16 +02:00
|
|
|
if( this.data.XRrenderer == 'canvas' ){
|
|
|
|
// setup slightly bigger black backdrop (this.el.getObject3D("mesh"))
|
|
|
|
// and terminal text (this.el.planeText.getObject("mesh"))
|
2024-10-21 14:03:16 +02:00
|
|
|
const w = 2;
|
|
|
|
const h = (this.data.rows*5/this.data.cols)
|
|
|
|
this.el.setAttribute("geometry",`primitive: box; width:${w}; height:${h}; depth: -0.12`)
|
2024-10-15 12:32:16 +02:00
|
|
|
this.el.setAttribute("material","shader:flat; color:black; opacity:0.5; transparent:true; ")
|
|
|
|
this.el.planeText = document.createElement('a-entity')
|
2024-10-21 14:03:16 +02:00
|
|
|
this.el.planeText.setAttribute("geometry",`primitive: plane; width:${w}; height:${h}`)
|
2024-10-15 12:32:16 +02:00
|
|
|
this.el.appendChild(this.el.planeText)
|
|
|
|
|
|
|
|
// we switch between dom/canvas rendering because canvas looks pixely in nonimmersive mode
|
|
|
|
this.el.sceneEl.addEventListener('enter-vr', this.enterImmersive.bind(this) )
|
|
|
|
this.el.sceneEl.addEventListener('enter-ar', this.enterImmersive.bind(this) )
|
|
|
|
this.el.sceneEl.addEventListener('exit-vr', this.exitImmersive.bind(this) )
|
|
|
|
this.el.sceneEl.addEventListener('exit-ar', this.exitImmersive.bind(this) )
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
this.tick = AFRAME.utils.throttleLeadingAndTrailing( () => {
|
|
|
|
if( this.el.sceneEl.renderer.xr.isPresenting ){
|
|
|
|
// workaround
|
|
|
|
// xterm relies on window.requestAnimationFrame (which is not called WebXR immersive mode)
|
|
|
|
//this.term._core.viewport._innerRefresh()
|
|
|
|
this.term._core.renderer._renderDebouncer._innerRefresh()
|
|
|
|
}
|
|
|
|
}, this.data.canvasLatency)
|
2024-10-02 21:03:04 +02:00
|
|
|
|
2024-10-01 19:07:03 +02:00
|
|
|
// Build up a theme object
|
|
|
|
const theme = Object.keys(this.data).reduce((theme, key) => {
|
|
|
|
if (!key.startsWith('theme_')) return theme
|
|
|
|
const data = this.data[key]
|
|
|
|
if(!data) return theme
|
|
|
|
theme[key.slice('theme_'.length)] = data
|
|
|
|
return theme
|
|
|
|
}, {})
|
|
|
|
|
2024-10-04 17:49:15 +02:00
|
|
|
this.fontSize = 14
|
|
|
|
|
2024-10-02 21:03:04 +02:00
|
|
|
const term = this.term = new Terminal({
|
2024-10-04 11:08:39 +02:00
|
|
|
logLevel:"off",
|
2024-10-01 19:07:03 +02:00
|
|
|
theme: theme,
|
|
|
|
allowTransparency: true,
|
|
|
|
cursorBlink: true,
|
|
|
|
disableStdin: false,
|
|
|
|
rows: this.data.rows,
|
|
|
|
cols: this.data.cols,
|
2024-10-18 13:50:56 +02:00
|
|
|
fontFamily: 'Cousine, monospace',
|
2024-10-04 17:49:15 +02:00
|
|
|
fontSize: this.fontSize,
|
2024-10-01 19:07:03 +02:00
|
|
|
lineHeight: 1.15,
|
2024-10-04 17:49:15 +02:00
|
|
|
useFlowControl: true,
|
2024-10-02 21:03:04 +02:00
|
|
|
rendererType: this.renderType // 'dom' // 'canvas'
|
2024-10-01 19:07:03 +02:00
|
|
|
})
|
|
|
|
|
2024-10-02 21:03:04 +02:00
|
|
|
this.term.open(terminalElement)
|
|
|
|
this.term.focus()
|
|
|
|
this.setRenderType('dom')
|
2024-10-01 19:07:03 +02:00
|
|
|
|
|
|
|
terminalElement.querySelector('.xterm-viewport').style.background = 'transparent'
|
|
|
|
|
2024-10-02 21:03:04 +02:00
|
|
|
// now we can scale canvases to the parent element
|
2024-10-01 19:07:03 +02:00
|
|
|
const $screen = terminalElement.querySelector('.xterm-screen')
|
|
|
|
$screen.style.width = '100%'
|
|
|
|
|
2024-10-04 17:49:15 +02:00
|
|
|
term.on('refresh', AFRAME.utils.throttleLeadingAndTrailing( () => this.update(), 150 ) )
|
2024-10-01 19:07:03 +02:00
|
|
|
term.on('data', (data) => {
|
|
|
|
this.el.emit('xterm-input', data)
|
|
|
|
})
|
|
|
|
|
|
|
|
this.el.addEventListener('serial-output-byte', (e) => {
|
|
|
|
const byte = e.detail
|
|
|
|
var chr = String.fromCharCode(byte);
|
|
|
|
this.term.write(chr)
|
|
|
|
})
|
|
|
|
|
2024-10-02 21:03:04 +02:00
|
|
|
this.el.addEventListener('serial-output-string', (e) => {
|
|
|
|
this.term.write(e.detail)
|
|
|
|
})
|
|
|
|
|
|
|
|
},
|
2024-10-01 19:07:03 +02:00
|
|
|
|
2024-10-02 21:03:04 +02:00
|
|
|
update: function(){
|
|
|
|
if( this.renderType == 'canvas' ){
|
2024-10-03 12:41:53 +02:00
|
|
|
const material = this.el.planeText.getObject3D('mesh').material
|
2024-10-02 21:03:04 +02:00
|
|
|
if (!material.map ) return
|
|
|
|
if( this.cursorCanvas ) this.canvasContext.drawImage(this.cursorCanvas, 0,0)
|
2024-10-04 11:08:39 +02:00
|
|
|
else console.log("no cursorCanvas")
|
2024-10-02 21:03:04 +02:00
|
|
|
material.map.needsUpdate = true
|
|
|
|
//material.needsUpdate = true
|
|
|
|
}
|
2024-10-01 19:07:03 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
setRenderType: function(type){
|
|
|
|
|
2024-10-02 21:03:04 +02:00
|
|
|
|
2024-10-01 19:07:03 +02:00
|
|
|
if( type.match(/(dom|canvas)/) ){
|
|
|
|
|
|
|
|
if( type == 'dom'){
|
2024-10-02 21:03:04 +02:00
|
|
|
this.el.dom.appendChild(this.el.terminalElement)
|
2024-10-04 17:49:15 +02:00
|
|
|
this.term.setOption('fontSize', this.fontSize )
|
2024-10-02 21:03:04 +02:00
|
|
|
this.term.setOption('rendererType',type )
|
|
|
|
this.renderType = type
|
2024-10-01 19:07:03 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if( type == 'canvas'){
|
2024-10-02 21:03:04 +02:00
|
|
|
this.el.appendChild(this.el.terminalElement)
|
2024-10-04 17:49:15 +02:00
|
|
|
this.term.setOption('fontSize', this.fontSize * 3 )
|
2024-10-02 21:03:04 +02:00
|
|
|
this.term.setOption('rendererType',type )
|
|
|
|
this.renderType = type
|
|
|
|
this.update()
|
|
|
|
setTimeout( () => {
|
|
|
|
this.canvas = this.el.terminalElement.querySelector('.xterm-text-layer')
|
|
|
|
this.canvas.id = "xterm-canvas"
|
|
|
|
this.canvasContext = this.canvas.getContext('2d')
|
|
|
|
this.cursorCanvas = this.el.terminalElement.querySelector('.xterm-cursor-layer')
|
|
|
|
// Create a texture from the canvas
|
|
|
|
const canvasTexture = new THREE.Texture(this.canvas)
|
2024-10-04 17:49:15 +02:00
|
|
|
//canvasTexture.minFilter = THREE.NearestFilter //LinearFilter
|
|
|
|
//canvasTexture.magFilter = THREE.LinearMipMapLinearFilter //THREE.NearestFilter //LinearFilter
|
2024-10-02 21:03:04 +02:00
|
|
|
canvasTexture.needsUpdate = true; // Ensure the texture updates
|
2024-10-03 12:41:53 +02:00
|
|
|
let plane = this.el.planeText.getObject3D("mesh") //this.el.getObject3D('mesh')
|
2024-10-02 21:03:04 +02:00
|
|
|
if( plane.material ) plane.material.dispose()
|
|
|
|
plane.material = new THREE.MeshBasicMaterial({
|
|
|
|
map: canvasTexture, // Set the texture from the canvas
|
|
|
|
transparent: false, // Set transparency
|
2024-10-03 12:41:53 +02:00
|
|
|
//side: THREE.DoubleSide // Set to double-sided rendering
|
|
|
|
//blending: THREE.AdditiveBlending
|
2024-10-02 21:03:04 +02:00
|
|
|
});
|
2024-10-03 12:41:53 +02:00
|
|
|
this.el.object3D.scale.x = 0.2
|
|
|
|
this.el.object3D.scale.y = 0.2
|
|
|
|
this.el.object3D.scale.z = 0.2
|
2024-10-02 21:03:04 +02:00
|
|
|
},100)
|
2024-10-01 19:07:03 +02:00
|
|
|
}
|
2024-10-02 21:03:04 +02:00
|
|
|
|
|
|
|
this.el.terminalElement.style.opacity = type == 'canvas' ? 0 : 1
|
|
|
|
|
2024-10-01 19:07:03 +02:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2024-10-02 21:03:04 +02:00
|
|
|
enterImmersive: function(){
|
|
|
|
if( this.mode == 'immersive' ) return
|
|
|
|
this.el.object3D.visible = true
|
|
|
|
this.mode = "immersive"
|
|
|
|
this.setRenderType('canvas')
|
|
|
|
this.term.focus()
|
|
|
|
},
|
|
|
|
|
|
|
|
exitImmersive: function(){
|
|
|
|
if( this.mode == 'nonimmersive' ) return
|
|
|
|
this.el.object3D.visible = false
|
|
|
|
this.mode = "nonimmersive"
|
|
|
|
this.setRenderType('dom')
|
|
|
|
},
|
|
|
|
|
2024-10-01 19:07:03 +02:00
|
|
|
})
|