Appendix B: Full source listings
The complete, verified Muninn project. These files are the destination Chapters 9 through 12 build toward; the copy in code/ is identical. Type-checks with odin check ., builds with odin build . -o:speed, runs as a single native binary. One package main, seven files at the project root.
Build and run:
cd code
./run.sh # odin run .
./build.sh # optimised release binary
main.odin
package main
import "core:bufio"
import "core:os"
main :: proc() {
enable_ansi()
store := load_notes()
scanner: bufio.Scanner
bufio.scanner_init(&scanner, os.to_reader(os.stdin))
defer bufio.scanner_destroy(&scanner)
run_menu(&store, &scanner)
}
note.odin
package main
Note :: struct {
id: int,
title: string,
body: string,
created_at: int, // an incrementing counter, not a real timestamp
}
store.odin
package main
import "core:strings"
Store :: struct {
notes: [dynamic]Note,
next_id: int,
}
add_note :: proc(store: ^Store, title, body: string) -> int {
store.next_id += 1
id := store.next_id
append(&store.notes, Note{
id = id,
title = strings.clone(title),
body = strings.clone(body),
created_at = id,
})
return id
}
find_note :: proc(store: ^Store, id: int) -> (Note, bool) {
for n in store.notes {
if n.id == id {
return n, true
}
}
return {}, false
}
delete_note :: proc(store: ^Store, id: int) -> bool {
for n, i in store.notes {
if n.id == id {
unordered_remove(&store.notes, i)
return true
}
}
return false
}
list_notes :: proc(store: ^Store) -> []Note {
return store.notes[:]
}
search_notes :: proc(store: ^Store, query: string, allocator := context.allocator) -> [dynamic]Note {
results: [dynamic]Note
results.allocator = allocator
lower_query := strings.to_lower(query)
defer delete(lower_query)
for n in store.notes {
lower_title := strings.to_lower(n.title)
defer delete(lower_title)
if strings.contains(lower_title, lower_query) {
append(&results, n)
}
}
return results
}
screen.odin
package main
import "core:bufio"
import "core:fmt"
import "core:strings"
import win32 "core:sys/windows"
when ODIN_OS == .Windows {
enable_ansi :: proc() {
handle := win32.GetStdHandle(win32.STD_OUTPUT_HANDLE)
mode: win32.DWORD
win32.GetConsoleMode(handle, &mode)
win32.SetConsoleMode(handle, mode | win32.ENABLE_VIRTUAL_TERMINAL_PROCESSING)
}
} else {
enable_ansi :: proc() {}
}
clear_screen :: proc() {
fmt.print("\x1b[2J\x1b[H")
}
move_cursor :: proc(row, col: int) {
fmt.printf("\x1b[%d;%dH", row, col)
}
Colour :: enum {
Default,
Red,
Green,
Yellow,
Cyan,
}
colour_code :: proc(c: Colour) -> int {
switch c {
case .Default: return 39
case .Red: return 31
case .Green: return 32
case .Yellow: return 33
case .Cyan: return 36
}
return 39
}
set_colour :: proc(c: Colour) {
fmt.printf("\x1b[%dm", colour_code(c))
}
reset_colour :: proc() {
fmt.print("\x1b[0m")
}
draw_box :: proc(width, height: int) {
b: strings.Builder
strings.builder_init(&b)
defer strings.builder_destroy(&b)
strings.write_rune(&b, 'โ')
for _ in 0 ..< width-2 {
strings.write_rune(&b, 'โ')
}
strings.write_rune(&b, 'โ')
strings.write_byte(&b, '\n')
for _ in 0 ..< height-2 {
strings.write_rune(&b, 'โ')
for _ in 0 ..< width-2 {
strings.write_byte(&b, ' ')
}
strings.write_rune(&b, 'โ')
strings.write_byte(&b, '\n')
}
strings.write_rune(&b, 'โ')
for _ in 0 ..< width-2 {
strings.write_rune(&b, 'โ')
}
strings.write_rune(&b, 'โ')
fmt.println(strings.to_string(b))
}
banner :: proc() {
set_colour(.Cyan)
fmt.println("โโโโโโโโโโโโโโโโโโโโ")
fmt.println("โ MUNINN โ")
fmt.println("โโโโโโโโโโโโโโโโโโโโ")
reset_colour()
}
press_enter_to_continue :: proc(scanner: ^bufio.Scanner) {
fmt.print("Press Enter to continue...")
read_line(scanner)
}
menu.odin
package main
import "core:bufio"
import "core:fmt"
print_main_menu :: proc() {
fmt.println("1. Add a note")
fmt.println("2. List notes")
fmt.println("3. Search notes")
fmt.println("4. Delete a note")
fmt.println("Q. Quit")
fmt.print("Choose: ")
}
print_notes_table :: proc(notes: []Note) {
if len(notes) == 0 {
fmt.println("(no notes yet)")
return
}
for n in notes {
fmt.printfln("%4d %-20s %s", n.id, n.title, n.body)
}
}
do_add :: proc(store: ^Store, scanner: ^bufio.Scanner) {
fmt.print("Title: ")
title, title_ok := read_line(scanner)
if !title_ok {
return
}
fmt.print("Body: ")
body, body_ok := read_line(scanner)
if !body_ok {
return
}
id := add_note(store, title, body)
save_notes(store)
fmt.printfln("Added note %d.", id)
}
do_list :: proc(store: ^Store) {
print_notes_table(list_notes(store))
}
do_search :: proc(store: ^Store, scanner: ^bufio.Scanner) {
fmt.print("Search title for: ")
query, ok := read_line(scanner)
if !ok {
return
}
results := search_notes(store, query)
defer delete(results)
print_notes_table(results[:])
}
do_delete :: proc(store: ^Store, scanner: ^bufio.Scanner) {
print_notes_table(list_notes(store))
if len(store.notes) == 0 {
return
}
id, ok := read_int_in_range(scanner, "Delete which id? ", 1, store.next_id)
if !ok {
return
}
n, found := find_note(store, id)
if !found {
fmt.println("No note with that id.")
return
}
fmt.printfln("Delete \"%s\"? (y/n): ", n.title)
confirm, confirm_ok := read_line(scanner)
if !confirm_ok || confirm != "y" {
fmt.println("Cancelled.")
return
}
delete_note(store, id)
save_notes(store)
fmt.println("Deleted.")
}
run_menu :: proc(store: ^Store, scanner: ^bufio.Scanner) {
for {
clear_screen()
banner()
fmt.println()
print_main_menu()
choice, ok := read_line(scanner)
if !ok {
return
}
switch choice {
case "1":
do_add(store, scanner)
case "2":
do_list(store)
case "3":
do_search(store, scanner)
case "4":
do_delete(store, scanner)
case "Q", "q":
reset_colour()
fmt.println("Goodbye!")
return
case:
fmt.println("Not a valid choice.")
}
press_enter_to_continue(scanner)
}
}
persist.odin
package main
import "core:fmt"
import "core:os"
import "core:strconv"
import "core:strings"
SAVE_FILE :: "muninn_notes.txt"
FIELD_SEP :: "\x1f"
encode_note :: proc(n: Note, b: ^strings.Builder) {
strings.write_int(b, n.id)
strings.write_string(b, FIELD_SEP)
strings.write_int(b, n.created_at)
strings.write_string(b, FIELD_SEP)
strings.write_string(b, n.title)
strings.write_string(b, FIELD_SEP)
strings.write_string(b, n.body)
strings.write_byte(b, '\n')
}
decode_note :: proc(line: string) -> (n: Note, ok: bool) {
parts := strings.split(line, FIELD_SEP)
defer delete(parts)
if len(parts) != 4 {
return {}, false
}
id, id_ok := strconv.parse_int(parts[0])
created_at, ca_ok := strconv.parse_int(parts[1])
if !id_ok || !ca_ok {
return {}, false
}
return Note{
id = id,
created_at = created_at,
title = strings.clone(parts[2]),
body = strings.clone(parts[3]),
}, true
}
save_notes :: proc(store: ^Store) {
b: strings.Builder
strings.builder_init(&b)
defer strings.builder_destroy(&b)
for n in store.notes {
encode_note(n, &b)
}
err := os.write_entire_file(SAVE_FILE, transmute([]byte)strings.to_string(b))
if err != nil {
fmt.eprintfln("could not save notes: %v", err)
}
}
load_notes :: proc() -> Store {
store: Store
data, err := os.read_entire_file_from_path(SAVE_FILE, context.allocator)
if err != nil {
return store // missing file -- start empty, not a crash
}
defer delete(data)
skipped := 0
for line in strings.split_lines(string(data)) {
if len(line) == 0 {
continue
}
n, ok := decode_note(line)
if !ok {
skipped += 1
continue
}
append(&store.notes, n)
if n.id > store.next_id {
store.next_id = n.id
}
}
if skipped > 0 {
fmt.eprintfln("warning: skipped %d unreadable line(s) in %s", skipped, SAVE_FILE)
}
return store
}
input.odin
package main
import "core:bufio"
import "core:fmt"
import "core:strconv"
import "core:strings"
read_line :: proc(scanner: ^bufio.Scanner) -> (line: string, ok: bool) {
if !bufio.scanner_scan(scanner) {
return "", false
}
return strings.trim_space(bufio.scanner_text(scanner)), true
}
// read_int_in_range loops until the user types a whole number between lo and hi
// inclusive, or input runs out entirely.
read_int_in_range :: proc(scanner: ^bufio.Scanner, prompt: string, lo, hi: int) -> (value: int, ok: bool) {
for {
fmt.print(prompt)
line, has_line := read_line(scanner)
if !has_line {
return 0, false
}
n, parsed := strconv.parse_int(line)
if !parsed {
fmt.println("Please enter a whole number.")
continue
}
if n < lo || n > hi {
fmt.printfln("Please enter a number between %d and %d.", lo, hi)
continue
}
return n, true
}
}