Download
293 lines 8.5 KiBTomoModified 2025-12-22 raw
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
# This is the Tomo koans program

_HELP := "tomo koans - A tutorial program for learning the Tomo programming language"

use colorful
use shell
use commands

editor := "vim"
user : Text?

LESSONS := [
    Lesson((./lessons/lesson-01-hello-world.tm), "Hello World", "Hello world\n"),
    Lesson((./lessons/lesson-02-tests.tm), "Testing Code"),
    Lesson((./lessons/lesson-03-variables.tm), "Variables"),
    Lesson((./lessons/lesson-04-functions.tm), "Functions"),
    Lesson((./lessons/lesson-05-basic-types.tm), "Basic Types"),
    Lesson((./lessons/lesson-06-arrays.tm), "Arrays"),
    Lesson((./lessons/lesson-07-optionals.tm), "Optionals"),
    Lesson((./lessons/lesson-08-tables.tm), "Tables"),
    Lesson((./lessons/lesson-09-text.tm), "Text"),
    Lesson((./lessons/lesson-10-structs.tm), "Structs"),
    Lesson((./lessons/lesson-11-enums.tm), "Enums"),
    Lesson((./lessons/lesson-12-allocating.tm), "Allocating Memory"),
    Lesson((./lessons/lesson-13-paths.tm), "File Paths"),
    Lesson((./lessons/lesson-14-langs.tm), "Embedded Languages"),
    Lesson((./lessons/lesson-15-min-max.tm), "Min and Max"),
    Lesson((./lessons/lesson-16-reducers.tm), "Reducers"),
]

enum TestResult(NotRun, Success(output:Text), Error(err:Text), WrongOutput(actual:Text, expected:Text))
    func print(result:TestResult)
        when result is NotRun
            pass
        is Success(s)
            $Colorful"
                @(b,u:Program Output:)
                @(green:$s)
            ".print()
        is Error(e)
            $Colorful"
                @(b,u:Program Errors:)

                $e
            ".print()
        is WrongOutput(actual, expected)
            $Colorful"
                @(b,u:Program Output:)
                @(red:$actual)
                @(b,u:Expected:)
                @(green:$expected)
            ".print()

    func is_success(result:TestResult -> Bool)
        when result is Success return yes
        else return no

struct Lesson(file:Path, description:Text, expected_output:Text?=none)
    func get_result(l:Lesson -> TestResult)
        result := $Shell"COLOR=1 tomo -O 0 $(l.file)".result()
        if not result.succeeded()
            return Error(Text.from_utf8(result.errors)!)

        output := Text.from_utf8(result.output)!
        if expected := l.expected_output
            if output != expected
                return WrongOutput(output, expected)

        return Success(output)

func ask_continue()
    _ := ask("\[2]Press Enter to continue...\[]", bold=no)

func clear_screen()
    say("\x1b[2J\x1b[H", newline=no)

func summarize_tests(results:[TestResult], highlight:Path?=none)
    $Colorful"
        
        @(yellow,b,u:Lessons)
    ".print()

    passing := 0
    failing := 0
    for i,lesson in LESSONS
        when results[i]! is Success
            passing += 1
            $Colorful"
                @(green,bold:$(Text(i).left_pad(2)): "$(lesson.description)" (passes))
            ".print()
        is NotRun
            failing += 1
            $Colorful"
                @(dim:$(Text(i).left_pad(2)): "$(lesson.description)" (not yet attempted))
            ".print()
        else
            failing += 1
            $Colorful"
                @(red:$(Text(i).left_pad(2)): "$(lesson.description)" (failing))
            ".print()

    completed := Num(passing)/Num(passing+failing)
    $Colorful"

        @(cyan,b:Progress: $(completed.percent()))

    ".print()

func short_summarize_tests(results:[TestResult])
    say("Progress: ", newline=no)
    for result in results
        when result is Success
            $Colorful"@(green,bold:#)".print(newline=no)
        is NotRun
            $Colorful"@(dim:#)".print(newline=no)
        else
            $Colorful"@(red:#)".print(newline=no)

    say("\n")

func choose_option(options:{Text:Text} -> Text)
    repeat
        for k,v in options
            $Colorful"
                @(b:($k)) $v
            ".print()
        say("")
        choice := (ask("Choose an option: ") or goodbye()).lower().to(1)
        if options.has(choice)
            return choice
        else if choice == "q"
            goodbye()
        else
            $Colorful"
                @(red:I'm sorry, I don't recognize that choice, please try again!")
            ".print()
    fail("Unreachable")

func show_lesson(lesson:Lesson, result:TestResult)
    clear_screen()
    $Colorful"

        @(yellow,b,u:$(lesson.description))
        Here's what we have right now:

    ".print()

    result.print()
    when result is Success
        $Colorful"
            @(green,b:✨ Great job, this test is passing! ✨)

        ".print()
    is NotRun
        $Colorful"
            @(dim,italic:...nothing, edit the file to make your first attempt...)

        ".print()
    else
        $Colorful"
            @(red,b:Looks like this test isn't passing yet! 😢)

        ".print()

func goodbye(-> Abort)
    clear_screen()
    $Colorful"

        @(b:Goodbye! Come back again soon!)

    ".print()
    exit(code=0)

func give_feedback(section:Text)
    clear_screen()
    user = user or ask("What name do go by? (optional) ") or "anon"
    if user == "" then user = "anon"
    feedback := ask("What's your feedback? ") or return
    (./feedback.txt).append("
        [$(user!)/$section] $feedback\n
    ")!

func main(clean=no -> Abort)
    clear_screen()
    $Colorful"
        
        @(bold,green:Hello and welcome to the Tomo Koans program!)

        We're going to run through some programs that don't work
        and you'll be fixing them up!

        I hope you have fun!

    ".print()

    if clean
        (./editor.txt).remove(ignore_missing=yes)!
        (./lessons).remove(ignore_missing=yes)!

    if (./editor.txt).exists()
        editor = (./editor.txt).read()!
        $Colorful"
            @(dim,i:You're using @(green:$(editor)) as your text editor. If you want to change it, just edit @(magenta:./editor.txt))

        ".print()
    else
        editor = ask("
            What command line text editor do you want to use?

                Ones with syntax highlighting: vim, nvim, emacs
                Simpler editors: nano, pico, edit

            editor:$(" ")
        ") or goodbye()
        while editor == "" or not $Shell"command -v $editor >/dev/null".run().succeeded()
            editor = ask("I don't recognize that editor. Try again? ") or goodbye()
        (./editor.txt).write(editor)!
        $Colorful"

            Great! From now on, I'll use @(b:$(editor)) to edit files.
            If you want to change it, just edit @(magenta:./editor.txt)

        ".print()

    $Shell"
        cp -r lesson-templates lessons
    ".run().or_fail("Could not make lessons directory")

    test_results := &[TestResult.NotRun for l in LESSONS]

    ask_continue()
    message_pending := no
    repeat
        unless message_pending
            clear_screen()
        message_pending = no

        summarize_tests(test_results)
        choice := ask("Choose a test, (q)uit, or give (f)eedback: ") or stop repeat
        stop repeat if choice == "q" or choice == "Q"

        if choice == "f" or choice == "F"
            give_feedback("menu")
            skip

        if choice == ""
            for i,result in test_results
                if not result.is_success()
                    choice = Text(i)
                    stop

        n := Int.parse(choice) or (do
            $Colorful"@(red:I don't know what that means! Type a test number or 'q'.)".print()
            message_pending = yes
            skip repeat
        )

        if n < 1 or n > LESSONS.length
            $Colorful"@(red:That's not a valid test number!)".print()
            message_pending = yes
            skip repeat

        repeat
            lesson := LESSONS[n]!
            show_lesson(lesson, test_results[n]!)
            short_summarize_tests(test_results)

            options := &{
                "e": "Edit file and try again",
                "l": "Show the lesson list",
                "q": "Quit",
                "f": "Give Feedback",
            }
            if n < LESSONS.length
                options["n"] = "Go to the next lesson"

            when choose_option(options) is "e"
                $Shell"
                    $(editor) $(lesson.file)
                ".run().or_fail("Could not open editor $(editor)")

                test_results[n] = lesson.get_result()
                test_results[n]!.print()
            is "l"
                stop
            is "n"
                n += 1
            is "q"
                goodbye()
            is "f"
                give_feedback("lesson$n")

    goodbye()