├── .gitignore ├── Dockerfile ├── README.md ├── LICENSE ├── stream.go └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | *sublime* 2 | /gomatrix 3 | /gomatrix.prof -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1-onbuild 2 | MAINTAINER Geert-Johan Riemer 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## THIS IS A FORK of github.com/GeertJohan/gomatrix 2 | 3 | > _This fork uses github.com/gdamore/tcell 4 | > It offers better (nicer/richer) colors and can work on terminals that don't 5 | > support Unicode. For example, it works on GB18030, and EUC-JP terminals. 6 | > If your environment is not UTF-8 compliant, the glyphs will be replaced with 7 | > "?"'s by default. Press "a" to see ASCII in that case._ 8 | 9 | ## gomatrix 10 | gomatrix connects to The Matrix and displays it's data streams in your terminal. 11 | 12 | ### Installation 13 | Install from source with `go get github.com/GeertJohan/gomatrix` 14 | 15 | ### Usage 16 | Just run `gomatrix`. Use `gomatrix --help` to view all options. 17 | 18 | ### Docker 19 | This application is available in docker. 20 | 21 | Build manually with `docker build -t gomatrix .` and `docker run -ti gomatrix`. 22 | 23 | Or pull the automated build version: `docker run -ti geertjohan/gomatrix` 24 | 25 | ### License: 26 | This project is licenced under a a Simplified BSD license. Please read the [LICENSE file](LICENSE). 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015 Garrett D'Amore 2 | Copyright (c) 2013, Geert-Johan Riemer 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 18 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 19 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 20 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 21 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /stream.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "math/rand" 6 | "sync" 7 | "time" 8 | 9 | "github.com/gdamore/tcell" 10 | ) 11 | 12 | // Stream updates a StreamDisplay with new data updates 13 | type Stream struct { 14 | display *StreamDisplay 15 | speed int 16 | length int 17 | headPos int 18 | tailPos int 19 | stopCh chan bool 20 | headDone bool 21 | } 22 | 23 | 24 | func (s *Stream) run() { 25 | blackStyle := tcell.StyleDefault. 26 | Foreground(tcell.ColorBlack). 27 | Background(tcell.ColorBlack) 28 | 29 | headStyle := blackStyle.Foreground(tcell.ColorSilver) 30 | tailStyle := blackStyle.Foreground(tcell.ColorGreen) 31 | midStyle := blackStyle.Foreground(tcell.ColorGreen) 32 | 33 | if screen.Colors() >= 16 { 34 | midStyle = headStyle.Foreground(tcell.ColorLime) 35 | // 33% of streams (arbitrary) get a bright white head 36 | if rand.Intn(100) < 33 { 37 | headStyle = headStyle.Foreground(tcell.ColorWhite) 38 | } else { 39 | headStyle = midStyle 40 | } 41 | } 42 | 43 | var lastRune rune 44 | var llastRune rune 45 | for { 46 | select { 47 | case <-s.stopCh: 48 | log.Printf("Stream on SD %d was stopped.\n", s.display.column) 49 | goto done 50 | case <-time.After(time.Duration(s.speed) * time.Millisecond): 51 | // add a new rune if there is space in the stream 52 | if !s.headDone && s.headPos <= curSizes.height { 53 | newRune := characters[rand.Intn(len(characters))] 54 | screen.SetCell(s.display.column, s.headPos-3, tailStyle, llastRune) 55 | screen.SetCell(s.display.column, s.headPos-1, midStyle, lastRune) 56 | screen.SetCell(s.display.column, s.headPos, headStyle, newRune) 57 | llastRune = lastRune 58 | lastRune = newRune 59 | s.headPos++ 60 | } else { 61 | s.headDone = true 62 | } 63 | 64 | // clear rune at the tail of the stream 65 | if s.tailPos > 0 || s.headPos >= s.length { 66 | if s.tailPos == 0 { 67 | // tail is being incremented for the first time. there is space for a new stream 68 | s.display.newStream <- true 69 | } 70 | if s.tailPos < curSizes.height { 71 | screen.SetCell(s.display.column, s.tailPos, blackStyle, ' ') //'\uFF60' 72 | s.tailPos++ 73 | } else { 74 | goto done 75 | } 76 | } 77 | } 78 | } 79 | done: 80 | delete(s.display.streams, s) 81 | } 82 | 83 | // StreamDisplay represents a horizontal line in the terminal on which `Stream`s are displayed. 84 | // StreamDisplay also creates the Streams themselves 85 | type StreamDisplay struct { 86 | column int 87 | stopCh chan bool 88 | streams map[*Stream]bool 89 | streamsLock sync.Mutex 90 | newStream chan bool 91 | } 92 | 93 | func (sd *StreamDisplay) run() { 94 | for { 95 | select { 96 | case <-sd.stopCh: 97 | // lock this SD forever 98 | sd.streamsLock.Lock() 99 | 100 | // stop streams for this SD 101 | for s, _ := range sd.streams { 102 | s.stopCh <- true 103 | } 104 | 105 | // log that SD has closed 106 | log.Printf("StreamDisplay on column %d stopped.\n", sd.column) 107 | 108 | // close this goroutine 109 | return 110 | 111 | case <-sd.newStream: 112 | // have some wait before the first stream starts.. 113 | // <-time.After(time.Duration(rand.Intn(9000)) * time.Millisecond) //++ TODO: .After or .Sleep?? 114 | time.Sleep(time.Duration(rand.Intn(9000)) * time.Millisecond) 115 | 116 | // lock map 117 | sd.streamsLock.Lock() 118 | 119 | // crekate new stream instance 120 | s := &Stream{ 121 | display: sd, 122 | stopCh: make(chan bool), 123 | speed: 30 + rand.Intn(110), 124 | length: 6 + rand.Intn(6), // length of a stream is between 6 and 12 runes 125 | } 126 | 127 | // store in streams map 128 | sd.streams[s] = true 129 | 130 | // run the stream in a goroutine 131 | go s.run() 132 | 133 | // unlock map 134 | sd.streamsLock.Unlock() 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "math/rand" 7 | "os" 8 | "os/signal" 9 | "runtime/pprof" 10 | "time" 11 | 12 | "github.com/davecgh/go-spew/spew" 13 | "github.com/jessevdk/go-flags" 14 | "github.com/gdamore/tcell" 15 | ) 16 | 17 | var screen tcell.Screen 18 | 19 | // command line flags variable 20 | var opts struct { 21 | // display ascii instead of kana's 22 | Ascii bool `short:"a" long:"ascii" description:"Use ascii/alphanumeric characters instead of japanese kana's."` 23 | 24 | // enable logging 25 | Logging bool `short:"l" long:"log" description:"Enable logging debug messages to ~/.gomatrix-log."` 26 | 27 | // enable profiling 28 | Profile string `short:"p" long:"profile" description:"Write profile to given file path"` 29 | 30 | // FPS 31 | FPS int `long:"fps" description:"required FPS, must be somewhere between 1 and 60" default:"25"` 32 | } 33 | 34 | // array with half width kanas as Go runes 35 | // source: http://en.wikipedia.org/wiki/Half-width_kana 36 | var halfWidthKana = []rune{ 37 | '。', '「', '」', '、', '・', 'ヲ', 'ァ', 'ィ', 'ゥ', 'ェ', 'ォ', 'ャ', 'ュ', 'ョ', 'ッ', 38 | 'ー', 'ア', 'イ', 'ウ', 'エ', 'オ', 'カ', 'キ', 'ク', 'ケ', 'コ', 'サ', 'シ', 'ス', 'セ', 'ソ', 39 | 'タ', 'チ', 'ツ', 'テ', 'ト', 'ナ', 'ニ', 'ヌ', 'ネ', 'ノ', 'ハ', 'ヒ', 'フ', 'ヘ', 'ホ', 'マ', 40 | 'ミ', 'ム', 'メ', 'モ', 'ヤ', 'ユ', 'ヨ', 'ラ', 'リ', 'ル', 'レ', 'ロ', 'ワ', 'ン', '゙', '゚', 41 | } 42 | 43 | // just basic alphanumeric characters 44 | var alphaNumerics = []rune{ 45 | 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 46 | 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 47 | '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 48 | } 49 | 50 | // characters to be used, is being set to alphaNumerics or halfWidthKana depending on flags 51 | var characters []rune 52 | 53 | // streamDisplays by column number 54 | var streamDisplaysByColumn = make(map[int]*StreamDisplay) 55 | 56 | // struct sizes contains terminal sizes (in amount of characters) 57 | type sizes struct { 58 | width int 59 | height int 60 | } 61 | 62 | var curSizes sizes // current sizes 63 | var curStreamsPerStreamDisplay = 0 // curent amount of streams per display allowed 64 | var sizesUpdateCh = make(chan sizes) //channel used to notify StreamDisplayManager 65 | 66 | // set the sizes and notify StreamDisplayManager 67 | func setSizes(width int, height int) { 68 | s := sizes{ 69 | width: width, 70 | height: height, 71 | } 72 | curSizes = s 73 | curStreamsPerStreamDisplay = 1 + height/10 74 | sizesUpdateCh <- s 75 | } 76 | 77 | func main() { 78 | // parse flags 79 | args, err := flags.Parse(&opts) 80 | if err != nil { 81 | flagError := err.(*flags.Error) 82 | if flagError.Type == flags.ErrHelp { 83 | return 84 | } 85 | if flagError.Type == flags.ErrUnknownFlag { 86 | fmt.Println("Use --help to view all available options.") 87 | return 88 | } 89 | fmt.Printf("Error parsing flags: %s\n", err) 90 | return 91 | } 92 | if len(args) > 0 { 93 | // we don't accept too much arguments.. 94 | fmt.Printf("Unknown argument '%s'.\n", args[0]) 95 | return 96 | } 97 | if opts.FPS < 1 || opts.FPS > 60 { 98 | fmt.Println("Error: option --fps not within range 1-60") 99 | os.Exit(1) 100 | } 101 | 102 | // Start profiling (if required) 103 | if len(opts.Profile) > 0 { 104 | f, err := os.Create(opts.Profile) 105 | if err != nil { 106 | fmt.Printf("Error opening profiling file: %s\n", err) 107 | os.Exit(1) 108 | } 109 | pprof.StartCPUProfile(f) 110 | } 111 | 112 | // Juse a println for fun.. 113 | fmt.Println("Opening connection to The Matrix.. Please stand by..") 114 | 115 | // setup logging with logfile /dev/null or ~/.gomatrix-log 116 | filename := os.DevNull 117 | if opts.Logging { 118 | filename = os.Getenv("HOME") + "/.gomatrix-log" 119 | } 120 | logfile, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) 121 | if err != nil { 122 | fmt.Printf("Could not open logfile. %s\n", err) 123 | os.Exit(1) 124 | } 125 | defer logfile.Close() 126 | log.SetOutput(logfile) 127 | log.Println("-------------") 128 | log.Println("Starting gomatrix. This logfile is for development/debug purposes.") 129 | 130 | characters = halfWidthKana 131 | if opts.Ascii { 132 | characters = alphaNumerics 133 | } 134 | 135 | // seed the rand package with time 136 | rand.Seed(time.Now().UnixNano()) 137 | 138 | // initialize tcell 139 | if screen, err = tcell.NewScreen(); err != nil { 140 | fmt.Println("Could not start tcell for gomatrix. View ~/.gomatrix-log for error messages.") 141 | log.Printf("Cannot alloc screen, tcell.NewScreen() gave an error:\n%s", err) 142 | os.Exit(1) 143 | } 144 | 145 | err = screen.Init() 146 | if err != nil { 147 | fmt.Println("Could not start tcell for gomatrix. View ~/.gomatrix-log for error messages.") 148 | log.Printf("Cannot start gomatrix, screen.Init() gave an error:\n%s", err) 149 | os.Exit(1) 150 | } 151 | screen.HideCursor() 152 | screen.SetStyle(tcell.StyleDefault. 153 | Background(tcell.ColorBlack). 154 | Foreground(tcell.ColorBlack)) 155 | screen.Clear() 156 | 157 | // StreamDisplay manager 158 | go func() { 159 | var lastWidth int 160 | 161 | for newSizes := range sizesUpdateCh { 162 | log.Printf("New width: %d\n", newSizes.width) 163 | diffWidth := newSizes.width - lastWidth 164 | 165 | if diffWidth == 0 { 166 | // same column size, wait for new information 167 | log.Println("Got resize over channel, but diffWidth = 0") 168 | continue 169 | } 170 | 171 | if diffWidth > 0 { 172 | log.Printf("Starting %d new SD's\n", diffWidth) 173 | for newColumn := lastWidth; newColumn < newSizes.width; newColumn++ { 174 | // create stream display 175 | sd := &StreamDisplay{ 176 | column: newColumn, 177 | stopCh: make(chan bool, 1), 178 | streams: make(map[*Stream]bool), 179 | newStream: make(chan bool, 1), // will only be filled at start and when a spawning stream has it's tail released 180 | } 181 | streamDisplaysByColumn[newColumn] = sd 182 | 183 | // start StreamDisplay in goroutine 184 | go sd.run() 185 | 186 | // create first new stream 187 | sd.newStream <- true 188 | } 189 | lastWidth = newSizes.width 190 | } 191 | 192 | if diffWidth < 0 { 193 | log.Printf("Closing %d SD's\n", diffWidth) 194 | for closeColumn := lastWidth - 1; closeColumn > newSizes.width; closeColumn-- { 195 | // get sd 196 | sd := streamDisplaysByColumn[closeColumn] 197 | 198 | // delete from map 199 | delete(streamDisplaysByColumn, closeColumn) 200 | 201 | // inform sd that it's being closed 202 | sd.stopCh <- true 203 | } 204 | lastWidth = newSizes.width 205 | } 206 | } 207 | }() 208 | 209 | // set initial sizes 210 | setSizes(screen.Size()) 211 | 212 | // flusher flushes the tcell every x miliseconds 213 | fpsSleepTime := time.Duration(1000000/opts.FPS) * time.Microsecond 214 | go func() { 215 | for { 216 | // <-time.After(40 * time.Millisecond) //++ TODO: find out wether .After() or .Sleep() is better performance-wise 217 | time.Sleep(fpsSleepTime) 218 | screen.Show() 219 | } 220 | }() 221 | 222 | // make chan for tembox events and run poller to send events on chan 223 | eventChan := make(chan tcell.Event) 224 | go func() { 225 | for { 226 | event := screen.PollEvent() 227 | eventChan <- event 228 | } 229 | }() 230 | 231 | // register signals to channel 232 | sigChan := make(chan os.Signal) 233 | signal.Notify(sigChan, os.Interrupt) 234 | signal.Notify(sigChan, os.Kill) 235 | 236 | // handle tcell events and unix signals 237 | 238 | done := false 239 | for !done { 240 | // select for either event or signal 241 | select { 242 | case event := <-eventChan: 243 | log.Printf("Have event: \n%s", spew.Sdump(event)) 244 | // switch on event type 245 | switch ev := event.(type) { 246 | case *tcell.EventKey: 247 | switch ev.Key() { 248 | case tcell.KeyCtrlZ, tcell.KeyCtrlC: 249 | done = true 250 | continue 251 | 252 | //++ TODO: add more fun keys (slowmo? freeze? rampage?) 253 | case tcell.KeyCtrlL: 254 | screen.Sync() 255 | 256 | case tcell.KeyRune: 257 | switch ev.Rune() { 258 | case 'q': 259 | done = true 260 | continue 261 | 262 | case 'c': 263 | screen.Clear() 264 | 265 | case 'a': 266 | characters = alphaNumerics 267 | 268 | case 'k': 269 | characters = halfWidthKana 270 | } 271 | } 272 | 273 | case *tcell.EventResize: // set sizes 274 | w, h := ev.Size() 275 | setSizes(w, h) 276 | 277 | case *tcell.EventError: // quit 278 | log.Fatalf("Quitting because of tcell error: %v", ev.Error()) 279 | done = true 280 | continue 281 | } 282 | 283 | case signal := <-sigChan: 284 | log.Printf("Have signal: \n%s", spew.Sdump(signal)) 285 | done = true 286 | continue 287 | } 288 | } 289 | 290 | // close down 291 | screen.Fini() 292 | 293 | log.Println("stopping gomatrix") 294 | fmt.Println("Thank you for connecting with Morpheus' Matrix API v4.2. Have a nice day!") 295 | 296 | // stop profiling (if required) 297 | if len(opts.Profile) > 0 { 298 | pprof.StopCPUProfile() 299 | } 300 | } 301 | --------------------------------------------------------------------------------