From fdc199dd63932b6c8b284514cb445ab606089272 Mon Sep 17 00:00:00 2001 From: CaffeinatedTech Date: Sun, 21 Apr 2024 14:51:08 +1000 Subject: [PATCH] Added bubbletea --- .gitignore | 48 ++++++++++ algo_kata.go | 246 ++++++++++++++++++++++++++++++++++++++++++++------- config.go | 100 ++++++++++++--------- config.toml | 69 +++++++++------ go.mod | 16 ++++ go.sum | 39 ++++++++ session.go | 101 ++++----------------- ui.go | 107 ++++++++++++++++++---- 8 files changed, 521 insertions(+), 205 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..522edc2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,48 @@ +# Created by https://www.toptal.com/developers/gitignore/api/vim,go +# Edit at https://www.toptal.com/developers/gitignore?templates=vim,go + +### Go ### +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work + +### Vim ### +# Swap +[._]*.s[a-v][a-z] +!*.svg # comment out if you don't need vector files +[._]*.sw[a-p] +[._]s[a-rt-v][a-z] +[._]ss[a-gi-z] +[._]sw[a-p] + +# Session +Session.vim +Sessionx.vim + +# Temporary +.netrwhist +*~ +# Auto-generated tag files +tags +# Persistent undo +[._]*.un~ + +# End of https://www.toptal.com/developers/gitignore/api/vim,go diff --git a/algo_kata.go b/algo_kata.go index 43f204f..1e58023 100644 --- a/algo_kata.go +++ b/algo_kata.go @@ -1,31 +1,26 @@ package main -import "flag" import "fmt" -import "strings" +import "strconv" import "time" +import "github.com/charmbracelet/bubbles/textinput" +import tea "github.com/charmbracelet/bubbletea" -type languages []string - -func (l *languages) String() string { - return fmt.Sprint(*l) -} -func (l *languages) Set(value string) error { - for _, lang := range strings.Split(value, ",") { - *l = append(*l, lang) - } - return nil +type Language struct { + Name string `mapstructure:"name"` + Selected bool `mapstructure:"selected"` } type Algorithm struct { - Name string `mapstructure:"name"` - Type string `mapstructure:"type"` - Sorted bool `mapstructure:"sorted"` + Name string `mapstructure:"name"` + Type string `mapstructure:"type"` + Sorted bool `mapstructure:"sorted"` + Selected bool `mapstructure:"selected"` } type Session struct { Algorithms []Algorithm - Languages []string + Languages []Language Num int } @@ -36,24 +31,207 @@ type Result struct { Correct bool } +type State int64 + +const ( + CHECK_CONFIG State = iota // Check the configuration toml file + WELCOME // Show the welcome screen, and allow selecting languages, and algorithms + QUESTION_COUNT // Ask the user for the number of questions they want to practice + INTERMISSION // Present the next question's language, and wait for user to be ready + QUESTION // Show the question, and await the response + COMPLETE // Show the final score table, and quit +) + +type Config struct { + Algorithms []Algorithm + Languages []Language + QuestionCount int +} + +type statusMsg int +type stateMsg State +type configMsg Config +type errMsg struct{ err error } + +func (e errMsg) Error() string { return e.err.Error() } + +type nextQuestion struct { + algorithm Algorithm + language string + array []int + expectedResult int + expectedResultIndex int + startTime time.Time +} + +type model struct { + state State + session Session + results []Result + nextQuestion nextQuestion + lastAnswerCorrect string + cursor int + num_ti textinput.Model + answer_ti textinput.Model +} + +func (m model) Init() tea.Cmd { + return m.checkConfig +} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + if msg.String() == "q" || msg.String() == "esc" || msg.String() == "ctrl+c" { + return m, tea.Quit + } + if m.state == WELCOME { + optionCount := len(m.session.Languages) + len(m.session.Algorithms) + switch msg.String() { + case "j", "down": + m.cursor++ + if m.cursor >= optionCount { + m.cursor = 0 + } + case "k", "up": + m.cursor-- + if m.cursor < 0 { + m.cursor = 0 + } + case " ": + if m.cursor < len(m.session.Languages) { + m.session.Languages[m.cursor].Selected = !m.session.Languages[m.cursor].Selected + } else { + m.session.Algorithms[m.cursor-len(m.session.Languages)].Selected = !m.session.Algorithms[m.cursor-len(m.session.Languages)].Selected + } + case "enter": + // Validate the options. Must have at least one language, and at least one algorithm. + if m.validateOptions() { + m.state = QUESTION_COUNT + } + } + } else if m.state == QUESTION_COUNT { + switch msg.String() { + case "enter": + m.session.Num, _ = strconv.Atoi(m.num_ti.Value()) + if m.session.Num > m.countChecked() { + m.session.Num = m.countChecked() + } else if m.session.Num < 1 { + m.session.Num = 1 + } + saveConfig(m.session.Algorithms, m.session.Languages, m.session.Num) + // Decide the next question and language, and generate the random array + nextAlgorithm, nextLanguage := m.randomAlgorithmAndLang() + m.nextQuestion = nextQuestion{algorithm: nextAlgorithm, language: nextLanguage} + m.nextQuestion.array = randomArray(10, nextAlgorithm.Sorted) + m.nextQuestion.expectedResult, m.nextQuestion.expectedResultIndex = expectedResult(m.nextQuestion.array) + m.state = INTERMISSION + } + } else if m.state == INTERMISSION { + switch msg.String() { + case "enter": + m.nextQuestion.startTime = time.Now() + m.state = QUESTION + } + } else if m.state == QUESTION { + switch msg.String() { + case "enter": + // Check the answer + correct := answerCheck(m.nextQuestion.array, m.nextQuestion.expectedResultIndex, m.nextQuestion.algorithm.Type, m.answer_ti.Value()) + m.lastAnswerCorrect = green("Correct") + if !correct { + m.lastAnswerCorrect = red("Incorrect") + } + elapsedTime := time.Since(m.nextQuestion.startTime) + m.results = append(m.results, Result{ + Algorithm: m.nextQuestion.algorithm.Name, + Language: m.nextQuestion.language, + Time: elapsedTime, + Correct: correct, + }) + if len(m.results) >= m.session.Num { + m.state = COMPLETE + } else { + nextAlgorithm, nextLanguage := m.randomAlgorithmAndLang() + m.nextQuestion = nextQuestion{algorithm: nextAlgorithm, language: nextLanguage} + m.nextQuestion.array = randomArray(10, nextAlgorithm.Sorted) + m.nextQuestion.expectedResult, m.nextQuestion.expectedResultIndex = expectedResult(m.nextQuestion.array) + m.answer_ti.SetValue("") + m.state = INTERMISSION + } + + } + } else if m.state == COMPLETE { + return m, tea.Quit + } + case errMsg: + return m, tea.Quit + case configMsg: + m.session.Languages = msg.Languages + m.session.Algorithms = msg.Algorithms + m.session.Num = msg.QuestionCount + m.state = WELCOME + return m, nil + } + if m.state == QUESTION_COUNT { + var cmd tea.Cmd + m.num_ti, cmd = m.num_ti.Update(msg) + return m, cmd + } + if m.state == QUESTION { + var cmd tea.Cmd + m.answer_ti, cmd = m.answer_ti.Update(msg) + return m, cmd + } + return m, nil +} + +func (m model) View() string { + switch m.state { + case WELCOME: + return m.welcomMessage() + case QUESTION_COUNT: + max_count := m.countChecked() + return fmt.Sprintf( + "How many questions will you do? (max: %d)\n\n%s\n\n%s", + max_count, + m.num_ti.View(), + "(esc to quit)", + ) + "\n" + case INTERMISSION: + return m.intermissionMessage() + case QUESTION: + msg := m.questionMessage() + return fmt.Sprintf( + "%s\n\nYour answer: %s\n\n%s", + msg, + m.answer_ti.View(), + "(esc to quit)", + ) + "\n" + case COMPLETE: + return printResults(m.results) + } + return "" +} + func main() { - var languagesFlag languages - flag.Var(&languagesFlag, "languages", "A comma separated list of languages to practice.") - flag.Var(&languagesFlag, "l", "A comma separated list of languages to practice.") - flag.Parse() - - checkConfig() - algos, languages := getConfig() - if len(languagesFlag) > 0 { - languages = languagesFlag + num_ti := textinput.New() + num_ti.Focus() + num_ti.CharLimit = 2 + num_ti.Width = 5 + answer_ti := textinput.New() + answer_ti.Focus() + answer_ti.Width = 50 + + initialModel := model{ + state: CHECK_CONFIG, + cursor: 0, + num_ti: num_ti, + answer_ti: answer_ti, + } + p := tea.NewProgram(initialModel) + if _, err := p.Run(); err != nil { + fmt.Println("Error starting the program", err) } - welcomMessage(algos, languages) - num := askForNumber() - s := Session{Algorithms: algos, Languages: languages, Num: num} - s = checkPracticeNumer(s) - fmt.Println("Starting session with", s.Num, "questions.") - results := runTheSession(s) - - // Print out the results - printResults(results) + } diff --git a/config.go b/config.go index 4e22fe1..ec10d59 100644 --- a/config.go +++ b/config.go @@ -3,27 +3,61 @@ package main import "fmt" import "github.com/spf13/cast" import "github.com/spf13/viper" +import tea "github.com/charmbracelet/bubbletea" -func checkConfig() { - algorithms, languages := getConfig() + +func (m model) validateOptions() bool { + // Check of there are any languages selected + noLangs := true + for _, lang := range m.session.Languages { + if lang.Selected { + noLangs = false + break + } + } + if noLangs { + return false + } + // Check of there are any algorithms selected + noAlgs := true + for _, alg := range m.session.Algorithms { + if alg.Selected { + noAlgs = false + break + } + } + if noAlgs { + return false + } + return true +} + +func (m model) checkConfig() tea.Msg { + algorithms, languages, questionCount := getConfig() if len(languages) == 0 { - fmt.Println("No languages found in config file") - panic(1) + return errMsg{fmt.Errorf("No languages found in config file")} } if len(algorithms) == 0 { - fmt.Println("No algorithms found in config file") - panic(1) + return errMsg{fmt.Errorf("No algorithms found in config file")} } // Check each algorithm in config file to make sure they have `name`, `type` keys. for _, alg := range algorithms { if alg.Name == "" || alg.Type == "" { - fmt.Println("One of the Algorithms in the config.toml file is missing either name or type") - panic(1) + return errMsg{fmt.Errorf("One of the Algorithms in the config.toml file is missing either name or type")} } } + config := Config{Algorithms: algorithms, Languages: languages, QuestionCount: questionCount} + return configMsg(config) +} + +func saveConfig(alg []Algorithm, lang []Language, questionCount int) { + viper.Set("algorithm", alg) + viper.Set("language", lang) + viper.Set("question_count", questionCount) + viper.WriteConfig() } -func getConfig() ([]Algorithm, []string) { +func getConfig() ([]Algorithm, []Language, int) { viper.SetConfigName("config") viper.SetConfigType("toml") viper.AddConfigPath(".") @@ -40,41 +74,23 @@ func getConfig() ([]Algorithm, []string) { } else { for _, table := range alg { if m, ok := table.(map[string]interface{}); ok { - c = append(c, Algorithm{Name: cast.ToString(m["name"]), Type: cast.ToString(m["type"]), Sorted: cast.ToBool(m["sorted"])}) + c = append(c, Algorithm{Name: cast.ToString(m["name"]), Type: cast.ToString(m["type"]), Sorted: cast.ToBool(m["sorted"]), Selected: cast.ToBool(m["selected"])}) } } } - languages := viper.GetStringSlice("languages") - return c, languages -} - -func configText(c []Algorithm, languages []string) (string, string) { - languages_text := "" - for _, lang := range languages { - if languages_text != "" { - languages_text += ", " - } - languages_text += lang + " " - } - algorithms_text := "" - for _, alg := range c { - if algorithms_text != "" { - algorithms_text += ", " - } - algorithms_text += alg.Name - } - return languages_text, algorithms_text -} - -func checkPracticeNumer(s Session) Session { - maxNum := len(s.Algorithms) * len(s.Languages) - if s.Num < 1 { - fmt.Println("You must do at least one question.") - s.Num = 1 + var languages []Language + lang, ok := viper.Get("language").([]interface{}) + if !ok { + fmt.Println("Error getting languages") + panic(1) + } else { + for _, table := range lang { + if m, ok := table.(map[string]interface{}); ok { + languages = append(languages, Language{Name: cast.ToString(m["name"]), Selected: cast.ToBool(m["selected"])}) + } + } } - if s.Num > maxNum { - fmt.Println("You can only do up to", maxNum, "questions.") - s.Num = maxNum - } - return s + questionCount := viper.GetInt("question_count") + return c, languages, questionCount } + diff --git a/config.toml b/config.toml index 4d9fc2e..8a623c8 100644 --- a/config.toml +++ b/config.toml @@ -1,38 +1,51 @@ -# The languages that you want to practice. -languages = ["Javascript", "PHP", "Python", "Go", "Bash"] - -# The algorithms that you want to practice. -# Each require a name, and type (search, or sort), and an optional sorted field for -# search algorithms that require a sorted list. -# EXAMPLE: -# [[algorithm]] -# name = "Binary Search" -# type = "search" -# sorted = true # Binary search requires a sorted array - +question_count = 2 [[algorithm]] -name = "Binary Search" -type = "search" -sorted = true # Binary search requires a sorted array +Name = 'Binary Search' +Type = 'search' +Sorted = true +Selected = true [[algorithm]] -name = "Linear Search" -type = "search" -sorted = false +Name = 'Linear Search' +Type = 'search' +Sorted = false +Selected = true [[algorithm]] -name = "Bubble Sort" -type = "sort" -sorted = false +Name = 'Bubble Sort' +Type = 'sort' +Sorted = false +Selected = true [[algorithm]] -name = "Quick Sort" -type = "sort" -sorted = false +Name = 'Quick Sort' +Type = 'sort' +Sorted = false +Selected = true [[algorithm]] -name = "Shell Sort" -type = "sort" -sorted = false - +Name = 'Shell Sort' +Type = 'sort' +Sorted = false +Selected = false + +[[language]] +Name = 'Javascript' +Selected = true + +[[language]] +Name = 'PHP' +Selected = false + +[[language]] +Name = 'Python' +Selected = true + +[[language]] +Name = 'Go' +Selected = true + +[[language]] +Name = 'Bash' +Selected = false diff --git a/go.mod b/go.mod index 092a551..0a86854 100644 --- a/go.mod +++ b/go.mod @@ -3,14 +3,28 @@ module CaffeinatedTech/algo_kata go 1.22.1 require ( + github.com/atotto/clipboard v0.1.4 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/bubbles v0.18.0 // indirect + github.com/charmbracelet/bubbletea v0.25.0 // indirect + github.com/charmbracelet/lipgloss v0.9.1 // indirect + github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect github.com/fatih/color v1.16.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.15.2 // indirect github.com/pelletier/go-toml/v2 v2.1.0 // indirect + github.com/rivo/uniseg v0.4.6 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect @@ -22,7 +36,9 @@ require ( go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect + golang.org/x/sync v0.5.0 // indirect golang.org/x/sys v0.15.0 // indirect + golang.org/x/term v0.6.0 // indirect golang.org/x/text v0.14.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 25d8119..116d5b7 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,15 @@ +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0= +github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw= +github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM= +github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg= +github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg= +github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I= +github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= +github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= @@ -6,6 +18,8 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -13,11 +27,31 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34= +github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.6 h1:Sovz9sDSwbOz9tgUy8JpT+KgCkPYJEN/oYzlJiYTNLg= +github.com/rivo/uniseg v0.4.6/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= @@ -47,10 +81,15 @@ go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= +golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= +golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/session.go b/session.go index d7e097b..0aa0683 100644 --- a/session.go +++ b/session.go @@ -1,30 +1,26 @@ package main -import "bufio" -import "fmt" -import "github.com/fatih/color" -import "github.com/spf13/cast" import "encoding/json" import "math/rand" -import "os" import "slices" -import "time" +import "github.com/spf13/cast" + +func (m model) randomAlgorithmAndLang() (Algorithm, string) { -func randomAlgorithmAndLang(s Session, results []Result) (Algorithm, string) { thisAlgorithm := Algorithm{} - thisLanguage := "" + var thisLanguage Language for { - thisAlgorithm = s.Algorithms[rand.Intn(len(s.Algorithms))] - thisLanguage = s.Languages[rand.Intn(len(s.Languages))] + thisAlgorithm = m.session.Algorithms[rand.Intn(len(m.session.Algorithms))] + thisLanguage = m.session.Languages[rand.Intn(len(m.session.Languages))] // Check if this algorithm, and language are already in the results array. - contains := slices.ContainsFunc(results, func(r Result) bool { - return r.Algorithm == thisAlgorithm.Name && r.Language == thisLanguage + contains := slices.ContainsFunc(m.results, func(r Result) bool { + return r.Algorithm == thisAlgorithm.Name && r.Language == thisLanguage.Name }) - if !contains { + if !contains && thisAlgorithm.Selected && thisLanguage.Selected { break } } - return thisAlgorithm, thisLanguage + return thisAlgorithm, thisLanguage.Name } func randomArray(num int, sorted bool) []int { @@ -43,77 +39,18 @@ func expectedResult(arr []int) (int, int) { return expected, slices.Index(arr, expected) } -// Check if the answer is correct. the answer is taken from bufio.NewScanner.Scan -func answerCheck(arr []int, expectedResultIndex int, algoType string, answerScanner *bufio.Scanner) bool { +// Check if the answer is correct. +func answerCheck(arr []int, expectedResultIndex int, algoType string, answerString string) bool { if algoType == "sort" { - // Unmarshal answer into a slice of ints + shellSort(arr) + // Convert the answerString to an array of integers var answer []int - json.Unmarshal([]byte(answerScanner.Text()), &answer) + err := json.Unmarshal([]byte(answerString), &answer) + if err != nil { + return false + } return compareArrays(arr, answer) } else { - return cast.ToInt(answerScanner.Text()) == expectedResultIndex - } -} - -func runTheSession(s Session) []Result { - green := color.New(color.FgGreen).Add(color.Bold).SprintFunc() - results := make([]Result, s.Num) - - for i := 0; i < s.Num; i++ { - // Get random algorithm, and language - make sure we don't repeat the same pair. - thisAlgorithm, thisLanguage := randomAlgorithmAndLang(s, results) - - // Generate an array of random numbers - questionArr := randomArray(10, thisAlgorithm.Sorted) - // Get the expected result, and the index of the expected result. - expectedResult, expectedResultIndex := expectedResult(questionArr) - - // Turn the question array into a JSON string for better printing. - questionJson, _ := json.Marshal(questionArr) - questionJsonString := string(questionJson) - - // Announce the language of the round - fmt.Printf("\nThis round will be in %s. Press Enter when you are ready...\n", green(thisLanguage)) - var input string - fmt.Scanln(&input) - - // Announce the algorithm of the round, and start a timer. - fmt.Printf("\nAlgorithm %d: %v\n", i+1, green(thisAlgorithm.Name)) - if thisAlgorithm.Sorted { - fmt.Println("This array is already sorted.") - } - - if thisAlgorithm.Type == "search" { - fmt.Printf("Find the index of %d in the following array:\n", expectedResult) - } - - fmt.Printf("\nArray: %v\n", questionJsonString) - - start := time.Now() - fmt.Println("\nEnter your answer when you are done. Starting timer...") - // If this is a sorting algorithm, then sort the array, ready to compare to the user's input later. - if thisAlgorithm.Type == "sort" { - shellSort(questionArr) - questionJson, _ = json.Marshal(questionArr) - questionJsonString = string(questionJson) - } - - answerScanner := bufio.NewScanner(os.Stdin) - answerScanner.Scan() - end := time.Now() - fmt.Println("Time taken: ", end.Sub(start).Round(time.Second)) - - answerCorrect := answerCheck(questionArr, expectedResultIndex, thisAlgorithm.Type, answerScanner) - - if answerCorrect { - fmt.Println("Correct!") - } else { - fmt.Println("Incorrect!") - } - - fmt.Print("\n") - // Save the results for this round - results[i] = Result{Algorithm: thisAlgorithm.Name, Language: thisLanguage, Time: end.Sub(start).Round(time.Second), Correct: answerCorrect} + return cast.ToInt(answerString) == expectedResultIndex } - return results } diff --git a/ui.go b/ui.go index 4917dba..ff391cb 100644 --- a/ui.go +++ b/ui.go @@ -1,33 +1,102 @@ package main import "fmt" +import "time" +import "encoding/json" +import "github.com/fatih/color" -func welcomMessage(c []Algorithm, languages []string) { - languages_text, algorithms_text := configText(c, languages) - fmt.Println("--== Algorithm Kata ==--") - fmt.Println("Lets practice some algorithms.") - fmt.Println("\nLanguages: ", languages_text) - fmt.Println("Algorithms: ", algorithms_text) +var green = color.New(color.FgGreen).Add(color.Bold).SprintFunc() +var red = color.New(color.FgRed).Add(color.Bold).SprintFunc() + +func (m model) welcomMessage() string { + msg := "Welcome to Algorithm Kata\n\n" + + msg += "Please select the languages, and algorithms that you would like to practice.\n\n" + msg += "Languages:\n" + for i, lang := range m.session.Languages { + cursor := " " + if m.cursor == i { + cursor = ">" + } + checked := " " + if lang.Selected { + checked = "x" + } + msg += fmt.Sprintf("%s [%s] %s\n", cursor, checked, lang.Name) + } + + numLangs := len(m.session.Languages) + msg += "\nAlgorithms:\n" + for i, alg := range m.session.Algorithms { + cursor := " " + if m.cursor == i+numLangs { + cursor = ">" + } + checked := " " + if alg.Selected { + checked = "x" + } + msg += fmt.Sprintf("%s [%s] %s\n", cursor, checked, alg.Name) + } + return msg +} + +func (m model) askForNumber() string { + maxQuestions := m.countChecked() + msg := fmt.Sprintf("How many questions would you like to answer? (max: %d)\n", maxQuestions) + return msg +} + +func (m model) countChecked() int { + count := 0 + for _, lang := range m.session.Languages { + if lang.Selected { + count++ + } + } + for _, alg := range m.session.Algorithms { + if alg.Selected { + count++ + } + } + return count +} + +func (m model) intermissionMessage() string { + msg := "" + if m.lastAnswerCorrect != "" { + msg += fmt.Sprintf("\nYour answer was %s\n", m.lastAnswerCorrect) + } + return fmt.Sprintf("%s\nThis round will be in %s, press Enter when ready.\n", msg, green(m.nextQuestion.language)) } -func askForNumber() int { - fmt.Println("\nHow many would you like to do this session?") - var num int - _, err := fmt.Scan(&num) - if err != nil { - fmt.Println("Error reading number of algorithms") - panic(1) +func (m model) questionMessage() string { + msg := fmt.Sprintf("\nThis round will be in %s\n", green(m.nextQuestion.language)) + msg += fmt.Sprintf("\nAlgorithm: %s\n", green(m.nextQuestion.algorithm.Name)) + + if m.nextQuestion.algorithm.Sorted { + msg += "This array is already sorted.\n" } - return num + + if m.nextQuestion.algorithm.Type == "search" { + msg += fmt.Sprintf("Find the index of %d in the following array:\n", m.nextQuestion.expectedResult) + } + + questionJson, _ := json.Marshal(m.nextQuestion.array) + questionJsonString := string(questionJson) + msg += fmt.Sprintf("\nArray: %v\n", questionJsonString) + return msg } -func printResults(results []Result) { - fmt.Println("Results:") +func printResults(results []Result) string { + msg := "Results:\n" for i, result := range results { - thisResultCorrect := "Incorrect" + thisResultCorrect := red("Incorrect") if result.Correct { - thisResultCorrect = "Correct" + thisResultCorrect = green("Correct") } - fmt.Printf("Algorithm %d: %s in %s %s %s\n", i+1, result.Algorithm, result.Language, result.Time, thisResultCorrect) + msg += fmt.Sprintf("Algorithm %d: %s in %s %s %s\n", i+1, result.Algorithm, result.Language, result.Time.Round(time.Second), thisResultCorrect) } + msg += "\nPress any key to exit.\n" + return msg }