diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 30bab2a..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - \ No newline at end of file diff --git a/cmd/lesson.go b/cmd/lesson.go index be0ad87..b91cffc 100644 --- a/cmd/lesson.go +++ b/cmd/lesson.go @@ -17,12 +17,24 @@ var lessonCmd = &cobra.Command{ Use: "lesson", Short: "Starts the activity / Lists available activities", Aliases: []string{"lessons", "list", "start"}, + //Args: func(cmd *cobra.Command, args []string) error { + // if err := cobra.RangeArgs(0, 2)(cmd, args); err != nil { + // return err + // } + // // Run the custom validation logic + // if myapp.IsValidColor(args[0]) { + // return nil + // } + // return fmt.Errorf("invalid color specified: %s", args[0]) + //}, + // TODO: impl arg validator + pre-run (if l/c exists) Run: func(cmd *cobra.Command, args []string) { if len(args) == 0 { - b := color.New(color.Bold) - for i := range lesson.Master.Lessons { + for i := 1; i < len(lesson.Master.Lessons); i++ { L := lesson.Master.Lessons[i] - fmt.Printf("%-2d %s\n %s\n Author: %s\n\n", L.Id, b.Sprint(L.Name), L.Description, L.Author) + fmt.Printf("%-2d %s\n %s\n Author: %s"+ + "\n\n", + i, color.New(color.Bold).Sprint(L.Name), L.Description, L.Author) } fmt.Println("Use \"csa start [number]\" to start the lesson that corresponds to the number.") } else { @@ -30,10 +42,12 @@ var lessonCmd = &cobra.Command{ if err != nil { util.LogErrorAndExit(err) } - for i := range lesson.Master.Lessons { - if lesson.Master.Lessons[i].Id == uint8(n) { - fmt.Println(lesson.Master.Lessons[i].Name) - } + if n > len(lesson.Master.Lessons) || n == 0 { + util.LogErrorAndExit(util.NonexistentLesson) + } + err = lesson.Master.Run(n, 0) + if err != nil { + util.LogErrorAndExit(err) } } }, diff --git a/cmd/read.go b/cmd/read.go index 99ba874..f00ad4c 100644 --- a/cmd/read.go +++ b/cmd/read.go @@ -7,22 +7,22 @@ import ( ) func init() { - readCmd.Hidden = true rootCmd.AddCommand(readCmd) } var readCmd = &cobra.Command{ Use: "read", Short: "Clears the lesson checkpoint", - Run: func(cmd *cobra.Command, args []string) { + RunE: func(cmd *cobra.Command, args []string) error { info, err := util.Load() if err != nil { - util.LogErrorAndExit(err) + return err } fmt.Printf( "Timestamp: %d\n"+ "LessonID: %d\n"+ "CheckptID: %d\n", info.Time, info.LessonId, info.CheckpointID) + return nil }, } diff --git a/cmd/root.go b/cmd/root.go index c93f074..0d6df1c 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -3,7 +3,6 @@ package cmd import ( "csclub-activities/util" "github.com/spf13/cobra" - "os" ) var rootCmd = &cobra.Command{ @@ -20,7 +19,5 @@ func Execute() { // https://github.com/fatih/color?tab=readme-ov-file#disableenable-colorx util.LogErrorAndExit(err) - - os.Exit(1) } } diff --git a/cmd/stop.go b/cmd/stop.go index 598cc2b..857016f 100644 --- a/cmd/stop.go +++ b/cmd/stop.go @@ -15,11 +15,12 @@ var stopCmd = &cobra.Command{ Use: "stop", Short: "Clears the lesson checkpoint", Aliases: []string{"clear", "new"}, - Run: func(cmd *cobra.Command, args []string) { + RunE: func(cmd *cobra.Command, args []string) error { err := util.NewSave() if err != nil { - util.LogErrorAndExit(err) + return err } fmt.Println("Checkpoint cleared") + return nil }, } diff --git a/go.mod b/go.mod index e938f3e..fb589f1 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,15 @@ module csclub-activities go 1.21.5 require ( - github.com/fatih/color v1.16.0 // indirect + github.com/fatih/color v1.16.0 + github.com/spf13/cobra v1.8.0 +) + +require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/spf13/cobra v1.8.0 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/spf13/pflag v1.0.5 // indirect - golang.org/x/sys v0.14.0 // indirect + golang.org/x/sys v0.15.0 // indirect ) diff --git a/go.sum b/go.sum index 83324cb..efd7e97 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ 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/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= @@ -17,5 +19,7 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/lesson/lesson.go b/lesson/lesson.go index 657ff09..917689f 100644 --- a/lesson/lesson.go +++ b/lesson/lesson.go @@ -1,6 +1,10 @@ package lesson -import "errors" +import ( + "csclub-activities/util" + "fmt" + "github.com/fatih/color" +) /* save checkpoint @@ -13,25 +17,45 @@ metadata: */ +func init() { + // Empty first struct to support the NoOngoingActivity functionality + l := []*Lesson{{}, lessonOne, lessonTwo} + for x := range l { + Master.Lessons = append(Master.Lessons, *l[x]) + } +} + var Master master type master struct { Lessons []Lesson } -func (m *master) append(newLesson *Lesson) error { - for i := range m.Lessons { - if m.Lessons[i].Id == newLesson.Id { - // here for future contributors - return errors.New("duplicate lesson ids") - } +func (m *master) Run(lesson int, checkpoint int) error { + + if m.Lessons == nil || + lesson >= len(m.Lessons) { + return util.NonexistentLesson + } + + if m.Lessons[lesson].Checkpoints == nil || + checkpoint >= len(m.Lessons[lesson].Checkpoints) { + return util.NonexistentCheckpoint + } + + for i := checkpoint; i < len(m.Lessons[lesson].Checkpoints); i++ { + + cp := m.Lessons[lesson].Checkpoints[i] + fmt.Printf("%-5s %s\n\n", + color.New(color.Bold).Add(color.FgGreen).Sprintf("%-2d/%-2d", i, len(m.Lessons)), + color.New(color.Bold).Sprintf("%s", cp.Title), + ) + cp.Action(uint8(lesson), uint8(checkpoint)) } - m.Lessons = append(m.Lessons, *newLesson) return nil } type Lesson struct { - Id uint8 Author string Name string Description string @@ -39,8 +63,14 @@ type Lesson struct { } type Checkpoint struct { - PreCondition func() bool // precondition could be previous checkpoint condition - Condition func() bool - Action func() - Position uint8 + Title string + Action func(lessonID uint8, checkpointID uint8) + // What is the condition that allows the user to go onto the next checkpoint? + ShouldPromote func() bool + ExpectedErrors []ExpectantError +} + +type ExpectantError struct { + MatchesError func() bool + Feedback string } diff --git a/lesson/one.go b/lesson/one.go index 0a24ee4..17ea2ad 100644 --- a/lesson/one.go +++ b/lesson/one.go @@ -1,16 +1,60 @@ package lesson -func init() { - err := Master.append(lessonOne) - if err != nil { - panic(err) - } -} +import ( + "csclub-activities/util" + "fmt" + "github.com/mitchellh/go-wordwrap" + "os" + "runtime" +) var lessonOne = &Lesson{ - Id: 1, Author: "Noah Mercedes", Name: "Introduction to the Terminal + Getting Started", Description: "Learn the basics of using the terminal! (todo desc)", - Checkpoints: nil, + Checkpoints: []Checkpoint{ + { + Title: "Getting Started", + Action: func(lessonID uint8, checkpointID uint8) { + defer func() { + err := util.Save(lessonID, checkpointID) + if err != nil { + util.LogErrorAndExit(err) + } + }() + + // TODO: find or develop a word wrapper. + + fmt.Println(wordwrap.WrapString( + "Welcome to the activities program! This program was designed to interactively learn concepts"+ + " within the terminal, as well as learning the terminal! To get started, first we'll learn basic"+ + " Linux-based terminal concepts through the shell program.", util.WordWrapLimit)) + + fmt.Println("\nFirst, we'll ensure that you're running a shell-based command-line interpreter.") + if runtime.GOOS == "windows" { + fmt.Println(wordwrap.WrapString( + "\nWindows users should consider installing a version of MSYS2, the easiest way to get started"+ + " with this is to use the Git Bash program from GitForWindows:\n"+ + "https://git-scm.com/download/win", util.WordWrapLimit)) + fmt.Println() + } + fmt.Println(wordwrap.WrapString( + "If you are running a non-windows based operating system, chances are that Git is already"+ + " installed; to verify, enter \"git\" into your shell and you should see a lengthy dialog.", + util.WordWrapLimit)) + fmt.Println() + fmt.Println( + "After installing Git, or to move on to the next checkpoint, simply re-run this program: `csa`") + + }, + ShouldPromote: func() bool { + switch os.Getenv("SHELL") { + case "/usr/bin/bash", "/usr/bin/zsh", "/usr/bin/sh": + return true + } + // for those who run other shells like fish, you could just use the env VALIDSHELL + return os.Getenv("VALIDSHELL") != "" + }, + }, + }, } diff --git a/lesson/two.go b/lesson/two.go index ad6daec..4e269ac 100644 --- a/lesson/two.go +++ b/lesson/two.go @@ -1,14 +1,6 @@ package lesson -func init() { - err := Master.append(lessonTwo) - if err != nil { - panic(err) - } -} - var lessonTwo = &Lesson{ - Id: 2, Author: "Noah Mercedes", Name: "Linux Filesystem Navigation and Manipulation", Description: "Let's ditch file explorer and become hackers* B)", diff --git a/main.go b/main.go index 86453a7..fa9f5a8 100644 --- a/main.go +++ b/main.go @@ -1,25 +1,27 @@ package main -import "csclub-activities/cmd" +import ( + "csclub-activities/cmd" + "csclub-activities/lesson" + "csclub-activities/util" +) func main() { - cmd.Execute() - - // Check if a checkpoint is active - //info, err := Load() - //if err != nil { - // if errors.Is(err, io.EOF) { - // err = NewSave() - // if err != nil { - // panic(err) - // } - // } else { - // panic(err) - // } - //} - - // LF checkpoint + // util.Load could return an empty or new information struct. + // new does not help us. + + information, err := util.Load() + if err != nil { + util.LogErrorAndExit(err) + } + + err = lesson.Master.Run(int(information.LessonId), int(information.CheckpointID)) + if err != nil { + util.LogErrorAndExit(err) + } else { + cmd.Execute() + } // commands during lesson // stop -> clears checkpoint diff --git a/util/cache.go b/util/cache.go index 828c35a..a49f1c4 100644 --- a/util/cache.go +++ b/util/cache.go @@ -2,7 +2,6 @@ package util import ( "encoding/binary" - "errors" "os" "time" ) @@ -22,8 +21,6 @@ var fileName = UserHomeDir() + string(os.PathSeparator) + ".csclub_activities" const NoOngoingActivity uint8 = 0 -var ErrReservedIDs = errors.New("reserved lessonID or checkpointID value: cannot be 0") - func openCache() (*os.File, error) { // 0666 -> r/w for all users return os.OpenFile(fileName, os.O_CREATE|os.O_RDWR, 0666) diff --git a/util/errors.go b/util/errors.go new file mode 100644 index 0000000..71960ed --- /dev/null +++ b/util/errors.go @@ -0,0 +1,9 @@ +package util + +import "errors" + +var ( + NonexistentLesson = errors.New("impossible index: nonexistent lesson") + NonexistentCheckpoint = errors.New("impossible index: nonexistent checkpoint") + ErrReservedIDs = errors.New("reserved lessonID or checkpointID value: cannot be 0") +) diff --git a/util/util.go b/util/util.go index 5835a6a..713baab 100644 --- a/util/util.go +++ b/util/util.go @@ -6,6 +6,8 @@ import ( "runtime" ) +const WordWrapLimit = 100 + // UserHomeDir https://github.com/spf13/viper/blob/e36638d8786b0b58231039fc6d7db32b904dd1ba/util.go#L140 func UserHomeDir() string { if //goland:noinspection GoBoolExpressions