diff --git a/.klyban.csv b/.klyban.csv index 8597fa5..c8a6e22 100644 --- a/.klyban.csv +++ b/.klyban.csv @@ -4,12 +4,12 @@ 2,"DONE","edit existing","When calling edit, populate defaults with existing data.","feat","","2023-07-29T17:51:23.166131" 3,"TODO","sanity checks","Check data consistency in load_data and save_data.","core,test","","2023-07-29T17:51:09.870813" 4,"TODO","find csv up dir","Try to find .klyban.csv on upper directories if not in the existing one.","data,feat","","2023-07-29T17:50:32.534829" -5,"TODO","semantic tags","Allow tags starting with # to refer to a Github issue if data file is in a git repository having a remote on Github.","github,feat","","2023-07-29T17:50:08.254833" +5,"HOLD","semantic tags","Allow tags starting with # to refer to a Github issue if data file is in a git repository having a remote on Github.","github,feat","","2023-07-30T10:25:37.048008" 6,"TODO","style metadata","Add a METADATA column to host styling tags.","feat","","2023-07-29T17:49:51.174446" 9,"TODO","fix add","'add' should prompt for columns content.","bug","","2023-07-29T17:49:19.286464" 10,"DONE","fix delete","'delete' print the kanban not updated","bug","","2023-07-29T17:48:14.950376" 11,"TODO","fix quotes","After 'add', empty columns got remaining quotes.","bug","","2023-07-29T17:49:33.862442" 12,"TODO","multiple select","Allow to pass several IDs or ranges to commands.","feat","","2023-07-29T17:49:05.411800" -13,"TODO","card widgets","Refactor into cards/list of cards with widget classes.","feat,UX","","2023-07-30T10:12:18.299798" +13,"DOING","card widgets","Refactor into cards/list of cards with widget classes.","feat,UX","","2023-07-30T15:26:41.900699" 14,"DONE","highlight last action","Use a virtual hint column to point to the last touched task.","feat,UX","","2023-07-30T10:20:36.795048" 15,"TODO","hide old done tasks","Hide tasks with last status that haven't been touched since a long time.","feat","","2023-07-30T10:16:39.517669" diff --git a/klyban.py b/klyban.py index 036d64a..2df136f 100644 --- a/klyban.py +++ b/klyban.py @@ -9,11 +9,12 @@ import numpy as np import pandas as pd import click # import tabulate -import rich.console as richconsole +import rich.console as rconsole from rich.table import Table from rich.text import Text from rich.panel import Panel from rich.columns import Columns +from rich.theme import Theme from rich import box @@ -132,6 +133,8 @@ def check_id(context, param, value): @click.option('-H','--show-headers', is_flag=True, help="Show the headers.") @click.option('-S', '--show-keys' , default='ID,TITLE,DETAILS,TAGS', type=str , show_default=True, help="Comma-separated, ordered list of fields that should be shown (use 'all' for everything).") @click.option('-G', '--highlight', type = int, default = None, help="Highlight a specific task.") +@click.option('-L', '--layout', type = click.Choice(['vertical-compact']), default = 'vertical-compact', help="How to display tasks.") # TODO , 'vertical-fancy', 'horizontal-compact', 'horizontal-fancy' +@click.option('-T', '--theme', type = click.Choice(['none', 'user', 'wb', 'blues', 'reds', 'greens'], case_sensitive=False), default = 'none', help="How to display tasks.") # Low-level configuration options. @click.option('--status-key' , default='STATUS' , type=str, show_default=True, help="Header key defining the status of tasks.") @click.option('--status-list' , default='TODO,DOING,HOLD,DONE', type=str, show_default=True, help="Comma-separated, ordered list of possible values for the status of tasks.") @@ -152,9 +155,6 @@ def cli(context, **kwargs): context.obj['input'] = kwargs['input'] - context.obj['show_headers'] = kwargs['show_headers'] - context.obj['highlight'] = kwargs['highlight'] - context.obj['id_key'] = kwargs['id_key'] context.obj['status_key'] = kwargs['status_key'] context.obj['title_key'] = kwargs['title_key'] @@ -163,6 +163,40 @@ def cli(context, **kwargs): context.obj['deadline_key'] = kwargs['deadline_key'] context.obj['touched_key'] = kwargs['touched_key'] + context.obj['show_headers'] = kwargs['show_headers'] + context.obj['highlight'] = kwargs['highlight'] + context.obj['layout'] = kwargs['layout'] + context.obj['layouts'] = { + 'vertical-compact': VerticalCompact, + } + context.obj['themes'] = { + 'none': Theme({ + 'H': '', + context.obj['id_key']: '', + context.obj['status_key']: '', + context.obj['title_key']: '', + context.obj['details_key']: '', + context.obj['tags_key']: '', + context.obj['deadline_key']: '', + context.obj['touched_key']: '', + 'row_odd': '', + 'row_even': '', + }), + 'wb': Theme({ + 'H': '', + context.obj['id_key']: 'white', + context.obj['status_key']: '', + context.obj['title_key']: 'bold white', + context.obj['details_key']: '', + context.obj['tags_key']: 'italic', + context.obj['deadline_key']: '', + context.obj['touched_key']: 'color(240)', + 'row_odd': 'on color(234)', + 'row_even': 'on color(235)', + }) + } + context.obj['theme'] = context.obj['themes'][kwargs['theme']] + context.obj['status_list'] = kwargs['status_list'].split(',') if kwargs['show_keys'].lower() == "all": context.obj['show_keys'] = [ @@ -185,11 +219,55 @@ def cli(context, **kwargs): # At the end, always load data, whatever the command will be. context.obj['data'] = load_data(context) - # If no command: defaults to `show`. + # Finally, if no command: defaults to `show`. if not context.invoked_subcommand: context.invoke(show) +class Layout: + def __init__(self, context): + self.context = context + +class VerticalCompact(Layout): + def __rich__(self): + df = self.context.obj['data'] + + # Show the kanban tables. + if df.empty: + return "No task." + + panels = [] + + # Group by status. + tables = df.groupby(self.context.obj['status_key']) + # Loop over the asked ordered status groups. + for section in self.context.obj['status_list']: # Ordered. + if section in tables.groups: + df = tables.get_group(section) + # Bring back TID as a regular column. + df = df.reset_index().fillna("") + try: + # Print asked columns. + t = df[self.context.obj['show_keys']] + except KeyError as e: + msg = "" + for section in self.context.obj['show_keys']: + if section not in df.columns: + msg += "cannot show field `{}`, not found in `{}` ".format(section, self.context.obj['input']) + error("INVALID_KEY", msg) + else: + table = Table(show_header = self.context.obj['show_headers'], box = None, row_styles = ['row_odd', 'row_even'], expand = True) + for h in self.context.obj['show_keys']: + table.add_column(h, style = h) + for i,row in t.iterrows(): + items = (str(row[k]) for k in self.context.obj['show_keys']) + table.add_row(*items) + panel = Panel.fit(table, title = section, title_align="left") + panels.append(panel) + + return rconsole.Group(*panels) + + @cli.command() @click.argument('TID', required=False, type=int, is_eager=True, callback=check_id) @click.pass_context @@ -201,51 +279,20 @@ def show(context, tid): df = load_data(context) if tid is None: - # Show the kanban tables. - if df.empty: - print("No task.") - return if context.obj['highlight'] is not None: df.loc[context.obj['highlight'], 'H'] = ':arrow_forward:' - # Group by status. - tables = df.groupby(context.obj['status_key']) - # Loop over the asked ordered status groups. - for section in context.obj['status_list']: # Ordered. - if section in tables.groups: - df = tables.get_group(section) - # Bring back TID as a regular column. - df = df.reset_index().fillna("") - try: - # Print asked columns. - t = df[context.obj['show_keys']] - except KeyError as e: - msg = "" - for section in context.obj['show_keys']: - if section not in df.columns: - msg += "cannot show field `{}`, not found in `{}` ".format(section, context.obj['input']) - error("INVALID_KEY", msg) - else: - console = richconsole.Console() - # table = Table(show_header = context.obj['show_headers'], row_styles=["color(39)","color(33)"], box = None) - table = Table(show_header = context.obj['show_headers'], box = None) - for h in context.obj['show_keys']: - table.add_column(h) - for i,row in t.iterrows(): - items = (str(row[k]) for k in context.obj['show_keys']) - table.add_row(*items) - # console.print(table) - # panel = Panel.fit(table, title = section, title_align="left", border_style="bold blue") - panel = Panel.fit(table, title = section, title_align="left") - console.print(panel) + layout = context.obj['layouts'][context.obj['layout']](context) + console = rconsole.Console(theme = context.obj['theme']) + console.print(layout) else: # tid is not None. # Show a task card. row = df.loc[tid] - console = richconsole.Console() + console = rconsole.Console() table = Table(box = None, show_header = False) table.add_column("Task")