def edit(): if "EDITOR" not in os.environ: raise NoEditor("Please set the 'EDITOR' environment variable") data = store.load() yml = yaml.safe_dump(data, default_flow_style=False, allow_unicode=True) cmd = os.getenv('EDITOR') fd, temp_path = tempfile.mkstemp(prefix='ti.') with open(temp_path, "r+") as f: f.write(yml.replace('\n- ', '\n\n- ')) f.seek(0) subprocess.check_call(cmd + ' ' + temp_path, shell=True) yml = f.read() f.truncate() os.close(fd) os.remove(temp_path) try: data = yaml.load(yml) except: raise InvalidYAML("Oops, that YAML doesn't appear to be valid!") store.dump(data)
def fin(time: Union[str, Arrow], back_from_interrupt=True) -> bool: ensure_working() data = store.load() current = data['work'][-1] item = Item(**data['work'][-1]) end: XArrow = formatted2arrow(time) if item.start > end: print(f'{c.orange("Cannot")} finish {item.name_colored} at {c.time(end.HHmmss)} because it only started at {c.time(item.start.HHmmss)}.') return False if item.start.day < end.day: print(end) if not confirm(f'{item.name_colored} started on {c.time(item.start.MMDDYYHHmmss)}, continue?'): return False current['end'] = time item.end = time ok = store.dump(data) print(f'{c.yellow("Stopped")} working on {item.name_colored} at {c.time(item.end.HHmmss)}') if not ok: return False if back_from_interrupt and len(data['interrupt_stack']) > 0: name = data['interrupt_stack'].pop()['name'] store.dump(data) on(name, time) if len(data['interrupt_stack']) > 0: print('You are now %d deep in interrupts.' % len(data['interrupt_stack'])) else: print("Congrats, you're out of interrupts!") return True
def tag(_tag, time="now"): time = human2arrow(time) if time > now(): raise BadTime(f"in the future: {time}") data = store.load() idx = -1 item = Item(**data['work'][idx]) if time < item.start: # Tag something in the past idx = -1 * next(i for i, work in enumerate(reversed(data['work']), 1) if Item(**work).start <= time) item_in_range = Item(**data['work'][idx]) if not confirm(f'{item.name_colored} started only at {c.time(item.start.strftime("%X"))}, ' f'Tag {item_in_range.name_colored} (started at {c.time(item_in_range.start.strftime("%X"))})?'): return item = item_in_range tag_colored = c.b(c.tag(_tag)) if _tag.lower() in [t.lower() for t in item.tags]: print(f'{item.name_colored} already has tag {tag_colored}.') return item.tags.append(_tag) data['work'][idx]['tags'] = item.tags store.dump(data) print(f"Okay, tagged {item.name_colored} with {tag_colored}.")
def on(name, time="now", _tag=None, _note=None): data = store.load() work = data['work'] if work and 'end' not in (current := work[-1]): # Finish current, then start (recursively) current_name__lower = current["name"].lower() name_lower = name.lower() if current_name__lower == name_lower: print(f'{c.orange("Already")} working on {c.task(current["name"])} since {c.b(c.time(reformat(current["start"], "HH:mm:ss")))} ;)') return True ok = fin(time) if ok: return on(name, time, _tag) return False
def interrupt(name, time): ensure_working() fin(time, back_from_interrupt=False) data = store.load() if 'interrupt_stack' not in data: data['interrupt_stack'] = [] interrupt_stack = data['interrupt_stack'] interrupted = data['work'][-1] interrupt_stack.append(interrupted) store.dump(data) on('interrupt: ' + c.green(name), time) print('You are now %d deep in interrupts.' % len(interrupt_stack))
def status(show_notes=False): ensure_working() data = store.load() current = data['work'][-1] start_time = formatted2arrow(current['start']) diff = timegap(start_time, now()) notes = current.get('notes') if not show_notes or not notes: print(f'You have been working on {c.task(current["name"])} for {c.b(c.time(diff))}.') return rprint('\n '.join([f'You have been working on {c.task(current["name"])} for {c.b(c.time(diff))}.\nNotes:[rgb(170,170,170)]', *[f'[rgb(100,100,100)]o[/rgb(100,100,100)] {n}' for n in notes], '[/]']))
def note(content, time="now"): # ensure_working() time = human2arrow(time) if time > now(): raise BadTime(f"in the future: {time}") breakpoint() content_and_time = content.strip() + f' ({time.HHmmss})' data = store.load() idx = -1 item = Item(**data['work'][idx]) if time < item.start: # Note for something in the past idx, item_in_range = next((i, item) for i, item in enumerate(map(lambda w: Item(**w), reversed(data['work'])), 1) if item.start.full == time.full) idx *= -1 if item_in_range.name == item.name: item = item_in_range else: if not confirm(f'{item.name_colored} started only at {c.time(item.start.strftime("%X"))},\n' f'note to {item_in_range.name_colored} (started at {c.time(item_in_range.start.strftime("%X"))})?'): return item = item_in_range # refactor this out when Note class content_lower = content.lower() for n in item.notes: if n.lower().startswith(content_lower): if not confirm(f'{item.name_colored} already has this note: {c.b(c.note(n))}.\n' 'Add anyway?'): return item.notes.append(content_and_time) data['work'][idx]['notes'] = item.notes store.dump(data) print(f'Noted {c.b(c.note(content_and_time))} to {item.name_colored}')
def is_working(): data = store.load() return data.get('work') and 'end' not in data['work'][-1]
def log(period="today", *, detailed=True, groupby: Literal['t', 'tag'] = None): if groupby and groupby not in ('t', 'tag'): raise ValueError( f"log({period = }, {groupby = }) groupby must be either 't' | 'tag'" ) data = store.load() work = data['work'] + data['interrupt_stack'] _log = defaultdict(lambda: { 'duration': timedelta(), 'times': [], 'notes': [] }) current = None period_arrow = human2arrow(period) _now = now() by_tag = defaultdict(set) try: item = next(item for item in map(lambda w: Item(**w), reversed(work)) if item.start.full == period_arrow.full) except StopIteration: # Optimization: for loop, stop when already passed print(f"{c.orange('Missing')} logs for {period_arrow.full}") return False # stop = False # total_secs = 0 # for i, item in enumerate(map(lambda w: Item(**w), reversed(work))): # if item.start.day != period_arrow.day: # if stop: # break # continue # if period_arrow.month != item.start.month and period_arrow.year != item.start.year: # break # if groupby and groupby in ('t', 'tag'): # if not tags: # by_tag[None].add(name) # else: # for t in tags: # by_tag[t].add(name) for note in item.notes: match = note_time_re.fullmatch(note) if match: match_groups = match.groups() note_content = match_groups[0] note_time = match_groups[1] _log[item.name]['notes'].append((note_time, note_content)) else: _log[item.name]['notes'].append((None, note)) # _log[item.name]['notes'].append(note) _log[item.name]['tags'] = item.tags _log[item.name]['times'].append((item.start, item.end)) if item.end: _log[item.name]['duration'] += item.end - item.start else: _log[item.name]['duration'] += _now - item.start current = item.name # Get total duration and make it pretty name_col_len = 0 total_secs = 0 for name, item in _log.items(): name_col_len = max(name_col_len, len(c.strip_color(name)), 24) duration = int(item['duration'].total_seconds()) total_secs += duration pretty = secs2human(duration) _log[name]['pretty'] = pretty title = c.b(c.w255(period_arrow.full)) ago = arrows2rel_time(_now, period_arrow) if ago: title += f' {c.dim("| " + arrows2rel_time(_now, period_arrow))}' # if len(period) > 2: # title = period.title() # else: # title = f"{period[0]} {times.ABBREVS[period[1]]} ago" print(title + '\n' if detailed else '') # rprint(f"[b bright_white]{title}'s logs:[/]" + arrows2rel_time(_now, period_arrow) + '\n' if detailed else '') if groupby: for _tag, names in by_tag.items(): print(c.tag(_tag)) # print(f"\x1b[38;2;204;120;50m{_tag}\x1b[39m") for name in names: print_log(name, _log[name], current, detailed, name_col_len) return for name, item in sorted( _log.items(), key=lambda entry: min(map(lambda _t: _t[0], entry[1]['times']))): print_log(name, item, current, detailed, name_col_len) rprint(f"[b bright_white]Total:[/] {secs2human(total_secs)}")