diff --git a/assets/index.html b/assets/index.html index 0b0c7ba..c22919a 100644 --- a/assets/index.html +++ b/assets/index.html @@ -8,7 +8,7 @@ - + Web Terminal diff --git a/assets/main.css b/assets/main.css index 257d19a..a0a7f89 100644 --- a/assets/main.css +++ b/assets/main.css @@ -35,4 +35,4 @@ .navbar-xs .navbar-nav>li>a { padding-top: 0px; padding-bottom: 0px; -} \ No newline at end of file +} diff --git a/assets/replay.html b/assets/replay.html index 85135ba..9a2a1d7 100644 --- a/assets/replay.html +++ b/assets/replay.html @@ -37,39 +37,40 @@
- -
-
-
+
- diff --git a/assets/replay.js b/assets/replay.js index 3fd84bc..427cbf6 100644 --- a/assets/replay.js +++ b/assets/replay.js @@ -1,46 +1,145 @@ +// create a xterm for replay function createReplayTerminal() { - // vscode-snazzy https://github.com/Tyriar/vscode-snazzy - // copied from xterm.js website - var baseTheme = { - foreground: '#eff0eb', - background: '#282a36', - selection: '#97979b33', - black: '#282a36', - brightBlack: '#686868', - red: '#ff5c57', - brightRed: '#ff5c57', - green: '#5af78e', - brightGreen: '#5af78e', - yellow: '#f3f99d', - brightYellow: '#f3f99d', - blue: '#57c7ff', - brightBlue: '#57c7ff', - magenta: '#ff6ac1', - brightMagenta: '#ff6ac1', - cyan: '#9aedfe', - brightCyan: '#9aedfe', - white: '#f1f1f0', - 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(); + // vscode-snazzy https://github.com/Tyriar/vscode-snazzy + // copied from xterm.js website + var baseTheme = { + foreground: '#eff0eb', + background: '#282a36', + selection: '#97979b33', + black: '#282a36', + brightBlack: '#686868', + red: '#ff5c57', + brightRed: '#ff5c57', + green: '#5af78e', + brightGreen: '#5af78e', + yellow: '#f3f99d', + brightYellow: '#f3f99d', + blue: '#57c7ff', + brightBlue: '#57c7ff', + magenta: '#ff6ac1', + brightMagenta: '#ff6ac1', + cyan: '#9aedfe', + brightCyan: '#9aedfe', + white: '#f1f1f0', + brightWhite: '#eff0eb' + }; - return term; - } \ No newline at end of file + 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 +} \ No newline at end of file diff --git a/assets/term.html b/assets/term.html index d2f9d93..eb4bffc 100644 --- a/assets/term.html +++ b/assets/term.html @@ -33,7 +33,7 @@ -
+
diff --git a/cmd/replay/replay.go b/cmd/replay/replay.go index dd1c439..96e9596 100644 --- a/cmd/replay/replay.go +++ b/cmd/replay/replay.go @@ -40,7 +40,7 @@ func main() { w, h, _ := term.GetSize(int(os.Stdout.Fd())) 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) @@ -49,6 +49,10 @@ func main() { 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() { var record writeRecord @@ -60,4 +64,6 @@ func main() { time.Sleep(record.Dur) t.Write(record.Data) } + + t.Write([]byte("\n\n---end of replay---\n\n")) } diff --git a/main.go b/main.go index f3f4566..8bbc33a 100644 --- a/main.go +++ b/main.go @@ -140,6 +140,7 @@ func main() { // handle static files rt.Static("/assets", "./assets") + rt.Static("/records", "./records") term_conn.Init(checkOrigin) diff --git a/term_conn/relay.go b/term_conn/relay.go index b60fbfd..284c1b6 100644 --- a/term_conn/relay.go +++ b/term_conn/relay.go @@ -249,6 +249,7 @@ out: log.Println("Failed to marshal record", err) } else { tc.record.Write(jbuf) + tc.record.Write([]byte(",")) // write a deliminator } tc.lastRecTime = time.Now() @@ -266,8 +267,17 @@ out: tc.record = nil } + tc.record.Write([]byte("[")) // write a [ for an array of json objs tc.lastRecTime = time.Now() } 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 = nil }