* Copyright (C) 2022 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { BaseElement, element } from '../../../base-ui/BaseElement';
import { HdcDeviceManager } from '../../../hdc/HdcDeviceManager';
import { SpRecordTrace } from '../SpRecordTrace';
import { DataMessage } from '../../../hdc/message/DataMessage';
@element('sp-web-hdc-shell')
export class SpWebHdcShell extends BaseElement {
private static MAX_DISPLAY_ROWS = 1000;
private static MAX_SAVE_SIZE = 2097152;
shellDiv: HTMLDivElement | null | undefined;
currentScreenRemain: number = 0;
private shellCanvas: HTMLCanvasElement | null | undefined;
private shellCanvasCtx: CanvasRenderingContext2D | null | undefined;
private resultStr = '';
private sendCallBack: ((keyboardEvent: KeyboardEvent | string) => void | undefined) | undefined;
private startShellDevice = '';
private intervalId: number | undefined;
private skipFlag: number[] = [7];
private clearFlag: number[] = [27, 91, 50, 74, 27, 91, 72];
private CRLFFlag: number[] = [13, 13, 10];
private startRealTimeFlag: number[] = [27, 91, 115];
private endRealTimeFlag: number[] = [27, 91, 117];
private clearRealTimeFlag: number[] = [27, 91, 72, 27, 91, 74];
private ctrlCFlag: number[] = [13, 10, 35, 32];
private points: Point | undefined;
private forwardFlag: boolean = false;
private cursorIndex: number = 3;
private shellStrLength: number = 0;
private textY: number = 0;
private cursorRow: string = '';
private textDecoder: TextDecoder = new TextDecoder();
private isDragging: boolean = false;
private static TOP_OFFSET = 48;
private static FIRST_ROW_OFFSET = 32;
private static LAST_ROW_OFFSET = 40;
private static MULTI_LINE_FLAG = '<\b';
private static LINE_BREAK_LENGTH = 2;
private static LEFT_OFFSET = 48;
private realTimeResult: string | null | undefined = '';
private startRealTime: boolean = false;
private prevTextY: number = 0;
public initElements(): void {
this.shellCanvas = this.shadowRoot!.querySelector<HTMLCanvasElement>('#shell_cmd');
this.shellCanvasCtx = this.shellCanvas!.getContext('2d');
this.shellCanvasCtx!.fillStyle = '#000';
this.shellCanvasCtx!.fillRect(0, 0, this.shellCanvas!.width, this.shellCanvas!.height);
this.shellDiv = this.shadowRoot!.querySelector<HTMLDivElement>('.shell_cmd_div');
this.shellCanvasAddMouseListener();
this.shellCanvas!.addEventListener('contextmenu', (event) => {
event.preventDefault();
event.stopPropagation();
});
this.shellCanvas!.addEventListener('keydown', async (keyboardEvent) => {
keyboardEvent.preventDefault();
if (keyboardEvent.ctrlKey && keyboardEvent.code === 'KeyC' && this.points) {
let rowText: string = this.getSelectedText();
this.points = undefined;
await navigator.clipboard.writeText(rowText);
} else {
if (this.sendCallBack) {
this.sendCallBack(keyboardEvent);
}
}
});
window.subscribe(window.SmartEvent.UI.DeviceConnect, (deviceName: string) => {
if (deviceName) {
this.hdcShellFocus();
}
});
window.subscribe(window.SmartEvent.UI.DeviceDisConnect, () => {
this.clear();
});
this.shellCanvas!.addEventListener('blur', () => {
if (this.intervalId) {
window.clearInterval(this.intervalId);
}
this.shellCanvasCtx!.clearRect(this.shellStrLength, this.textY, 12, 3);
});
new ResizeObserver(() => {
this.resizeCanvas();
this.refreshShellPage(true);
}).observe(this);
}
resizeCanvas(): void {
if (this.shellCanvas !== null && this.shellCanvas !== undefined) {
this.shellCanvas.width = this.shellCanvas.clientWidth;
this.shellCanvas.height = this.shellCanvas.clientHeight;
}
}
clear(): void {
this.sendCallBack = undefined;
this.resultStr = '';
this.cursorRow = '';
this.shellCanvasCtx!.clearRect(0, 0, this.shellCanvas!.width, this.shellCanvas!.height);
this.shellCanvasCtx!.fillStyle = '#000';
this.shellCanvasCtx!.fillRect(0, 0, this.shellCanvas!.width, this.shellCanvas!.height);
window.clearInterval(this.intervalId);
}
public hdcShellFocus(): void {
HdcDeviceManager.connect(SpRecordTrace.serialNumber).then((connected) => {
if (connected) {
if (this.sendCallBack && this.startShellDevice === SpRecordTrace.serialNumber) {
this.shellCanvas!.focus();
this.refreshShellPage(true);
} else {
this.clear();
this.sendCallBack = HdcDeviceManager.startShell((result: DataMessage) => {
if (result.channelClose) {
this.clear();
return;
}
this.startShellDevice = SpRecordTrace.serialNumber;
this.handleHdcRecvData(result);
});
this.shellCanvas!.focus();
this.refreshShellPage(true);
}
} else {
this.clear();
}
});
}
arrayBufferCompare(compareA: ArrayBuffer, compareB: number[]): boolean {
const arrayA = new Uint8Array(compareA);
if (arrayA.length === compareB.length) {
for (let i = 0; i < arrayA.length; i++) {
const dd = arrayA[i];
if (dd !== compareB[i]) {
return false;
}
}
return true;
}
return false;
}
getSelectedText(): string {
let selectedText = '';
let textLines = [...this.finalArr];
let startX = this.points!.startX < SpWebHdcShell.LEFT_OFFSET ? SpWebHdcShell.LEFT_OFFSET : this.points!.startX;
let startY = this.points!.startY;
let endX = this.points!.endX < SpWebHdcShell.LEFT_OFFSET ? SpWebHdcShell.LEFT_OFFSET : this.points!.endX;
let endY = this.points!.endY;
let endTop = Math.ceil((endY - SpWebHdcShell.TOP_OFFSET) / 16);
let startTop = Math.floor((startY - SpWebHdcShell.TOP_OFFSET - 2) / 16);
let selectRangeList = textLines.slice(startTop + 1, endTop);
let charWidth = this.shellCanvasCtx!.measureText(selectRangeList[0].split('')[0]).width;
for (let index = 0; index < selectRangeList.length; index++) {
let currentIndexLine = selectRangeList[index];
if (index === 0) {
let startNum = Math.floor((startX - SpWebHdcShell.LEFT_OFFSET) / charWidth);
selectedText = currentIndexLine.slice(startNum);
} else if (index === selectRangeList.length - 1) {
let endNum = Math.ceil((endX - SpWebHdcShell.LEFT_OFFSET) / charWidth);
selectedText += currentIndexLine.slice(0, endNum);
} else {
selectedText += `${currentIndexLine}\n`;
}
}
return selectedText.trim();
}
forwardSelected(startX: number, startY: number, endX: number, endY: number): void {
let depth = Math.ceil((endY - startY) / 16);
let startPointX = 0;
let startPointY = 0;
let endPointX = 0;
let endPointY = 0;
if (depth <= 1) {
this.shellCanvasCtx!.fillRect(startX, startY, endX - startX, endY - startY);
startPointX = startX;
startPointY = startY;
endPointX = endX;
endPointY = endY;
} else {
for (let index = 1; index <= depth; index++) {
if (index === 1) {
this.shellCanvasCtx!.fillRect(startX, startY, this.shellCanvas!.width - startX, index * 16);
startPointX = startX;
startPointY = startY;
} else if (index === depth) {
this.shellCanvasCtx!.fillRect(
SpWebHdcShell.LEFT_OFFSET,
startY + (index - 1) * 16,
endX - SpWebHdcShell.LEFT_OFFSET,
endY - (startY + (index - 1) * 16)
);
endPointX = endX;
endPointY = endY;
} else {
this.shellCanvasCtx!.fillRect(
SpWebHdcShell.LEFT_OFFSET,
startY + (index - 1) * 16,
this.shellCanvas!.width,
16
);
}
}
}
this.points = { startX: startPointX, startY: startPointY, endX: endPointX, endY: endPointY };
}
getCurrentLineBackSize(currentLine: string, maxBackSize: number, isStart: boolean): number {
let fillText = '';
let strings = currentLine.split('');
for (let index = 0; index < strings.length; index++) {
let text = strings[index];
if (
this.shellCanvasCtx!.measureText(fillText).width < maxBackSize &&
this.shellCanvasCtx!.measureText(fillText + text).width >= maxBackSize
) {
if (!isStart) {
fillText += text;
}
break;
}
fillText += text;
}
return fillText.length;
}
reverseSelected(startX: number, startY: number, endX: number, endY: number): void {
let depth = Math.ceil((startY - endY) / 16);
let startPointX = 0;
let startPointY = 0;
let endPointX = 0;
let endPointY = 0;
if (depth <= 1) {
this.shellCanvasCtx!.fillRect(endX, endY, startX - endX, startY - endY);
startPointX = endX;
startPointY = endY;
endPointX = startX;
endPointY = startY;
} else {
for (let index = 1; index <= depth; index++) {
if (index === 1) {
this.shellCanvasCtx!.fillRect(SpWebHdcShell.LEFT_OFFSET, startY - 16, startX - SpWebHdcShell.LEFT_OFFSET, 16);
endPointX = startX;
endPointY = startY;
} else if (index === depth) {
this.shellCanvasCtx!.fillRect(endX, endY, this.shellCanvas!.width - endX, startY - (index - 1) * 16 - endY);
startPointX = endX;
startPointY = endY;
} else {
this.shellCanvasCtx!.fillRect(SpWebHdcShell.LEFT_OFFSET, startY - index * 16, this.shellCanvas!.width, 16);
this.shellCanvasCtx!.textBaseline = 'middle';
}
}
}
this.points = { startX: startPointX, startY: startPointY, endX: endPointX, endY: endPointY };
}
private singleLineToMultiLine(shellStr: string, foundationWidth: number, maxWidth: number): string[] {
let result = [];
while (shellStr.length * foundationWidth > maxWidth) {
let bfb = maxWidth / (shellStr.length * foundationWidth);
let cutIndex = Math.floor(shellStr.length * bfb);
let ss = shellStr.substring(0, cutIndex);
result.push(ss);
shellStr = shellStr.substring(cutIndex);
}
if (shellStr.length > 0) {
result.push(shellStr);
}
return result;
}
private finalArr: Array<string> = [];
private drawShellPage(resultStrArr: string[]): void {
let maxWidth = this.shellCanvas!.width;
let foundationWidth = Math.ceil(this.shellCanvasCtx!.measureText(' ').width);
for (let index = 0; index < resultStrArr.length - 1; index++) {
let shellStr = resultStrArr[index];
let strWidth = this.shellCanvasCtx!.measureText(shellStr).width;
if (strWidth > maxWidth) {
let lines = this.singleLineToMultiLine(shellStr, foundationWidth, maxWidth - SpWebHdcShell.LEFT_OFFSET);
this.finalArr.push(...lines);
} else {
this.finalArr.push(shellStr);
}
}
if (this.finalArr.length > SpWebHdcShell.MAX_DISPLAY_ROWS) {
this.finalArr.splice(0, this.finalArr.length - SpWebHdcShell.MAX_DISPLAY_ROWS + 1);
}
this.shellCanvasCtx!.fillStyle = '#fff';
this.shellCanvasCtx!.font = '16px serif';
this.textY = SpWebHdcShell.TOP_OFFSET;
this.finalArr.push(this.cursorRow);
for (let index: number = 0; index < this.finalArr.length; index++) {
let shellStr: string = this.finalArr[index];
this.textY = SpWebHdcShell.TOP_OFFSET + index * 16;
this.shellCanvasCtx!.fillText(shellStr, SpWebHdcShell.LEFT_OFFSET, this.textY);
}
}
private drawCursorStyle(): void {
if (this.intervalId) {
window.clearInterval(this.intervalId);
}
let needClear = false;
this.intervalId = window.setInterval(() => {
if (needClear) {
needClear = false;
this.shellCanvasCtx!.fillStyle = '#000';
this.shellCanvasCtx!.fillRect(this.shellStrLength, this.textY, 12, 3);
} else {
needClear = true;
this.shellCanvasCtx!.fillStyle = '#fff';
this.shellCanvasCtx!.fillRect(this.shellStrLength, this.textY, 12, 3);
}
}, 500);
}
refreshShellPage(scroller: boolean): void {
try {
if (this.resultStr.length === 0 && this.cursorRow.length === 0) {
return;
}
this.shellCanvasCtx!.clearRect(0, 0, this.shellCanvas!.width, this.shellCanvas!.height);
this.shellCanvasCtx!.fillStyle = '#000';
this.shellCanvasCtx!.fillRect(0, 0, this.shellCanvas!.width, this.shellCanvas!.height);
let resultStrArr = this.resultStr.split('\r\n');
if (this.realTimeResult !== '') {
resultStrArr = (this.resultStr + this.realTimeResult).split('\r\n');
}
this.finalArr = [];
if (this.shellCanvas!.width > 0) {
this.drawShellPage(resultStrArr);
this.shellStrLength =
this.shellCanvasCtx!.measureText(this.cursorRow.slice(0, this.cursorIndex)).width + SpWebHdcShell.LEFT_OFFSET;
this.prevTextY = this.shellDiv!.scrollTop + this.shellDiv!.clientHeight - 3;
if (scroller && this.textY > this.shellDiv!.clientHeight && this.textY > this.prevTextY) {
this.shellDiv!.scrollTop = this.textY - this.shellDiv!.clientHeight + 3;
this.currentScreenRemain = this.shellDiv!.scrollTop;
}
this.drawCursorStyle();
}
} catch (e) { }
}
public initHtml(): string {
return `
<style>
:host{
display: block;
border-radius: 0 16px 16px 0;
width: 100%;
position: relative;
}
.shell_cmd_div {
width: 90%;
margin-left: 5%;
margin-top: 3%;
overflow-y: scroll;
height: 40rem;
background: #000;
border: 1px solid var(--dark-color1,#4D4D4D);
border-radius: 16px;
}
::-webkit-scrollbar{
width: 13px;
height: 10px;
background-color: #FFFFFF;
}
::-webkit-scrollbar-track{
border-top-right-radius: 16px;
border-bottom-right-radius: 16px;
background-color: #000000;
}
::-webkit-scrollbar-thumb{
background: #5A5A5A;
border-radius: 6px;
}
canvas {
display: inline-block;
outline: none;
}
</style>
<div class="shell_cmd_div">
<canvas id="shell_cmd" style="width: 100%;height:${16000 + SpWebHdcShell.TOP_OFFSET}px;" tabindex="0"></canvas>
</div>
`;
}
private refreshCurrentRow(): void {
let lastRow: string = this.resultStr;
if (this.resultStr.lastIndexOf('\r\n') !== -1) {
lastRow = this.resultStr.substring(this.resultStr.lastIndexOf('\r\n') + 2);
}
let currentRow: string[] = [...lastRow];
let result: string[] = [];
this.cursorIndex = 0;
for (let index: number = 0; index < currentRow.length; index++) {
let currentResult: string = currentRow[index];
if (currentResult === '\b') {
this.cursorIndex--;
} else {
result[this.cursorIndex] = currentResult;
this.cursorIndex++;
}
}
this.cursorRow = result.join('');
}
private handleHdcRecvData(result: DataMessage): void {
const resData = result.getData();
if (resData) {
if (this.arrayBufferCompare(resData, this.skipFlag)) {
return;
} else if (this.arrayBufferCompare(resData, this.clearFlag)) {
this.resultStr = '';
this.shellDiv!.scrollTop = 0;
this.cursorIndex = 3;
} else if (this.arrayBufferCompare(resData, this.CRLFFlag)) {
if (this.resultStr.lastIndexOf('\r\n') !== -1) {
this.resultStr = this.resultStr.substring(0, this.resultStr.lastIndexOf('\r\n') + 2) + this.cursorRow;
} else {
this.resultStr = this.cursorRow;
}
this.resultStr += result.getDataToString();
this.cursorIndex = 3;
} else {
if (this.resultStr.length > SpWebHdcShell.MAX_SAVE_SIZE) {
this.resultStr = this.resultStr.substring(this.resultStr.length / 2);
}
const arrayA = new Uint8Array(resData);
if (arrayA[0] === 13 && arrayA[1] !== 10 && arrayA[1] !== 13) {
this.hdcRecvEnterAndBracket(arrayA, result);
} else if (this.isStartWidthArrayBuffer(arrayA, this.startRealTimeFlag)) {
let lastIndex = this.getLastRestorationIndex(arrayA, this.endRealTimeFlag);
this.realTimeResult = this.removeTextAndColorSequenceStr(
this.textDecoder.decode(arrayA.slice(lastIndex, arrayA.length))
);
this.startRealTime = true;
} else if (this.isStartWidthArrayBuffer(arrayA, this.clearRealTimeFlag)) {
this.realTimeResult = this.removeTextAndColorSequenceStr(
this.textDecoder.decode(arrayA.slice(6, arrayA.length))
);
this.startRealTime = true;
} else {
if (this.isStartWidthArrayBuffer(arrayA, this.ctrlCFlag)) {
this.resultStr += this.realTimeResult;
this.startRealTime = false;
}
this.hdcRecvRealMessage(result);
}
}
this.resultStr = this.removeTextAndColorSequenceStr(this.resultStr);
this.refreshCurrentRow();
this.refreshShellPage(true);
}
}
private hdcRecvEnterAndBracket(arrayA: Uint8Array, result: DataMessage): void {
const index = this.resultStr.lastIndexOf('\n');
const resultStrLength = this.resultStr.length;
if (index > -1 && resultStrLength > index) {
this.resultStr = this.resultStr.substring(0, index + 1) + this.textDecoder.decode(arrayA.slice(1, arrayA.length));
} else {
if (this.resultStr.split('\n').length === 1) {
const index = this.cursorRow.lastIndexOf('\n');
this.cursorRow =
this.cursorRow.substring(0, index + 1) + this.textDecoder.decode(arrayA.slice(1, arrayA.length));
this.resultStr = this.cursorRow;
} else {
this.resultStr += result.getDataToString();
}
}
this.realTimeResult = '';
}
private hdcRecvRealMessage(result: DataMessage): void {
if (this.startRealTime) {
if (result.getDataToString().includes(SpWebHdcShell.MULTI_LINE_FLAG)) {
this.realTimeResult += result.getDataToString().substring(result.getDataToString().indexOf('\r'));
} else {
this.realTimeResult += result.getDataToString();
}
this.realTimeResult = this.removeTextAndColorSequenceStr(this.realTimeResult!);
} else {
this.realTimeResult = '';
if (result.getDataToString().includes(SpWebHdcShell.MULTI_LINE_FLAG)) {
this.resultStr = this.resultStr.substring(
0,
this.resultStr.lastIndexOf('\r\n') + SpWebHdcShell.LINE_BREAK_LENGTH
);
this.resultStr += result.getDataToString().substring(result.getDataToString().indexOf('\r'));
} else {
this.resultStr += result.getDataToString();
}
}
}
private removeTextAndColorSequenceStr(currentStr: string): string {
return currentStr.replace(new RegExp(/\x1B\[[0-9;]*[a-zA-Z]/g), '');
}
private isStartWidthArrayBuffer(sourceArray: Uint8Array, compareArray: number[]): boolean {
for (let index = 0; index < compareArray.length; index++) {
if (sourceArray[index] !== compareArray[index]) {
return false;
}
}
return true;
}
private getLastRestorationIndex(sourceArray: Uint8Array, compareArray: number[]): number {
let lastIndex = -1;
for (let index = sourceArray.length - 1; index >= 0; index--) {
if (sourceArray[index] === compareArray[0]) {
let isLast = true;
for (let j = 1; j < compareArray.length; j++) {
if (sourceArray[index + j] !== compareArray[j]) {
isLast = false;
break;
}
}
if (isLast) {
lastIndex = index;
break;
}
}
}
return lastIndex + compareArray.length;
}
private shellCanvasAddMouseListener(): void {
let startX: number;
let startY: number;
let endX: number;
let endY: number;
this.shellCanvas!.addEventListener('mousedown', (event) => {
if (this.resultStr.length === 0 && this.cursorRow.length === 0) {
return;
}
this.isDragging = true;
startX = event.offsetX;
startY = event.offsetY;
this.refreshShellPage(false);
});
this.shellCanvas!.addEventListener('mousemove', (event) => {
if (!this.isDragging) {
return;
}
if (this.resultStr.length === 0 && this.cursorRow.length === 0) {
return;
}
endX = event.offsetX;
endY = event.offsetY;
this.refreshShellPage(false);
this.points = undefined;
this.shellCanvasCtx!.fillStyle = 'rgba(128, 128, 128, 0.5)';
if (endY > startY) {
this.forwardFlag = true;
this.forwardSelected(startX, startY, endX, endY);
} else {
this.forwardFlag = false;
this.reverseSelected(startX, startY, endX, endY);
}
});
this.shellCanvasAddMouseUpListener();
}
private shellCanvasAddMouseUpListener(): void {
this.shellCanvas!.addEventListener('mouseup', async (event)=> {
if (!this.isDragging) {
return;
}
if (this.resultStr.length === 0 && this.cursorRow.length === 0) {
return;
}
this.isDragging = false;
if (event.button === 2) {
let text: string = await navigator.clipboard.readText();
if (text) {
if (this.sendCallBack) {
this.sendCallBack(text);
}
return;
}
}
});
}
}
export class Point {
startX: number = 0;
startY: number = 0;
endX: number = 0;
endY: number = 0;
}