//! `koh log` — the line, newest first. //! //! On a terminal this opens the history view; piped or with `--json` it prints //! plain text, so an agent always gets something it can parse. module cli import json import time import src.history pub fn cmd_log(raw []string) int { args := parse('log', raw, Options{ values: ['since', 'session'] flags: ['json'] }) or { return usage_error(err.msg()) } store := open_store() or { return refuse(args.json(), err.msg()) } defer { store.close() } mut cutoff := i64(0) if expr := args.value_or_none('since') { cutoff = parse_since(expr) or { return refuse(args.json(), err.msg()) } } session := args.value('session') head_id := history.head_id(store) mut shown := []history.Save{} for save in history.line(store) or { return refuse(args.json(), err.msg()) } { if cutoff > 0 && save.created_at < cutoff { continue } if session != '' && save.session != session { continue } shown << save } if args.json() { emit_json(json.encode(LogJson{ head_id: head_id saves: shown.map(to_json(it)) })) return 0 } if shown.len == 0 { ok('no saves in that range') return 0 } for save in shown { println(log_line(save, save.id == head_id)) } return 0 } struct SaveJson { id string parent ?string message string timestamp i64 machine string agent ?string session ?string is_checkpoint bool restores ?string replayed_from ?string } struct LogJson { head_id string saves []SaveJson } fn to_json(save history.Save) SaveJson { return SaveJson{ id: save.id parent: optional(save.parent) message: save.message timestamp: save.created_at machine: save.machine agent: optional(save.agent) session: optional(save.session) is_checkpoint: save.is_checkpoint restores: optional(save.restores) replayed_from: optional(save.replayed_from) } } fn optional(value string) ?string { if value == '' { return none } return value } fn log_line(save history.Save, is_head bool) string { marker := if is_head { amber('▸') } else { ' ' } mut line := ' ${marker} ${dim('[' + save.id + ']')} ${stamp(save.created_at)} ${save.message}' mut tags := []string{} if save.agent != '' || save.session != '' { mut inner := []string{} if save.agent != '' { inner << save.agent } if save.session != '' { inner << save.session } tags << '[${inner.join(' · ')}]' } if save.is_checkpoint { tags << '◐' } if save.restores != '' { tags << '↩ ${save.restores}' } if save.replayed_from != '' { tags << 'replayed' } if tags.len > 0 { line += ' ${dim(tags.join(' '))}' } return line } // parse_since turns the natural-language expressions Koh has always accepted // into a unix timestamp. pub fn parse_since(expr string) !i64 { now := time.now().unix() text := expr.trim_space().to_lower() match text { 'today' { return start_of_day(now) } 'yesterday' { return start_of_day(now) - 86400 } 'last week' { return now - 7 * 86400 } 'last month' { return now - 30 * 86400 } 'last year' { return now - 365 * 86400 } else {} } if date := parse_date(text) { return date } parts := text.split(' ') if parts.len >= 2 { amount := parts[0].int() if amount > 0 { if unit := unit_seconds(parts[1]) { return now - i64(amount) * unit } } return error('cannot understand "--since ${expr}"') } days := text.int() if days > 0 { return now - i64(days) * 86400 } return error('cannot understand "--since ${expr}"') } fn unit_seconds(unit string) ?i64 { return match unit { 'm', 'min', 'mins', 'minute', 'minutes' { i64(60) } 'h', 'hr', 'hrs', 'hour', 'hours' { i64(3600) } 'd', 'day', 'days' { i64(86400) } 'w', 'week', 'weeks' { i64(7 * 86400) } 'month', 'months' { i64(30 * 86400) } 'y', 'year', 'years' { i64(365 * 86400) } else { none } } } fn parse_date(text string) ?i64 { parts := text.split('-') if parts.len != 3 || parts[0].len != 4 { return none } if parts[0].int() == 0 || parts[1].int() == 0 || parts[2].int() == 0 { return none } return midnight(parts[0].int(), parts[1].int(), parts[2].int()) } fn start_of_day(unix_seconds i64) i64 { moment := time.unix(unix_seconds).local() return midnight(moment.year, moment.month, moment.day) } // midnight builds a local-time timestamp for a calendar day. fn midnight(year int, month int, day int) i64 { return time.Time{ year: year month: month day: day is_local: true }.unix() }