1234567module cli89import json210import os11import time12import src.events13import src.fmt14import src.history15import src.storage16import src.view1718pub fn cmd_log(raw []string) int {19 return history_command(raw, .all)20}2122pub fn cmd_saves(raw []string) int {23 return history_command(raw, .saves)24}2526pub fn cmd_checkpoints(raw []string) int {27 return history_command(raw, .checkpoints)28}2930fn history_command(raw []string, tab view.Tab) int {31 verb := match tab {32 .all { 'log' }33 .saves { 'saves' }34 else { 'checkpoints' }35 }3637 args := parse(verb, raw, Options{38 values: ['since', 'session']39 flags: ['json']40 }) or { return usage_error(err.msg()) }41 store := open_store() or { return refuse(args.json(), err.msg()) }42 defer {43 store.close()44 }45 46 47 filtered := args.value('since') != '' || args.value('session') != ''48 if !args.json() && !filtered && on_a_terminal() {49 return history_view(store, tab)50 }51 mut cutoff := i64(0)52 if expr := args.value_or_none('since') {53 cutoff = parse_since(expr) or { return refuse(args.json(), err.msg()) }54 }55 session := args.value('session')56 head_id := history.head_id(store)57 flags := events.latest_flags(store) or {58 map[string]events.Flag{}59 }60 mut shown := []history.Save{}61 for save in line_for_tab(store, tab) or { return refuse(args.json(), err.msg()) } {62 if cutoff > 0 && save.created_at < cutoff {63 continue64 }65 if session != '' && save.session != session {66 continue67 }68 shown << save69 }70 if args.json() {71 emit_json(json2.encode(LogJson{72 head_id: head_id73 saves: shown.map(to_json(it))74 }))75 return 076 }77 if shown.len == 0 {78 ok('no saves in that range')79 return 080 }81 for save in shown {82 println(log_line(save, save.id == head_id, flags))83 }84 return 085}86878889fn line_for_tab(store &storage.Store, tab view.Tab) ![]history.Save {90 if tab != .checkpoints {91 return history.line(store)92 }93 mut out := []history.Save{}94 for save in history.line(store)! {95 if save.is_checkpoint {96 out << save97 }98 }99 return out100}101102fn history_view(store &storage.Store, tab view.Tab) int {103 view.run(store, tab, history_view_action) or { return fail(err.msg()) }104 return 0105}106107108109fn history_view_action(store &storage.Store, action view.Action) string {110 match action.kind {111 .load {112 target := history.by_id(store, action.save_id) or {113 return 'no such save: ${action.save_id}'114 }115 restore_now(store, target, 'load') or { return 'could not load: ${err.msg()}' }116 return 'loaded [${action.save_id}]'117 }118 .flag {119 events.flag(store, action.save_id, action.reason) or {120 return 'could not flag: ${err.msg()}'121 }122 return 'flagged [${action.save_id}]'123 }124 .clear_flag {125 events.clear_flag(store, action.save_id) or { return 'could not clear: ${err.msg()}' }126 return 'cleared the flag on [${action.save_id}]'127 }128 .none {129 return ''130 }131 }132}133134fn on_a_terminal() bool {135 return os.is_atty(0) != 0 && os.is_atty(1) != 0136}137138struct SaveJson {139 140 141 id string142 parent string143 message string144 timestamp i64145 machine string146 agent string147 session string148 is_checkpoint bool149 restores string150 replayed_from string151}152153struct LogJson {154 head_id string155 saves []SaveJson156}157158fn to_json(save history.Save) SaveJson {159 return SaveJson{160 id: save.id161 parent: save.parent162 message: save.message163 timestamp: save.created_at164 machine: save.machine165 agent: save.agent166 session: save.session167 is_checkpoint: save.is_checkpoint168 restores: save.restores169 replayed_from: save.replayed_from170 }171}172173fn log_line(save history.Save, is_head bool, flags map[string]events.Flag) string {174 marker := if is_head { fmt.amber('▸') } else { ' ' }175 mut line := ' ${marker} ${fmt.dim('[' + save.id + ']')} ${fmt.stamp(save.created_at)} ${save.message}'176 mut tags := []string{}177 if save.agent != '' || save.session != '' {178 mut inner := []string{}179 if save.agent != '' {180 inner << save.agent181 }182 if save.session != '' {183 inner << save.session184 }185 tags << '[${inner.join(' · ')}]'186 }187 if save.is_checkpoint {188 tags << '◐'189 }190 if (flags[save.id] or {191 events.Flag{192 save_id: save.id193 }194 }).flagged {195 tags << '⚑'196 }197 if save.restores != '' {198 tags << '↩ ${save.restores}'199 }200 if save.replayed_from != '' {201 tags << 'replayed'202 }203 if tags.len > 0 {204 line += ' ${fmt.dim(tags.join(' '))}'205 }206 return line207}208209210211pub fn parse_since(expr string) !i64 {212 now := time.now().unix()213 text := expr.trim_space().to_lower()214 match text {215 'today' { return start_of_day(now) }216 'yesterday' { return start_of_day(now) - 86400 }217 'last week' { return now - 7 * 86400 }218 'last month' { return now - 30 * 86400 }219 'last year' { return now - 365 * 86400 }220 else {}221 }222223 if date := parse_date(text) {224 return date225 }226 parts := text.split(' ')227 if parts.len >= 2 {228 amount := parts[0].int()229 if amount > 0 {230 if unit := unit_seconds(parts[1]) {231 return now - i64(amount) * unit232 }233 }234 return error('cannot understand "--since ${expr}"')235 }236 days := text.int()237 if days > 0 {238 return now - i64(days) * 86400239 }240 return error('cannot understand "--since ${expr}"')241}242243fn unit_seconds(unit string) ?i64 {244 return match unit {245 'm', 'min', 'mins', 'minute', 'minutes' { i64(60) }246 'h', 'hr', 'hrs', 'hour', 'hours' { i64(3600) }247 'd', 'day', 'days' { i64(86400) }248 'w', 'week', 'weeks' { i64(7 * 86400) }249 'month', 'months' { i64(30 * 86400) }250 'y', 'year', 'years' { i64(365 * 86400) }251 else { none }252 }253}254255fn parse_date(text string) ?i64 {256 parts := text.split('-')257 if parts.len != 3 || parts[0].len != 4 {258 return none259 }260 if parts[0].int() == 0 || parts[1].int() == 0 || parts[2].int() == 0 {261 return none262 }263 return midnight(parts[0].int(), parts[1].int(), parts[2].int())264}265266fn start_of_day(unix_seconds i64) i64 {267 moment := time.unix(unix_seconds).local()268 return midnight(moment.year, moment.month, moment.day)269}270271272fn midnight(year int, month int, day int) i64 {273 return time.Time{274 year: year275 month: month276 day: day277 is_local: true278 }.unix()279}