Skip to content

Commit d2ded76

Browse files
authored
feat: add projects to cli tool (#7)
2 parents 4ff648d + 32172ad commit d2ded76

35 files changed

Lines changed: 1103 additions & 143 deletions

File tree

README.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
# GoLearn
2-
3-
Rustlings‑style Go exercises in a tiny CLI.
1+
<p align="center">
2+
<img align="center" width="20%" src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/93hg76euzrulrc59x36s.png" alt="logo"/>
3+
<h1 align="center">GoLearn</h1>
4+
<h6 align="center">Rustlings‑style Go exercises in a tiny CLI.</h6>
5+
</p>
46

57
## Why this exists
68

@@ -62,4 +64,6 @@ make watch
6264
- Non-code lesson content and any included gopher artwork: [CC BY 3.0](./CONTENT_LICENSE).
6365
- Inspired by rustlings and Go by Example. [NOTICE](./NOTICE) for attributions.
6466

65-
Built with love for learning and knowledge sharing.
67+
<div align="center">
68+
<img src="https://img.shields.io/badge/Built%20with%20%E2%9D%A4%EF%B8%8F-for%20learning%20and%20knowledge%20sharing-blueviolet"/>
69+
</div>

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,6 @@ require (
1111
require (
1212
github.com/davecgh/go-spew v1.1.1 // indirect
1313
github.com/pmezard/go-difflib v1.0.0 // indirect
14+
github.com/stretchr/testify v1.11.0 // indirect
1415
golang.org/x/sys v0.4.0 // indirect
1516
)

internal/cli/commands.go

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,15 @@ import (
2121
)
2222

2323
func runList() error {
24-
items, err := exercises.List()
24+
cat, err := exercises.ListAll()
2525
if err != nil {
2626
return err
2727
}
28-
if h := theme.Heading("Exercises"); h != "" {
28+
29+
if h := theme.Heading("Concepts"); h != "" {
2930
fmt.Println(h)
3031
}
31-
for _, ex := range items {
32+
for _, ex := range cat.Concepts {
3233
status := "pending"
3334
done, _ := progress.IsCompleted(ex.Slug)
3435
if done {
@@ -39,6 +40,22 @@ func runList() error {
3940
}
4041
fmt.Printf("%s - %s [%s]\n", ex.Slug, ex.Title, status)
4142
}
43+
44+
if h := theme.Heading("Projects"); h != "" {
45+
fmt.Println(h)
46+
}
47+
for _, ex := range cat.Projects {
48+
status := "pending"
49+
done, _ := progress.IsCompleted(ex.Slug)
50+
if done {
51+
status = theme.Success("done")
52+
}
53+
if !done {
54+
status = theme.Muted(status)
55+
}
56+
fmt.Printf("%s - %s [%s]\n", ex.Slug, ex.Title, status)
57+
}
58+
4259
return nil
4360
}
4461

@@ -50,12 +67,18 @@ func runVerify(name string) error {
5067
}
5168
return verifyOne(ex)
5269
}
53-
items, err := exercises.List()
70+
71+
cat, err := exercises.ListAll()
5472
if err != nil {
5573
return err
5674
}
75+
76+
var allExercises []exercises.Exercise
77+
allExercises = append(allExercises, cat.Concepts...)
78+
allExercises = append(allExercises, cat.Projects...)
79+
5780
var anyFailed bool
58-
for _, ex := range items {
81+
for _, ex := range allExercises {
5982
if err := verifyOne(ex); err != nil {
6083
anyFailed = true
6184
}
@@ -204,14 +227,19 @@ func runWatch() error {
204227
}
205228

206229
func runProgress() error {
207-
items, err := exercises.List()
230+
cat, err := exercises.ListAll()
208231
if err != nil {
209232
return err
210233
}
211-
sort.Slice(items, func(i, j int) bool { return items[i].Slug < items[j].Slug })
234+
235+
var allExercises []exercises.Exercise
236+
allExercises = append(allExercises, cat.Concepts...)
237+
allExercises = append(allExercises, cat.Projects...)
238+
239+
sort.Slice(allExercises, func(i, j int) bool { return allExercises[i].Slug < allExercises[j].Slug })
212240
doneCount := 0
213-
statuses := make([]bool, len(items))
214-
for i, ex := range items {
241+
statuses := make([]bool, len(allExercises))
242+
for i, ex := range allExercises {
215243
done, _ := progress.IsCompleted(ex.Slug)
216244
statuses[i] = done
217245
if done {
@@ -226,13 +254,24 @@ func runProgress() error {
226254
fmt.Println(theme.Heading("GoLearn Progress Dashboard"))
227255
fmt.Println(strings.Repeat("=", 26))
228256
width := progressBarWidth()
229-
fmt.Printf("\nCompleted: %d/%d\n", doneCount, len(items))
230-
fmt.Println(renderProgressBar(doneCount, len(items), width))
257+
fmt.Printf("\nCompleted: %d/%d\n", doneCount, len(allExercises))
258+
fmt.Println(renderProgressBar(doneCount, len(allExercises), width))
231259
fmt.Println()
232-
fmt.Println(theme.Emph("Exercises:"))
233-
for i, ex := range items {
260+
fmt.Println(theme.Emph("Concepts:"))
261+
for _, ex := range cat.Concepts {
234262
box := "[ ]"
235-
if statuses[i] {
263+
done, _ := progress.IsCompleted(ex.Slug)
264+
if done {
265+
box = theme.Success("[x]")
266+
}
267+
fmt.Printf(" %s %s - %s\n", box, ex.Slug, ex.Title)
268+
}
269+
fmt.Println()
270+
fmt.Println(theme.Emph("Projects:"))
271+
for _, ex := range cat.Projects {
272+
box := "[ ]"
273+
done, _ := progress.IsCompleted(ex.Slug)
274+
if done {
236275
box = theme.Success("[x]")
237276
}
238277
fmt.Printf(" %s %s - %s\n", box, ex.Slug, ex.Title)

internal/exercises/catalog.yaml

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
concepts:
12
- slug: 01_hello
23
title: Hello, Go!
34
test_regex: ".*"
@@ -134,3 +135,45 @@
134135
hints:
135136
- Define a custom error type and return it from a function.
136137

138+
projects:
139+
- slug: 28_text_analyzer
140+
title: Text Analyzer (Easy)
141+
test_regex: ".*"
142+
hints:
143+
- Implement functions to count characters, words, and unique words in a given text.
144+
- slug: 29_shape_calculator
145+
title: Shape Area Calculator (Medium)
146+
test_regex: ".*"
147+
hints:
148+
- Define structs for different shapes, implement methods to calculate their areas, and use an interface for common shape behavior.
149+
- slug: 30_task_scheduler
150+
title: Task Scheduler (Hard)
151+
test_regex: ".*"
152+
hints:
153+
- Create a task scheduler with features like adding/removing tasks, a custom iterator, closures for task execution, and custom error handling.
154+
- slug: 31_http_server
155+
title: HTTP Server (Easy)
156+
test_regex: ".*"
157+
hints:
158+
- Implement a basic HTTP server that responds to GET requests.
159+
- slug: 32_cli_todo_list
160+
title: CLI Todo List (Medium)
161+
test_regex: ".*"
162+
hints:
163+
- Build a command-line tool to manage a todo list, including adding, listing, and completing tasks.
164+
- slug: 33_simple_chat_app
165+
title: Simple Chat Application (Medium)
166+
test_regex: ".*"
167+
hints:
168+
- Create a basic client-server chat application.
169+
- slug: 34_image_processing_utility
170+
title: Image Processing Utility (Hard)
171+
test_regex: ".*"
172+
hints:
173+
- Develop a command-line utility for basic image transformations like resizing or converting to grayscale.
174+
- slug: 35_basic_key_value_store
175+
title: Basic Key-Value Store (Hard)
176+
test_regex: ".*"
177+
hints:
178+
- Implement an in-memory key-value store with basic CRUD operations and optional persistence.
179+

internal/exercises/exercises.go

Lines changed: 44 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -25,35 +25,44 @@ type Exercise struct {
2525
Hints []string `yaml:"hints"`
2626
}
2727

28+
type Catalog struct {
29+
Concepts []Exercise `yaml:"concepts"`
30+
Projects []Exercise `yaml:"projects"`
31+
}
32+
2833
var (
2934
catalogOnce sync.Once
30-
catalogData []Exercise
35+
catalogData Catalog
3136
)
3237

33-
func catalog() []Exercise {
38+
func catalog() Catalog {
3439
catalogOnce.Do(func() {
3540
b, err := catalogFS.ReadFile("catalog.yaml")
3641
if err != nil {
3742
// Fallback minimal catalog
38-
catalogData = []Exercise{{
39-
Slug: "01_hello",
40-
Title: "Hello, Go!",
41-
TestRegex: ".*",
42-
Hints: []string{"Implement Hello() to return 'Hello, Go!'"},
43-
}}
43+
catalogData = Catalog{
44+
Concepts: []Exercise{{
45+
Slug: "01_hello",
46+
Title: "Hello, Go!",
47+
TestRegex: ".*",
48+
Hints: []string{"Implement Hello() to return 'Hello, Go!'"},
49+
}},
50+
}
4451
return
4552
}
46-
var items []Exercise
47-
if err := yaml.Unmarshal(b, &items); err != nil || len(items) == 0 {
48-
catalogData = []Exercise{{
49-
Slug: "01_hello",
50-
Title: "Hello, Go!",
51-
TestRegex: ".*",
52-
Hints: []string{"Implement Hello() to return 'Hello, Go!'"},
53-
}}
53+
var cat Catalog
54+
if err := yaml.Unmarshal(b, &cat); err != nil {
55+
catalogData = Catalog{
56+
Concepts: []Exercise{{
57+
Slug: "01_hello",
58+
Title: "Hello, Go!",
59+
TestRegex: ".*",
60+
Hints: []string{"Implement Hello() to return 'Hello, Go!'"},
61+
}},
62+
}
5463
return
5564
}
56-
catalogData = items
65+
catalogData = cat
5766
})
5867
return catalogData
5968
}
@@ -81,23 +90,31 @@ func discoverLocal() ([]Exercise, error) {
8190
return items, nil
8291
}
8392

84-
func List() ([]Exercise, error) {
93+
func ListAll() (Catalog, error) {
8594
locals, err := discoverLocal()
8695
if err != nil {
87-
return nil, err
96+
return Catalog{}, err
8897
}
98+
8999
if len(locals) > 0 {
90-
return locals, nil
100+
// For simplicity, if local exercises are present, we'll only return them for now.
101+
// A more robust solution might merge local and catalog exercises.
102+
return Catalog{Concepts: locals}, nil
91103
}
92104
return catalog(), nil
93105
}
94106

95107
func Get(slug string) (Exercise, error) {
96-
for _, ex := range catalog() {
108+
for _, ex := range catalog().Concepts {
97109
if ex.Slug == slug {
98110
return ex, nil
99111
}
100112
}
113+
for _, ex := range catalog().Projects {
114+
if ex.Slug == slug {
115+
return ex, nil
116+
}
117+
}
101118
locals, err := discoverLocal()
102119
if err != nil {
103120
return Exercise{}, err
@@ -125,7 +142,12 @@ func templateExists(slug string) bool {
125142
}
126143

127144
func InitAll() error {
128-
for _, ex := range catalog() {
145+
for _, ex := range catalog().Concepts {
146+
if err := copyExerciseTemplate(ex.Slug); err != nil {
147+
return err
148+
}
149+
}
150+
for _, ex := range catalog().Projects {
129151
if err := copyExerciseTemplate(ex.Slug); err != nil {
130152
return err
131153
}
Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,14 @@
11
package closures
22

3-
import "fmt"
3+
// TODO:
4+
// - Implement a closure function that captures state and returns a func() int
5+
// which increments and returns an internal counter on each call.
6+
// - Each call to closure() must produce an independent counter.
7+
// - Do not change function names or signatures; tests call closure().
48

5-
func closure() func() int {
6-
i := 0
7-
return func() int {
8-
i++
9-
return i
10-
}
11-
}
9+
// NOTE: Keep imports minimal in skeletons to avoid unused import errors.
1210

13-
func main() {
14-
next := closure()
15-
fmt.Println(next())
16-
fmt.Println(next())
17-
fmt.Println(next())
18-
19-
anotherNext := closure()
20-
fmt.Println(anotherNext())
21-
fmt.Println(anotherNext())
11+
func closure() func() int {
12+
// TODO: implement a counting closure
13+
return func() int { return 0 }
2214
}
Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package recursion
22

3+
// TODO:
4+
// - Implement factorial using recursion.
5+
// - Handle the base case: factorial(0) == 1.
6+
// - Keep the signature the same; tests call factorial(n).
7+
38
func factorial(n int) int {
4-
if n == 0 {
5-
return 1
6-
}
7-
return n * factorial(n-1)
9+
// TODO: implement recursive factorial
10+
return 0
811
}
Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
11
package range_built_in
22

3+
// TODO:
4+
// - Use range to iterate over slices and maps.
5+
// - sumSlice: return the sum of all elements in the slice.
6+
// - countMap: return the number of key/value pairs in the map.
7+
38
func sumSlice(s []int) int {
4-
sum := 0
5-
for _, v := range s {
6-
sum += v
7-
}
8-
return sum
9+
// TODO: implement summation with range
10+
return 0
911
}
1012

1113
func countMap(m map[string]int) int {
12-
count := 0
13-
for range m {
14-
count++
15-
}
16-
return count
14+
// TODO: count items in map using range
15+
return 0
1716
}
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
package pointers
22

3+
// TODO:
4+
// - Modify the value pointed to by the incoming *int.
5+
// - Return the same pointer after updating the underlying value.
6+
// - Avoid allocating any new memory; operate in place.
7+
38
func modifyValue(ptr *int) *int {
4-
*ptr = 100
9+
// TODO: implement mutation via pointer
510
return ptr
611
}

0 commit comments

Comments
 (0)