login-app: use tcell/tview to make the form and add cinematic

This commit is contained in:
nemunaire 2021-02-17 15:03:06 +01:00
parent 1d8146d8ad
commit 4ce6f09a8d
8 changed files with 516 additions and 328 deletions

View File

@ -1,6 +1,6 @@
tuto1: token-validator/token-validator server.iso
pkg/login-app: pkg/login-app/cmd/login.go pkg/login-app/cmd/dialog-checklogin.go pkg/login-app/cmd/cmd pkg/login-app/cmd/dialog-login.go pkg/login-app/cmd/login-app pkg/login-app/cmd/dialog-errmsg.go pkg/login-app/cmd/main.go pkg/login-app/cmd/dialog-reboot.go pkg/login-app/cmd/debug.log pkg/login-app/build.yml pkg/login-app/Dockerfile
pkg/login-app: pkg/login-app/cmd/login.go pkg/login-app/cmd/dialog-checklogin.go pkg/login-app/cmd/cmd pkg/login-app/cmd/dialog-login.go pkg/login-app/cmd/login-app pkg/login-app/cmd/dialog-errmsg.go pkg/login-app/cmd/main.go pkg/login-app/cmd/stream.go pkg/login-app/cmd/cinematic.go pkg/login-app/build.yml pkg/login-app/Dockerfile
linuxkit pkg build -org nemunaire pkg/login-app/
#linuxkit pkg push -org nemunaire --sign=false pkg/login-app/
touch pkg/login-app

View File

@ -0,0 +1,258 @@
package main
import (
"log"
"math/rand"
"os"
"os/signal"
"strings"
"time"
"github.com/gdamore/tcell"
)
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()
}

View File

@ -4,76 +4,41 @@ import (
"math"
"time"
ui "github.com/VladimirMarkelov/clui"
"github.com/gdamore/tcell"
"github.com/rivo/tview"
)
type CheckDialog struct {
View *ui.Window
func CreateCheckDialog(app *tview.Application) *tview.Box {
progress := 0
box := tview.NewBox().
SetBorder(true).
SetTitle(" SRS Adlin - Login ").
SetDrawFunc(func(screen tcell.Screen, x int, y int, width int, height int) (int, int, int, int) {
tview.Print(screen, "Please wait", x, y+2, width-2, tview.AlignCenter, tcell.ColorYellow)
tview.Print(screen, "Connecting to login server...", x, y+3, width-2, tview.AlignCenter, tcell.ColorYellow)
result int
beforeClose func(ui.Event)
onClose func()
}
func CreateCheckDialog(title, username, password string) *CheckDialog {
dlg := new(CheckDialog)
sWidth, sHeight := ui.ScreenSize()
wWidth, wHeight := 40, 5
dlg.View = ui.AddWindow(sWidth/2-wWidth/2, sHeight/2-wHeight/2, wWidth, wHeight, title)
ui.WindowManager().BeginUpdate()
defer ui.WindowManager().EndUpdate()
dlg.View.SetModal(true)
dlg.View.SetSizable(false)
dlg.View.SetTitleButtons(ui.ButtonDefault)
dlg.View.SetPack(ui.Vertical)
textfrm := ui.CreateFrame(dlg.View, 1, 1, ui.BorderNone, ui.Fixed)
textfrm.SetPaddings(1, 1)
textfrm.SetPack(ui.Vertical)
textfrm.SetGaps(2, 1)
ui.CreateLabel(textfrm, ui.AutoSize, ui.AutoSize, " Please wait", ui.Fixed)
progress := ui.CreateProgressBar(textfrm, ui.AutoSize, ui.AutoSize, 1)
progress.SetLimits(0, 100)
ui.CreateLabel(textfrm, ui.AutoSize, ui.AutoSize, "Connecting to login server...", ui.Fixed)
dlg.View.OnKeyDown(func(ev ui.Event, data interface{}) bool {
if ev.Key == 0 && ev.Msg == "login-complete" {
if dlg.beforeClose != nil {
dlg.beforeClose(ev)
// Draw the progress bar
for cx := x + 2; cx < x+width-2; cx++ {
if (cx-x)*100/(width-2) > progress {
screen.SetContent(cx, y+5, tview.BoxDrawingsLightHorizontal, nil, tcell.StyleDefault.Background(tcell.ColorBlack))
} else {
screen.SetContent(cx, y+5, ' ', nil, tcell.StyleDefault.Background(tcell.ColorBlue))
}
}
ui.WindowManager().DestroyWindow(dlg.View)
ui.WindowManager().BeginUpdate()
return x, y, width, height
})
closeFunc := dlg.onClose
ui.WindowManager().EndUpdate()
if closeFunc != nil {
closeFunc()
}
return true
}
return false
}, nil)
go func() {
if ok, err := checkLogin(username, password); ok {
ui.PutEvent(ui.Event{Type: ui.EventKey, Msg: "login-complete"})
} else {
ui.PutEvent(ui.Event{Type: ui.EventKey, Msg: "login-complete", Err: err})
}
}()
app.SetRoot(modal(box, 40, 8), true)
app.SetFocus(box)
go func() {
for i := 0; i < 422; i += 1 {
progress.SetValue(int(math.Floor(math.Log(float64(i)*8)*16 - 30)))
progress = int(math.Floor(math.Log(float64(i)*8)*16 - 30))
time.Sleep(64 * time.Millisecond)
ui.PutEvent(ui.Event{Type: ui.EventRedraw})
app.Draw()
}
}()
return dlg
return box
}

View File

@ -1,85 +1,36 @@
package main
import (
ui "github.com/VladimirMarkelov/clui"
term "github.com/nsf/termbox-go"
"fmt"
"github.com/rivo/tview"
)
type ErrMsgDialog struct {
View *ui.Window
func CreateErrMsgDialog(app *tview.Application, err error) {
textView := tview.NewTextView().
SetDynamicColors(true).
SetRegions(true).
SetChangedFunc(func() {
app.Draw()
})
result int
beforeClose func()
onClose func()
}
func CreateErrMsgDialog(title string, err error) *ErrMsgDialog {
dlg := new(ErrMsgDialog)
sWidth, sHeight := ui.ScreenSize()
wWidth, wHeight := 60, 10
dlg.View = ui.AddWindow(sWidth/2-wWidth/2, sHeight/2-wHeight/2, wWidth, wHeight, title)
ui.WindowManager().BeginUpdate()
defer ui.WindowManager().EndUpdate()
dlg.View.SetModal(true)
dlg.View.SetSizable(false)
dlg.View.SetTitleButtons(ui.ButtonDefault)
dlg.View.SetPack(ui.Vertical)
textfrm := ui.CreateFrame(dlg.View, 1, 1, ui.BorderNone, ui.Fixed)
textfrm.SetPaddings(1, 1)
textfrm.SetPack(ui.Vertical)
textfrm.SetGaps(2, 1)
lbl := ui.CreateLabel(textfrm, ui.AutoSize, ui.AutoSize, "An error occurs:", ui.Fixed)
lbl.SetTextColor(ui.ColorRedBold)
tv := ui.CreateTextView(textfrm, ui.AutoSize, 4, ui.Fixed)
tv.SetWordWrap(true)
tv.SetText([]string{err.Error()})
tv.SetTextColor(ui.ColorWhite)
tv.SetBackColor(ui.ColorBlack)
ui.CreateLabel(textfrm, ui.AutoSize, ui.AutoSize, "Please try again.", ui.Fixed)
btnOk := ui.CreateButton(textfrm, 20, 4, "Ok", 1)
btnOk.OnClick(func(ev ui.Event) {
if dlg.beforeClose != nil {
dlg.beforeClose()
}
ui.WindowManager().DestroyWindow(dlg.View)
ui.WindowManager().BeginUpdate()
closeFunc := dlg.onClose
ui.WindowManager().EndUpdate()
if closeFunc != nil {
closeFunc()
}
})
dlg.View.OnKeyDown(func(ev ui.Event, data interface{}) bool {
if ev.Key == term.KeyEnter {
if dlg.beforeClose != nil {
dlg.beforeClose()
}
ui.WindowManager().DestroyWindow(dlg.View)
ui.WindowManager().BeginUpdate()
closeFunc := dlg.onClose
ui.WindowManager().EndUpdate()
if closeFunc != nil {
closeFunc()
}
return true
}
return false
}, nil)
ui.ActivateControl(dlg.View, btnOk)
return dlg
form := tview.NewForm().
AddButton("Authenticate me", func() {
askLogin(app)
})
flex := tview.NewFlex().
AddItem(nil, 0, 1, false).
AddItem(tview.NewFlex().SetDirection(tview.FlexRow).
AddItem(textView, 12, 1, false).
AddItem(form, 1, 1, false), 37, 1, false).
AddItem(nil, 0, 1, false)
flex.SetBorder(true).
SetTitle(" SRS Adlin - Login ")
fmt.Fprintf(textView, "\nAn error occurs:\n\n[red]%s\n\n[yellow]Press Enter to retry", err.Error())
app.SetRoot(modal(flex, 42, 15), true)
app.SetFocus(form)
}

View File

@ -1,124 +1,24 @@
package main
import (
ui "github.com/VladimirMarkelov/clui"
term "github.com/nsf/termbox-go"
"github.com/rivo/tview"
)
const (
LoginOk = iota
LoginCanceled
LoginInvalid
)
func CreateLoginDialog(app *tview.Application, next func(username, password string)) {
var form *tview.Form
form = tview.NewForm().
AddInputField("Login", "", 27, nil, nil).
AddPasswordField("Password", "", 27, '*', nil).
AddButton("Authenticate me", func() {
next(
form.GetFormItemByLabel("Login").(*tview.InputField).GetText(),
form.GetFormItemByLabel("Password").(*tview.InputField).GetText(),
)
})
form.SetBorder(true).SetTitle(" SRS Adlin - Login ")
type LoginDialog struct {
View *ui.Window
Username string
Password string
Action int
app.SetRoot(modal(form, 40, 9), true)
app.SetFocus(form)
result int
beforeClose func()
onClose func()
onCheck func(string, string) bool
}
func CreateLoginDialog(title string) *LoginDialog {
dlg := new(LoginDialog)
sWidth, sHeight := ui.ScreenSize()
wWidth, wHeight := 40, 15
dlg.View = ui.AddWindow(sWidth/2-wWidth/2, sHeight/2-wHeight/2, wWidth, wHeight, title)
ui.WindowManager().BeginUpdate()
defer ui.WindowManager().EndUpdate()
dlg.View.SetModal(true)
dlg.View.SetSizable(false)
dlg.View.SetTitleButtons(ui.ButtonDefault)
dlg.View.SetPack(ui.Vertical)
userfrm := ui.CreateFrame(dlg.View, 1, 1, ui.BorderNone, ui.Fixed)
userfrm.SetPaddings(1, 1)
userfrm.SetPack(ui.Horizontal)
userfrm.SetGaps(1, 0)
ui.CreateLabel(userfrm, ui.AutoSize, ui.AutoSize, " Login", ui.Fixed)
edUser := ui.CreateEditField(userfrm, 20, "", 1)
passfrm := ui.CreateFrame(dlg.View, 1, 1, ui.BorderNone, 1)
passfrm.SetPaddings(1, 1)
passfrm.SetPack(ui.Horizontal)
passfrm.SetGaps(1, 0)
ui.CreateLabel(passfrm, ui.AutoSize, ui.AutoSize, "Password", ui.Fixed)
edPass := ui.CreateEditField(passfrm, 20, "", 1)
edPass.SetPasswordMode(true)
filler := ui.CreateFrame(dlg.View, 1, 1, ui.BorderNone, 1)
filler.SetPack(ui.Horizontal)
lbRes := ui.CreateLabel(filler, ui.AutoSize, ui.AutoSize, "", 1)
blist := ui.CreateFrame(dlg.View, 1, 1, ui.BorderNone, ui.Fixed)
blist.SetPack(ui.Horizontal)
blist.SetPaddings(1, 1)
btnOk := ui.CreateButton(blist, 20, 4, "Authenticate me", 1)
btnOk.OnClick(func(ev ui.Event) {
if dlg.onCheck != nil && !dlg.onCheck(edUser.Title(), edPass.Title()) {
lbRes.SetTitle("Invalid username or password")
dlg.Action = LoginInvalid
return
}
dlg.Action = LoginOk
if dlg.onCheck == nil {
dlg.Username = edUser.Title()
dlg.Password = edPass.Title()
}
if dlg.beforeClose != nil {
dlg.beforeClose()
}
ui.WindowManager().DestroyWindow(dlg.View)
ui.WindowManager().BeginUpdate()
closeFunc := dlg.onClose
ui.WindowManager().EndUpdate()
if closeFunc != nil {
closeFunc()
}
})
dlg.View.OnKeyDown(func(ev ui.Event, data interface{}) bool {
if ev.Key == term.KeyEnter {
if edUser.Title() == "" {
ui.ActivateControl(dlg.View, edUser)
} else if edPass.Title() == "" {
ui.ActivateControl(dlg.View, edPass)
} else {
if dlg.beforeClose != nil {
dlg.beforeClose()
}
ui.WindowManager().DestroyWindow(dlg.View)
ui.WindowManager().BeginUpdate()
closeFunc := dlg.onClose
ui.WindowManager().EndUpdate()
if closeFunc != nil {
closeFunc()
}
}
return true
}
return false
}, nil)
edUser.OnChange(func(ev ui.Event) {
lbRes.SetTitle("")
})
edPass.OnChange(func(ev ui.Event) {
lbRes.SetTitle("")
})
ui.ActivateControl(dlg.View, edUser)
return dlg
return
}

View File

@ -1,61 +0,0 @@
package main
import (
"time"
ui "github.com/VladimirMarkelov/clui"
)
type RebootDialog struct {
View *ui.Window
}
func CreateRebootDialog(title, login string) *RebootDialog {
dlg := new(RebootDialog)
sWidth, sHeight := ui.ScreenSize()
wWidth, wHeight := 48, 10
dlg.View = ui.AddWindow(sWidth/2-wWidth/2, sHeight/2-wHeight/2, wWidth, wHeight, title)
ui.WindowManager().BeginUpdate()
defer ui.WindowManager().EndUpdate()
dlg.View.SetModal(true)
dlg.View.SetSizable(false)
dlg.View.SetTitleButtons(ui.ButtonDefault)
dlg.View.SetPack(ui.Vertical)
textfrm := ui.CreateFrame(dlg.View, 1, 1, ui.BorderNone, ui.Fixed)
textfrm.SetPaddings(1, 1)
textfrm.SetPack(ui.Vertical)
textfrm.SetGaps(2, 1)
lbl1 := ui.CreateLabel(textfrm, ui.AutoSize, ui.AutoSize, " You are now successfully logged in as:", ui.Fixed)
lbl1.SetTextColor(ui.ColorWhiteBold)
for i := 0; i <= wWidth/2-len(login)/2; i += 1 {
login = " " + login
}
lbl2 := ui.CreateLabel(textfrm, ui.AutoSize, ui.AutoSize, login, 1)
lbl2.SetTextColor(ui.ColorGreenBold)
lbl3 := ui.CreateLabel(textfrm, 40, 2, " Your computer will automatically\n reboot in a few seconds...", ui.Fixed)
lbl3.SetMultiline(true)
progress := ui.CreateProgressBar(textfrm, ui.AutoSize, ui.AutoSize, ui.Fixed)
progress.SetLimits(0, 100)
lbl4 := ui.CreateLabel(textfrm, 40, 4, "\nThe challenge begins right after the reboot. Good luck!", ui.Fixed)
lbl4.SetTextColor(ui.ColorWhiteBold)
lbl4.SetMultiline(true)
go func() {
for i := 0; i < 100; i += 1 {
progress.SetValue(i)
ui.PutEvent(ui.Event{Type: ui.EventRedraw})
time.Sleep(64 * time.Millisecond)
}
ui.WindowManager().DestroyWindow(dlg.View)
}()
return dlg
}

View File

@ -1,19 +1,45 @@
package main
import (
"math/rand"
"os"
"time"
ui "github.com/VladimirMarkelov/clui"
"github.com/gdamore/tcell"
"github.com/rivo/tview"
)
const URLLogin = "https://auth.adlin.nemunai.re/login"
var logged = false
var (
loggedAs = ""
)
func askLogin() (lgd *LoginDialog) {
lgd = CreateLoginDialog(" SRS AdLin - Login ")
func modal(p tview.Primitive, width, height int) tview.Primitive {
return tview.NewFlex().
AddItem(nil, 0, 1, false).
AddItem(tview.NewFlex().SetDirection(tview.FlexRow).
AddItem(nil, 0, 1, false).
AddItem(p, height, 1, false).
AddItem(nil, 0, 1, false), width, 1, false).
AddItem(nil, 0, 1, false)
}
lgd.beforeClose = func() {
func askLogin(app *tview.Application) {
CreateLoginDialog(app, func(username, password string) {
// Display check dialog
CreateCheckDialog(app)
go func() {
if ok, err := checkLogin(username, password); ok {
loggedAs = username
app.Stop()
} else {
CreateErrMsgDialog(app, err)
}
}()
})
/*lgd.beforeClose = func() {
// Display next dialoag
ckd := CreateCheckDialog(" SRS AdLin - Login ", lgd.Username, lgd.Password)
@ -28,20 +54,31 @@ func askLogin() (lgd *LoginDialog) {
}
}
}
}
return
}*/
}
func main() {
ui.InitLibrary()
defer ui.DeinitLibrary()
// seed the rand package with time
rand.Seed(time.Now().UnixNano())
askLogin()
app := tview.NewApplication()
ui.MainLoop()
app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Key() == tcell.KeyCtrlQ {
app.Stop()
}
return event
})
if !logged {
askLogin(app)
if err := app.Run(); err != nil {
panic(err)
}
if loggedAs == "" {
os.Exit(1)
}
runCinematic(loggedAs)
}

138
pkg/login-app/cmd/stream.go Normal file
View File

@ -0,0 +1,138 @@
package main
import (
"math/rand"
"sync"
"time"
"github.com/gdamore/tcell"
)
// Stream updates a StreamDisplay with new data updates
type Stream struct {
display *StreamDisplay
speed int
length int
headPos int
tailPos int
stopCh chan bool
killCh chan bool
headDone bool
fini bool
}
func (s *Stream) run() {
blackStyle := tcell.StyleDefault.
Foreground(tcell.ColorBlack).
Background(tcell.ColorBlack)
midStyleA := blackStyle.Foreground(tcell.ColorGreen)
midStyleB := blackStyle.Foreground(tcell.ColorLime)
headStyleA := blackStyle.Foreground(tcell.ColorSilver)
headStyleB := blackStyle.Foreground(tcell.ColorWhite)
var lastRune rune
STREAM:
for {
select {
case <-s.stopCh:
s.fini = true
case <-time.After(time.Duration(s.speed) * time.Millisecond):
// add a new rune if there is space in the stream
if !s.fini && !s.headDone && s.headPos <= curSizes.height {
newRune := characters[rand.Intn(len(characters))]
// Making most of the green characters bright/bold...
if rand.Intn(100) < 66 {
screen.SetCell(s.display.column, s.headPos-1, midStyleA, lastRune)
} else {
screen.SetCell(s.display.column, s.headPos-1, midStyleB, lastRune)
}
// ...and turning about a third of the heads from gray to white
if rand.Intn(100) < 33 {
screen.SetCell(s.display.column, s.headPos, headStyleA, newRune)
} else {
screen.SetCell(s.display.column, s.headPos, headStyleB, newRune)
}
lastRune = newRune
s.headPos++
} else {
if s.fini && (s.length <= 0 || s.headPos <= 0) {
break STREAM
} else if s.fini && s.headPos < s.length {
s.length = s.headPos
}
s.headDone = true
}
// clear rune at the tail of the stream
if s.tailPos > 0 || s.headPos >= s.length {
if s.tailPos == 0 && !s.fini {
// tail is being incremented for the first time. there is space for a new stream
s.display.newStream <- true
}
if s.tailPos < curSizes.height {
screen.SetCell(s.display.column, s.tailPos, blackStyle, ' ') //'\uFF60'
s.tailPos++
} else {
break STREAM
}
}
}
}
delete(s.display.streams, s)
}
// StreamDisplay represents a vertical line in the terminal on which `Stream`s are displayed.
// StreamDisplay also creates the Streams themselves
type StreamDisplay struct {
column int
stopCh chan bool
streams map[*Stream]bool
streamsLock sync.Mutex
newStream chan bool
}
func (sd *StreamDisplay) run() {
for {
select {
case <-sd.stopCh:
// lock this SD forever
sd.streamsLock.Lock()
// stop streams for this SD
for s := range sd.streams {
s.stopCh <- true
}
// close this goroutine
return
case <-sd.newStream:
// have some wait before the first stream starts..
time.Sleep(time.Duration(rand.Intn(9000)) * time.Millisecond)
// lock map
sd.streamsLock.Lock()
// create new stream instance
s := &Stream{
display: sd,
stopCh: make(chan bool),
speed: 30 + rand.Intn(110),
length: 10 + rand.Intn(8), // length of a stream is between 10 and 18 runes
}
// store in streams map
sd.streams[s] = true
// run the stream in a goroutine
go s.run()
// unlock map
sd.streamsLock.Unlock()
}
}
}