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, ['json'])20}21222324pub fn cmd_view(raw []string) int {25 args := parse('view', raw, Options{26 values: ['since', 'session']27 flags: ['checkpoints', 'json']28 }) or { return usage_error(err.msg()) }29 if args.positional.len > 0 {30 return usage_error('view takes no arguments, not "${args.positional_at(0)}"')31 }32 tab := if args.flag('checkpoints') { view.Tab.checkpoints } else { view.Tab.saves }33 return history_command(raw, tab, ['checkpoints', 'json'])34}3536fn history_command(raw []string, tab view.Tab, option_flags []string) int {37 verb := match tab {38 .all { 'log' }39 else { 'view' }40 }4142 args := parse(verb, raw, Options{43 values: ['since', 'session']44 flags: option_flags45 }) or { return usage_error(err.msg()) }46 if args.positional.len > 0 {47 return usage_error('${verb} takes no arguments, not "${args.positional_at(0)}"')48 }49 store := open_store() or { return refuse(args.json(), err.msg()) }50 defer {51 store.close()52 }53 54 55 filtered := args.value('since') != '' || args.value('session') != ''56 if !args.json() && !filtered && on_a_terminal() {57 return history_view(store, tab)58 }59 mut cutoff := i64(0)60 if expr := args.value_or_none('since') {61 cutoff = parse_since(expr) or { return refuse(args.json(), err.msg()) }62 }63 session := args.value('session')64 head_id := history.head_id(store)65 flags := events.latest_flags(store) or {66 map[string]events.Flag{}67 }68 mut shown := []history.Save{}69 for save in line_for_tab(store, tab) or { return refuse(args.json(), err.msg()) } {70 if cutoff > 0 && save.created_at < cutoff {71 continue72 }73 if session != '' && save.session != session {74 continue75 }76 shown << save77 }78 if args.json() {79 emit_json(json2.encode(LogJson{80 head_id: head_id81 saves: shown.map(to_json(it))82 }))83 return 084 }85 if shown.len == 0 {86 ok('no saves in that range')87 return 088 }89 for save in shown {90 println(log_line(save, save.id == head_id, flags))91 }92 return 093}9495969798fn line_for_tab(store &storage.Store, tab view.Tab) ![]history.Save {99 line := history.line(store)!100 if tab == .all {101 return line102 }103 want_checkpoints := tab == .checkpoints104 return line.filter(it.is_checkpoint == want_checkpoints)105}106107fn history_view(store &storage.Store, tab view.Tab) int {108 view.run(store, tab, history_view_action) or { return fail(err.msg()) }109 return 0110}111112113114fn history_view_action(store &storage.Store, action view.Action) string {115 match action.kind {116 .load {117 target := history.by_id(store, action.save_id) or {118 return 'no such save: ${action.save_id}'119 }120 restore_now(store, target, 'load') or { return 'could not load: ${err.msg()}' }121 return 'loaded [${action.save_id}]'122 }123 .flag {124 events.flag(store, action.save_id, action.reason) or {125 return 'could not flag: ${err.msg()}'126 }127 return 'flagged [${action.save_id}]'128 }129 .clear_flag {130 events.clear_flag(store, action.save_id) or { return 'could not clear: ${err.msg()}' }131 return 'cleared the flag on [${action.save_id}]'132 }133 .none {134 return ''135 }136 }137}138139fn on_a_terminal() bool {140 return os.is_atty(0) != 0 && os.is_atty(1) != 0141}142143struct SaveJson {144 145 146 id string147 parent string148 message string149 timestamp i64150 machine string151 agent string152 session string153 is_checkpoint bool154 restores string155 replayed_from string156}157158struct LogJson {159 head_id string160 saves []SaveJson161}162163fn to_json(save history.Save) SaveJson {164 return SaveJson{165 id: save.id166 parent: save.parent167 message: save.message168 timestamp: save.created_at169 machine: save.machine170 agent: save.agent171 session: save.session172 is_checkpoint: save.is_checkpoint173 restores: save.restores174 replayed_from: save.replayed_from175 }176}177178fn log_line(save history.Save, is_head bool, flags map[string]events.Flag) string {179 marker := if is_head { fmt.amber('▸') } else { ' ' }180 mut line := ' ${marker} ${fmt.dim('[' + save.id + ']')} ${fmt.stamp(save.created_at)} ${save.message}'181 mut tags := []string{}182 if save.agent != '' || save.session != '' {183 mut inner := []string{}184 if save.agent != '' {185 inner << save.agent186 }187 if save.session != '' {188 inner << save.session189 }190 tags << '[${inner.join(' · ')}]'191 }192 if save.is_checkpoint {193 tags << '◐'194 }195 if (flags[save.id] or {196 events.Flag{197 save_id: save.id198 }199 }).flagged {200 tags << '⚑'201 }202 if save.restores != '' {203 tags << '↩ ${save.restores}'204 }205 if save.replayed_from != '' {206 tags << 'replayed'207 }208 if tags.len > 0 {209 line += ' ${fmt.dim(tags.join(' '))}'210 }211 return line212}213214215216pub fn parse_since(expr string) !i64 {217 now := time.now().unix()218 text := expr.trim_space().to_lower()219 match text {220 'today' { return start_of_day(now) }221 'yesterday' { return start_of_day(now) - 86400 }222 'last week' { return now - 7 * 86400 }223 'last month' { return now - 30 * 86400 }224 'last year' { return now - 365 * 86400 }225 else {}226 }227228 if date := parse_date(text) {229 return date230 }231 parts := text.split(' ')232 if parts.len >= 2 {233 amount := parts[0].int()234 if amount > 0 {235 if unit := unit_seconds(parts[1]) {236 return now - i64(amount) * unit237 }238 }239 return error('cannot understand "--since ${expr}"')240 }241 days := text.int()242 if days > 0 {243 return now - i64(days) * 86400244 }245 return error('cannot understand "--since ${expr}"')246}247248fn unit_seconds(unit string) ?i64 {249 return match unit {250 'm', 'min', 'mins', 'minute', 'minutes' { i64(60) }251 'h', 'hr', 'hrs', 'hour', 'hours' { i64(3600) }252 'd', 'day', 'days' { i64(86400) }253 'w', 'week', 'weeks' { i64(7 * 86400) }254 'month', 'months' { i64(30 * 86400) }255 'y', 'year', 'years' { i64(365 * 86400) }256 else { none }257 }258}259260fn parse_date(text string) ?i64 {261 parts := text.split('-')262 if parts.len != 3 || parts[0].len != 4 {263 return none264 }265 if parts[0].int() == 0 || parts[1].int() == 0 || parts[2].int() == 0 {266 return none267 }268 return midnight(parts[0].int(), parts[1].int(), parts[2].int())269}270271fn start_of_day(unix_seconds i64) i64 {272 moment := time.unix(unix_seconds).local()273 return midnight(moment.year, moment.month, moment.day)274}275276277fn midnight(year int, month int, day int) i64 {278 return time.Time{279 year: year280 month: month281 day: day282 is_local: true283 }.unix()284}