├── README.md ├── conn.go ├── hub.go ├── LICENSE.md ├── main.go ├── gotail.go ├── home.html ├── home.html.go └── websocket.article /README.md: -------------------------------------------------------------------------------- 1 | This demo is based on [fsnotify](https://github.com/howeyc/fsnotify) 2 | and [Implementing Chat with WebSockets and Go](http://gary.beagledreams.com/page/go-websocket-chat.html). 3 | It's the web version of my [gotail](https://github.com/spin6lock/gotail). 4 | The home.html.go is packed by [go-bindata](https://github.com/jteeuwen/go-bindata). 5 | I really enjoy log.io but it's a little bit heavy for me. 6 | So I write my own version with golang. Hope you like it. 7 | 8 | Usage: 9 | ====== 10 | $gowebtail -addr=":8081" test.log 11 | 12 | Then you can read it in your browser through http://localhost:8081 13 | -------------------------------------------------------------------------------- /conn.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "code.google.com/p/go.net/websocket" 5 | "fmt" 6 | "time" 7 | ) 8 | 9 | type connection struct { 10 | // The websocket connection. 11 | ws *websocket.Conn 12 | 13 | // Buffered channel of outbound messages. 14 | send chan string 15 | } 16 | 17 | func (c *connection) writer() { 18 | for { 19 | select { 20 | case message := <-c.send: 21 | err := websocket.Message.Send(c.ws, message) 22 | if err != nil { 23 | break 24 | } 25 | } 26 | } 27 | c.ws.Close() 28 | } 29 | 30 | func (c *connection) timeTeller() { 31 | t := time.Tick(2 * time.Second) 32 | for now := range t { 33 | fmt.Println(now) 34 | h.broadcast <- now.String() 35 | } 36 | } 37 | 38 | func wsHandler(ws *websocket.Conn) { 39 | c := &connection{send: make(chan string, 256), ws: ws} 40 | h.register <- c 41 | defer func() { h.unregister <- c }() 42 | c.writer() 43 | } 44 | -------------------------------------------------------------------------------- /hub.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type hub struct { 4 | // Registered connections. 5 | connections map[*connection]bool 6 | 7 | // Inbound messages from the connections. 8 | broadcast chan string 9 | 10 | // Register requests from the connections. 11 | register chan *connection 12 | 13 | // Unregister requests from connections. 14 | unregister chan *connection 15 | } 16 | 17 | var h = hub{ 18 | broadcast: make(chan string), 19 | register: make(chan *connection), 20 | unregister: make(chan *connection), 21 | connections: make(map[*connection]bool), 22 | } 23 | 24 | func (h *hub) run() { 25 | for { 26 | select { 27 | case c := <-h.register: 28 | h.connections[c] = true 29 | case c := <-h.unregister: 30 | delete(h.connections, c) 31 | close(c.send) 32 | case m := <-h.broadcast: 33 | for c := range h.connections { 34 | select { 35 | case c.send <- m: 36 | default: 37 | delete(h.connections, c) 38 | close(c.send) 39 | go c.ws.Close() 40 | } 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 John Luk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "code.google.com/p/go.net/websocket" 5 | "flag" 6 | "fmt" 7 | "github.com/howeyc/fsnotify" 8 | "log" 9 | "net/http" 10 | "os" 11 | "strings" 12 | "text/template" 13 | ) 14 | 15 | var homeHtml = string(home_html()) 16 | var addr = flag.String("addr", ":8080", "http service address") 17 | var homeTempl = template.Must(template.New("homeHtml").Parse(homeHtml)) 18 | 19 | func homeHandler(c http.ResponseWriter, req *http.Request) { 20 | homeTempl.Execute(c, req.Host) 21 | } 22 | 23 | func MonitorMultiFiles(names []string, out chan []string, 24 | h hub) { 25 | watcher, err := fsnotify.NewWatcher() 26 | if err != nil { 27 | log.Fatal(err) 28 | } 29 | for _, name := range names { 30 | result, _ := ReadLastNLines(name, 10) 31 | PrintMultiLines(result) 32 | MonitorFile(name, out, watcher) 33 | } 34 | for { 35 | select { 36 | case lines := <-out: 37 | content := strings.Join(lines, "\n") 38 | fmt.Print(content) 39 | h.broadcast <- content 40 | } 41 | } 42 | watcher.Close() 43 | } 44 | 45 | var usage = func() { 46 | fmt.Fprintf(os.Stderr, 47 | "Usage: gowebtail [FILE]...\n") 48 | flag.PrintDefaults() 49 | os.Exit(2) 50 | } 51 | 52 | func main() { 53 | flag.Usage = usage 54 | flag.Parse() 55 | if len(flag.Args()) < 1 { 56 | usage() 57 | } 58 | out := make(chan []string) 59 | go h.run() 60 | go MonitorMultiFiles(flag.Args(), out, h) 61 | http.HandleFunc("/", homeHandler) 62 | http.Handle("/ws", websocket.Handler(wsHandler)) 63 | if err := http.ListenAndServe(*addr, nil); err != nil { 64 | log.Fatal("ListenAndServe:", err) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /gotail.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/howeyc/fsnotify" 7 | "log" 8 | "os" 9 | "strings" 10 | ) 11 | 12 | func LineCount(lines []byte) int { 13 | return bytes.Count(lines, []byte("\n")) 14 | } 15 | 16 | func GetFileSize(filename string) int { 17 | fileInfo, err := os.Stat(filename) 18 | if err != nil { 19 | log.Fatal(err) 20 | } 21 | return int(fileInfo.Size()) 22 | } 23 | 24 | func ByteArrayToMultiLines(bytes []byte) []string { 25 | lines := string(bytes) 26 | return strings.Split(lines, "\n") 27 | } 28 | 29 | func ReadNBytes(filename string, start int, end int) []byte { 30 | fh, _ := os.Open(filename) 31 | defer fh.Close() 32 | fh.Seek(int64(start), 0) 33 | size := end - start + 1 34 | buff := make([]byte, size) 35 | fh.Read(buff) 36 | return buff 37 | } 38 | 39 | func ReadLastNLines(name string, n int) ([]string, error) { 40 | curr := GetFileSize(name) 41 | var end int 42 | count := n 43 | result := make([]byte, n) 44 | for count > 0 && curr != 0 { 45 | curr -= n 46 | end = curr + n - 1 47 | if curr < 0 { 48 | curr = 0 49 | } 50 | buff := ReadNBytes(name, curr, end) 51 | result = append(buff, result...) 52 | count -= LineCount(buff) 53 | } 54 | return ByteArrayToMultiLines(result), nil 55 | } 56 | 57 | func MonitorFile(filename string, out chan []string, 58 | watcher *fsnotify.Watcher) { 59 | size := GetFileSize(filename) 60 | go func() { 61 | for { 62 | select { 63 | case ev := <-watcher.Event: 64 | if ev.IsModify() { 65 | NewSize := GetFileSize(ev.Name) 66 | if NewSize <= size { 67 | MonitorFile(ev.Name, out, watcher) 68 | return 69 | } 70 | content := ReadNBytes(ev.Name, size, 71 | NewSize-1) 72 | size = NewSize 73 | out <- ByteArrayToMultiLines(content) 74 | } 75 | case err := <-watcher.Error: 76 | log.Println("error:", err) 77 | } 78 | } 79 | }() 80 | err := watcher.Watch(filename) 81 | if err != nil { 82 | log.Fatal(err) 83 | } 84 | } 85 | 86 | func PrintMultiLines(lines []string) { 87 | for _, line := range lines { 88 | fmt.Println(line) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | gowebtail 4 | 5 | 46 | 83 | 84 | 85 |
86 | 87 | 88 | -------------------------------------------------------------------------------- /home.html.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | "io" 7 | ) 8 | 9 | // home_html returns raw, uncompressed file data. 10 | func home_html() []byte { 11 | gz, err := gzip.NewReader(bytes.NewBuffer([]byte{ 12 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x00, 0xff, 0x94, 0x55, 13 | 0x4d, 0x6f, 0xdb, 0x38, 0x10, 0xbd, 0xfb, 0x57, 0x70, 0x99, 0x2c, 0x20, 14 | 0x1f, 0x56, 0x72, 0xb6, 0xed, 0x45, 0x91, 0x7d, 0xe9, 0xa5, 0x87, 0xde, 15 | 0x12, 0xa0, 0x28, 0x82, 0x1c, 0x28, 0x91, 0x96, 0xe8, 0x50, 0x1c, 0x95, 16 | 0xa4, 0xac, 0x18, 0x81, 0xff, 0x7b, 0x87, 0xfa, 0xb6, 0x63, 0xa3, 0x8d, 17 | 0x0e, 0xb6, 0xf8, 0x86, 0x33, 0xef, 0xcd, 0x70, 0x86, 0x4a, 0x0a, 0x57, 18 | 0xaa, 0xcd, 0x22, 0x29, 0x04, 0xe3, 0xf8, 0xe7, 0xa4, 0x53, 0x62, 0x93, 19 | 0x43, 0x23, 0x52, 0xc7, 0xa4, 0x4a, 0xa2, 0x0e, 0x58, 0x24, 0x36, 0x33, 20 | 0xb2, 0x72, 0xc4, 0x1d, 0x2a, 0xb1, 0xa6, 0x4e, 0xbc, 0xba, 0x68, 0xc7, 21 | 0xf6, 0xac, 0x43, 0x29, 0xb1, 0x26, 0x5b, 0xd3, 0xc2, 0xb9, 0x2a, 0x8e, 22 | 0x22, 0xb6, 0x63, 0xaf, 0x61, 0x0e, 0x90, 0x2b, 0xc1, 0x2a, 0x69, 0xc3, 23 | 0x0c, 0xca, 0x16, 0x8b, 0x94, 0x4c, 0x6d, 0xb4, 0xfb, 0x55, 0x0b, 0x73, 24 | 0x88, 0xee, 0xc2, 0xcf, 0xe1, 0xff, 0xfd, 0x22, 0x2c, 0xa5, 0x0e, 0x77, 25 | 0x96, 0x6e, 0x92, 0xa8, 0x8b, 0xf7, 0x27, 0xba, 0xcd, 0x82, 0xe0, 0x73, 26 | 0x1b, 0x6c, 0x6b, 0x9d, 0x39, 0x09, 0x3a, 0x58, 0x92, 0xb7, 0x45, 0x8b, 27 | 0xed, 0x99, 0x21, 0x19, 0x68, 0x7d, 0x3f, 0xae, 0x4a, 0x9b, 0x93, 0x35, 28 | 0xee, 0xa5, 0x37, 0xf8, 0x46, 0x97, 0x93, 0x41, 0xc1, 0x60, 0xc0, 0x37, 29 | 0x6f, 0x68, 0x2d, 0x43, 0x48, 0xc2, 0xaa, 0x4a, 0x68, 0xfe, 0x1d, 0xf2, 30 | 0x00, 0xfd, 0x7c, 0x7c, 0xd2, 0x3f, 0xde, 0x97, 0xa3, 0x27, 0x7a, 0x3d, 31 | 0xad, 0x9e, 0x4f, 0x61, 0x78, 0xc8, 0x0c, 0x28, 0x85, 0x56, 0x1e, 0xda, 32 | 0xf6, 0xf5, 0x11, 0x2a, 0xb2, 0x9e, 0x96, 0xdf, 0x84, 0xcc, 0x0b, 0x47, 33 | 0xfe, 0x43, 0x20, 0x53, 0x52, 0x68, 0xd7, 0x01, 0xf7, 0x63, 0x18, 0x24, 34 | 0x0b, 0x3b, 0xea, 0x47, 0x08, 0x90, 0x62, 0x39, 0x5a, 0xe4, 0x96, 0x04, 35 | 0x03, 0xc1, 0x5c, 0x8f, 0x7f, 0x4e, 0xd8, 0xfe, 0x9e, 0xec, 0xb8, 0xe8, 36 | 0x7e, 0xfb, 0x72, 0xd2, 0x9b, 0x2d, 0x98, 0x92, 0x2e, 0x43, 0x5b, 0xa7, 37 | 0xa5, 0x74, 0xa7, 0xe5, 0x9d, 0xcb, 0xf8, 0xc7, 0x97, 0xf8, 0x5c, 0x83, 38 | 0x11, 0xae, 0x36, 0x9a, 0x6c, 0x99, 0xb2, 0xe2, 0x9c, 0x62, 0x74, 0xf4, 39 | 0xe9, 0xed, 0x99, 0x0a, 0x96, 0x1f, 0xf5, 0xf6, 0x8c, 0xa1, 0xc5, 0xb2, 40 | 0x04, 0x53, 0x88, 0xd3, 0xaa, 0x79, 0x8c, 0xd2, 0x19, 0x38, 0x0f, 0xd9, 41 | 0x25, 0x3a, 0x1c, 0xb2, 0x97, 0xd2, 0x48, 0xcd, 0xa1, 0x79, 0xa2, 0x3f, 42 | 0x44, 0xfa, 0x00, 0xd9, 0x8b, 0x70, 0xf4, 0x79, 0xae, 0xc9, 0xf3, 0x61, 43 | 0x29, 0xb5, 0x68, 0xc8, 0xb8, 0x23, 0xa0, 0x8d, 0xc5, 0xfe, 0x7e, 0x7b, 44 | 0xbb, 0x3d, 0x1e, 0xa3, 0xc6, 0xce, 0xb9, 0x5a, 0x79, 0xa0, 0x33, 0x05, 45 | 0x56, 0xa0, 0xdb, 0x58, 0x39, 0xb1, 0x77, 0xe7, 0x99, 0x4e, 0x8d, 0x85, 46 | 0x15, 0x4f, 0xb8, 0xdc, 0x6f, 0x92, 0x74, 0xf3, 0x15, 0xfd, 0x45, 0xd7, 47 | 0x78, 0x6d, 0x0c, 0x1e, 0x26, 0x51, 0x8a, 0xf3, 0xe0, 0xcd, 0x74, 0xb9, 48 | 0xbc, 0x56, 0x10, 0xd0, 0xa5, 0xb0, 0x96, 0xe5, 0x1f, 0xe6, 0x8c, 0x30, 49 | 0x6a, 0xe8, 0xc7, 0xca, 0xef, 0x0e, 0x39, 0x73, 0xec, 0x1d, 0xc9, 0x91, 50 | 0x08, 0x2c, 0xdc, 0x2c, 0xd0, 0x45, 0xe1, 0x3f, 0xa1, 0x36, 0x24, 0x35, 51 | 0xd0, 0x58, 0xe1, 0xfb, 0x5f, 0x58, 0xa2, 0xc1, 0x11, 0x5b, 0x57, 0x15, 52 | 0x18, 0x37, 0x95, 0xce, 0x5e, 0xc8, 0xe7, 0x38, 0x1e, 0xca, 0x7c, 0xee, 53 | 0xdd, 0x41, 0x89, 0xf9, 0xd8, 0x67, 0x16, 0x2f, 0x86, 0x85, 0xbf, 0xa4, 54 | 0x7a, 0x29, 0xb0, 0x17, 0x66, 0xab, 0xa0, 0x89, 0x49, 0x21, 0x39, 0x17, 55 | 0x38, 0xea, 0xd8, 0xc0, 0x29, 0xf0, 0xc3, 0x55, 0xbb, 0x47, 0x2b, 0xc6, 56 | 0xb9, 0xd4, 0x79, 0x4c, 0x56, 0xdd, 0xba, 0x64, 0x26, 0x97, 0x7a, 0x5c, 57 | 0x36, 0x92, 0xbb, 0x22, 0x26, 0x77, 0xab, 0xd5, 0xbf, 0x1d, 0x50, 0xb4, 58 | 0xa3, 0x32, 0x47, 0x52, 0x96, 0xbd, 0xe4, 0x06, 0x6a, 0xcd, 0x63, 0x92, 59 | 0x1b, 0x76, 0x68, 0x79, 0xfd, 0xdd, 0xd1, 0xf3, 0xce, 0xed, 0x4d, 0x21, 60 | 0x9d, 0xb8, 0x48, 0x34, 0xe9, 0x08, 0xbf, 0x88, 0xf2, 0xfd, 0x6f, 0xbf, 61 | 0x0b, 0xac, 0xf4, 0x67, 0x19, 0x13, 0x96, 0x5a, 0x50, 0xf5, 0x10, 0xcc, 62 | 0x41, 0x15, 0xcf, 0xf7, 0x29, 0xb1, 0x75, 0x27, 0x80, 0xe9, 0x54, 0xcf, 63 | 0x90, 0x14, 0x9c, 0x83, 0x32, 0x26, 0x9f, 0x06, 0x60, 0xaa, 0x0f, 0xab, 64 | 0x1d, 0x74, 0x59, 0xf8, 0xb9, 0xef, 0xd3, 0x98, 0x04, 0x0e, 0xb2, 0xe6, 65 | 0xd1, 0xce, 0xb3, 0xb9, 0xa2, 0x73, 0x20, 0xbd, 0x3b, 0x13, 0x5a, 0xbd, 66 | 0x5e, 0x29, 0xf7, 0xc5, 0x43, 0xc5, 0xb6, 0xf0, 0xdd, 0x80, 0x5d, 0x11, 67 | 0xf5, 0x5f, 0x27, 0x7f, 0xcc, 0xf8, 0x87, 0x5d, 0x44, 0x24, 0x5f, 0x53, 68 | 0x7f, 0x71, 0xf7, 0x4d, 0x85, 0x7b, 0x7a, 0x63, 0xd4, 0x7d, 0xd0, 0x7e, 69 | 0x07, 0x00, 0x00, 0xff, 0xff, 0xf8, 0x9e, 0xa3, 0x2f, 0xd8, 0x06, 0x00, 70 | 0x00, 71 | })) 72 | 73 | if err != nil { 74 | panic("Decompression failed: " + err.Error()) 75 | } 76 | 77 | var b bytes.Buffer 78 | io.Copy(&b, gz) 79 | gz.Close() 80 | 81 | return b.Bytes() 82 | } 83 | -------------------------------------------------------------------------------- /websocket.article: -------------------------------------------------------------------------------- 1 | Implementing Chat with WebSockets and Go 2 | 3 | Gary Burd 4 | @gburd 5 | 6 | * Introduction 7 | 8 | This example application shows how to use 9 | [[http://www.websockets.org/][WebSockets]], the 10 | [[http://golang.org/][Go programming language]] and 11 | [[http://jquery.com/][jQuery]] to create a simple 12 | web chat application. 13 | 14 | * Running the example 15 | 16 | The example requires a working Go development environment. The 17 | [[http://golang.org/doc/install.html][Getting Started]] page describes 18 | how to install the development environment. 19 | 20 | Once you have Go up and running, you can download, build and run the example 21 | using the following commands. 22 | 23 | go get code.google.com/p/go.net/websocket 24 | git clone git://gist.github.com/1316852.git websocket-example 25 | cd websocket-example 26 | go run *.go 27 | 28 | Open http://127.0.0.1:8080/ in a websocket capable browser to try the application. 29 | 30 | * Server 31 | 32 | The server application is implemented using the 33 | [[http://golang.org/pkg/net/http/][http]] package included with the Go 34 | distribution and the Go Project's 35 | [[http://go.pkgdoc.org/code.google.com/p/go.net/websocket][websocket]] package. 36 | 37 | The application defines two types, `connection` and 38 | `hub`. The server creates an instance of the `connection` 39 | type for each webscocket connection. Connections act as an intermediary 40 | between the websocket and a single instance of the `hub` type. The 41 | hub maintains a set of registered connections and broadcasts messages to the 42 | connections. 43 | 44 | The application runs one goroutine for the hub and two goroutines for each 45 | connection. The goroutines communicate with each other using channels. The hub 46 | has channels for registering connections, unregistering connections and 47 | broadcasting messages. A connection has a buffered channel of outbound messages. 48 | One of the connection's goroutines reads messages from this channel and writes 49 | the messages to the webscoket. The other connection goroutine reads messages 50 | from the websocket and sends them to the hub. 51 | 52 | Here's the code for the `hub` type. A description of the code follows. 53 | 54 | .code hub.go 55 | 56 | The application's `main` function starts the hub `run` 57 | method as a goroutine. Connections send requests to the hub using the 58 | `register`, `unregister` and `broadcast` 59 | channels. 60 | 61 | The hub registers connections by adding the connection pointer as a key in 62 | the `connections` map. The map value is always `true`. 63 | 64 | The unregister code is a little more complicated. In addition to deleting 65 | the connection pointer from the `connections` map, the hub closes 66 | the connection's `send` channel to signal the connection that no 67 | more messages will be sent to the connection. 68 | 69 | The hub handles messages by looping over the registered connections and 70 | sending the message to the connection's `send` channel. If the 71 | connection's `send` buffer is full, then the hub assumes that 72 | the client is dead or stuck. In this case, the hub unregisters the connection 73 | and closes the websocket. 74 | 75 | Here's the code related to the `connection` type. 76 | 77 | .code conn.go 78 | 79 | The `wsHandler` function is registered by the application's 80 | `main` function as a 81 | [[http://golang.org/pkg/websocket/#Handler][websocket handler]]. The 82 | function creates a connection object, registers the connection with the hub and 83 | schedules the connection to be unregistered using a 84 | [[http://weekly.golang.org/doc/effective_go.html#defer][defer]] statement. 85 | 86 | Next, the `wsHandler` function starts the connection's 87 | `writer` method as a goroutine. The `writer` method 88 | transfers messages from the connection's `send` channel to the 89 | websocket. The `writer` method exits when the channel is closed by 90 | the hub or there's an error writing to the websocket. 91 | 92 | Finally, the `wsHandler` function calls the connection's 93 | `reader` method. The `reader` method transfers inbound 94 | messages from the websocket to the hub. 95 | 96 | The remainder of the server code follows: 97 | 98 | .code main.go 99 | 100 | The application's `main` function starts the hub goroutine. Next, 101 | the main function registers handlers for the home page and websocket 102 | connections. Finally, the main function starts the HTTP server. 103 | 104 | * Client 105 | 106 | The client is implemented in a single HTML file. 107 | 108 | .code home.html 109 | 110 | The client uses [[http://jquery.com/][jQuery]] to manipulate 111 | objects in the browser. 112 | 113 | On document load, the script checks for websocket functionality in the 114 | browser. If websocket functionality is available, then the script opens a 115 | connection to the server and registers a callback to handle messages from the 116 | server. The callback appends the message to the chat log using the 117 | `appendLog` function. 118 | 119 | To allow the user to manually scroll through the chat log without interruption 120 | from new messages, the `appendLog` function checks the scroll position before 121 | adding new content. If the chat log is scrolled to the bottom, then the 122 | function scrolls new content into view after adding the content. Otherwise, 123 | the scroll position is not changed. 124 | 125 | The form handler writes the user input to the websocket and clears the input 126 | field. 127 | --------------------------------------------------------------------------------