├── .gitignore
├── README.md
├── data
├── example.md
├── index.template
└── prefix.md
├── go.mod
├── go.sum
├── release.sh
├── ssh-external-win.go
├── ssh-external.go
├── ssh-internal.go
├── tech-talk.go
└── www
├── amy.gif
├── favicon.png
├── script.js
├── style.css
└── wetty
├── hterm_all.js
├── index.html
└── wetty.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | bindata.go
3 | tech-talk
4 | tech-talk.exe
5 | vendor
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Tech Talk
2 |
3 | An opinionated Markdown-based technical slideshow tool with a built-in terminal that just works.
4 |
5 | 
6 |
7 | - Write your slides with [Markdown](https://github.com/gnab/remark/wiki/Markdown)
8 | - Built-in terminal (both local or via SSH)
9 | - Supports presenter mode with notes & timer, cloned displays, etc.
10 | - Simple, self-contained executable for Winows, Mac & Linux
11 | - PDF export with your browser's built-in print to PDF
12 | - Trivial to customize and distribute within your company
13 |
14 | ## Usage
15 |
16 | Start by downloading a [release](https://github.com/danielgtaylor/tech-talk/releases) or your company's variant. You probably want to put it into `/usr/local/bin/tech-talk` on Mac / Linux. On Windows you can save `tech-talk.exe` anywhere as it is fully self-contained / portable.
17 |
18 | ```sh
19 | # See a demo slideshow
20 | tech-talk
21 |
22 | # Use your own slides
23 | tech-talk slides.md
24 |
25 | # Use SSH to connect to some server for the terminal
26 | tech-talk -host user@hostname slides.md
27 |
28 | # Custom ports are also allowed
29 | tech-talk -host user@hostname:port slides.md
30 |
31 | # Windows users must pass an authentication method as an internal SSH
32 | # mechanism is used instead of OpenSSH. Keys are recommended over passwords.
33 | tech-talk -host user@hostname -pass cleartext-password slides.md
34 | tech-talk -host user@hostname -key id_rsa slides.md
35 | tech-talk -host user@hostname -key my-key.pem slides.md
36 |
37 | # Mac / Linux users can force the use of the internal SSH client and use the
38 | # same options that Windows users would use from above.
39 | tech-talk -ssh internal ...
40 | ```
41 |
42 | Then go to [http://localhost:4000/](http://localhost:4000/) to view.
43 |
44 | ## Customization
45 |
46 | Sending around boilerplate or configs to everyone sucks. Build your own self-contained executable with your company's theme and let people focus on making great talks.
47 |
48 | First you'll want to install [Go](https://golang.org/). Then:
49 |
50 | ```sh
51 | mkdir -p $GOPATH/src/github.com/danielgtaylor
52 | cd $GOPATH/src/github.com/danielgtaylor
53 | git clone https://github.com/danielgtaylor/tech-talk.git
54 | cd tech-talk
55 | go install
56 | ```
57 |
58 | Now, make your customizations!
59 |
60 | - [Slideshow prefix (prepended to first slide)](https://github.com/danielgtaylor/tech-talk/tree/master/data/prefix.md)
61 | - [Stylesheet (fonts, colors, layout, etc)](https://github.com/danielgtaylor/tech-talk/tree/master/www/style.css)
62 | - [Javascript (transitions)](https://github.com/danielgtaylor/tech-talk/tree/master/www/script.js)
63 | - [Terminal default font size](https://github.com/danielgtaylor/tech-talk/tree/master/www/wetty/wetty.js)
64 | - [Example slideshow](https://github.com/danielgtaylor/tech-talk/tree/master/data/example.md)
65 | - [HTML template](https://github.com/danielgtaylor/tech-talk/blob/master/data/index.template)
66 |
67 | Once you are ready:
68 |
69 | ```sh
70 | # Build for your OS and test
71 | ./build.sh
72 | ./tech-talk
73 |
74 | # Or, cross-compile, e.g:
75 | GOOS=linux GOARCH=386 ./build.sh
76 |
77 | # Automated release (cross-compile to supported platforms):
78 | ./release.sh
79 | ```
80 |
81 | Remember to run `./build.sh` each time you make a change, and your browser may cache items so Cmd+Shift+R or Ctrl+Shift+R to force a refresh are useful.
82 |
83 | Now you can upload your executables somewhere like Google Drive and share them within the company.
84 |
85 | ## Acknowledgments
86 |
87 | This project is possible because of the amazing work done by many people in the following projects, many of which are used with slight modifications or custom settings:
88 |
89 | - [Remark.js](https://github.com/gnab/remark#remark)
90 | - [Socket.io](https://github.com/googollee/go-socket.io)
91 | - [pty](https://github.com/kr/pty)
92 | - [Wetty](https://github.com/krishnasrinivas/wetty)
93 | - [crypto/ssh](https://godoc.org/golang.org/x/crypto/ssh)
94 |
95 | So, how does this beast work? Simple, really. It starts both a web server and Socket.IO server, renders an index page with Remark.js using the user-supplied Markdown, which in turn contains an `iframe` with a terminal that uses the socket to communicate with a PTY running either a login shell or `ssh` session. For the internal SSH client there is no client-side PTY but instead a couple pipes through an SSH connection.
96 |
97 | ## License
98 |
99 | https://dgt.mit-license.org/
100 |
--------------------------------------------------------------------------------
/data/example.md:
--------------------------------------------------------------------------------
1 | # Sample Tech Talk
2 |
3 | This is an example. Press ⇨ to continue.
4 |
5 | [View example source](https://github.com/danielgtaylor/tech-talk/blob/master/data/example.md)
6 |
7 | ---
8 |
9 | # Slide Title
10 |
11 | Slides support Markdown content. **Strong** and *emphasized* text, [links](https://github.com/danielgtaylor/tech-talk), lists, tables, and code syntax highlighting.
12 |
13 | 1. List item
14 | 2. Another item
15 | 3. Just use standard Markdown!
16 |
17 | ```js
18 | // Code example using Javascript
19 | const pi = 3.14159;
20 | let r = 10;
21 |
22 | console.log(2 * pi * r * r);
23 | ```
24 |
25 | [More formatting info](https://github.com/gnab/remark/wiki/Markdown)
26 |
27 | ---
28 |
29 | # Navigation
30 |
31 | Key | Description
32 | --- | -----------
33 | ⇨ | Move to next slide (spacebar also works)
34 | ⇦ | Move to previous slide
35 | ~ | Show terminal (any selected text is copy/pasted)
36 | f | Go to fullscreen mode
37 | esc | Exit fullscreen mode
38 | p | Go to presenter mode (to show slide notes)
39 | c | Clone window (to display current slide)
40 |
41 | ---
42 |
43 | # Incremental slide
44 |
45 | Partial slide updates are also supported by using the `--` delimiter or by simply using the same slide title.
46 |
47 | - A list
48 | - Of items
49 | --
50 |
51 | - One more item
52 | --
53 |
54 | - And one last one!
55 |
56 | But you aren't limited to just list items.
57 |
58 | ---
59 |
60 | # Math & Formulas
61 |
62 | Complex formulas are easy to display with [AsciiMath](http://asciimath.org/#syntax) using `%%`, or Tex / LaTeX using `$$` delimiters thanks to MathJax.
63 |
64 | .center[
65 | **AsciiMath example**
66 |
67 | %%i = sum(1.65 \* 0.000125^(o - 1) \* (1 - 2.718^(-0.04t) / 4.15) \* (7490duz) / (100h))%%
68 | ]
69 |
70 | .center[
71 | **LaTeX example**
72 | $$ax^2 + bx + c = 0$$
73 | ]
74 |
75 | ---
76 |
77 | # Images & Videos
78 |
79 | Assets in the same folder as the Markdown slides can be referenced relative to the root of the server.
80 |
81 | .center[
82 | ]
83 |
84 | ```html
85 |
86 |
87 | ```
88 |
89 | ---
90 | class: center, middle
91 |
92 | Optional classes can control layout
93 |
94 | Now go and give great tech talks!
95 |
--------------------------------------------------------------------------------
/data/index.template:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Tech Talk
5 |
6 |
7 |
8 |
9 |
10 |
11 |
15 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/data/prefix.md:
--------------------------------------------------------------------------------
1 | class: center, middle
2 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/danielgtaylor/tech-talk
2 |
3 | go 1.18
4 |
5 | require (
6 | github.com/googollee/go-socket.io v0.0.0-20160811131822-e9f0a9b7a612
7 | github.com/kr/pty v0.0.0-20160716204620-ce7fa45920dc
8 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2
9 | )
10 |
11 | require (
12 | github.com/googollee/go-engine.io v0.0.0-20160510131259-ca9ced27e02c // indirect
13 | github.com/gorilla/websocket v1.1.1-0.20170123185551-0674c7c7968d // indirect
14 | github.com/smartystreets/goconvey v1.7.2 // indirect
15 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect
16 | )
17 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/googollee/go-engine.io v0.0.0-20160510131259-ca9ced27e02c h1:DrUMKTnPnYYiKw/aJ1yMKUFbQbtMNa76O/VEidVtec4=
2 | github.com/googollee/go-engine.io v0.0.0-20160510131259-ca9ced27e02c/go.mod h1:MBpz1MS3P4HtRcBpQU4HcjvWXZ9q+JWacMEh2/BFYbg=
3 | github.com/googollee/go-socket.io v0.0.0-20160811131822-e9f0a9b7a612 h1:svnjU5+ojiS1dzrOx53GELERHoHSrQEK3NmxH1OJxVQ=
4 | github.com/googollee/go-socket.io v0.0.0-20160811131822-e9f0a9b7a612/go.mod h1:ftBGBMhSYToR5oV4ImIPKvAIsNaTkLC+tTvoNafqxlQ=
5 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
6 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
7 | github.com/gorilla/websocket v1.1.1-0.20170123185551-0674c7c7968d h1:fOhMs1xPiz7DJYHGU/pmXHmmLzNTMr2yLWCVdWmS/mA=
8 | github.com/gorilla/websocket v1.1.1-0.20170123185551-0674c7c7968d/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
9 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
10 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
11 | github.com/kr/pty v0.0.0-20160716204620-ce7fa45920dc h1:k0VIhqlzaXZGfIA9bvTKsMnSi/u5Rp7zuoK+tZJYaoA=
12 | github.com/kr/pty v0.0.0-20160716204620-ce7fa45920dc/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
13 | github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
14 | github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
15 | github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg=
16 | github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM=
17 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
18 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
19 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
20 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
21 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s=
22 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
23 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
24 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
25 |
--------------------------------------------------------------------------------
/release.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Build release versions for all supported platforms to make it easy to
5 | # upload to GitHub or your own company storage (Google Drive, Dropbox, S3).
6 | #
7 |
8 | # Build for supported operating systems & architectures
9 | GOOS=darwin GOARCH=amd64 go build -o tech-talk-mac
10 | GOOS=linux GOARCH=386 go build -o tech-talk-linux-386
11 | GOOS=linux GOARCH=amd64 go build -o tech-talk-linux-amd64
12 | GOOS=windows GOARCH=386 go build
13 |
--------------------------------------------------------------------------------
/ssh-external-win.go:
--------------------------------------------------------------------------------
1 | // +build windows
2 |
3 | package main
4 |
5 | const CAN_USE_EXTERNAL = false
6 |
7 | func externalSSH(so interface{}) {
8 | // Not available on Windows.
9 | }
10 |
--------------------------------------------------------------------------------
/ssh-external.go:
--------------------------------------------------------------------------------
1 | // +build !windows
2 |
3 | package main
4 |
5 | import (
6 | "log"
7 | "os"
8 | "os/exec"
9 | "strings"
10 | "syscall"
11 | "unsafe"
12 |
13 | "github.com/googollee/go-socket.io"
14 | "github.com/kr/pty"
15 | )
16 |
17 | const CAN_USE_EXTERNAL = true
18 |
19 | // Resize a PTY using system calls. This functionality / utility is missing
20 | // from the kr/pty project so it is added here.
21 | func resizePty(t *os.File, row, col int) error {
22 | ws := struct {
23 | ws_row uint16
24 | ws_col uint16
25 | ws_xpixel uint16
26 | ws_ypixel uint16
27 | }{
28 | uint16(row),
29 | uint16(col),
30 | 0,
31 | 0,
32 | }
33 |
34 | _, _, errno := syscall.Syscall(
35 | syscall.SYS_IOCTL,
36 | t.Fd(),
37 | syscall.TIOCSWINSZ,
38 | uintptr(unsafe.Pointer(&ws)),
39 | )
40 | if errno != 0 {
41 | return syscall.Errno(errno)
42 | }
43 | return nil
44 | }
45 |
46 | // Connect to an external SSH client (or other external login shell)
47 | func externalSSH(so socketio.Socket) {
48 | var c *exec.Cmd
49 |
50 | var args []string
51 |
52 | parts := strings.Split(*sshHost, ":")
53 | if len(parts) == 1 {
54 | args = append(args, parts[0])
55 | } else if len(parts) == 2 {
56 | args = append(args, parts[0])
57 | args = append(args, "-p")
58 | args = append(args, parts[1])
59 | } else {
60 | log.Panicf("Not sure what to do with host: ", *sshHost)
61 | }
62 |
63 | // If SSH was explicitly set, then prefer it.
64 | if *sshHost != DEFAULT_HOST {
65 | log.Printf("Using external SSH client for %s\n", *sshHost)
66 | c = exec.Command("/usr/bin/ssh", args...)
67 | } else {
68 | // On Mac we should have `/usr/bin/login` which does not require root,
69 | // so there we use it. Otherwise just start an SSH session with the
70 | // current user on localhost to get a shell without root.
71 | if check_access("/usr/bin/login") {
72 | log.Printf("Using /usr/bin/login for shell as %s\n", currentUser.Username)
73 | c = exec.Command("/usr/bin/login", "-f", currentUser.Username)
74 | } else {
75 | log.Printf("Using external SSH client for %s\n", *sshHost)
76 | c = exec.Command("/usr/bin/ssh", args...)
77 | }
78 | }
79 |
80 | f, err := pty.Start(c)
81 | if err != nil {
82 | panic(err)
83 | }
84 |
85 | so.On("input", func(msg string) {
86 | f.Write([]byte(msg))
87 | })
88 |
89 | so.On("resize", func(msg map[string]int) {
90 | rows, cols, err := pty.Getsize(f)
91 |
92 | if err != nil {
93 | log.Printf("Error: could not get PTY size. %s\n", err)
94 | return
95 | }
96 |
97 | if rows != msg["row"] || cols != msg["col"] {
98 | log.Printf("Resize: %d cols x %d row\n", msg["col"], msg["row"])
99 | resizePty(f, msg["row"], msg["col"])
100 | }
101 | })
102 |
103 | so.On("disconnection", func() {
104 | log.Println("Terminal disconnect")
105 | c.Process.Kill()
106 | })
107 |
108 | go copyToSocket(f, so)
109 | }
110 |
--------------------------------------------------------------------------------
/ssh-internal.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "io/ioutil"
5 | "log"
6 | "strings"
7 |
8 | "github.com/googollee/go-socket.io"
9 | "golang.org/x/crypto/ssh"
10 | )
11 |
12 | // Provides a built-in internal SSH mechanism, which works on all operating
13 | // systems. It does _not_ take into account your SSH config (e.g. ~/.ssh/config)
14 | // and so on systems that provide an external SSH client it's better to shell
15 | // out and use your standard config.
16 | func internalSSH(so socketio.Socket) {
17 | var user string
18 | var hostPort string
19 |
20 | parts := strings.Split(*sshHost, "@")
21 |
22 | if len(parts) == 1 {
23 | user = currentUser.Username
24 | hostPort = parts[0]
25 | } else {
26 | user = parts[0]
27 | hostPort = parts[1]
28 | }
29 |
30 | if !strings.Contains(hostPort, ":") {
31 | hostPort = hostPort + ":22"
32 | }
33 |
34 | var auth []ssh.AuthMethod
35 |
36 | if *key != "" {
37 | key, err := ioutil.ReadFile(*key)
38 | if err != nil {
39 | log.Fatalf("unable to read private key: %v", err)
40 | }
41 |
42 | signer, err := ssh.ParsePrivateKey(key)
43 |
44 | if err != nil {
45 | log.Fatalf("unable to parse private key: %v", err)
46 | }
47 |
48 | auth = append(auth, ssh.PublicKeys(signer))
49 | }
50 |
51 | if *pass != "" {
52 | auth = append(auth, ssh.Password(*pass))
53 | }
54 |
55 | sshConfig := &ssh.ClientConfig{
56 | User: user,
57 | Auth: auth,
58 | }
59 |
60 | log.Println("Connecting to SSH server...")
61 | connection, err := ssh.Dial("tcp", hostPort, sshConfig)
62 | if err != nil {
63 | log.Printf("Failed to dial: %s", err)
64 | return
65 | }
66 |
67 | session, err := connection.NewSession()
68 | if err != nil {
69 | log.Printf("Failed to create session: %s", err)
70 | return
71 | }
72 |
73 | modes := ssh.TerminalModes{
74 | ssh.ECHO: 1, // enable echoing
75 | ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
76 | ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
77 | }
78 |
79 | if err := session.RequestPty("xterm-256color", 80, 40, modes); err != nil {
80 | session.Close()
81 | log.Printf("Request for pseudo terminal failed: %s", err)
82 | return
83 | }
84 |
85 | so.On("resize", func(msg map[string]int) {
86 | log.Printf("Resize: %d cols x %d row\n", msg["col"], msg["row"])
87 |
88 | // The Go SSH implementation doesn't provide this call but does let us
89 | // send custom commands. See here for a description of the command and
90 | // structure: https://www.ietf.org/rfc/rfc4254.txt
91 | data := struct {
92 | Col uint32
93 | Row uint32
94 | W uint32
95 | H uint32
96 | }{uint32(msg["col"]), uint32(msg["row"]), 0, 0}
97 |
98 | if _, err := session.SendRequest("window-change", false, ssh.Marshal(&data)); err != nil {
99 | log.Printf("Request for window resize failed: %s", err)
100 | return
101 | }
102 | })
103 |
104 | stdin, err := session.StdinPipe()
105 | if err != nil {
106 | log.Printf("Unable to setup stdin for session: %v", err)
107 | return
108 | }
109 |
110 | so.On("input", func(msg string) {
111 | stdin.Write([]byte(msg))
112 | })
113 |
114 | so.On("disconnection", func() {
115 | log.Println("Terminal disconnect")
116 | session.Close()
117 | connection.Close()
118 | })
119 |
120 | stdout, err := session.StdoutPipe()
121 | if err != nil {
122 | log.Printf("Unable to setup stdout for session: %v", err)
123 | return
124 | }
125 |
126 | go copyToSocket(stdout, so)
127 |
128 | log.Println("Starting remote shell...")
129 | err = session.Shell()
130 | if err != nil {
131 | log.Printf("Unable to start shell: %v", err)
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/tech-talk.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "embed"
5 | "flag"
6 | "fmt"
7 | "io"
8 | "io/ioutil"
9 | "log"
10 | "net/http"
11 | "os"
12 | "os/exec"
13 | "os/user"
14 | "path"
15 | "path/filepath"
16 | "text/template"
17 | "time"
18 |
19 | socketio "github.com/googollee/go-socket.io"
20 | )
21 |
22 | //go:embed data www
23 | var fs embed.FS
24 |
25 | type TemplateValues struct {
26 | Prefix string
27 | Markdown string
28 | }
29 |
30 | const DEFAULT_HOST = "localhost"
31 | const VERSION = "1.2.0"
32 |
33 | var indexTemplate *template.Template
34 | var socketServer *socketio.Server
35 | var mdFilename string
36 | var currentUser *user.User
37 |
38 | var sshType *string
39 | var sshHost *string
40 | var key *string
41 | var pass *string
42 | var noBrowser *bool
43 | var templatePath *string
44 |
45 | // Checks if a file exists and can be accessed.
46 | func check_access(filename string) bool {
47 | _, err := os.Stat(filename)
48 | return err == nil
49 | }
50 |
51 | // Copy data from a reader (e.g. PTY or Stdin pipe) to the web socket.
52 | func copyToSocket(r io.Reader, so socketio.Socket) {
53 | for {
54 | data := make([]byte, 512)
55 | n, err := r.Read(data)
56 | if err != nil {
57 | log.Println(err)
58 | break
59 | }
60 | if n > 0 {
61 | so.Emit("output", string(data))
62 | }
63 | }
64 | }
65 |
66 | // Return an HTML page with the slideshow
67 | func indexHandler(w http.ResponseWriter, r *http.Request) {
68 | // If we aren't getting the index itself, serve static files in the
69 | // same directory as the input Markdown slides file.
70 | if r.URL.Path != "/" {
71 | http.FileServer(http.Dir(filepath.Dir(mdFilename))).ServeHTTP(w, r)
72 | return
73 | }
74 |
75 | var data TemplateValues
76 |
77 | // Read the file on each request so that updates get applied when working
78 | // on the slideshow.
79 | var b []byte
80 |
81 | if mdFilename != "" {
82 | b, _ = ioutil.ReadFile(mdFilename)
83 | data.Markdown = string(b)
84 | } else {
85 | b, _ = fs.ReadFile("data/example.md")
86 | data.Markdown = string(b)
87 | }
88 |
89 | b, _ = fs.ReadFile("data/prefix.md")
90 | data.Prefix = string(b)
91 |
92 | if *templatePath != "" {
93 | b, err := ioutil.ReadFile(*templatePath)
94 | if err != nil {
95 | panic(err)
96 | }
97 | indexTemplate = template.Must(template.New("index").Parse(string(b)))
98 | }
99 |
100 | w.Header().Add("Content-Type", "text/html")
101 | indexTemplate.Execute(w, data)
102 | }
103 |
104 | // Create a new socket server to handle communication with a PTY shell.
105 | // This allows you to run stuff in a terminal without ever leaving the
106 | // slideshow.
107 | func createSocketServer() {
108 | server, err := socketio.NewServer(nil)
109 | if err != nil {
110 | log.Fatal(err)
111 | }
112 |
113 | socketServer = server
114 |
115 | socketServer.On("connection", func(so socketio.Socket) {
116 | log.Printf("Terminal connected from %s\n", so.Request().RemoteAddr)
117 |
118 | if *sshType == "internal" || CAN_USE_EXTERNAL == false {
119 | log.Println("Using internal SSH client")
120 | internalSSH(so)
121 | } else {
122 | externalSSH(so)
123 | }
124 | })
125 |
126 | socketServer.On("error", func(so socketio.Socket, err error) {
127 | log.Println("Error:", err)
128 | })
129 | }
130 |
131 | func main() {
132 | u, err := user.Current()
133 | if err != nil {
134 | log.Fatal("Couldn't get current user!")
135 | }
136 | currentUser = u
137 |
138 | flag.Usage = func() {
139 | fmt.Fprintf(os.Stderr, "Usage: tech-talk [slides.md]\n")
140 | flag.PrintDefaults()
141 | os.Exit(2)
142 | }
143 |
144 | // Connection options
145 | sshHost = flag.String("host", DEFAULT_HOST, "SSH connection [user@]`hostname`[:port]")
146 | sshType = flag.String("ssh", "auto", "SSH `method` [auto, internal]")
147 |
148 | // Auth options
149 | keyDefault := ""
150 |
151 | idRsaPath := path.Join(currentUser.HomeDir, ".ssh", "id_rsa")
152 | if check_access(idRsaPath) {
153 | keyDefault = idRsaPath
154 | }
155 |
156 | key = flag.String("key", keyDefault, "SSH private key `path` (for internal SSH)")
157 | pass = flag.String("pass", "", "SSH `password` (for internal SSH)")
158 |
159 | // Misc options
160 | noBrowser = flag.Bool("n", false, "Do not automatically open browser")
161 | version := flag.Bool("v", false, "Alias for --version")
162 | flag.BoolVar(version, "version", false, "Print program version and exit")
163 |
164 | templatePath = flag.String("template", "", "Path to custom HTML template")
165 |
166 | flag.Parse()
167 | args := flag.Args()
168 |
169 | if *version {
170 | fmt.Printf("tech-talk: %s\n", VERSION)
171 | return
172 | }
173 |
174 | if len(args) > 0 {
175 | if !check_access(args[0]) {
176 | log.Fatalf("Cannot access %s", args[0])
177 | }
178 |
179 | mdFilename = args[0]
180 | }
181 |
182 | // Start web sockets
183 | createSocketServer()
184 | http.Handle("/wetty/socket.io/", socketServer)
185 |
186 | // Setup web server
187 | indexBytes, _ := fs.ReadFile("data/index.template")
188 | indexTemplate = template.Must(template.New("index").Parse(string(indexBytes)))
189 |
190 | http.HandleFunc("/", indexHandler)
191 |
192 | http.Handle("/static/",
193 | http.StripPrefix("/static/",
194 | http.FileServer(http.FS(fs))))
195 |
196 | s := &http.Server{
197 | Addr: ":4000",
198 | ReadTimeout: 10 * time.Second,
199 | WriteTimeout: 10 * time.Second,
200 | MaxHeaderBytes: 1 << 20,
201 | }
202 |
203 | log.Println("Server started on http://localhost:4000/")
204 |
205 | if !*noBrowser && check_access("/usr/bin/open") {
206 | c := exec.Command("/usr/bin/open", "http://localhost:4000")
207 | c.Start()
208 | }
209 |
210 | log.Panic(s.ListenAndServe())
211 | }
212 |
--------------------------------------------------------------------------------
/www/amy.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielgtaylor/tech-talk/fb96e85bc5b174db6d0eacaac6564a869f594d24/www/amy.gif
--------------------------------------------------------------------------------
/www/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielgtaylor/tech-talk/fb96e85bc5b174db6d0eacaac6564a869f594d24/www/favicon.png
--------------------------------------------------------------------------------
/www/script.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Slideshow Configuration
3 | */
4 | // One of https://github.com/isagalaev/highlight.js/tree/master/src/styles
5 | const HIGHLIGHT_THEME = "github";
6 |
7 | // See options at https://daneden.github.io/animate.css/
8 | const TRANSITIONS = {
9 | NEXT: "animate__slideInUp",
10 | PREV: "animate__slideInLeft",
11 | INCREMENTAL: "animate__fadeIn",
12 | };
13 |
14 | // Get the first title element of a slide and return its text content.
15 | function getTitle(element) {
16 | const header = element.querySelector("h1, h2, h3, h4, h5");
17 | let title = "";
18 |
19 | if (header) {
20 | title = header.textContent;
21 | }
22 |
23 | return title;
24 | }
25 |
26 | // Create the slideshow on the page.
27 | const slideshow = remark.create({
28 | highlightStyle: HIGHLIGHT_THEME,
29 | ratio: "16:9",
30 | navigation: {
31 | scroll: false,
32 | },
33 | });
34 |
35 | // Set up transitions between slides by monitoring which direction we are
36 | // traveling and whether the slides are incremental (by checking titles).
37 | slideshow.on("beforeShowSlide", (next) => {
38 | const nextIndex = next.getSlideIndex();
39 | const prevIndex = slideshow.getCurrentSlideIndex();
40 | const slides = document.querySelectorAll(".remark-slide-container");
41 | const nextDiv = slides[nextIndex];
42 | const prevDiv = slides[prevIndex];
43 | const nextTitle = getTitle(nextDiv);
44 | const prevTitle = getTitle(prevDiv);
45 |
46 | let direction = nextIndex > prevIndex ? TRANSITIONS.NEXT : TRANSITIONS.PREV;
47 |
48 | if (prevTitle === nextTitle) {
49 | // Special case, either incremental slides or similar enough.
50 | direction = TRANSITIONS.INCREMENTAL;
51 | }
52 |
53 | nextDiv.classList.add("animate__animated");
54 | nextDiv.classList.add(direction);
55 |
56 | prevDiv.classList.remove("animate__animated");
57 | prevDiv.classList.remove(TRANSITIONS.NEXT);
58 | prevDiv.classList.remove(TRANSITIONS.PREV);
59 | prevDiv.classList.remove(TRANSITIONS.INCREMENTAL);
60 | });
61 |
62 | const term = document.querySelector("#terminal");
63 |
64 | // Slide the term into or out of the viewport.
65 | function toggleTerm() {
66 | term.classList.toggle("animate__slideInDown");
67 | term.classList.toggle("animate__slideOutUp");
68 | }
69 |
70 | // Toggle either from keypress or pressing the close button.
71 | window.addEventListener("keyup", (event) => {
72 | //console.log(event);
73 | if (event.keyCode === 192 /* Key: ~ */) {
74 | toggleTerm();
75 |
76 | // If there is a selection, send it to the terminal. To make
77 | // the code examples a bit nicer, we don't need a `\` at the end
78 | // of each continued line. Instead, indent subsequent lines and this
79 | // will replace the newline + join with a single space.
80 | const selected = document.getSelection().toString().replace(/\n /gm, " ");
81 |
82 | let msg = "";
83 | if (selected) {
84 | msg = `clear\n${selected}\n`;
85 | }
86 |
87 | document.querySelector("iframe").contentWindow.postMessage(msg, "*");
88 | }
89 | });
90 |
91 | // Toggle terminal when pressing the `close` button.
92 | term.querySelector("a.btn").addEventListener("click", toggleTerm);
93 |
94 | window.addEventListener("load", () => {
95 | setTimeout(() => {
96 | // Enable animations
97 | document.body.classList.remove("preload");
98 | }, 200);
99 | });
100 |
101 | // Setup MathJax
102 | MathJax.Hub.Config({
103 | asciimath2jax: {
104 | // Since Markdown makes heavy use of backticks, prefer a syntax that
105 | // won't conflict with Markdown processing.
106 | delimiters: [["%%", "%%"]],
107 | },
108 | });
109 |
110 | MathJax.Hub.Configured();
111 |
--------------------------------------------------------------------------------
/www/style.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css?family=Raleway:400,700');
2 | @import url(https://fonts.googleapis.com/css?family=Lato:400,700,400italic);
3 | @import url(https://fonts.googleapis.com/css?family=Ubuntu+Mono:400,700,400italic);
4 |
5 | body {
6 | font-family: 'Lato', 'Arial', sans-serif;
7 | }
8 |
9 | h1, h2, h3 {
10 | font-family: 'Raleway', 'Helvetica', sans-serif;
11 | font-weight: normal;
12 | }
13 |
14 | a {
15 | color: #0086b3;
16 | }
17 |
18 | table {
19 | width: 100%;
20 | border-collapse: collapse;
21 | border: 1px solid #f2f2f2;
22 | }
23 |
24 | th {
25 | color: #000;
26 | background-color: #f2f2f2;
27 | padding: 12px;
28 | }
29 |
30 | td {
31 | padding: 12px;
32 | border-bottom: 1px solid #f2f2f2;
33 | border-right: 1px solid #f2f2f2;
34 | }
35 |
36 | code.remark-inline-code {
37 | background: #f2f2f2;
38 | padding: 2px 6px;
39 | border-radius: 3px;
40 | }
41 |
42 | kbd {
43 | display: inline-block;
44 | min-width: 1em;
45 | padding: .2em .3em;
46 | font: normal .85em/1 "Lato", Arial, sans-serif;
47 | text-align: center;
48 | text-decoration: none;
49 | border-radius: .3em;
50 | border: none;
51 | cursor: default;
52 | user-select: none;
53 | background: rgb(80, 80, 80);
54 | background: linear-gradient(to bottom, rgb(60, 60, 60), rgb(80, 80, 80));
55 | color: rgb(250, 250, 250);
56 | text-shadow: -1px -1px 0 rgb(70, 70, 70);
57 | box-shadow: inset 0 0 1px rgb(150, 150, 150), inset 0 -.05em .4em rgb(80, 80, 80), 0 .1em 0 rgb(30, 30, 30), 0 .1em .1em rgba(0, 0, 0, .3);
58 | }
59 |
60 | .preload * {
61 | animation-duration: 1ms !important;
62 | }
63 |
64 | .animate__animated {
65 | animation-duration: 0.5s;
66 | }
67 |
68 | .hljs-default .hljs {
69 | background: #f2f2f2;
70 | border-radius: 3px;
71 | }
72 |
73 | .remark-container {
74 | background-color: #333;
75 | }
76 |
77 | .remark-slide-content {
78 | color: #000;
79 | background-color: #fff;
80 | font-size: 25px;
81 | border-top: 6px solid #0086b3;
82 | }
83 |
84 | .remark-slide-content h1 {
85 | font-size: 50px;
86 | }
87 |
88 | .remark-slide-scaler {
89 | box-shadow: 0 0 30px #888;
90 | }
91 |
92 | .remark-code, .remark-inline-code {
93 | font-family: 'Ubuntu Mono';
94 | font-size: 19pt;
95 | }
96 |
97 | .remark-slide-number {
98 | opacity: 0.1;
99 | }
100 |
101 | #terminal {
102 | position: fixed;
103 | left: 0;
104 | right: 0;
105 | top: 0;
106 | bottom: 0;
107 | z-index: 999;
108 | opacity: 0.98;
109 | animation-duration: 0.3s;
110 | animation-timing: ease-out;
111 | }
112 |
113 | #terminal iframe {
114 | width: 100%;
115 | height: 95vh;
116 | padding: 0;
117 | margin: 0;
118 | border: none;
119 | shadow: none;
120 | }
121 |
122 | #terminal a.btn {
123 | display: inline-block;
124 | width: 100%;
125 | height: 5vh;
126 | margin: 0;
127 | background: #000;
128 | color: #fff;
129 | text-align: center;
130 | cursor: pointer;
131 | margin-top: -5px;
132 | padding-top: 6px;
133 | }
134 |
--------------------------------------------------------------------------------
/www/wetty/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Wetty - The WebTTY Terminal Emulator
7 |
8 |
9 |
10 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/www/wetty/wetty.js:
--------------------------------------------------------------------------------
1 | var term;
2 | var socket = io(location.origin, {path: '/wetty/socket.io'})
3 | var buf = '';
4 |
5 | function Wetty(argv) {
6 | this.argv_ = argv;
7 | this.io = null;
8 | this.pid_ = -1;
9 | }
10 |
11 | Wetty.prototype.run = function() {
12 | this.io = this.argv_.io.push();
13 |
14 | this.io.onVTKeystroke = this.sendString_.bind(this);
15 | this.io.sendString = this.sendString_.bind(this);
16 | this.io.onTerminalResize = this.onTerminalResize.bind(this);
17 | }
18 |
19 | Wetty.prototype.sendString_ = function(str) {
20 | socket.emit('input', str);
21 | };
22 |
23 | Wetty.prototype.onTerminalResize = function(col, row) {
24 | socket.emit('resize', { col: col, row: row });
25 | };
26 |
27 | socket.on('connect', function() {
28 | lib.init(function() {
29 | hterm.defaultStorage = new lib.Storage.Local();
30 | term = new hterm.Terminal();
31 | window.term = term;
32 | term.decorate(document.getElementById('terminal'));
33 |
34 | term.setCursorPosition(0, 0);
35 | term.setCursorVisible(true);
36 | term.prefs_.set('font-size', 20);
37 | term.prefs_.set('ctrl-c-copy', true);
38 | term.prefs_.set('ctrl-v-paste', true);
39 | term.prefs_.set('use-default-window-copy', true);
40 |
41 | term.runCommandClass(Wetty, document.location.hash.substr(1));
42 | socket.emit('resize', {
43 | col: term.screenSize.width,
44 | row: term.screenSize.height
45 | });
46 |
47 | if (buf && buf != '')
48 | {
49 | term.io.writeUTF16(buf);
50 | buf = '';
51 | }
52 | });
53 | });
54 |
55 | socket.on('output', function(data) {
56 | if (!term) {
57 | buf += data;
58 | return;
59 | }
60 | term.io.writeUTF16(data);
61 | });
62 |
63 | socket.on('disconnect', function() {
64 | console.log("Socket.io connection closed");
65 | });
66 |
67 | // Support sending text from outside iframe
68 | window.addEventListener("message", (msg) => {
69 | if (msg.data && msg.data.target && msg.data.target.includes("metamask")) {
70 | return;
71 | }
72 | socket.emit("input", msg.data);
73 | term.focus();
74 | });
75 |
--------------------------------------------------------------------------------