package main import ( "log" "math/rand" "os" "os/signal" "strings" "time" "github.com/gdamore/tcell/v2" ) var screen tcell.Screen // just basic alphanumeric characters var characters = []rune{ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', } // streamDisplays by column number var streamDisplaysByColumn = make(map[int]*StreamDisplay) // current sizes var curSizes sizes // channel used to notify StreamDisplayManager var sizesUpdateCh = make(chan sizes) // struct sizes contains terminal sizes (in amount of characters) type sizes struct { width int height int curStreamsPerStreamDisplay int // current amount of streams per display allowed } // set the sizes and notify StreamDisplayManager func (s *sizes) setSizes(width int, height int) { s.width = width s.height = height s.curStreamsPerStreamDisplay = 1 + height/10 } func gomatrix() { screen.SetStyle(tcell.StyleDefault. Background(tcell.ColorBlack). Foreground(tcell.ColorBlack)) screen.Clear() // StreamDisplay manager go func() { var lastWidth int for newSizes := range sizesUpdateCh { diffWidth := newSizes.width - lastWidth if diffWidth == 0 { // same column size, wait for new information continue } if diffWidth > 0 { for newColumn := lastWidth; newColumn < newSizes.width; newColumn++ { // create stream display sd := &StreamDisplay{ column: newColumn, stopCh: make(chan bool, 1), streams: make(map[*Stream]bool), newStream: make(chan bool, 1), // will only be filled at start and when a spawning stream has it's tail released } streamDisplaysByColumn[newColumn] = sd // start StreamDisplay in goroutine go sd.run() // create first new stream sd.newStream <- true } lastWidth = newSizes.width } if diffWidth < 0 { for closeColumn := lastWidth - 1; closeColumn > newSizes.width; closeColumn-- { // get sd sd := streamDisplaysByColumn[closeColumn] // delete from map delete(streamDisplaysByColumn, closeColumn) // inform sd that it's being closed sd.stopCh <- true } lastWidth = newSizes.width } } }() // set initial sizes curSizes.setSizes(screen.Size()) sizesUpdateCh <- curSizes // flusher flushes the termbox every x milliseconds curFPS := 25 fpsSleepTime := time.Duration(1000000/curFPS) * time.Microsecond go func() { for { time.Sleep(fpsSleepTime) screen.Show() } }() // make chan for tembox events and run poller to send events on chan eventChan := make(chan tcell.Event) go func() { for { event := screen.PollEvent() eventChan <- event } }() // register signals to channel sigChan := make(chan os.Signal) signal.Notify(sigChan, os.Interrupt, os.Kill) maxRun := time.After(8 * time.Second) stopRun := time.After(12 * time.Second) // handle tcell events and unix signals EVENTS: for { // select for either event or signal select { case event := <-eventChan: // switch on event type switch ev := event.(type) { case *tcell.EventKey: switch ev.Key() { case tcell.KeyCtrlZ: break EVENTS case tcell.KeyCtrlL: screen.Sync() case tcell.KeyRune: switch ev.Rune() { case 'q': break EVENTS case 'c': screen.Clear() } } case *tcell.EventError: // quit log.Fatalf("Quitting because of tcell error: %v", ev.Error()) } case <-maxRun: for _, sd := range streamDisplaysByColumn { sd.stopCh <- true } case <-stopRun: break EVENTS } } } func displayLine(line string, textStyle tcell.Style, x, y, speed int) int { for _, r := range line { screen.SetCell(x, y, textStyle, r) x += 1 screen.Show() if r == ' ' { time.Sleep(time.Duration(speed/2+rand.Intn(speed/2)) * time.Millisecond) } else if r == '.' || r == ',' { time.Sleep(time.Duration(speed*5+rand.Intn(speed*25/10)) * time.Millisecond) } else { time.Sleep(time.Duration(speed+rand.Intn(speed*2)) * time.Millisecond) } } return x } func blinkingCursor(textStyle tcell.Style, x, y, nb int) { for i := 0; i < nb; i += 1 { if i%2 == 0 { screen.SetCell(x, y, textStyle, '_') } else { screen.SetCell(x, y, textStyle, ' ') } screen.Show() time.Sleep(750 * time.Millisecond) } } func scenario(login string) { blackStyle := tcell.StyleDefault. Foreground(tcell.ColorBlack). Background(tcell.ColorBlack) textStyle := blackStyle.Foreground(tcell.ColorGreen) displayLine("Wake up, "+login+"...", textStyle, 1, 2, 100) time.Sleep(1000 * time.Millisecond) screen.Clear() displayLine("The Matrix has you...", textStyle, 1, 2, 100) time.Sleep(1500 * time.Millisecond) screen.Clear() blinkingCursor(textStyle, 1, 2, 2) displayLine("Pour t'empêcher de sortir de la Matrice, des agents", textStyle, 1, 2, 25) displayLine("ont piégé ton poste de travail et le réseau environnant.", textStyle, 1, 3, 25) blinkingCursor(textStyle, 1, 5, 3) displayLine("J'ai peur que tu ne doives te débrouiller tout.e seul.e", textStyle, 1, 5, 25) displayLine("pour retrouver la route vers Internet, d'où l'on pourra", textStyle, 1, 6, 25) pos := displayLine("t'extraire sans risque.", textStyle, 1, 7, 25) blinkingCursor(textStyle, pos, 7, 6) displayLine("Ils te tiennent !", textStyle, 1, 10, 15) displayLine("Je redémarre ta machine pour effacer notre échange.", textStyle, 1, 11, 25) pos = displayLine("Bonne chance.", textStyle, 1, 13, 50) blinkingCursor(textStyle, pos, 13, 6) displayLine("Au fait, le pass root est: hax&i6aes2so5niec8XeeLei_", textStyle, 1, 15, 2) time.Sleep(400 * time.Millisecond) } func runCinematic(login string) { var err error // initialize tcell if screen, err = tcell.NewScreen(); err != nil { os.Exit(1) } err = screen.Init() if err != nil { os.Exit(1) } gomatrix() screen.Clear() // Keep only the first part of the login (firstname) login = strings.SplitN(login, ".", 2)[0] scenario(login) // close down screen.Fini() }