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='timefred.') 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 tag(_tag, time="now") -> bool: time = XArrow.from_human(time) # time = human2arrow(time) if time > time.now(): raise BadTime(f"in the future: {time}") work = store.load() idx = -1 item = Entry(**work[idx]) if time < item.start: # Tag something in the past idx = -1 * next(i for i, work in enumerate(reversed(work), 1) if Entry(**work).start <= time) item_in_range = Entry(**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 False item = item_in_range tag_colored = c.tag(_tag) if any( util.normalize_str(_tag) == t for t in map(util.normalize_str, item.tags)): print(f'{item.name_colored} already has tag {tag_colored}.') return False item.tags.add(_tag) work[idx]['tags'] = item.tags ok = store.dump(work) if ok: print(f"Okay, tagged {item.name_colored} with {tag_colored}.") else: print(f"Failed writing to sheet") return ok
def note(content, time="now"): time = XArrow.from_human(time) # time = human2arrow(time) if time > XArrow.now(): raise BadTime(f"in the future: {time}") content_and_time = content.strip() + f' ({time.HHmmss})' work = store.load() idx = -1 item = Entry(**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: Entry(**w), reversed(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 for n in item.notes: if n.is_similar(content): 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) work[idx]['notes'] = item.notes store.dump(work) print(f'Noted {c.b(c.note(content_and_time))} to {item.name_colored}')
def test_load_store(self, work=None): log.title(f"test_load_store({work = })") if not work: work = default_work() with temp_sheet( "/tmp/timefred-sheet-test_on_device_validation_08_30.toml" ): store.dump(work) work = store.load() self.test_sanity(work=work)
def test_dump(self): work = default_work() log.title(f"test_dump({work = })") # os.environ['TIMEFRED_SHEET'] = "/tmp/timefred-sheet-test_on_device_validation_08_30.toml" # config.sheet.path = "/tmp/timefred-sheet-test_on_device_validation_08_30.toml" # store.path = "/tmp/timefred-sheet-test_on_device_validation_08_30.toml" with temp_sheet( "/tmp/timefred-sheet-test_on_device_validation_08_30.toml"): store.dump(work) work = store.load()
def on(name: str, time: Union[str, XArrow], tag=None, note=None): work = store.load() if work: day = work[time.DDMMYY] activity = day[name] if activity.ongoing() and activity.has_similar_name(name): print( f'{c.orange("Already")} working on {activity.name.colored} since {c.time(activity.start.DDMMYYHHmmss)} ;)' ) return True ok = stop(time) if ok: return on(name, time, tag) breakpoint() return False entry = Entry(start=time) # assert entry # assert entry.start # assert isinstance(entry.start, XArrow) activity = Activity(name=name) # assert not activity # assert len(activity) == 0 # assert activity.name == 'Got to office', f"activity.name is not 'Got to office' but rather {activity.name!r}" # assert isinstance(activity.name, Colored), f'Not Colored, but rather {type(activity.name)}' activity.append(entry) # assert len(activity) == 1 if tag: entry.tags.add(tag) if note: note = Note(note, time) entry.notes.append(note) # work[entry.start.DDMMYY].append({str(activity.name): activity.dict(exclude=('timespan', 'name'))}) day = work[entry.start.DDMMYY] day[str(activity.name)] = activity # work[activity.start.DDMMYY].append(activity) # work.append(activity.dict()) ok = store.dump(work) if not ok: breakpoint() # message = f'{c.green("Started")} working on {activity.name_colored} at {c.time(reformat(time, timeutils.FORMATS.date_time))}' message = f'{c.green("Started")} working on {activity.name.colored} at {c.time(entry.start.DDMMYYHHmmss)}' if tag: message += f". Tag: {c.tag(tag)}" if note: message += f". Note: {note.pretty()}" print(message)
def test_subtable_activity__time_in_proper(self): raw_data = '["02/12/21"]\n[["02/12/21"."Got to office"]]\nstart = 09:40:00' sheet_path = '/tmp/timefred-sheet--test-store--test-load--test-subtable-activity--time-in-proper.toml' with open(sheet_path, 'w') as sheet: sheet.write(raw_data) with temp_sheet(sheet_path): work = store.load() day: Day = work['02/12/21'] activity: Activity = day['Got to office'] entry: Entry = activity.safe_last_entry() assert isinstance(entry.start, XArrow) assert entry.start.HHmmss == "09:40:00" assert entry.end is UNSET
def stop(end: XArrow, tag: Tag = None, note: Note = None) -> bool: # ensure_working() work: Work = store.load() ongoing_activity: Activity = work.ongoing_activity() entry: Entry = ongoing_activity.stop(end, tag, note) if entry.start.day < end.day: if not util.confirm(f'{ongoing_activity.name} started on {c.time(entry.start.DDMMYYHHmmss)}, continue?'): # TODO: activity is wrongly stopped now, should revert or prevent return False ok = store.dump(work) print(f'{c.yellow("Stopped")} working on {ongoing_activity.name.colored} at {c.time(entry.end.DDMMYYHHmmss)}. ok: {ok}') return ok
def status(show_notes=False): ensure_working() data = store.load() current = Entry(**data[-1]) duration = Timespan(current.start, XArrow.now()).human_duration # diff = timegap(current.start, now()) # notes = current.get('notes') if not show_notes or not current.notes: print( f'You have been working on {current.name.colored} for {c.time(duration)}.' ) return print('\n '.join([ f'You have been working on {current.name_colored} for {c.time(duration)}.\nNotes:', # [rgb(170,170,170)] *[f'{c.grey100("o")} {n.pretty()}' for n in current.notes] ]))
def generate_completion(): work = store.load() tags = set([x.get('tags')[0] for x in work if x.get('tags')]) print(f"""function completion.tf(){{ current=${{COMP_WORDS[COMP_CWORD]}} prev=${{COMP_WORDS[$COMP_CWORD - 1]}} possible_completions='' case "$prev" in tf) possible_completions='l h e on f' ;; esac if [[ "$prev" == -t ]]; then possible_completions="{" ".join(tags)}" fi COMPREPLY=($(compgen -W "$possible_completions" -- "${{current}}")) }} complete -o default -F completion.tf tf """)
def log(time: Union[str, XArrow] = "today", *, detailed=True, groupby: Literal['t', 'tag'] = None) -> bool: if groupby and groupby not in ('t', 'tag'): raise ValueError( f"log({time = !r}, {detailed = }, {groupby = !r}) groupby must be either 't' | 'tag'" ) work = store.load() if not work: raise EmptySheet() current = None arrow = XArrow.from_human(time) # now = arrow.now() # TODO: should support a range of times, spanning several days etc day = work[arrow.DDMMYY] if not day: raise NoActivities(arrow.DDMMYY) # _log = Log() activities: list[Activity] = day.values() # activity: Activity = activities[0] # entry: Entry = activity[0] # entry_timespan = entry.timespan # print(f'{entry_timespan = !r}') by_tag = defaultdict(set) # activities = list(day.values()); # for i, entry in enumerate(reversed(work)): # for day_key in reversed(work): # day: Day = work[day_key] # # day # # item = Entry(**entry) # # for i, item in enumerate(map(lambda w: Entry(**w), reversed(work))): # item_start = item.start # item_start_DDMMYY = item_start.DDMMYY # period_arrow_DDMMYY = period_arrow.DDMMYY # if item_start_DDMMYY != period_arrow_DDMMYY: # if period_arrow > item.start: # # We have iterated past period # break # continue # if period_arrow.month != item.start.month and period_arrow.year != item.start.year: # break # # noinspection PyUnreachableCode # 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) # log_entry = _log[item.name] # log_entry.name = item.name # item_notes = item.notes # log_entry_notes = log_entry.notes # log_entry_notes.extend(item_notes) # log_entry.tags |= item.tags # timespan = Timespan(item.start, item.end or now) # log_entry.timespans.append(timespan) # if not timespan.end: # log_entry.is_current = True title = c.title(f"{arrow.isoweekday('full')}, " + arrow.DDMMYY) now = XArrow.now() # ago = now.humanize(arrow, granularity=["year", "month", "week", "day", "hour", "minute"]) ago = arrows2rel_time(now, arrow) if ago: title += f' {c.dim("| " + ago)}' print(title + '\n') # if not _log: # return True name_column_width = max( *map(len, map(lambda _activity: _activity.name, activities)), 24) + 8 if groupby: for _tag, names in by_tag.items(): print(c.tag(_tag)) for name in names: # noinspection PyUnresolvedReferences print_log(name, _log[name], current, detailed, name_column_width) return True # for name, log_entry in _log.sorted_entries(): for activity in activities: print(activity.pretty(detailed, name_column_width)) print( c.title('Total: ') + re.sub(r'\d', lambda match: f'{c.digit(match.group())}', day.human_duration))
def is_working() -> bool: data = store.load() return data and 'end' not in data[-1]