This commit is contained in:
Zhi Wang 2022-01-15 18:54:15 -05:00
parent 98e37801c4
commit b458da4414
8 changed files with 188 additions and 71 deletions

View File

@ -8,7 +8,7 @@
<meta name="author" content=""> <meta name="author" content="">
<!-- automatically refresh the page every 30 seconds --> <!-- automatically refresh the page every 30 seconds -->
<meta http-equiv="refresh" content="20"> <meta http-equiv="refresh" content="30">
<title>Web Terminal</title> <title>Web Terminal</title>

View File

@ -35,4 +35,4 @@
.navbar-xs .navbar-nav>li>a { .navbar-xs .navbar-nav>li>a {
padding-top: 0px; padding-top: 0px;
padding-bottom: 0px; padding-bottom: 0px;
} }

View File

@ -37,39 +37,40 @@
<div id="terminal_view"></div> <div id="terminal_view"></div>
</div> </div>
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<button type="button" class="btn btn-primary btn-sm" style="margin-right: 3px;"> <button type="button" class="btn btn-primary btn-sm" style="margin-right: 3px;" onclick="playbtn()">
<img src="/assets/play.svg" id="play-btn" height="18px"> <img src="/assets/play.svg" id="play-btn" height="18px">
</button> </button>
<div class="progress" id="replay-control" style="width: 906px;"> <input type="range" class="form-range" min="0" max="100" id="replay-control"
<div class="progress-bar bg-secondary" role="progressbar" style="width: 25%" aria-valuenow="25" onchange="console.log(this.value)">
aria-valuemin="0" aria-valuemax="100"></div>
</div>
</div> </div>
</div> </div>
<script> <script>
function Init() { term = Init()
let term = createReplayTerminal();
var str = [
' ┌────────────────────────────────────────────────────────────────────────────┐\n',
' │ \u001b[32;1mhttps://github.com/syssecfsu/witty\x1b[0m <- click it! \n',
' └────────────────────────────────────────────────────────────────────────────┘\n',
''
].join('');
term.writeln(str); var rc = document.querySelector("#replay-control")
rc.value = 0
// adjust the progress bar size to that of terminal var icon = document.getElementById("play-btn")
let vp = document.querySelector("#terminal") var path = "/records/5FGD56YdzAYDGF9s_61e33645.rec"
let pbar = document.querySelector("#replay-control") var pause = false
pbar.setAttribute("style", "width:" + (vp.offsetWidth - 32) + "px");
function playbtn() {
document.getElementById("play-btn").src = "/assets/pause.svg"; if (icon.src.includes("play")) {
icon.src = "/assets/pause.svg"
pause = false
replay_session(term, path, rc.value,
function () {
return pause
},
function (percent) {
rc.value = percent
})
} else {
icon.src = "/assets/play.svg"
pause = true
}
} }
Init()
</script> </script>
</body> </body>

View File

@ -1,46 +1,145 @@
// create a xterm for replay
function createReplayTerminal() { function createReplayTerminal() {
// vscode-snazzy https://github.com/Tyriar/vscode-snazzy // vscode-snazzy https://github.com/Tyriar/vscode-snazzy
// copied from xterm.js website // copied from xterm.js website
var baseTheme = { var baseTheme = {
foreground: '#eff0eb', foreground: '#eff0eb',
background: '#282a36', background: '#282a36',
selection: '#97979b33', selection: '#97979b33',
black: '#282a36', black: '#282a36',
brightBlack: '#686868', brightBlack: '#686868',
red: '#ff5c57', red: '#ff5c57',
brightRed: '#ff5c57', brightRed: '#ff5c57',
green: '#5af78e', green: '#5af78e',
brightGreen: '#5af78e', brightGreen: '#5af78e',
yellow: '#f3f99d', yellow: '#f3f99d',
brightYellow: '#f3f99d', brightYellow: '#f3f99d',
blue: '#57c7ff', blue: '#57c7ff',
brightBlue: '#57c7ff', brightBlue: '#57c7ff',
magenta: '#ff6ac1', magenta: '#ff6ac1',
brightMagenta: '#ff6ac1', brightMagenta: '#ff6ac1',
cyan: '#9aedfe', cyan: '#9aedfe',
brightCyan: '#9aedfe', brightCyan: '#9aedfe',
white: '#f1f1f0', white: '#f1f1f0',
brightWhite: '#eff0eb' brightWhite: '#eff0eb'
}; };
const term = new Terminal({
fontFamily: `'Fira Code', ui-monospace,SFMono-Regular,'SF Mono',Menlo,Consolas,'Liberation Mono',monospace`,
fontSize: 12,
theme: baseTheme,
convertEol: true,
cursorBlink: true,
});
term.open(document.getElementById('terminal_view'));
term.resize(124, 37);
const weblinksAddon = new WebLinksAddon.WebLinksAddon();
term.loadAddon(weblinksAddon);
// fit the xterm viewpoint to parent element
const fitAddon = new FitAddon.FitAddon();
term.loadAddon(fitAddon);
fitAddon.fit();
return term; const term = new Terminal({
} fontFamily: `'Fira Code', ui-monospace,SFMono-Regular,'SF Mono',Menlo,Consolas,'Liberation Mono',monospace`,
fontSize: 12,
theme: baseTheme,
convertEol: true,
cursorBlink: true,
});
term.open(document.getElementById('terminal_view'));
term.resize(124, 37);
const weblinksAddon = new WebLinksAddon.WebLinksAddon();
term.loadAddon(weblinksAddon);
// fit the xterm viewpoint to parent element
const fitAddon = new FitAddon.FitAddon();
term.loadAddon(fitAddon);
fitAddon.fit();
return term;
}
// sleep for ms seconds
function _sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// we could sleep for a long time
// periodically check if we need to end replay.
// This is pretty ugly but the callback mess otherwise
async function sleep(ms, end) {
var loop_cnt = parseInt(ms / 20) + 1
for (i = 0; i < loop_cnt; i++) {
if (end()) {
return end()
}
await _sleep(20)
}
return end()
}
// convert data to uint8array, we cannot convert it to string as
// it will mess up special characters
function base64ToUint8array(base64) {
var raw = window.atob(base64);
var rawLength = raw.length;
var array = new Uint8Array(new ArrayBuffer(rawLength));
for (i = 0; i < rawLength; i++) {
array[i] = raw.charCodeAt(i);
}
return array;
}
// replay session
// term: xterm, path: session file to replay,
// start: start position to replay in percentile, range 0-100
// callback to update the progress bar
async function replay_session(term, path, start, end, prog) {
var session
// read file from server
await fetch(path)
.then(res => res.json())
.then(out => {
session = out
})
var total_dur = 0
var cur = 0
//calculate the total duration
for (const item of session) {
item.Duration = parseInt(item.Duration / 1000000)
total_dur += item.Duration
}
start = parseInt(total_dur * start / 100)
console.log("Total duration:", total_dur, "start replay on", start)
term.reset()
for (const item of session) {
cur += item.Duration
// we will blast through the beginning of the session
if (cur >= start) {
if (await sleep(item.Duration, end) == true) {
return
}
}
if (item.Duration >= total_dur / 100) {
prog(cur * 100 / total_dur)
}
term.write(base64ToUint8array(item.Data))
}
}
function Init() {
let term = createReplayTerminal();
var str = [
' ┌────────────────────────────────────────────────────────────────────────────┐\n',
' │ \u001b[32;1mhttps://github.com/syssecfsu/witty\x1b[0m <- click it! │\n',
' └────────────────────────────────────────────────────────────────────────────┘\n',
''
].join('');
term.writeln(str);
// adjust the progress bar size to that of terminal
var view = document.querySelector("#terminal")
var pbar = document.querySelector("#replay-control")
pbar.setAttribute("style", "width:" + (view.offsetWidth - 32) + "px");
return term
}

View File

@ -33,7 +33,7 @@
</header> </header>
<div style="margin-top: 5em;"> <div style="margin-top: 3em;">
<div id="terminal"> <div id="terminal">
<div id="terminal_view"></div> <div id="terminal_view"></div>
</div> </div>

View File

@ -40,7 +40,7 @@ func main() {
w, h, _ := term.GetSize(int(os.Stdout.Fd())) w, h, _ := term.GetSize(int(os.Stdout.Fd()))
if (w != 120) || (h != 36) { if (w != 120) || (h != 36) {
log.Fatalln("Set terminal window to 120x36 before continue") log.Println("Set terminal window to 120x36 before continue")
} }
decoder := json.NewDecoder(fp) decoder := json.NewDecoder(fp)
@ -49,6 +49,10 @@ func main() {
log.Fatalln("Failed to create JSON decoder") log.Fatalln("Failed to create JSON decoder")
} }
// To work with javascript decoder, we organize the file as
// an array of writeRecord. golang decode instead decode
// as individual record. Call decoder.Token to skip opening [
decoder.Token()
for decoder.More() { for decoder.More() {
var record writeRecord var record writeRecord
@ -60,4 +64,6 @@ func main() {
time.Sleep(record.Dur) time.Sleep(record.Dur)
t.Write(record.Data) t.Write(record.Data)
} }
t.Write([]byte("\n\n---end of replay---\n\n"))
} }

View File

@ -140,6 +140,7 @@ func main() {
// handle static files // handle static files
rt.Static("/assets", "./assets") rt.Static("/assets", "./assets")
rt.Static("/records", "./records")
term_conn.Init(checkOrigin) term_conn.Init(checkOrigin)

View File

@ -249,6 +249,7 @@ out:
log.Println("Failed to marshal record", err) log.Println("Failed to marshal record", err)
} else { } else {
tc.record.Write(jbuf) tc.record.Write(jbuf)
tc.record.Write([]byte(",")) // write a deliminator
} }
tc.lastRecTime = time.Now() tc.lastRecTime = time.Now()
@ -266,8 +267,17 @@ out:
tc.record = nil tc.record = nil
} }
tc.record.Write([]byte("[")) // write a [ for an array of json objs
tc.lastRecTime = time.Now() tc.lastRecTime = time.Now()
} else { } else {
fsinfo, err := tc.record.Stat()
if err == nil {
tc.record.Truncate(fsinfo.Size() - 1)
tc.record.Seek(0, 2) // truncate does not change read/write location
tc.record.Write([]byte("]"))
}
tc.record.Close() tc.record.Close()
tc.record = nil tc.record = nil
} }