From 032b6788364f6c283bb7c1e49d491344119c754b Mon Sep 17 00:00:00 2001 From: nojhan Date: Mon, 28 Aug 2023 12:21:20 +0200 Subject: [PATCH] remove deprecated klyban --- .klyban.conf | 26 -- klyban.py | 908 --------------------------------------------------- 2 files changed, 934 deletions(-) delete mode 100644 .klyban.conf delete mode 100644 klyban.py diff --git a/.klyban.conf b/.klyban.conf deleted file mode 100644 index 119d9ca..0000000 --- a/.klyban.conf +++ /dev/null @@ -1,26 +0,0 @@ - -[options] -layout = horizontal-spaced -theme = nojhan -status_list = TODO,DOING,HOLD,DONE -show_status = TODO,DOING,HOLD -show_headers = False -show_fields = ID,TITLE,DETAILS - -id_key = ID -status_key = STATUS -title_key = TITLE -details_key = DETAILS -tags_key = TAGS -deadline_key = DEADLINE - -[options.add] -status = TODO - -[options.find] -all = True -# ▶ 🖈 🢂 🡪 🡆 🠲 🠚 🠊 ⧐ ➤ ❯❱ -mark = ▶ - -[options.filter] -all = True diff --git a/klyban.py b/klyban.py deleted file mode 100644 index ac68866..0000000 --- a/klyban.py +++ /dev/null @@ -1,908 +0,0 @@ -import sys -import csv -import json -import datetime -import textwrap -from configparser import ConfigParser - -import numpy as np -import pandas as pd -import click -# import tabulate -import rich.console as rconsole -from rich.table import Table as richTable -from rich.text import Text as richText -from rich.panel import Panel as richPanel -from rich.columns import Columns as richColumns -from rich.theme import Theme as richTheme -from rich.layout import Layout as richLayout -from rich.console import Group as richGroup -from rich import box - - -error_codes = { - "INVALID_KEY": 100, - "ID_NOT_FOUND": 101, - "UNKNOWN_STATUS": 102, -} - - -def error(name,msg): - print("ERROR:",msg) - sys.exit(error_codes[name]) - - -def load_data(context): - try: - # Automagically manages standard input if input=="-", thanks to allow_dash=True. - with click.open_file(context.obj['input'], mode='r') as fd: - df = pd.read_csv(fd) - # FIXME data sanity checks: unique index, aligned status, valid dates - - except FileNotFoundError: - # Create an empty file. - df = pd.DataFrame(columns=[ - 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'], - ]) - df = df.set_index(context.obj['id_key']) - save_data(context, df) - - else: - # set index on ID. - df = df.astype({context.obj['id_key']:int}) - df = df.set_index(context.obj['id_key']) - - # Remove any values consisting of empty spaces or quotes. - df = df.replace(r'^[\s"\']*$', np.nan, regex=True) - - finally: - # Virtual "hints" column. - df['H'] = '' - if context.obj['debug']: - print("Loaded:") - print(df) - return df - - -def save_data(context, df): - if context.obj['debug']: - print("Save:") - print(df) - # FIXME double check that there are actually data. - - # Remove the virtual "hints" column. - df = df.drop('H', axis = 1) - - # Bring back ID as a regular column. - df = df.reset_index() - - # Remove any values consisting of empty spaces or quotes. - df = df.replace(r'^[\s"\']*$', np.nan, regex=True) - - # Automagically manages standard input if input=="-", thanks to allow_dash=True. - with click.open_file(context.obj['input'], mode='w') as fd: - df.to_csv(fd, index=False, quoting=csv.QUOTE_NONNUMERIC) - - -def configure(context, param, filename): - """Overwrite defaults for options.""" - cfg = ConfigParser() - cfg.read(filename) - context.default_map = {} - for sect in cfg.sections(): - command_path = sect.split('.') - if command_path[0] != 'options': - continue - defaults = context.default_map - for cmdname in command_path[1:]: - defaults = defaults.setdefault(cmdname, {}) - defaults.update(cfg[sect]) - - -def check_id(context, param, value): - """Callback checking if task exists.""" - if value is None: # For optional TID. - return value - assert(type(value) == int) - df = load_data(context) - if value not in df.index: - error("ID_NOT_FOUND", "{} `{}` was not found in data `{}`".format(context.obj['id_key'], value, context.obj['input'])) - return value - - - -# Global group holding global options. -@click.group(invoke_without_command=True) -# Core options. -@click.option( - '-c', '--config', - type = click.Path(dir_okay=False), - default = '.klyban.conf', - callback = configure, - is_eager = True, - expose_value = False, - help = 'Read option defaults from the specified configuration file.', - show_default = True, -) -@click.option('-i', '--input' , help="CSV data file.", default='.klyban.csv', type=click.Path(writable=True, readable=True, allow_dash=True), show_default=True) -# Display options. -@click.option('-h','--show-headers', is_flag=True, help="Show the headers.") -@click.option('-s', '--show-fields' , 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('--show-status' , default='TODO,DOING,HOLD', type=str, show_default=True, help="Comma-separated, ordered list of status to show.") -@click.option('-g', '--highlight', type = int, default = None, help="Highlight a specific task.") -@click.option('--highlight-mark', type = str, default = '▶', help="String used to highlight a specific task.") -@click.option('-l', '--layout', type = click.Choice(['vertical-compact', 'vertical-spaced', 'horizontal-compact', 'horizontal-spaced']), default = 'vertical-compact', help="How to display tasks.") # TODO , 'horizontal-compact', 'horizontal-spaced' -@click.option('-t', '--theme', type = click.Choice(['none', 'user', 'BW', 'BY', 'RW', 'nojhan'], case_sensitive=False), default = 'none', help="How to display tasks.") - -# Low-level configuration options. -@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.") -@click.option('--status-key' , default='STATUS' , type=str, show_default=True, help="Header key defining the status of tasks.") -@click.option('--id-key' , default='ID' , type=str, show_default=True, help="Header key defining the unique ID of tasks.") -@click.option('--title-key' , default='TITLE' , type=str, show_default=True, help="Header key defining the title (short description) of tasks.") -@click.option('--details-key' , default='DETAILS' , type=str, show_default=True, help="Header key defining the details (long description) of tasks.") -@click.option('--tags-key' , default='TAGS' , type=str, show_default=True, help="Header key defining the tags associated to tasks.") -@click.option('--deadline-key', default='DEADLINE', type=str, show_default=True, help="Header key defining the deadlines tasks.") -@click.option('--touched-key', default='TOUCHED', type=str, show_default=True, help="Header key defining the deadlines tasks.") -@click.option('--debug', is_flag=True, help="Print debugging information.") -@click.pass_context -def cli(context, **kwargs): - - # print(json.dumps(kwargs, sort_keys=True, indent=4)) - - # Ensure that context.obj exists and is a dict. - context.ensure_object(dict) - - def store(context_key, kw_key = None): - if not kw_key: - kw_key = context_key - context.obj[context_key] = kwargs[kw_key] - - store('input') - - store('debug') - - store('id_key') - store('status_key') - store('title_key') - store('details_key') - store('tags_key') - store('deadline_key') - store('touched_key') - - store('show_headers') - store('highlight') - store('highlight_mark') - - store('layout') - context.obj['layouts'] = { - 'vertical-compact': VerticalCompact, - 'vertical-spaceds': VerticalSpaced, - 'horizontal-compact': HorizontalCompact, - 'horizontal-spaced': HorizontalSpaced, - } - - context.obj['themes'] = { - 'none': richTheme({ - 'H': '', - 'matching': 'italic', - 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': '', - }), - 'BW': richTheme({ - 'H': 'white', - 'matching': 'italic', - context.obj['id_key']: 'bold black', - context.obj['status_key']: 'bold', - context.obj['title_key']: 'bold white', - context.obj['details_key']: 'white', - context.obj['tags_key']: 'italic white', - context.obj['deadline_key']: 'white', - context.obj['touched_key']: 'black', - 'row_odd': 'on color(237)', - 'row_even': 'on color(239)', - }), - 'BY': richTheme({ - 'H': 'color(220)', - 'matching': 'italic', - context.obj['id_key']: 'bold color(39)', - context.obj['status_key']: 'bold color(227)', - context.obj['title_key']: 'color(220)', - context.obj['details_key']: 'color(33)', - context.obj['tags_key']: 'color(27)', - context.obj['deadline_key']: 'white', - context.obj['touched_key']: 'black', - 'row_odd' : 'bold', - 'row_even': '', - }), - 'RW': richTheme({ - 'H': 'red', - 'matching': 'italic', - context.obj['id_key']: 'bold red', - context.obj['status_key']: 'bold white', - context.obj['title_key']: 'bold white', - context.obj['details_key']: 'white', - context.obj['tags_key']: 'red', - context.obj['deadline_key']: 'white', - context.obj['touched_key']: 'color(240)', - 'row_odd' : '', - 'row_even': '', - }), - 'nojhan': richTheme({ - 'H': '#4E9A06', - 'matching': 'on #464141', - context.obj['id_key']: 'bold color(214)', - context.obj['status_key']: 'bold italic white', - context.obj['title_key']: 'bold white', - context.obj['details_key']: 'white', - context.obj['tags_key']: 'color(27)', - context.obj['deadline_key']: 'white', - context.obj['touched_key']: 'color(240)', - 'row_odd': 'on #262121', - 'row_even' : 'on #2d2929', - }), - } - context.obj['theme'] = context.obj['themes'][kwargs['theme']] - - context.obj['show_status'] = kwargs['show_status'].split(',') - context.obj['status_list'] = kwargs['status_list'].split(',') - if kwargs['show_fields'].lower() == "all": - context.obj['show_fields'] = [ - 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'], - ] - else: - context.obj['show_fields'] = kwargs['show_fields'].split(',') - - # Always show the 'Hint' column. - context.obj['show_fields'] = ['H'] + context.obj['show_fields'] - - # At the end, always load data, whatever the command will be. - context.obj['data'] = load_data(context) - - # 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 Vertical(Layout): - def __init__(self, context, table_box, show_lines, panel_box): - super().__init__(context) - self.show_lines = show_lines - self.table_box = table_box - self.panel_box = panel_box - - def section_prefix(self, sections): - pass - - def section_suffix(self, sections): - pass - - def section(self, section, table, sections): - # Title styling does not work because of bug #2466 in Rich, fixed after 32d6e99. - # See https://github.com/Textualize/rich/issues/2466 - title = richText(section, style = self.context.obj['status_key'], overflow = 'ellipsis') - panel = richPanel(table, title = title, title_align="left", border_style = self.context.obj['status_key'], box = self.panel_box, expand = False, padding = (0,0)) - sections.append(panel) - - def __rich__(self): - df = self.context.obj['data'] - - # Show the kanban tables. - if df.empty: - return "No task." - - sections = [] - - if self.context.obj['highlight'] is not None: - df.loc[self.context.obj['highlight'], 'H'] = self.context.obj['highlight_mark'] - - # Group by status. - tables = df.groupby(self.context.obj['status_key']) - # Loop over the asked ordered status groups. - for section in self.context.obj['show_status']: # Ordered. - if section in tables.groups: - df = tables.get_group(section) - - # Bring back TID as a regular column. - df = df.reset_index().fillna("") - - # Always consider the hint column. - if 'H' not in self.context.obj['show_fields']: - self.context.obj['show_fields'] = ['H'] + self.context.obj['show_fields'] - - try: - # Print asked columns. - t = df[self.context.obj['show_fields']] - except KeyError as e: - msg = "" - for section in self.context.obj['show_fields']: - if section not in df.columns: - msg += "cannot show field `{}`, not found in `{}` ".format(section, self.context.obj['input']) - error("INVALID_KEY", msg) - else: - if len(df.index) <= 1: - show_lines = False - table_box = None - else: - show_lines = self.show_lines - table_box = self.table_box - - table = richTable(show_header = self.context.obj['show_headers'], box = table_box, row_styles = ['row_odd', 'row_even'], show_lines = show_lines, expand = True) - - for h in self.context.obj['show_fields']: - table.add_column(h, style = h) - - for i,row in t.iterrows(): - items = (str(row[k]) for k in self.context.obj['show_fields']) - if row['H']: - row_style = 'matching' - else: - row_style = None - table.add_row(*items, style = row_style) - - self.section_prefix(sections) - self.section(section, table, sections) - self.section_suffix(sections) - - return rconsole.Group(*sections) - -class VerticalCompact(Vertical): - def __init__(self, context): - super().__init__(context, table_box = None, show_lines = False, panel_box = box.ROUNDED) - # FIXME find a way to use console.rule instead of richPanel in self.section - -class VerticalSpaced(Vertical): - def __init__(self, context): - super().__init__(context, table_box = box.HORIZONTALS, show_lines = True, panel_box = box.HEAVY_EDGE) - - def section_prefix(self, sections): - sections.append("\n") - -class Horizontal(Layout): - def __init__(self, context, table_box, show_lines, panel_box): - super().__init__(context) - self.show_lines = show_lines - self.table_box = table_box - self.panel_box = panel_box - -class HorizontalCompact(Horizontal): - def __init__(self, context): - super().__init__(context, table_box = None, show_lines = False, panel_box = box.ROUNDED) - - def __rich__(self): - df = self.context.obj['data'] - - # Show the kanban tables. - if df.empty: - return "No task." - - sections = [] - - if self.context.obj['highlight'] is not None: - df.loc[self.context.obj['highlight'], 'H'] = self.contex.obj['highlight_mark'] - - # Group by status. - tables = df.groupby(self.context.obj['status_key']) - # Loop over the asked ordered status groups. - for section in self.context.obj['show_status']: # Ordered. - if section in tables.groups: - df = tables.get_group(section) - - # Bring back TID as a regular column. - df = df.reset_index().fillna("") - - # Always consider the hint column. - if 'H' not in self.context.obj['show_fields']: - self.context.obj['show_fields'] = ['H'] + self.context.obj['show_fields'] - - console = rconsole.Console() - - try: - # Print asked columns. - t = df[self.context.obj['show_fields']] - except KeyError as e: - msg = "" - for section in self.context.obj['show_fields']: - if section not in df.columns: - msg += "cannot show field `{}`, not found in `{}` ".format(section, self.context.obj['input']) - error("INVALID_KEY", msg) - else: - if len(df.index) <= 1: - show_lines = False - table_box = None - else: - show_lines = self.show_lines - table_box = self.table_box - - table = richTable(show_header = self.context.obj['show_headers'], box = table_box, row_styles = ['row_odd', 'row_even'], show_lines = show_lines, expand = True) - - for h in self.context.obj['show_fields']: - table.add_column(h, style = h) - - for i,row in t.iterrows(): - items = (str(row[k]) for k in self.context.obj['show_fields']) - if row['H']: - row_style = 'matching' - else: - row_style = None - table.add_row(*items, style = row_style) - - title = richText(section, style = self.context.obj['status_key'], overflow = 'ellipsis') - panel = richPanel(table, title = title, title_align="left", border_style = self.context.obj['status_key'], box = self.panel_box, expand = True, padding = (0,0)) - - sections.append(richLayout(panel, name = section)) - - layout = richLayout() - layout.split_row(*sections) - - # FIXME ugly hack: pre-render the englobing panel, then count the number of "non empty" lines. - fakepan = richPanel(layout, box = box.SIMPLE, border_style = 'none', padding = (0,0)) - console = rconsole.Console(theme = self.context.obj['theme'], no_color = True) - with console.capture() as capture: - console.print(fakepan) - lines = capture.get().split('\n') - nb_lines = 0 - for line in lines: - letters = set(line) - if letters != set({'m', '[', '\x1b', '│', '1', '3', '0', ' ', ';'}): - nb_lines += 1 - - # FIXME get rid of the space padding added by the panel, even without border. - superpan = richPanel(layout, height = nb_lines, box = box.SIMPLE, border_style = 'none', padding = (0,0)) - return superpan - -class HorizontalSpaced(Horizontal): - def __init__(self, context): - super().__init__(context, table_box = None, show_lines = False, panel_box = box.ROUNDED) - - def __rich__(self): - df = self.context.obj['data'] - - # Show the kanban tables. - if df.empty: - return "No task." - - sections = [] - - if self.context.obj['highlight'] is not None: - df.loc[self.context.obj['highlight'], 'H'] = self.context.obj['highlight_mark'] - - # Group by status. - tables = df.groupby(self.context.obj['status_key']) - # Loop over the asked ordered status groups. - for section in self.context.obj['show_status']: # Ordered. - if section in tables.groups: - df = tables.get_group(section) - - # Bring back TID as a regular column. - df = df.reset_index().fillna("") - - # Always consider the hint column. - if 'H' not in self.context.obj['show_fields']: - self.context.obj['show_fields'] = ['H'] + self.context.obj['show_fields'] - - console = rconsole.Console() - - try: - # Print asked columns. - t = df[self.context.obj['show_fields']] - except KeyError as e: - msg = "" - for section in self.context.obj['show_fields']: - if section not in df.columns: - msg += "cannot show field `{}`, not found in `{}` ".format(section, self.context.obj['input']) - error("INVALID_KEY", msg) - else: - if len(df.index) <= 1: - show_lines = False - table_box = None - else: - show_lines = self.show_lines - table_box = self.table_box - - table = richTable(show_header = False, box = None, show_lines = False, expand = True, row_styles = ['row_odd', 'row_even']) - table.add_column('') - - # One task_table per task. - for i,task in t.iterrows(): - - task_table = richTable(show_header = self.context.obj['show_headers'], box = table_box, show_lines = show_lines, expand = True, padding = (0,0)) - - # One column. - task_table.add_column('') - - # Add one row for spacing, - # using a non-breakable space to bypass fakepan row filtering. - task_table.add_row(' ') - - nb_title_keys = 0 - for h in self.context.obj['show_fields']: - if h in ['H', self.context.obj['id_key'], self.context.obj['title_key']]: - nb_title_keys += 1 - - task_title = [] - for h in self.context.obj['show_fields']: - item = str(task[h]) - - if h == 'H': - task_title.append(richText(task['H'], style = 'H')) - if len(task_title) == nb_title_keys: - task_table.add_row(task_title[0]+task_title[1]+' '+task_title[2], style = h) - task_title = [] - continue - elif h == self.context.obj['id_key']: - task_title.append(richText(item, style = h)) - if len(task_title) == nb_title_keys: - task_table.add_row(task_title[0]+task_title[1]+' '+task_title[2], style = h) - task_title = [] - continue - elif h == self.context.obj['title_key']: - task_title.append(richText(item, style = h)) - if len(task_title) == nb_title_keys: - task_table.add_row(task_title[0]+task_title[1]+' '+task_title[2], style = h) - task_title = [] - continue - - if len(task_title) == nb_title_keys: - task_table.add_row(task_title[0]+task_title[1]+' '+task_title[2], style = h) - task_title = [] - else: - task_table.add_row(item, style = h) - - if task['H']: - row_style = 'matching' - else: - row_style = None - table.add_row(task_table, style = row_style) - - title = richText(section, style = self.context.obj['status_key'], overflow = 'ellipsis') - panel = richPanel(table, title = title, title_align="left", border_style = self.context.obj['status_key'], box = self.panel_box, expand = True, padding = (0,0)) - - sections.append(richLayout(panel, name = section)) - - layout = richLayout() - layout.split_row(*sections) - - # FIXME ugly hack: pre-render the englobing panel, then count the number of "non empty" lines. - fakepan = richPanel(layout, box = box.SIMPLE, border_style = 'none', padding = (0,0)) - console = rconsole.Console(theme = self.context.obj['theme'], no_color = True) - with console.capture() as capture: - console.print(fakepan) - lines = capture.get().split('\n') - nb_lines = 0 - for line in lines: - letters = set(line) - if letters != set({'m', '[', '\x1b', '│', '1', '3', '0', ' ', ';'}): - nb_lines += 1 - - # FIXME get rid of the space padding added by the panel, even without border. - superpan = richPanel(layout, height = nb_lines, box = box.SIMPLE, border_style = 'none', padding = (0,0)) - return superpan - - -@cli.command() -@click.argument('TID', required=False, type=int, is_eager=True, callback=check_id) -@click.pass_context -def show(context, tid): - """Show a task card (if ID is passed) or the whole the kanban (else).""" - - # Because commands invoked before may alter the table, - # we need to reload the data. - df = load_data(context) - - if tid is None: - - 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 = rconsole.Console(theme = context.obj['theme']) - - table = richTable(box = None, show_header = False, expand = False, row_styles = ['row_odd', 'row_even']) - table.add_column("Task") - - def add_row_text(table, key, icon = ''): - if context.obj[key] in context.obj['show_fields']: - if str(row[context.obj[key]]) != "nan": # FIXME WTF? - table.add_row(icon + row[context.obj[key]], style = context.obj[key]) - else: - return - - def add_row_list(table, key = context.obj['tags_key'], icon = ''): - if context.obj[key] in context.obj['show_fields']: - if str(row[context.obj[key]]) != "nan": # FIXME WTF? - tags = [icon+t for t in row[context.obj[key]].split(',')] - columns = richColumns(tags, expand = False) - table.add_row(columns, style = context.obj[key]) - else: - return - - add_row_text(table, 'status_key') - add_row_text(table, 'details_key') - add_row_list(table, 'tags_key', '🏷 ') - add_row_text(table, 'deadline_key', '🗓') - add_row_text(table, 'touched_key', ':calendar-text:') - - # Label content. - label = richText() - if context.obj['id_key'] in context.obj['show_fields']: - label += richText(str(tid)+":", style = context.obj['id_key']) - if context.obj['title_key'] in context.obj['show_fields']: - label += richText(" "+row[context.obj['title_key']], style = context.obj['title_key']) - - panel = richPanel(table, title = label, title_align="left", expand = False, padding = (0,0)) - console.print(panel) - - -@cli.command() -@click.argument('TITLE', required=True, nargs=-1) -@click.option('-d', '--details' , type=str, prompt=True) -@click.option('-t', '--tags' , type=str, prompt=True) -@click.option('-a', '--deadline', type=str, prompt=True) -@click.option('-s', '--status' , type=str, default='TODO') -@click.pass_context -def add(context, title, status, details, tags, deadline): - """Add a new task.""" - df = context.obj['data'] - if df.index.empty: - next_id = 0 - else: - next_id = df.index.max() + 1 - - df.loc[next_id] = pd.Series({ - context.obj['status_key']: status, - context.obj['title_key']: " ".join(title), - context.obj['details_key']: details, - context.obj['tags_key']: tags, - context.obj['deadline_key']: deadline, - context.obj['touched_key']: datetime.datetime.now().isoformat(), - }) - - save_data(context,df) - - context.obj['highlight'] = next_id - context.invoke(show) - - -def default_from_existing(key): - class OptionDefaultFromContext(click.Option): - def get_default(self, context): - tid = context.params['tid'] - df = context.obj['data'] - assert(tid in df.index) - row = df.loc[tid] - value = row[context.obj[key]] - if str(value) != "nan": # FIXME WTF? - self.default = value - else: - self.default = "" - return super(OptionDefaultFromContext, self).get_default(context) - return OptionDefaultFromContext - -@cli.command() -@click.argument('TID', required=True, type=int, is_eager=True, callback=check_id) -@click.option('-t', '--title' , type=str, prompt=True, cls = default_from_existing('title_key')) -@click.option('-s', '--status' , type=str, prompt=True, cls = default_from_existing('status_key')) -@click.option('-d', '--details' , type=str, prompt=True, cls = default_from_existing('details_key')) -@click.option('-t', '--tags' , type=str, prompt=True, cls = default_from_existing('tags_key')) -@click.option('-a', '--deadline', type=str, prompt=True, cls = default_from_existing('deadline_key')) -@click.pass_context -def edit(context, tid, title, status, details, tags, deadline): - """Add a new task.""" - df = context.obj['data'] - assert(tid in df.index) - df.loc[tid] = pd.Series({ - context.obj['status_key']: status, - context.obj['title_key']: title, - context.obj['details_key']: details, - context.obj['tags_key']: tags, - context.obj['deadline_key']: deadline, - context.obj['touched_key']: datetime.datetime.now().isoformat(), - }) - save_data(context,df) - - context.obj['highlight'] = tid - context.invoke(show) - - -def check_yes(context, param, value): - """Callback cheking for explicit user consent.""" - if not value: - context.abort() - return value - -@cli.command() -@click.argument('TID', required=True, type=int, is_eager=True, callback=check_id) -@click.option('-y', '--yes', is_flag=True, expose_value=False, callback=check_yes, prompt="Permanently remove task from records?") -@click.pass_context -def remove(context, tid): - """Delete a task.""" - df = context.obj['data'] - df = df.drop(index=tid) - save_data(context, df) - - context.invoke(show) - - -def change_status(context, tid, new_status): - """Edit the status of a task.""" - df = context.obj['data'] - - row = df.loc[tid] - if row.empty: - error("ID_NOT_FOUND", "{} = {} not found in `{}`".format(context.obj['id_key'], tid, context.obj['input'])) - - if new_status not in context.obj['status_list']: - error("UNKNOWN_STATUS", "Unknown status `{}`".format(new_status)) - else: - df.loc[tid, context.obj['status_key']] = new_status - df.loc[tid, context.obj['touched_key']] = datetime.datetime.now().isoformat() - - save_data(context, df) - -@cli.command() -@click.argument('TID', required=True, type=int, is_eager=True, callback=check_id) -@click.argument('STATUS', required=True, type=str) -@click.pass_context -def status(context, tid, status): - """Explicitely change the status of a task. - - Use status names configured with --show-status.""" - - change_status(context, tid, status) - - context.obj['highlight'] = tid - context.invoke(show) - - -@cli.command() -@click.argument('TID', required=True, type=int, is_eager=True, callback=check_id) -@click.pass_context -def promote(context, tid): - """Upgrade the status of a task to the next one. - - Use status names configured with --show-status.""" - - df = context.obj['data'] - - row = df.loc[tid] - if row.empty: - error("ID_NOT_FOUND", "{} = {} not found in `{}`".format(context.obj['id_key'], tid, context.obj['input'])) - - i=0 - for i in range(len(context.obj['status_list'])): - if row[context.obj['status_key']] == context.obj['status_list'][i]: - break - else: - i += 1 - if i >= len(context.obj['status_list'])-1: - error("UNKNOWN_STATUS", "Cannot promote task {}, already at the last status.".format(tid)) - else: - change_status(context, tid, context.obj['status_list'][i+1]) - - context.obj['highlight'] = tid - context.invoke(show) - - -@cli.command() -@click.argument('TID', required=True, type=int, is_eager=True, callback=check_id) -@click.pass_context -def demote(context, tid): - """Downgrade the status of a task to the previous one. - - Use status names configured with --show-status.""" - - df = context.obj['data'] - - row = df.loc[tid] - if row.empty: - error("ID_NOT_FOUND", "{} = {} not found in `{}`".format(context.obj['id_key'], tid, context.obj['input'])) - - i=0 - for i in range(len(context.obj['status_list'])): - if row[context.obj['status_key']] == context.obj['status_list'][i]: - break - else: - i += 1 - if i == 0: - error("UNKNOWN_STATUS", "Cannot demote task {}, already at the first status.".format(tid)) - else: - change_status(context, tid, context.obj['status_list'][i-1]) - - context.obj['highlight'] = tid - context.invoke(show) - - -@cli.command() -@click.pass_context -def config(context): - """Show the current configuration.""" - click.echo('Configuration:') - click.echo(f"Data file: `{context.obj['input']}`") - - -@cli.command() -@click.argument('REGEX', required=True, type=str) -@click.option('-a', '--all' , is_flag = True, type = bool, default = False, help="Search even in hidden fields.") -@click.pass_context -def filter(context, regex, all): - """Only show the tasks for which showed columns do contains a string matching the given regexp. - - Example: klyban filter '[Aa]nd'""" - - df = context.obj['data'] - - # Bring back TID as a regular *string* column. - df = df.reset_index().fillna("").astype('string') - - # Filter mask. - if all: - mask = np.column_stack([ df[col].str.contains(regex, na=False) for col in df ] ) - else: - mask = np.column_stack([ df[col].str.contains(regex, na=False) for col in df[context.obj['show_fields']] ] ) - - # Update in context for `show` to see. - context.obj['data'] = df.loc[mask.any(axis=1)] - - context.invoke(show) - - -@cli.command() -@click.argument('REGEX', required=True, type=str) -@click.option('-m', '--mark', type = str, default = '▶', help="String used to highlight matching tasks.") -@click.option('-a', '--all' , is_flag = True, type = bool, default = False, help="Search even in hidden fields.") -@click.pass_context -def find(context, regex, mark, all): - """Point out tasks containing a string matching the given regexp in any of the showed columns. - - Example: klyban find '[Aa]nd'""" - - df = context.obj['data'] - - # Bring back TID as a regular *string* column. - df = df.reset_index().fillna("").astype('string') - - # Filter mask. - if all: - mask = np.column_stack([ df[col].str.contains(regex, na=False) for col in df ] ) - else: - mask = np.column_stack([ df[col].str.contains(regex, na=False) for col in df[context.obj['show_fields']] ] ) - - # Mark out matching tasks. - df.loc[mask.any(axis=1), 'H'] = mark - - # Update in context for `show` to see. - context.obj['data'] = df - - context.invoke(show) - - -if __name__ == '__main__': - cli(obj={})