Commit 6f41fce

Richard Luby <richluby@gmail.com>
2016-11-16 14:26:00
lost christmas, but gained so much more
program has view panes. panes can be updated independently and display separate information using the gocui package
1 parent bfd0dc5
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{}
 }