package doodle import ( "bytes" "fmt" "strings" "git.kirsle.net/apps/doodle/balance" "git.kirsle.net/apps/doodle/events" "git.kirsle.net/apps/doodle/render" "github.com/robertkrimen/otto" ) // Flash a message to the user. func (d *Doodle) Flash(template string, v ...interface{}) { d.shell.Write(fmt.Sprintf(template, v...)) } // Shell implements the developer console in-game. type Shell struct { parent *Doodle Open bool Prompt string Text string History []string Output []string Flashes []Flash // Blinky cursor variables. cursor byte // cursor symbol cursorFlip uint64 // ticks until cursor flip cursorRate uint64 // Paging through history variables. historyPaging bool historyIndex int // JavaScript shell interpreter. js *otto.Otto } // Flash holds a message to flash on screen. type Flash struct { Text string Expires uint64 // tick that it expires } // NewShell initializes the shell helper (the "Shellper"). func NewShell(d *Doodle) Shell { s := Shell{ parent: d, History: []string{}, Output: []string{}, Flashes: []Flash{}, Prompt: ">", cursor: '_', cursorRate: balance.ShellCursorBlinkRate, js: otto.New(), } // Make the Doodle instance available to the shell. bindings := map[string]interface{}{ "d": d, "log": log, "RGBA": render.RGBA, "Point": render.NewPoint, } for name, v := range bindings { err := s.js.Set(name, v) if err != nil { log.Error("Failed to make `%s` available to JS shell: %s", name, err) } } return s } // Close the shell, resetting its internal state. func (s *Shell) Close() { log.Debug("Shell: closing shell") s.Open = false s.Prompt = ">" s.Text = "" s.historyPaging = false s.historyIndex = 0 } // Execute a command in the shell. func (s *Shell) Execute(input string) { command := s.Parse(input) if command.Raw != "" { s.Output = append(s.Output, s.Prompt+command.Raw) s.History = append(s.History, command.Raw) } if command.Command == "clear" { s.Output = []string{} } else { err := command.Run(s.parent) if err != nil { s.Write(err.Error()) } } // Reset the text buffer in the shell. s.Text = "" } // Write a line of output text to the console. func (s *Shell) Write(line string) { s.Output = append(s.Output, line) s.Flashes = append(s.Flashes, Flash{ Text: line, Expires: s.parent.ticks + balance.FlashTTL, }) } // Parse the command line. func (s *Shell) Parse(input string) Command { input = strings.TrimSpace(input) if len(input) == 0 { return Command{} } var ( inQuote bool buffer = bytes.NewBuffer([]byte{}) words = []string{} ) for i := 0; i < len(input); i++ { char := input[i] switch char { case ' ': if inQuote { buffer.WriteByte(char) continue } if word := buffer.String(); word != "" { words = append(words, word) buffer.Reset() } case '"': if !inQuote { // An opening quote character. inQuote = true } else { // The closing quote. inQuote = false if word := buffer.String(); word != "" { words = append(words, word) buffer.Reset() } } default: buffer.WriteByte(char) } } if remainder := buffer.String(); remainder != "" { words = append(words, remainder) } return Command{ Raw: input, Command: words[0], Args: words[1:], ArgsLiteral: strings.TrimSpace(input[len(words[0]):]), } } // Draw the shell. func (s *Shell) Draw(d *Doodle, ev *events.State) error { if ev.EscapeKey.Read() { s.Close() return nil } else if ev.EnterKey.Read() || ev.EscapeKey.Read() { s.Execute(s.Text) s.Close() return nil } else if (ev.Up.Now || ev.Down.Now) && len(s.History) > 0 { // Paging through history. if !s.historyPaging { s.historyPaging = true s.historyIndex = len(s.History) } // Consume the inputs and make convenient variables. ev.Down.Read() isUp := ev.Up.Read() // Scroll through the input history. if isUp { s.historyIndex-- if s.historyIndex < 0 { s.historyIndex = 0 } } else { s.historyIndex++ if s.historyIndex >= len(s.History) { s.historyIndex = len(s.History) - 1 } } s.Text = s.History[s.historyIndex] } // Compute the line height we can draw. lineHeight := balance.ShellFontSize + int(balance.ShellPadding) // If the console is open, draw the console. if s.Open { // Cursor flip? if d.ticks > s.cursorFlip { s.cursorFlip = d.ticks + s.cursorRate if s.cursor == ' ' { s.cursor = '_' } else { s.cursor = ' ' } } // Read a character from the keyboard. if key := ev.ReadKey(); key != "" { // Backspace? if key == `\b` { if len(s.Text) > 0 { s.Text = s.Text[:len(s.Text)-1] } } else { s.Text += key } } // How tall is the box? boxHeight := int32(lineHeight*(balance.ShellHistoryLineCount+1)) + balance.ShellPadding // Draw the background color. d.Engine.DrawBox( balance.ShellBackgroundColor, render.Rect{ X: 0, Y: d.height - boxHeight, W: d.width, H: boxHeight, }, ) // Draw the recent commands. outputY := d.height - int32(lineHeight*2) for i := 0; i < balance.ShellHistoryLineCount; i++ { if len(s.Output) > i { line := s.Output[len(s.Output)-1-i] d.Engine.DrawText( render.Text{ Text: line, Size: balance.ShellFontSize, Color: render.Grey, }, render.Point{ X: balance.ShellPadding, Y: outputY, }, ) } outputY -= int32(lineHeight) } // Draw the command prompt. d.Engine.DrawText( render.Text{ Text: s.Prompt + s.Text + string(s.cursor), Size: balance.ShellFontSize, Color: balance.ShellForegroundColor, }, render.Point{ X: balance.ShellPadding, Y: d.height - int32(balance.ShellFontSize) - balance.ShellPadding, }, ) } else if len(s.Flashes) > 0 { // Otherwise, just draw flashed messages. valid := false // Did we actually draw any? outputY := d.height - int32(lineHeight*2) for i := len(s.Flashes); i > 0; i-- { flash := s.Flashes[i-1] if d.ticks >= flash.Expires { continue } d.Engine.DrawText( render.Text{ Text: flash.Text, Size: balance.ShellFontSize, Color: render.SkyBlue, Stroke: render.Grey, Shadow: render.Black, }, render.Point{ X: balance.ShellPadding, Y: outputY, }, ) outputY -= int32(lineHeight) valid = true } // If we've exhausted all flashes, free up the memory. if !valid { s.Flashes = []Flash{} } } return nil }