package main import "core:flags" import "core:fmt" import "core:strconv" import "core:strings" import "core:time" import "../core" Command :: enum { Usage, Add, Symptom, Event, Today, Week, Show, Coffee, Events, Version, } Command_Func :: distinct proc(args: []string, store: ^core.Storage) Command_Data := [Command]struct { func: Command_Func, desc: string, } { .Usage = {func = command_usage, desc = "this screen"}, .Add = {func = command_add, desc = "log today's daily entry"}, .Symptom = {func = command_symptom, desc = "note a symptom"}, .Coffee = {func = command_coffee, desc = "alias to note a cup of coffee"}, .Event = {func = command_event, desc = "log a timestamped event"}, .Today = {func = command_today, desc = "show today"}, .Week = {func = command_week, desc = "show current week"}, .Show = {func = command_show, desc = "show a specific date"}, .Events = {func = command_events, desc = "list events"}, .Version = {func = command_version, desc = "print version"}, } command_from_string :: proc(cmd_str: string) -> Command { cmd: Command switch cmd_str { case "usage": cmd = .Usage case "add": cmd = .Add case "symptom": cmd = .Symptom case "coffee": cmd = .Coffee case "event": cmd = .Event case "today": cmd = .Today case "week": cmd = .Week case "show": cmd = .Show case "events": cmd = .Events case "version": cmd = .Version } return cmd } command_usage :: proc(args: []string, store: ^core.Storage) { fmt.printf("%s๐ŸงŒ Data Goblin%s v%s\n\n", core.BOLD, core.RESET, "0.1") fmt.printf(" %shoarding tiny truths since 2026%s\n", core.DIM, core.RESET) fmt.printf(" %sThe goblin stares at you. You forgot to say what you want.%s\n\n", core.DIM, core.RESET) fmt.printf(" Available commands:\n\n") for data, cmd in Command_Data { fmt.printf( " %s%-14s%s %s%s%s\n", core.GREEN, strings.to_lower(fmt.tprintf("%s", cmd), context.temp_allocator), core.RESET, core.DIM, data.desc, core.RESET, ) } fmt.printf("\n Usage: %sdgob [flags]%s\n\n", core.YELLOW, core.RESET) } Init_Options :: struct { db: string `usage:"path to sqlite-file to use (defaults to ./data.db)"`, } validate_ranges :: proc(model: rawptr, name: string, value: any, args_tag: string) -> string { switch name { case "mood", "sleep_quality": if n, ok := value.(int); ok { if n < 1 || n > 10 { return "must be between 1 and 10" } } case "sleep": if n, ok := value.(f64); ok { if n < 0 || n > 24 { return "must be between 0.0 and 24.0" } } } return "" } Add_Options :: struct { mood: int `args:"required" usage:"mood of the day, 1-10"`, sleep: f64 `args:"required" usage:"the amount of slept hours 0.0-24.0"`, sleep_quality: int `args:"required" usage:"the previous night's sleep quality, 1-10"`, date: time.Time `usage:"date to record this data to (defaults to today)"`, notes: string `usage:"any extra notes for the day"`, } command_add :: proc(args: []string, store: ^core.Storage) { opt: Add_Options flags.register_flag_checker(validate_ranges) flags.parse_or_exit(&opt, args, .Unix) if time.time_to_unix(opt.date) == 0 { opt.date = time.now() } // FIXME: remove timezone offset if err := core.storage_add_to_daily_log(store^, opt.mood, opt.sleep, opt.sleep_quality, opt.date, opt.notes); err != nil { fmt.eprintf("failed adding log: %v\n", err) } tz_date := core.new_time_with_timezone_offset(opt.date) buf: [time.MIN_YYYY_DATE_LEN]u8 fmt.printf( " hoarded: daily log for %s %sโœ“%s\n", time.to_string_yyyy_mm_dd(tz_date, buf[:]), core.GREEN, core.RESET, ) } Symptom_Options :: struct { symptom: string `args:"required" usage:"felt symptom"`, severity: int `args:"required" usage:"the severity of the symptom, 1-10"`, date: time.Time `usage:"date and time to record this data to (defaults to now, RFC 3339)"`, notes: string `usage:"any extra notes for the day"`, } command_symptom :: proc(args: []string, store: ^core.Storage) { opt: Symptom_Options flags.register_flag_checker(validate_ranges) flags.parse_or_exit(&opt, args, .Unix) if time.time_to_unix(opt.date) == 0 { opt.date = time.now() } if err := core.storage_add_to_symptoms(store^, opt.symptom, opt.severity, opt.date, opt.notes); err != nil { fmt.eprintf("failed adding symptom: %v\n", err) } tz_date := core.new_time_with_timezone_offset(opt.date) date_buf: [time.MIN_YYYY_DATE_LEN]u8 time_buf: [time.MIN_HMS_LEN]u8 fmt.printf( " hoarded: symptom for %s %s@%s %s %sโœ“%s\n", time.to_string_yyyy_mm_dd(tz_date, date_buf[:]), core.DIM, time.to_string_hms(tz_date, time_buf[:]), core.RESET, core.GREEN, core.RESET, ) } Event_Options :: struct { kind: string `args:"required" usage:"what kind of event"`, amount: f64 `usage:"the amount of event (if applicable, say 'coffe')"`, unit: string `usage:"the unit of the amount (if applicable, say a 'cup' of coffe)"`, date: time.Time `usage:"date and time to record this data to (defaults to now, RFC 3339)"`, } // FIXME: Simplify, this is ridiculous command_coffee :: proc(args: []string, store: ^core.Storage) { command_event({"event", "--kind", "coffee", "--amount", "1", "--unit", "cup"}, store) } // FIXME: Only known kinds // FIXME: Limit valid units? // FIXME: Validate that unit is set if amount is set command_event :: proc(args: []string, store: ^core.Storage) { opt: Event_Options flags.parse_or_exit(&opt, args, .Unix) if time.time_to_unix(opt.date) == 0 { opt.date = time.now() } if err := core.storage_add_to_events(store^, opt.kind, opt.amount, opt.unit, opt.date); err != nil { fmt.eprintf("failed adding event: %v\n", err) } tz_date := core.new_time_with_timezone_offset(opt.date) date_buf: [time.MIN_YYYY_DATE_LEN]u8 time_buf: [time.MIN_HMS_LEN]u8 fmt.printf( " hoarded: event for %s %s@%s %s %sโœ“%s\n", time.to_string_yyyy_mm_dd(tz_date, date_buf[:]), core.DIM, time.to_string_hms(tz_date, time_buf[:]), core.RESET, core.GREEN, core.RESET, ) } command_today :: proc(args: []string, store: ^core.Storage) { buf: [time.MIN_YYYY_DATE_LEN]u8 command_show({"show", time.to_string_yyyy_mm_dd(time.now(), buf[:])}, store) } SPARK_CHARS :: []string{"โ–", "โ–‚", "โ–ƒ", "โ–„", "โ–…", "โ–†", "โ–‡", "โ–ˆ"} spark_char :: proc(val, max: int) -> string { spark_chars := SPARK_CHARS idx := int((f64(val) / f64(max)) * f64(len(SPARK_CHARS) - 1)) return spark_chars[clamp(idx, 0, len(SPARK_CHARS) - 1)] } spark_days :: proc(values: []int) -> string { b, err := strings.builder_make(context.temp_allocator) ensure(err == nil) defer strings.builder_destroy(&b) num_values := len(values) for v in values { if v < 0 { strings.write_string(&b, core.DIM) strings.write_string(&b, "ยท ") strings.write_string(&b, core.RESET) continue } color := 31 p := f64(v) / 10 if p > 0.25 { color = 33 } if p > 0.75 { color = 32 } strings.write_string(&b, fmt.tprintf("\e[%dm", color)) strings.write_string(&b, spark_char(v, 10)) strings.write_string(&b, " ") strings.write_string(&b, core.RESET) } strings.write_string(&b, core.DIM) for _ in num_values ..< 7 { strings.write_string(&b, "ยท ") } strings.write_string(&b, core.RESET) return strings.to_string(b) } // FIXME: naive, does not take gaps in days into account command_week :: proc(args: []string, store: ^core.Storage) { day := time.Hour * 24 today := time.now() weekday := int(time.weekday(today)) // NOTE: Corrects for Sunday being first day of the week in the library weekday = (weekday + int(time.Weekday.Saturday)) % (int(time.Weekday.Saturday) + 1) week_start := time.time_add(today, -(day * time.Duration(weekday))) week_end := time.time_add(week_start, day * 6) week_start_buf: [time.MIN_YYYY_DATE_LEN]u8 week_end_buf: [time.MIN_YYYY_DATE_LEN]u8 week_start_ymd := time.to_string_yyyy_mm_dd(week_start, week_start_buf[:]) week_end_ymd := time.to_string_yyyy_mm_dd(week_end, week_end_buf[:]) fmt.printf( "%sโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€%s\n", core.DIM, core.RESET, ) fmt.printf("%sโ—† week of %s - %s%s\n", core.YELLOW, week_start_ymd, week_end_ymd, core.RESET) if data, get_err := core.storage_get_daily_log_span(store^, week_start, week_end); len(data) == 0 || get_err != nil { fmt.printf(" %sno data for this week%s\n", core.RED, core.RESET) } else { fmt.printf("\n%sDAILY LOG%s\n", core.DIM, core.RESET) fmt.printf("\n\t\t%smon tue wed thu fri sat sun%s\n", core.DIM, core.RESET) current := week_start current_buf: [time.MIN_YYYY_DATE_LEN]u8 current_str_ymd := time.to_string_yyyy_mm_dd(current, current_buf[:]) mood_values: [dynamic; 7]int sleep_values: [dynamic; 7]int sleep_quality_values: [dynamic; 7]int for d in data { for d.date != current_str_ymd { append(&mood_values, -1) append(&sleep_values, -1) append(&sleep_quality_values, -1) current = time.time_add(current, day) current_str_ymd = time.to_string_yyyy_mm_dd(current, current_buf[:]) } append(&mood_values, d.mood) append(&sleep_values, int((d.sleep / 8) * 10)) append(&sleep_quality_values, d.sleep_quality) current = time.time_add(current, day) current_str_ymd = time.to_string_yyyy_mm_dd(current, current_buf[:]) } fmt.printf(" %smood%s\t\t%s%s\n", core.DIM, core.RESET, spark_days(mood_values[:]), core.RESET) fmt.printf(" %ssleep%s\t\t%s%s\n", core.DIM, core.RESET, spark_days(sleep_values[:]), core.RESET) fmt.printf( " %ssleep quality%s\t%s%s\n", core.DIM, core.RESET, spark_days(sleep_quality_values[:]), core.RESET, ) } fmt.printf( "%sโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€%s\n", core.DIM, core.RESET, ) } build_bar :: proc(b: ^strings.Builder, val, max, width: int, invert := false) -> string { strings.builder_reset(b) filled := int(f64(val * width) / f64(max)) p := f64(filled) / f64(width) for i in 0 ..< width { color := 31 if !invert else 32 if p > 0.25 { color = 33 } if p > 0.75 { color = 32 if !invert else 31 } strings.write_string(b, fmt.tprintf("\e[%s%dm", "2;" if i >= filled else "", color)) strings.write_rune(b, 'โ–ˆ') strings.write_string(b, core.RESET) } return strings.to_string(b^) } command_show :: proc(args: []string, store: ^core.Storage) { if len(args) < 2 { // TODO: print usage return } date_parts := strings.split(args[1], "-", context.temp_allocator) yyyy, _ := strconv.parse_i64(date_parts[0]) mm, _ := strconv.parse_i64(date_parts[1]) dd, _ := strconv.parse_i64(date_parts[2]) date, ok := time.components_to_time(yyyy, mm, dd, 0, 0, 0) ensure(ok) // TODO: print usage/invalid date b, err := strings.builder_make_len(10) ensure(err == nil) defer strings.builder_destroy(&b) fmt.printf( "%sโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€%s\n", core.DIM, core.RESET, ) day_name := strings.to_lower(fmt.tprintf("%s", time.weekday(date)), context.temp_allocator) fmt.printf("%sโ—† %s, %s%s\n", core.YELLOW, day_name, args[1], core.RESET) if data, get_err := core.storage_get_daily_log(store^, date); get_err != nil { fmt.printf(" %sno daily data for this day%s\n", core.RED, core.RESET) } else { fmt.printf("\n%sDAILY LOG%s\n", core.DIM, core.RESET) fmt.printf( " %smood%s\t\t%s %d/%s10%s\n", core.DIM, core.RESET, build_bar(&b, data.mood, 10, 10), data.mood, core.DIM, core.RESET, ) fmt.printf( " %ssleep%s\t\t%s %.1f%sh%s\n", core.DIM, core.RESET, build_bar(&b, int(data.sleep * 10), 80, 10), data.sleep, core.DIM, core.RESET, ) fmt.printf( " %ssleep quality%s\t%s %d/%s10%s\n", core.DIM, core.RESET, build_bar(&b, data.sleep_quality, 10, 10), data.sleep_quality, core.DIM, core.RESET, ) fmt.printf(" %snotes%s\t\t%s\n", core.DIM, core.RESET, data.notes) } if data, get_err := core.storage_get_daily_symptoms(store^, date); get_err == nil { fmt.printf("\n%sSYMPTOMS%s\n", core.DIM, core.RESET) for d in data { fmt.printf( " %s%s%s\t%s %d/%s10%s\n", core.DIM, d.symptom, core.RESET, build_bar(&b, d.severity, 10, 10, true), d.severity, core.DIM, core.RESET, ) } } if data, get_err := core.storage_get_daily_events(store^, date); get_err == nil { fmt.printf("\n%sEVENTS%s\n", core.DIM, core.RESET) for d in data { tz_time := core.new_time_with_timezone_offset(d.timestamp) hh, mm, _ := time.clock_from_time(tz_time) icon: string switch d.kind { case "coffee": icon = "โ˜• " case "walk": icon = "๐Ÿšถ " case "painkiller": icon = "๐Ÿ’Š " } amount := fmt.tprintf("%s ยท %.1f %s%s", core.DIM, d.amount, d.unit, core.RESET) if d.amount > 1 else "" fmt.printf(" %s%02d:%02d%s %s%s%s\n", core.DIM, hh, mm, core.RESET, icon, d.kind, amount) } } fmt.printf( "%sโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€%s\n", core.DIM, core.RESET, ) } command_events :: proc(args: []string, store: ^core.Storage) { fmt.printf("events\n") fmt.printf("args: %v\n", args) } command_version :: proc(args: []string, store: ^core.Storage) { fmt.printf("version\n") fmt.printf("args: %v\n", args) }