Commit 6f41fce
Changed files (3)
clientVisualization.go
@@ -1,57 +1,258 @@
package main
import (
- "bufio"
- "fmt"
+ "github.com/jroimartin/gocui"
"log"
"os"
"strings"
)
+type ScreenView struct {
+ MainGui *gocui.Gui
+ MAIN_WINDOW_NAME string
+ USER_ENTRY_NAME string
+ USER_ENTRY_HEIGHT int
+ COMMAND_PALLET_WIDTH int
+ COMMAND_PALLET_NAME string
+ STATUS_BAR_HEIGHT int
+ STATUS_BAR_NAME string
+}
+
+var ApplicationView ScreenView
+var readUserInputSemaphore *CountingSemaphore
+
// displayStringToMainScreen prints the given string
// to the primary string used by the application
func displayStringToMainScreen(str string) {
- fmt.Printf("%s", str)
+ if ApplicationView.MainGui == nil {
+ return
+ }
+ view, _ := ApplicationView.MainGui.View(ApplicationView.MAIN_WINDOW_NAME)
+ view.Write([]byte(str))
+ return
+}
+
+// updateMainView causes the main view to update
+// the display
+func updateMainView() {
+ ApplicationView.MainGui.Execute(func(arg1 *gocui.Gui) error {
+ if ApplicationView.MainGui == nil {
+ return nil
+ }
+ for _, view := range ApplicationView.MainGui.Views() {
+ view.Write([]byte(""))
+ }
+ return nil
+ })
}
// reads input from the user in the main screen
func readInputFromMainScreen() (string, error) {
- reader := bufio.NewReader(os.Stdin)
- input, err := reader.ReadString('\n')
- return input, err
+ if ApplicationView.MainGui == nil {
+ return "", nil
+ }
+ v, err := ApplicationView.MainGui.View(ApplicationView.USER_ENTRY_NAME)
+ if err != nil {
+ return "", err
+ }
+ buffer := v.ViewBuffer()
+ v.Clear()
+ v.SetCursor(0, 1)
+ v.MoveCursor(-len(buffer), 0, true)
+ return string(buffer), err
+}
+
+// initView initializes the view for the application
+// see https://github.com/jroimartin/gocui/blob/master/_examples/active.go
+// for inspiration
+func initView() error {
+ var err error
+ ApplicationView.MainGui, err = gocui.NewGui(gocui.OutputNormal)
+ if err != nil {
+ return err
+ }
+ ApplicationView.MainGui.Cursor = true
+ ApplicationView.MainGui.Highlight = true
+ ApplicationView.MainGui.SetManagerFunc(manageLayout)
+ ApplicationView.MainGui.SelFgColor = gocui.ColorWhite
+ if err = ApplicationView.MainGui.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil {
+ log.Printf("%+v", err)
+ }
+ return err
+}
+
+// quit breaks out of the main program loop to allow the application to exit
+func quit(g *gocui.Gui, v *gocui.View) error {
+ return gocui.ErrQuit
+}
+
+func initMainView(g *gocui.Gui, maxX int, maxY int) error {
+ // Main View
+ v, err := g.SetView(ApplicationView.MAIN_WINDOW_NAME,
+ ApplicationView.COMMAND_PALLET_WIDTH+1, 0+1, //x0, y0
+ maxX-5, maxY-(ApplicationView.STATUS_BAR_HEIGHT+ApplicationView.USER_ENTRY_HEIGHT)-3) // x1, y1
+ if err != nil {
+ if err != gocui.ErrUnknownView {
+ return err
+ }
+ v.Title = "Shadow Quiz"
+ v.Autoscroll = true
+ }
+ return nil
+}
+
+func initCommandView(g *gocui.Gui, maxX int, maxY int) error {
+ // Command Palette
+ v, err := g.SetView(ApplicationView.COMMAND_PALLET_NAME,
+ 0, 0+1,
+ ApplicationView.COMMAND_PALLET_WIDTH+1, maxY-1-ApplicationView.STATUS_BAR_HEIGHT)
+ if err != nil {
+ if err != gocui.ErrUnknownView {
+ return err
+ }
+ v.Title = "Commands"
+ v.Editable = false
+ for _, command := range commandArray {
+ v.Write([]byte(command.Command + "\n"))
+ }
+ }
+ return nil
+}
+
+func initProgressView(g *gocui.Gui, maxX int, maxY int) error {
+ //Progress Bar
+ v, err := g.SetView(ApplicationView.STATUS_BAR_NAME,
+ 0, maxY-ApplicationView.STATUS_BAR_HEIGHT-3,
+ maxX-4, maxY-2)
+ if err != nil {
+ if err != gocui.ErrUnknownView {
+ return err
+ }
+ v.Title = "Progress"
+ v.Editable = false
+ }
+ return nil
+}
+
+func initUserEntryView(g *gocui.Gui, maxX int, maxY int) error {
+ // User Input View
+ v, err := g.SetView(ApplicationView.USER_ENTRY_NAME,
+ ApplicationView.COMMAND_PALLET_WIDTH+1, maxY-
+ (ApplicationView.STATUS_BAR_HEIGHT+ApplicationView.USER_ENTRY_HEIGHT)-3, //x0, y0
+ maxX-5, maxY-ApplicationView.STATUS_BAR_HEIGHT-3) // x1, y1
+ if err != nil {
+ if err != gocui.ErrUnknownView {
+ return err
+ }
+ v.Editable = true
+ v.Wrap = true
+ v.Autoscroll = true
+ _, err = ApplicationView.MainGui.SetCurrentView(ApplicationView.USER_ENTRY_NAME)
+ if err != nil {
+ log.Printf("Could not set current view: %+v", err)
+ }
+ _, err = ApplicationView.MainGui.SetViewOnTop(ApplicationView.USER_ENTRY_NAME)
+ if err != nil {
+ log.Printf("Could not set top view: %+v", err)
+ }
+ }
+ return nil
+}
+
+// manageLayout handles drawing the layout for the application
+// the magic numbers come from somewhere, and they make things
+// do stuff
+func manageLayout(g *gocui.Gui) error {
+ maxX, maxY := g.Size()
+ if err := initMainView(g, maxX, maxY); err != nil {
+ return err
+ }
+ if err := initCommandView(g, maxX, maxY); err != nil {
+ return err
+ }
+ if err := initProgressView(g, maxX, maxY); err != nil {
+ return err
+ }
+ if err := initUserEntryView(g, maxX, maxY); err != nil {
+ return err
+ }
+ return nil
}
// initUserSession starts the interactive prompt for the user
func initUserSession() {
- displayStringToMainScreen("Welcome to the question interface. Use 'help' for more information.")
+ var err error
if clientConfig.USER == "" {
displayStringToMainScreen("\nUsername is blank. Setting to 'anonymous.' Use 'user' to change user name.")
clientConfig.USER = "anonymous"
}
+ err = initView()
+ if err != nil {
+ log.Printf("\nFailed to initiate user screen: %+v", err)
+ os.Exit(EXIT_CODE.BAD_CONFIG)
+ }
+ defer ApplicationView.MainGui.Close()
+ if err = ApplicationView.MainGui.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil {
+ log.Printf("\nFailed to bind key: %+v", err)
+ }
+ if err = ApplicationView.MainGui.SetKeybinding("", gocui.KeyEnter, gocui.ModNone, handleEnterKeyPress); err != nil {
+ log.Printf("\nFailed to bind key: %+v", err)
+ }
+ if err = ApplicationView.MainGui.MainLoop(); err != nil && err != gocui.ErrQuit {
+ log.Printf("Error from CUI: %+v", err)
+ exitApplication(Command{})
+ }
+}
+
+// handleEnterKeyPress parses the input supplied by the user after
+// pressing 'Enter'
+func handleEnterKeyPress(g *gocui.Gui, v *gocui.View) error {
+ if readUserInputSemaphore.Queued() > 0 {
+ readUserInputSemaphore.V() //taken whenever user input is required
+ return nil
+ }
var input string
var command Command
var err error
- for {
- displayStringToMainScreen("\n:> ")
- input, _ = readInputFromMainScreen()
- args := strings.Fields(input)
- if len(args) < 1 { // skip the empty strings
- continue
- }
- // select a command from the list of available commands
- if command, err = SelectCommand(strings.TrimSpace(args[0]), commandArray); err != nil {
- log.Printf(COLOR_RED+"Error while parsing command: %+v"+COLOR_RESET, err)
- continue
- }
- // parse the flags for the command
- err = ParseUserCommands(args[1:], &command)
- if err != nil {
- log.Printf(COLOR_RED+"Error while parsing parameters: %+v"+COLOR_RESET, err)
- continue
- }
- // execute the command and check for any errors that may have occurred
+ input, err = readInputFromMainScreen()
+ if err != nil {
+ log.Printf("Error reading input: %+v", err)
+ return nil
+ }
+ args := strings.Fields(input)
+ if len(args) < 1 { // skip the empty strings
+ return nil
+ }
+ // select a command from the list of available commands
+ if command, err = SelectCommand(strings.TrimSpace(args[0]), commandArray); err != nil {
+ log.Printf("Error while parsing command: %+v", err)
+ return nil
+ }
+ // parse the flags for the command
+ err = ParseUserCommands(args[1:], &command)
+ if err != nil {
+ log.Printf("Error while parsing parameters: %+v", err)
+ return nil
+ }
+ // execute the command and check for any errors that may have occurred
+ go func() {
if err = command.Run(command); err != nil {
- log.Printf(COLOR_RED+"Error while executing command: %+v"+COLOR_RESET, err)
+ log.Printf("Error while executing command: %+v", err)
}
- }
+ }()
+ return nil
+}
+
+// initializes the values for the ApplicationView
+func init() {
+ ApplicationView = ScreenView{}
+ ApplicationView.MAIN_WINDOW_NAME = "mainwindow"
+ ApplicationView.USER_ENTRY_NAME = "userentry"
+ ApplicationView.USER_ENTRY_HEIGHT = 2
+ ApplicationView.COMMAND_PALLET_WIDTH = 20
+ ApplicationView.COMMAND_PALLET_NAME = "commandpallet"
+ ApplicationView.STATUS_BAR_HEIGHT = 1
+ ApplicationView.STATUS_BAR_NAME = "statusbar"
+ readUserInputSemaphore = &CountingSemaphore{}
+ readUserInputSemaphore.SetCapacity(0)
}
command.go
@@ -4,7 +4,6 @@ import (
"bytes"
"encoding/json"
"fmt"
- "golang.org/x/crypto/ssh/terminal"
"io/ioutil"
"log"
"net/http"
@@ -17,11 +16,7 @@ import (
// note that subcommands are not currently supported
var commandArray = []Command{Command{Command: "exit",
Description: "Exit the self-assessment application and close the connection.",
- Run: func(command Command) error {
- displayStringToMainScreen("Exiting application.\n")
- os.Exit(0)
- return nil
- }},
+ Run: exitApplication},
Command{Command: "score",
Usage: "score [n]",
Description: "Display the score for the user. If supplied, the number displays the selected test.",
@@ -55,6 +50,14 @@ func setUserName(command Command) error {
return nil
}
+// exitApplication cleans up the gui and exits
+func exitApplication(command Command) error {
+ displayStringToMainScreen("Exiting application.\n")
+ ApplicationView.MainGui.Close()
+ os.Exit(0)
+ return nil
+}
+
// helpCommand prints the help for the list of available commands.
// it cannot exist in the initialization due to initialization loops
var helpCommand = Command{Command: "help",
@@ -66,6 +69,7 @@ var helpCommand = Command{Command: "help",
// displayHelp prints the help. if a command is specified,
// help is displayed for that particular command
func displayHelp(command Command) error {
+ displayStringToMainScreen("\n")
for _, checkCommand := range commandArray {
if len(command.PositionalParameters) > 0 && command.PositionalParameters[0] != checkCommand.Command { // only print help for desired command
continue
@@ -94,6 +98,7 @@ func displayHelp(command Command) error {
}
}
}
+ updateMainView()
return nil
}
@@ -124,6 +129,7 @@ func displayTests(clientTests *[]ClientTest) {
displayStringToMainScreen("\n" + hardRule + "\n")
displayStringToMainScreen(fmt.Sprintf("Average: %.2f%%\n%s\nQuestions Answered: %.0f\t Questions Correct: %.0f\n%s",
numCorrect/numQuestions*100, hardRule, numQuestions, numCorrect, hardRule))
+ updateMainView()
}
// displayTest displays a single test to the user
@@ -182,28 +188,37 @@ func calculateScore(numCorrect int, numQuestions int) float32 {
// this function updates the records with the user responses
func runTest(clientTest *ClientTest) {
numCorrect := 0
+ displayStringToMainScreen("\n")
for i, record := range clientTest.Records {
displayStringToMainScreen(fmt.Sprintf("%d) [%s] %s\n", i+1,
COLOR_BLUE+categoryKeys[record.Category]+COLOR_RESET,
record.Question))
- input, _ := readInputFromMainScreen()
+ updateMainView()
+ readUserInputSemaphore.P() // released when user presses 'Enter'
+ input, err := readInputFromMainScreen()
+ if err != nil {
+ log.Printf("Error reading input: %+v", err)
+ }
input = strings.TrimSpace(input)
- width, _, _ := terminal.GetSize(int(os.Stdin.Fd()))
+ maxX, _ := ApplicationView.MainGui.Size()
if strings.Compare(input, record.Answer) == 0 {
// dynamically determine width of screen for right-alignment
- displayStringToMainScreen(fmt.Sprintf("%"+strconv.Itoa(width)+"s\n",
- COLOR_GREEN+"[✓]"+COLOR_RESET))
+ fieldWidth := strconv.Itoa(maxX - ApplicationView.COMMAND_PALLET_WIDTH - len(record.Answer) - 10)
+ displayStringToMainScreen(fmt.Sprintf("%s%"+fieldWidth+"s\n",
+ COLOR_GREEN+record.Answer, "[✓]"+COLOR_RESET))
clientTest.Records[i].AnsweredCorrectly = true
numCorrect++
} else if strings.Compare(input, "exit") == 0 {
clientTest.Score = calculateScore(numCorrect, len(clientTest.Records))
return
} else {
- displayStringToMainScreen(fmt.Sprintf("%s%"+strconv.Itoa(width-len(record.Answer)-5)+"s\n",
+ fieldWidth := strconv.Itoa(maxX - ApplicationView.COMMAND_PALLET_WIDTH - len(record.Answer) - 10)
+ displayStringToMainScreen(fmt.Sprintf("%s%"+fieldWidth+"s\n",
COLOR_RED+record.Answer,
"[X]"+COLOR_RESET))
clientTest.Records[i].ClientAnswer = input
}
+ updateMainView()
}
clientTest.Score = calculateScore(numCorrect, len(clientTest.Records))
}
structures.go
@@ -1,5 +1,7 @@
package main
+import "log"
+
// API_ROOT defines the root path for the web api interface
const API_ROOT = "/api"
@@ -59,6 +61,53 @@ type ClientTest struct {
Username string
}
+// CountingSemaphore implements a counting semaphore
+// using go channels as suggested in the forums
+type CountingSemaphore struct {
+ counter chan bool //store empty structs since they take 0 memory
+ locksTaken int
+ // P func()
+ // V func()
+ // Queued func() int
+ // SetCapacity (int)
+}
+
+// P incrememnts the counter here
+// note: race conditions can totally happen
+func (cSem *CountingSemaphore) P() {
+ if cSem.counter != nil {
+ cSem.locksTaken++
+ cSem.counter <- true
+ }
+}
+
+// V decrements the counter here
+// note: race conditions can totally happen
+func (cSem *CountingSemaphore) V() {
+ if cSem.counter != nil && cSem.locksTaken > 0 {
+ <-cSem.counter
+ cSem.locksTaken--
+ }
+}
+
+// Queued returns the number of items in the
+// semaphore, or -1 if the semaphore is unitialized
+// note: race conditions can totally happen
+func (cSem *CountingSemaphore) Queued() int {
+ if cSem.counter != nil {
+ log.Printf("Len: %d", cSem.locksTaken)
+ return cSem.locksTaken
+ }
+ return -1
+}
+
+// SetCapacity sets the max number of items accessing this semaphore
+// note: race conditions can totally happen
+func (cSem *CountingSemaphore) SetCapacity(cap int) {
+ cSem.counter = make(chan bool, cap)
+ //cSem.counter <- struct{}{}
+}
+
func init() {
recordMap = map[string][]Record{}
}