|  |  | @ -43,6 +43,8 @@ const themes = [ | 
			
		
	
		
		
			
				
					
					|  |  |  |   ] |  |  |  |   ] | 
			
		
	
		
		
			
				
					
					|  |  |  | ] |  |  |  | ] | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | // TODO move this to the initializer so it's not run on non-terminal pages
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | // 256color lookup table
 |  |  |  | // 256color lookup table
 | 
			
		
	
		
		
			
				
					
					|  |  |  | // should not be used to look up 0-15 (will return transparent)
 |  |  |  | // should not be used to look up 0-15 (will return transparent)
 | 
			
		
	
		
		
			
				
					
					|  |  |  | const colorTable256 = new Array(16).fill('rgba(0, 0, 0, 0)') |  |  |  | const colorTable256 = new Array(16).fill('rgba(0, 0, 0, 0)') | 
			
		
	
	
		
		
			
				
					|  |  | @ -160,17 +162,20 @@ class TermScreen { | 
			
		
	
		
		
			
				
					
					|  |  |  |     this.resetCursorBlink() |  |  |  |     this.resetCursorBlink() | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |     let selecting = false |  |  |  |     let selecting = false | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |     let selectStart = (x, y) => { |  |  |  |     let selectStart = (x, y) => { | 
			
		
	
		
		
			
				
					
					|  |  |  |       if (selecting) return |  |  |  |       if (selecting) return | 
			
		
	
		
		
			
				
					
					|  |  |  |       selecting = true |  |  |  |       selecting = true | 
			
		
	
		
		
			
				
					
					|  |  |  |       this.selection.start = this.selection.end = this.screenToGrid(x, y) |  |  |  |       this.selection.start = this.selection.end = this.screenToGrid(x, y) | 
			
		
	
		
		
			
				
					
					|  |  |  |       this.scheduleDraw() |  |  |  |       this.scheduleDraw() | 
			
		
	
		
		
			
				
					
					|  |  |  |     } |  |  |  |     } | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |     let selectMove = (x, y) => { |  |  |  |     let selectMove = (x, y) => { | 
			
		
	
		
		
			
				
					
					|  |  |  |       if (!selecting) return |  |  |  |       if (!selecting) return | 
			
		
	
		
		
			
				
					
					|  |  |  |       this.selection.end = this.screenToGrid(x, y) |  |  |  |       this.selection.end = this.screenToGrid(x, y) | 
			
		
	
		
		
			
				
					
					|  |  |  |       this.scheduleDraw() |  |  |  |       this.scheduleDraw() | 
			
		
	
		
		
			
				
					
					|  |  |  |     } |  |  |  |     } | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |     let selectEnd = (x, y) => { |  |  |  |     let selectEnd = (x, y) => { | 
			
		
	
		
		
			
				
					
					|  |  |  |       if (!selecting) return |  |  |  |       if (!selecting) return | 
			
		
	
		
		
			
				
					
					|  |  |  |       selecting = false |  |  |  |       selecting = false | 
			
		
	
	
		
		
			
				
					|  |  | @ -187,9 +192,11 @@ class TermScreen { | 
			
		
	
		
		
			
				
					
					|  |  |  |           e.button + 1) |  |  |  |           e.button + 1) | 
			
		
	
		
		
			
				
					
					|  |  |  |       } |  |  |  |       } | 
			
		
	
		
		
			
				
					
					|  |  |  |     }) |  |  |  |     }) | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |     window.addEventListener('mousemove', e => { |  |  |  |     window.addEventListener('mousemove', e => { | 
			
		
	
		
		
			
				
					
					|  |  |  |       selectMove(e.offsetX, e.offsetY) |  |  |  |       selectMove(e.offsetX, e.offsetY) | 
			
		
	
		
		
			
				
					
					|  |  |  |     }) |  |  |  |     }) | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |     window.addEventListener('mouseup', e => { |  |  |  |     window.addEventListener('mouseup', e => { | 
			
		
	
		
		
			
				
					
					|  |  |  |       selectEnd(e.offsetX, e.offsetY) |  |  |  |       selectEnd(e.offsetX, e.offsetY) | 
			
		
	
		
		
			
				
					
					|  |  |  |     }) |  |  |  |     }) | 
			
		
	
	
		
		
			
				
					|  |  | @ -198,15 +205,18 @@ class TermScreen { | 
			
		
	
		
		
			
				
					
					|  |  |  |     let touchDownTime = 0 |  |  |  |     let touchDownTime = 0 | 
			
		
	
		
		
			
				
					
					|  |  |  |     let touchSelectMinTime = 500 |  |  |  |     let touchSelectMinTime = 500 | 
			
		
	
		
		
			
				
					
					|  |  |  |     let touchDidMove = false |  |  |  |     let touchDidMove = false | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |     let getTouchPositionOffset = touch => { |  |  |  |     let getTouchPositionOffset = touch => { | 
			
		
	
		
		
			
				
					
					|  |  |  |       let rect = this.canvas.getBoundingClientRect() |  |  |  |       let rect = this.canvas.getBoundingClientRect() | 
			
		
	
		
		
			
				
					
					|  |  |  |       return [touch.clientX - rect.left, touch.clientY - rect.top] |  |  |  |       return [touch.clientX - rect.left, touch.clientY - rect.top] | 
			
		
	
		
		
			
				
					
					|  |  |  |     } |  |  |  |     } | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |     this.canvas.addEventListener('touchstart', e => { |  |  |  |     this.canvas.addEventListener('touchstart', e => { | 
			
		
	
		
		
			
				
					
					|  |  |  |       touchPosition = getTouchPositionOffset(e.touches[0]) |  |  |  |       touchPosition = getTouchPositionOffset(e.touches[0]) | 
			
		
	
		
		
			
				
					
					|  |  |  |       touchDidMove = false |  |  |  |       touchDidMove = false | 
			
		
	
		
		
			
				
					
					|  |  |  |       touchDownTime = Date.now() |  |  |  |       touchDownTime = Date.now() | 
			
		
	
		
		
			
				
					
					|  |  |  |     }) |  |  |  |     }) | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |     this.canvas.addEventListener('touchmove', e => { |  |  |  |     this.canvas.addEventListener('touchmove', e => { | 
			
		
	
		
		
			
				
					
					|  |  |  |       touchPosition = getTouchPositionOffset(e.touches[0]) |  |  |  |       touchPosition = getTouchPositionOffset(e.touches[0]) | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
	
		
		
			
				
					|  |  | @ -221,10 +231,12 @@ class TermScreen { | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |       touchDidMove = true |  |  |  |       touchDidMove = true | 
			
		
	
		
		
			
				
					
					|  |  |  |     }) |  |  |  |     }) | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |     this.canvas.addEventListener('touchend', e => { |  |  |  |     this.canvas.addEventListener('touchend', e => { | 
			
		
	
		
		
			
				
					
					|  |  |  |       if (e.touches[0]) { |  |  |  |       if (e.touches[0]) { | 
			
		
	
		
		
			
				
					
					|  |  |  |         touchPosition = getTouchPositionOffset(e.touches[0]) |  |  |  |         touchPosition = getTouchPositionOffset(e.touches[0]) | 
			
		
	
		
		
			
				
					
					|  |  |  |       } |  |  |  |       } | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |       if (selecting) { |  |  |  |       if (selecting) { | 
			
		
	
		
		
			
				
					
					|  |  |  |         e.preventDefault() |  |  |  |         e.preventDefault() | 
			
		
	
		
		
			
				
					
					|  |  |  |         selectEnd(...touchPosition) |  |  |  |         selectEnd(...touchPosition) | 
			
		
	
	
		
		
			
				
					|  |  | @ -282,12 +294,14 @@ class TermScreen { | 
			
		
	
		
		
			
				
					
					|  |  |  |         Input.onMouseMove(...this.screenToGrid(e.offsetX, e.offsetY)) |  |  |  |         Input.onMouseMove(...this.screenToGrid(e.offsetX, e.offsetY)) | 
			
		
	
		
		
			
				
					
					|  |  |  |       } |  |  |  |       } | 
			
		
	
		
		
			
				
					
					|  |  |  |     }) |  |  |  |     }) | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |     this.canvas.addEventListener('mouseup', e => { |  |  |  |     this.canvas.addEventListener('mouseup', e => { | 
			
		
	
		
		
			
				
					
					|  |  |  |       if (!selecting) { |  |  |  |       if (!selecting) { | 
			
		
	
		
		
			
				
					
					|  |  |  |         Input.onMouseUp(...this.screenToGrid(e.offsetX, e.offsetY), |  |  |  |         Input.onMouseUp(...this.screenToGrid(e.offsetX, e.offsetY), | 
			
		
	
		
		
			
				
					
					|  |  |  |           e.button + 1) |  |  |  |           e.button + 1) | 
			
		
	
		
		
			
				
					
					|  |  |  |       } |  |  |  |       } | 
			
		
	
		
		
			
				
					
					|  |  |  |     }) |  |  |  |     }) | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |     this.canvas.addEventListener('wheel', e => { |  |  |  |     this.canvas.addEventListener('wheel', e => { | 
			
		
	
		
		
			
				
					
					|  |  |  |       if (this.mouseMode.clicks) { |  |  |  |       if (this.mouseMode.clicks) { | 
			
		
	
		
		
			
				
					
					|  |  |  |         Input.onMouseWheel(...this.screenToGrid(e.offsetX, e.offsetY), |  |  |  |         Input.onMouseWheel(...this.screenToGrid(e.offsetX, e.offsetY), | 
			
		
	
	
		
		
			
				
					|  |  | @ -297,6 +311,7 @@ class TermScreen { | 
			
		
	
		
		
			
				
					
					|  |  |  |         e.preventDefault() |  |  |  |         e.preventDefault() | 
			
		
	
		
		
			
				
					
					|  |  |  |       } |  |  |  |       } | 
			
		
	
		
		
			
				
					
					|  |  |  |     }) |  |  |  |     }) | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |     this.canvas.addEventListener('contextmenu', e => { |  |  |  |     this.canvas.addEventListener('contextmenu', e => { | 
			
		
	
		
		
			
				
					
					|  |  |  |       if (this.mouseMode.clicks) { |  |  |  |       if (this.mouseMode.clicks) { | 
			
		
	
		
		
			
				
					
					|  |  |  |         // prevent mouse keys getting stuck
 |  |  |  |         // prevent mouse keys getting stuck
 | 
			
		
	
	
		
		
			
				
					|  |  | @ -573,7 +588,6 @@ class TermScreen { | 
			
		
	
		
		
			
				
					
					|  |  |  |       Notify.show('Copied to clipboard') |  |  |  |       Notify.show('Copied to clipboard') | 
			
		
	
		
		
			
				
					
					|  |  |  |     } else { |  |  |  |     } else { | 
			
		
	
		
		
			
				
					
					|  |  |  |       Notify.show('Failed to copy') |  |  |  |       Notify.show('Failed to copy') | 
			
		
	
		
		
			
				
					
					|  |  |  |       // unsuccessful copy
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |     } |  |  |  |     } | 
			
		
	
		
		
			
				
					
					|  |  |  |     document.body.removeChild(textarea) |  |  |  |     document.body.removeChild(textarea) | 
			
		
	
		
		
			
				
					
					|  |  |  |   } |  |  |  |   } | 
			
		
	
	
		
		
			
				
					|  |  | @ -596,10 +610,8 @@ class TermScreen { | 
			
		
	
		
		
			
				
					
					|  |  |  |   drawCellBackground ({ x, y, cellWidth, cellHeight, bg }) { |  |  |  |   drawCellBackground ({ x, y, cellWidth, cellHeight, bg }) { | 
			
		
	
		
		
			
				
					
					|  |  |  |     const ctx = this.ctx |  |  |  |     const ctx = this.ctx | 
			
		
	
		
		
			
				
					
					|  |  |  |     ctx.fillStyle = this.getColor(bg) |  |  |  |     ctx.fillStyle = this.getColor(bg) | 
			
		
	
		
		
			
				
					
					|  |  |  |     ctx.clearRect(x * cellWidth, y * cellHeight, |  |  |  |     ctx.clearRect(x * cellWidth, y * cellHeight, Math.ceil(cellWidth), Math.ceil(cellHeight)) | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |       Math.ceil(cellWidth), Math.ceil(cellHeight)) |  |  |  |     ctx.fillRect(x * cellWidth, y * cellHeight, Math.ceil(cellWidth), Math.ceil(cellHeight)) | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |     ctx.fillRect(x * cellWidth, y * cellHeight, |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       Math.ceil(cellWidth), Math.ceil(cellHeight)) |  |  |  |  | 
			
		
	
		
		
	
		
		
	
		
		
			
				
					
					|  |  |  |   } |  |  |  |   } | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |   drawCell ({ x, y, charSize, cellWidth, cellHeight, text, fg, bg, attrs }) { |  |  |  |   drawCell ({ x, y, charSize, cellWidth, cellHeight, text, fg, bg, attrs }) { | 
			
		
	
	
		
		
			
				
					|  |  | @ -1020,19 +1032,21 @@ class TermScreen { | 
			
		
	
		
		
			
				
					
					|  |  |  |   } |  |  |  |   } | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |   showNotification (text) { |  |  |  |   showNotification (text) { | 
			
		
	
		
		
			
				
					
					|  |  |  |     console.log(`Notification: ${text}`) |  |  |  |     console.info(`Notification: ${text}`) | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |     // TODO: request permission earlier
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |     // the requestPermission should be user-triggered; asking upfront seems
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |     // a little awkward
 |  |  |  |  | 
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |     if (Notification && Notification.permission === 'granted') { |  |  |  |     if (Notification && Notification.permission === 'granted') { | 
			
		
	
		
		
			
				
					
					|  |  |  |       let notification = new Notification('ESPTerm', { |  |  |  |       let notification = new Notification('ESPTerm', { | 
			
		
	
		
		
			
				
					
					|  |  |  |         body: text |  |  |  |         body: text | 
			
		
	
		
		
			
				
					
					|  |  |  |       }) |  |  |  |       }) | 
			
		
	
		
		
			
				
					
					|  |  |  |       notification.addEventListener('click', () => window.focus()) |  |  |  |       notification.addEventListener('click', () => window.focus()) | 
			
		
	
		
		
			
				
					
					|  |  |  |     } else { |  |  |  |     } else { | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |       if (Notification && Notification.permission !== 'denied') { | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         Notification.requestPermission() | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |       } else { | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         // Fallback using the built-in notification balloon
 | 
			
		
	
		
		
			
				
					
					|  |  |  |         Notify.show(text) |  |  |  |         Notify.show(text) | 
			
		
	
		
		
			
				
					
					|  |  |  |       } |  |  |  |       } | 
			
		
	
		
		
			
				
					
					|  |  |  |     } |  |  |  |     } | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |   } | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |   load (str, theme = -1) { |  |  |  |   load (str, theme = -1) { | 
			
		
	
		
		
			
				
					
					|  |  |  |     const content = str.substr(1) |  |  |  |     const content = str.substr(1) | 
			
		
	
	
		
		
			
				
					|  |  | @ -1044,17 +1058,21 @@ class TermScreen { | 
			
		
	
		
		
			
				
					
					|  |  |  |       case 'S': |  |  |  |       case 'S': | 
			
		
	
		
		
			
				
					
					|  |  |  |         this.loadContent(content) |  |  |  |         this.loadContent(content) | 
			
		
	
		
		
			
				
					
					|  |  |  |         break |  |  |  |         break | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |       case 'T': |  |  |  |       case 'T': | 
			
		
	
		
		
			
				
					
					|  |  |  |         this.loadLabels(content) |  |  |  |         this.loadLabels(content) | 
			
		
	
		
		
			
				
					
					|  |  |  |         break |  |  |  |         break | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |       case 'B': |  |  |  |       case 'B': | 
			
		
	
		
		
			
				
					
					|  |  |  |         this.beep() |  |  |  |         this.beep() | 
			
		
	
		
		
			
				
					
					|  |  |  |         break |  |  |  |         break | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |       case 'G': |  |  |  |       case 'G': | 
			
		
	
		
		
			
				
					
					|  |  |  |         this.showNotification(content) |  |  |  |         this.showNotification(content) | 
			
		
	
		
		
			
				
					
					|  |  |  |         break |  |  |  |         break | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |       default: |  |  |  |       default: | 
			
		
	
		
		
			
				
					
					|  |  |  |         console.warn(`Bad data message type; ignoring.\n${JSON.stringify(content)}`) |  |  |  |         console.warn(`Bad data message type; ignoring.\n${JSON.stringify(str)}`) | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |     } |  |  |  |     } | 
			
		
	
		
		
			
				
					
					|  |  |  |   } |  |  |  |   } | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
	
		
		
			
				
					|  |  | 
 |