1234567module cli89import json210import os11import time12import src.events13import src.fmt14import src.history15import src.storage16import src.view1718192021pub fn cmd_view(raw []string) int {22 args := parse('view', raw, Options{23 values: ['since', 'session']24 flags: ['checkpoints', 'json']25 }) or { return usage_error(err.msg()) }26 if args.positional.len > 0 {27 return usage_error('view takes no arguments, not "${args.positional_at(0)}"')28 }29 checkpoints := args.flag('checkpoints')30 view_tab := if checkpoints { view.Tab.checkpoints } else { view.Tab.saves }31 print_tab := if checkpoints { view.Tab.checkpoints } else { view.Tab.all }32 store := open_store() or { return refuse(args.json(), err.msg()) }33 defer {34 store.close()35 }36 37 38 filtered := args.value('since') != '' || args.value('session') != ''39 if !args.json() && !filtered && on_a_terminal() {40 return history_view(store, view_tab)41 }42 mut cutoff := i64(0)43 if expr := args.value_or_none('since') {44 cutoff = parse_since(expr) or { return refuse(args.json(), err.msg()) }45 }46 session := args.value('session')47 head_id := history.head_id(store)48 flags := events.latest_flags(store) or {49 map[string]events.Flag{}50 }51 mut shown := []history.Save{}52 for save in line_for_tab(store, print_tab) or { return refuse(args.json(), err.msg()) } {53 if cutoff > 0 && save.created_at < cutoff {54 continue55 }56 if session != '' && save.session != session {57 continue58 }59 shown << save60 }61 if args.json() {62 emit_json(json2.encode(ViewJson{63 head_id: head_id64 saves: shown.map(to_json(it))65 }))66 return 067 }68 if shown.len == 0 {69 ok('no saves in that range')70 return 071 }72 for save in shown {73 println(view_line(save, save.id == head_id, flags))74 }75 return 076}7778798081fn line_for_tab(store &storage.Store, tab view.Tab) ![]history.Save {82 line := history.line(store)!83 if tab == .all {84 return line85 }86 want_checkpoints := tab == .checkpoints87 return line.filter(it.is_checkpoint == want_checkpoints)88}8990fn history_view(store &storage.Store, tab view.Tab) int {91 view.run(store, tab, history_view_action) or { return fail(err.msg()) }92 return 093}94959697fn history_view_action(store &storage.Store, action view.Action) string {98 match action.kind {99 .load {100 target := history.by_id(store, action.save_id) or {101 return 'no such save: ${action.save_id}'102 }103 restore_now(store, target, 'load') or { return 'could not load: ${err.msg()}' }104 return 'loaded [${action.save_id}]'105 }106 .flag {107 events.flag(store, action.save_id, action.reason) or {108 return 'could not flag: ${err.msg()}'109 }110 return 'flagged [${action.save_id}]'111 }112 .clear_flag {113 events.clear_flag(store, action.save_id) or { return 'could not clear: ${err.msg()}' }114 return 'cleared the flag on [${action.save_id}]'115 }116 .none {117 return ''118 }119 }120}121122fn on_a_terminal() bool {123 return os.is_atty(0) != 0 && os.is_atty(1) != 0124}125126struct SaveJson {127 128 129 id string130 parent string131 message string132 timestamp i64133 machine string134 agent string135 session string136 is_checkpoint bool137 restores string138 replayed_from string139}140141struct ViewJson {142 head_id string143 saves []SaveJson144}145146fn to_json(save history.Save) SaveJson {147 return SaveJson{148 id: save.id149 parent: save.parent150 message: save.message151 timestamp: save.created_at152 machine: save.machine153 agent: save.agent154 session: save.session155 is_checkpoint: save.is_checkpoint156 restores: save.restores157 replayed_from: save.replayed_from158 }159}160161fn view_line(save history.Save, is_head bool, flags map[string]events.Flag) string {162 marker := if is_head { fmt.amber('▸') } else { ' ' }163 mut line := ' ${marker} ${fmt.dim('[' + save.id + ']')} ${fmt.stamp(save.created_at)} ${save.message}'164 mut tags := []string{}165 if save.agent != '' || save.session != '' {166 mut inner := []string{}167 if save.agent != '' {168 inner << save.agent169 }170 if save.session != '' {171 inner << save.session172 }173 tags << '[${inner.join(' · ')}]'174 }175 if save.is_checkpoint {176 tags << '◐'177 }178 if (flags[save.id] or {179 events.Flag{180 save_id: save.id181 }182 }).flagged {183 tags << '⚑'184 }185 if save.restores != '' {186 tags << '↩ ${save.restores}'187 }188 if save.replayed_from != '' {189 tags << 'replayed'190 }191 if tags.len > 0 {192 line += ' ${fmt.dim(tags.join(' '))}'193 }194 return line195}196197198199pub fn parse_since(expr string) !i64 {200 now := time.now().unix()201 text := expr.trim_space().to_lower()202 match text {203 'today' { return start_of_day(now) }204 'yesterday' { return start_of_day(now) - 86400 }205 'last week' { return now - 7 * 86400 }206 'last month' { return now - 30 * 86400 }207 'last year' { return now - 365 * 86400 }208 else {}209 }210211 if date := parse_date(text) {212 return date213 }214 parts := text.split(' ')215 if parts.len >= 2 {216 amount := parts[0].int()217 if amount > 0 {218 if unit := unit_seconds(parts[1]) {219 return now - i64(amount) * unit220 }221 }222 return error('cannot understand "--since ${expr}"')223 }224 days := text.int()225 if days > 0 {226 return now - i64(days) * 86400227 }228 return error('cannot understand "--since ${expr}"')229}230231fn unit_seconds(unit string) ?i64 {232 return match unit {233 'm', 'min', 'mins', 'minute', 'minutes' { i64(60) }234 'h', 'hr', 'hrs', 'hour', 'hours' { i64(3600) }235 'd', 'day', 'days' { i64(86400) }236 'w', 'week', 'weeks' { i64(7 * 86400) }237 'month', 'months' { i64(30 * 86400) }238 'y', 'year', 'years' { i64(365 * 86400) }239 else { none }240 }241}242243fn parse_date(text string) ?i64 {244 parts := text.split('-')245 if parts.len != 3 || parts[0].len != 4 {246 return none247 }248 if parts[0].int() == 0 || parts[1].int() == 0 || parts[2].int() == 0 {249 return none250 }251 return midnight(parts[0].int(), parts[1].int(), parts[2].int())252}253254fn start_of_day(unix_seconds i64) i64 {255 moment := time.unix(unix_seconds).local()256 return midnight(moment.year, moment.month, moment.day)257}258259260fn midnight(year int, month int, day int) i64 {261 return time.Time{262 year: year263 month: month264 day: day265 is_local: true266 }.unix()267}