diff --git a/MANIFEST.in b/MANIFEST.in index 0d08dc3..945326a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,3 @@ -include README.md +include docs/README.md include LICENSE include flomo/beep.mp3 diff --git a/docs/README.md b/docs/README.md index 745e356..c13fb86 100644 --- a/docs/README.md +++ b/docs/README.md @@ -93,15 +93,21 @@ flomo s -t work -n "math" ``` flomo --help - Usage: flomo [OPTIONS] COMMAND [ARGS]... + Usage: flomo [OPTIONS] COMMAND [ARGS]... A Flowmodoro CLI for productivity enthusiasts. - Options: - --help Show this message and exit. - - Commands: - start (s) Start a Flowmodoro session. + Options: + --help Show this message and exit. + + Commands: + init (i) Initialize the required files for Flomo. + start (s) Start a Flowmodoro session. + tracking (t) Show the tracking history. + delete (d) Delete sessions. + change (ch) Change session data. + config (cf) Print config file path + error (er) Show the error log. ``` #### Pro Tip for Linux Users diff --git a/flomo/cli.py b/flomo/cli.py index b5bcf51..b02c729 100644 --- a/flomo/cli.py +++ b/flomo/cli.py @@ -1,4 +1,7 @@ import datetime +import gc +import sys +from typing import Tuple import click import click_aliases @@ -9,9 +12,6 @@ import flomo.tracker as tracker import flomo.ui as ui -# TODO: Ability for users to see message.log file. -# TODO: Change Config data from a Command - class OrderCommands(click.Group): def list_commands(self, ctx: click.Context) -> list[str]: @@ -73,9 +73,12 @@ def start(tag: str, name: str): errors.DBFileNotFoundError, errors.NoConfigError, errors.InvalidConfigKeyError, + Exception, ) as e: - # TODO: print(e) isnt printing anything + helpers.error_log(f"{datetime.datetime.now()} - Error: {e}") print(e) + finally: + sys.exit() @flomo.command(aliases=["t"]) @@ -90,26 +93,32 @@ def tracking(): errors.NoSessionsError, errors.NoSessionError, ) as e: - helpers.message_log(str(e)) + helpers.error_log(str(e)) print(e) -# TODO: delete multiple sessions at once @flomo.command(aliases=["d"]) -@click.argument("session_id") -def delete(session_id: str): +@click.argument("session_ids", nargs=-1) +def delete(session_ids: Tuple): """ - Delete a session. + Delete sessions. """ try: db = tracker.Tracker() - db.delete_session(int(session_id)) + if not db.get_sessions(): + raise errors.NoSessionsError() + click.confirm("Are you sure you want to delete the session(s)?", abort=True) + db.delete_session(session_ids) db.conn.close() + if len(session_ids) == 0: + return print("Deleted the last session.") + print(f"Deleted session(s): {', '.join(map(str, session_ids))}") except ( errors.DBFileNotFoundError, errors.NoSessionError, + errors.NoSessionsError ) as e: - helpers.message_log(str(e)) + helpers.error_log(str(e)) print(e) @@ -129,21 +138,85 @@ def change(session_id: str, tag: str | None, name: str | None): errors.DBFileNotFoundError, errors.NoSessionError, ) as e: - helpers.message_log(str(e)) + helpers.error_log(str(e)) print(e) @flomo.command(aliases=["cf"]) -def config(): +@click.option( + "-n", "--notif", help="Set notification priority to 'off', 'normal', or 'high'." +) +@click.option("-tc", "--tag-color", help="Set or delete tag colors. (tag_name, color)") +@click.option("-ds", "--default-session", help="Set default session data. (tag, Name)") +def config(notif: str, tag_color: str, default_session: str): """ - Print config file path + Change the config values or get the config file path. """ try: - print(helpers.get_path("config.json", True)) + print(f"File Path: {helpers.get_path("config.json", True)}") + conf_ = conf.Config() + + if notif: + notif = notif.lower() + if notif.lower() in ["off", "normal", "high"]: + conf_.set_config(conf.NOTIFICATION_PRIORITY, notif) + print(f"Notification Priority set to {notif}") + else: + raise click.BadOptionUsage("notif", "Invalid input") + + if tag_color: + tag_color = tag_color.lower() + tc = tag_color.split(" ") + if len(tc) == 2: + if not tc[1]: + raise click.BadOptionUsage("tag-color", "Invalid input") + conf_.set_config(conf.TAG_COLORS, tag_color, nested_value=True) + print(f"Tag {tc[0]}'s color set to '{tc[1]}'") + elif len(tc) == 1: + tag_colors = conf_.get_config(conf.TAG_COLORS) + if not tc[0] in list(tag_colors.keys()): + raise click.BadOptionUsage("tag-color", "Invalid input") + conf_.delete_tag_color(tc[0]) + print(f"Deleted color for tag '{tc[0]}'") + else: + raise click.BadOptionUsage("tag-color", "Invalid input") + + if default_session: + ds = default_session.split(" ") + ds[0] = ds[0].lower() + if len(ds) == 2 and ds[0] and ds[1]: + conf_.set_config( + conf.DEFAULT_SESSION_DATA, " ".join(ds), nested_value=True + ) + print(f"Default Session Data set to Tag: {ds[0]} and Name: {ds[1]}") + else: + raise click.BadOptionUsage("default-session", "Invalid input") except errors.NoConfigError as e: - helpers.message_log(str(e)) + helpers.error_log(str(e)) print(e) +@flomo.command(aliases=["er"]) +@click.option("-c", "--clear", is_flag=True, help="Clear the error log.") +def error(clear: bool): + """ + Show the error log. + """ + try: + path = helpers.get_path("error.log", True) + print(f"File Path: {path}") + if clear: + with open(path, "w") as f: + f.write("") + return print("Message log cleared.") + + with open(path, "r") as f: + if not f.read(): + return print("No errors found till now.") + print(f.read()) + except FileNotFoundError: + print("No errors found till now.") + + if __name__ == "__main__": flomo() diff --git a/flomo/config.py b/flomo/config.py index d4d451a..08f31e0 100644 --- a/flomo/config.py +++ b/flomo/config.py @@ -5,11 +5,16 @@ import flomo.helpers as helpers default_session_data = { - "tag": "Work", + "tag": "work", "name": "Working", } -tag_colors = {"Work": "red", "Study": "blue", "Exercise": "green"} +tag_colors = {"work": "red", "study": "blue", "exercise": "green"} + + +DEFAULT_SESSION_DATA = "default_session_data" +NOTIFICATION_PRIORITY = "notification_priority" +TAG_COLORS = "tag_colors" class Config: @@ -28,25 +33,25 @@ def _config_file_exists(self): def _get_missing_keys(self): if not os.path.exists(self.path): - return ["default_session_data", "notification_priority", "tag_colors"] + return [DEFAULT_SESSION_DATA, NOTIFICATION_PRIORITY, TAG_COLORS] with open(self.path, "r") as f: data = json.load(f) missing_keys = [] if ( - "default_session_data" not in data - or data["default_session_data"].keys() != default_session_data.keys() + DEFAULT_SESSION_DATA not in data + or data[DEFAULT_SESSION_DATA].keys() != default_session_data.keys() ): - missing_keys.append("default_session_data") + missing_keys.append(DEFAULT_SESSION_DATA) - if "notification_priority" not in data or data[ - "notification_priority" + if NOTIFICATION_PRIORITY not in data or data[ + NOTIFICATION_PRIORITY ].lower() not in ["off", "normal", "high"]: - missing_keys.append("notification_priority") + missing_keys.append(NOTIFICATION_PRIORITY) - if "tag_colors" not in data: - missing_keys.append("tag_colors") + if TAG_COLORS not in data: + missing_keys.append(TAG_COLORS) return missing_keys @@ -62,18 +67,19 @@ def create_config(self): with open(self.path, "r+") as f: data = json.load(f) for missing_key in missing_keys: - if missing_key == "default_session_data": + if missing_key == DEFAULT_SESSION_DATA: data[missing_key] = default_session_data - if missing_key == "notification_priority": + if missing_key == NOTIFICATION_PRIORITY: data[missing_key] = "normal" - if missing_key == "tag_colors": + if missing_key == TAG_COLORS: data[missing_key] = tag_colors f.seek(0) + f.truncate(0) json.dump(data, f, indent=4) def get_config(self, key: str): - if key == "default_session_data" and not self._config_file_exists()[0]: + if key == DEFAULT_SESSION_DATA and not self._config_file_exists()[0]: return default_session_data try: @@ -83,10 +89,36 @@ def get_config(self, key: str): except KeyError: raise errors.InvalidConfigKeyError(key) + def set_config(self, key: str, value: str, nested_value: bool = False): + with open(self.path, "r+") as f: + data = json.load(f) + if nested_value: + if key == DEFAULT_SESSION_DATA: + tag, name = value.split(" ") + data[key]["tag"] = tag + data[key]["name"] = name + elif key == TAG_COLORS: + key_, value_ = value.split(" ") + data[key][key_] = value_ + else: + data[key] = value + f.seek(0) + f.truncate(0) + json.dump(data, f, indent=4) + + def delete_tag_color(self, tag_name: str): + with open(self.path, "r+") as f: + data = json.load(f) + key = [k for k, v in data[TAG_COLORS].items() if k.lower() == tag_name][0] + del data[TAG_COLORS][key] + f.seek(0) + f.truncate(0) + json.dump(data, f, indent=4) + def get_default_session_data(): try: - conf = Config(get_default_session_data=True).get_config("default_session_data") + conf = Config(get_default_session_data=True).get_config(DEFAULT_SESSION_DATA) return conf["tag"], conf["name"] - except errors.InvalidConfigKeyError: + except (errors.InvalidConfigKeyError, KeyError): return default_session_data["tag"], default_session_data["name"] diff --git a/flomo/helpers.py b/flomo/helpers.py index 11a8b1d..0ffd3ae 100644 --- a/flomo/helpers.py +++ b/flomo/helpers.py @@ -41,7 +41,7 @@ def play_sound(): path = get_path("beep.mp3") conf = config.Config() - notification_priority = str(conf.get_config("notification_priority")) + notification_priority = str(conf.get_config(config.NOTIFICATION_PRIORITY)) if notification_priority.lower() == "off": return @@ -60,11 +60,11 @@ def play_sound(): pass # Error is already getting logged from ui.py -def message_log(message: str): - path = get_path("message.log", in_data=True) +def error_log(error: str): + path = get_path("error.log", in_data=True) with open(path, "a") as f: - f.write(message + "\n") + f.write(error + "\n") def format_time(seconds: int) -> str: diff --git a/flomo/tracker.py b/flomo/tracker.py index a0c8a21..e0cc268 100644 --- a/flomo/tracker.py +++ b/flomo/tracker.py @@ -1,5 +1,6 @@ import datetime import sqlite3 +from typing import Tuple import pandas import tabulate @@ -29,6 +30,7 @@ def create_table(self): self.conn.commit() def create_session(self, tag: str, name: str, start_time: datetime.datetime) -> int: + # TODO: Better way of generating session_id session_id = int(start_time.timestamp() % 1000000) self.cursor.execute( "INSERT INTO sessions (id, date_time, tag, name) VALUES (?, ?, ?, ?)", @@ -59,10 +61,24 @@ def get_session(self, session_id: int): self.cursor.execute("SELECT * FROM sessions WHERE id = ?", (session_id,)) return self.cursor.fetchone() - def delete_session(self, session_id: int): - if not self.get_session(session_id): - raise errors.NoSessionError(session_id) - self.cursor.execute("DELETE FROM sessions WHERE id = ?", (session_id,)) + def delete_session(self, session_ids: Tuple[str] | Tuple): + if len(session_ids) == 0: + self.cursor.execute( + "DELETE FROM sessions WHERE id = (SELECT MAX(id) FROM sessions)" + ) + self.conn.commit() + + for session_id in session_ids: + session_id = int(session_id) + if not self.get_session(session_id): + raise errors.NoSessionError(session_id) + + self.cursor.execute( + "DELETE FROM sessions WHERE id IN ({seq})".format( + seq=",".join(["?"] * len(session_ids)) + ), + session_ids, + ) self.conn.commit() def update_session(self, session_id: int, tag: str | None, name: str | None): diff --git a/flomo/ui.py b/flomo/ui.py index 812b4bd..02f8dba 100644 --- a/flomo/ui.py +++ b/flomo/ui.py @@ -1,5 +1,4 @@ import datetime -import sys import threading import time @@ -37,7 +36,7 @@ def _border_color(self): tag = self.tag.split("#")[1].lower() tag_colors: dict[str, str] = { k.lower(): v.lower() - for k, v in config.Config().get_config("tag_colors").items() + for k, v in config.Config().get_config(config.TAG_COLORS).items() } if tag in [i.lower() for i in tag_colors.keys()]: @@ -126,7 +125,7 @@ def main(tag: str, name: str, session_id: int): # chilling_panel_thread.join() del chilling_ui - except (KeyboardInterrupt, Exception) as e: + except KeyboardInterrupt: if "flowing_ui" in locals() and flowing_ui is not None: flowing_ui.close_live_panel = True if "chilling_ui" in locals() and chilling_ui is not None: @@ -137,9 +136,5 @@ def main(tag: str, name: str, session_id: int): play_sound_thread.join() # if 'chilling_panel_thread' in locals() and chilling_panel_thread.is_alive(): # chilling_panel_thread.join() - - if isinstance(e, Exception): - helpers.message_log(f"{datetime.datetime.now()} - Error: {e}") finally: tracker.end_session(session_id) - sys.exit() diff --git a/setup.py b/setup.py index 3c33edf..637c98c 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,9 @@ AUTHOR = "Dark Circles" VERSION = "1.0.0" SHORT_DESCRIPTION = "A Flowmodoro CLI for producitivity enthusiasts." -AUTHOR_EMAIL = "" +AUTHOR_EMAIL = ( + ", , " +) URL = "https://github.com/moiSentineL/flomo" INSTALL_REQUIRES = [ "click",