cmd_log.v

4.5 KB · raw

1//! `koh log` — the line, newest first.2//!3//! On a terminal this opens the history view; piped or with `--json` it prints4//! plain text, so an agent always gets something it can parse.56module cli78import json9import time10import src.history1112pub fn cmd_log(raw []string) int {13	args := parse('log', raw, Options{14		values: ['since', 'session']15		flags:  ['json']16	}) or { return usage_error(err.msg()) }17	store := open_store() or { return refuse(args.json(), err.msg()) }18	defer {19		store.close()20	}21	mut cutoff := i64(0)22	if expr := args.value_or_none('since') {23		cutoff = parse_since(expr) or { return refuse(args.json(), err.msg()) }24	}25	session := args.value('session')26	head_id := history.head_id(store)27	mut shown := []history.Save{}28	for save in history.line(store) or { return refuse(args.json(), err.msg()) } {29		if cutoff > 0 && save.created_at < cutoff {30			continue31		}32		if session != '' && save.session != session {33			continue34		}35		shown << save36	}37	if args.json() {38		emit_json(json.encode(LogJson{39			head_id: head_id40			saves:   shown.map(to_json(it))41		}))42		return 043	}44	if shown.len == 0 {45		ok('no saves in that range')46		return 047	}48	for save in shown {49		println(log_line(save, save.id == head_id))50	}51	return 052}5354struct SaveJson {55	id            string56	parent        ?string57	message       string58	timestamp     i6459	machine       string60	agent         ?string61	session       ?string62	is_checkpoint bool63	restores      ?string64	replayed_from ?string65}6667struct LogJson {68	head_id string69	saves   []SaveJson70}7172fn to_json(save history.Save) SaveJson {73	return SaveJson{74		id:            save.id75		parent:        optional(save.parent)76		message:       save.message77		timestamp:     save.created_at78		machine:       save.machine79		agent:         optional(save.agent)80		session:       optional(save.session)81		is_checkpoint: save.is_checkpoint82		restores:      optional(save.restores)83		replayed_from: optional(save.replayed_from)84	}85}8687fn optional(value string) ?string {88	if value == '' {89		return none90	}91	return value92}9394fn log_line(save history.Save, is_head bool) string {95	marker := if is_head { amber('▸') } else { ' ' }96	mut line := '  ${marker} ${dim('[' + save.id + ']')} ${stamp(save.created_at)}  ${save.message}'97	mut tags := []string{}98	if save.agent != '' || save.session != '' {99		mut inner := []string{}100		if save.agent != '' {101			inner << save.agent102		}103		if save.session != '' {104			inner << save.session105		}106		tags << '[${inner.join(' · ')}]'107	}108	if save.is_checkpoint {109		tags << '◐'110	}111	if save.restores != '' {112		tags << '↩ ${save.restores}'113	}114	if save.replayed_from != '' {115		tags << 'replayed'116	}117	if tags.len > 0 {118		line += '  ${dim(tags.join('  '))}'119	}120	return line121}122123// parse_since turns the natural-language expressions Koh has always accepted124// into a unix timestamp.125pub fn parse_since(expr string) !i64 {126	now := time.now().unix()127	text := expr.trim_space().to_lower()128	match text {129		'today' { return start_of_day(now) }130		'yesterday' { return start_of_day(now) - 86400 }131		'last week' { return now - 7 * 86400 }132		'last month' { return now - 30 * 86400 }133		'last year' { return now - 365 * 86400 }134		else {}135	}136137	if date := parse_date(text) {138		return date139	}140	parts := text.split(' ')141	if parts.len >= 2 {142		amount := parts[0].int()143		if amount > 0 {144			if unit := unit_seconds(parts[1]) {145				return now - i64(amount) * unit146			}147		}148		return error('cannot understand "--since ${expr}"')149	}150	days := text.int()151	if days > 0 {152		return now - i64(days) * 86400153	}154	return error('cannot understand "--since ${expr}"')155}156157fn unit_seconds(unit string) ?i64 {158	return match unit {159		'm', 'min', 'mins', 'minute', 'minutes' { i64(60) }160		'h', 'hr', 'hrs', 'hour', 'hours' { i64(3600) }161		'd', 'day', 'days' { i64(86400) }162		'w', 'week', 'weeks' { i64(7 * 86400) }163		'month', 'months' { i64(30 * 86400) }164		'y', 'year', 'years' { i64(365 * 86400) }165		else { none }166	}167}168169fn parse_date(text string) ?i64 {170	parts := text.split('-')171	if parts.len != 3 || parts[0].len != 4 {172		return none173	}174	if parts[0].int() == 0 || parts[1].int() == 0 || parts[2].int() == 0 {175		return none176	}177	return midnight(parts[0].int(), parts[1].int(), parts[2].int())178}179180fn start_of_day(unix_seconds i64) i64 {181	moment := time.unix(unix_seconds).local()182	return midnight(moment.year, moment.month, moment.day)183}184185// midnight builds a local-time timestamp for a calendar day.186fn midnight(year int, month int, day int) i64 {187	return time.Time{188		year:     year189		month:    month190		day:      day191		is_local: true192	}.unix()193}