From cadc9d51407f9d8389108c8b86c6a2ddbf916fbd Mon Sep 17 00:00:00 2001 From: nojhan Date: Fri, 25 Aug 2023 19:17:24 +0200 Subject: [PATCH] refactoring: "->', `color.` prefix in swatches --- taskwarrior-deluxe.py | 328 +++++++++++++++++++++++------------------- 1 file changed, 180 insertions(+), 148 deletions(-) diff --git a/taskwarrior-deluxe.py b/taskwarrior-deluxe.py index 7044925..de444b1 100755 --- a/taskwarrior-deluxe.py +++ b/taskwarrior-deluxe.py @@ -93,18 +93,18 @@ class task: self.show_only = task.keys() sid = str(task["id"]) - if ':' in task["description"]: + if ":" in task["description"]: short, desc = task["description"].split(":") - title = rich.text.Text(sid, style='id') + rich.text.Text(":", style="default") + rich.text.Text(short.strip(), style='short_description') - desc = rich.text.Text("\n".join(textwrap.wrap(desc.strip(), self.wrap_width)), style='long_description') + title = rich.text.Text(sid, style="color.id") + rich.text.Text(":", style="default") + rich.text.Text(short.strip(), style="color.description.short") + desc = rich.text.Text("\n".join(textwrap.wrap(desc.strip(), self.wrap_width)), style="color.description.long") elif len(task["description"]) <= self.wrap_width - 8: d = task["description"].strip() - title = rich.text.Text(sid, style='id') + rich.text.Text(":", style="default") + rich.text.Text(d, style='short_description') + title = rich.text.Text(sid, style="color.id") + rich.text.Text(":", style="default") + rich.text.Text(d, style="color.description.short") desc = None else: desc = task["description"] - desc = rich.text.Text("\n".join(textwrap.wrap(desc.strip(), self.wrap_width)), style='description') - title = rich.text.Text(sid, style='id') + desc = rich.text.Text("\n".join(textwrap.wrap(desc.strip(), self.wrap_width)), style="color.description") + title = rich.text.Text(sid, style="color.id") segments = [] for key in self.show_only: @@ -112,22 +112,22 @@ class task: val = task[key] segment = f"{key}: " if type(val) == str: - segments.append(rich.text.Text(segment+val, style=key)) + segments.append(rich.text.Text(segment+val, style=f"color.{key}")) elif type(val) == list: # FIXME Columns does not fit. # g = Columns([f"+{t}" for t in val], expand = False) lst = [] for t in val: lst.append( \ - rich.text.Text(self.tag_icons[0], style="tags_ends") + \ - rich.text.Text(t, style=key) + \ - rich.text.Text(self.tag_icons[1], style="tags_ends") \ + rich.text.Text(self.tag_icons[0], style="color.tags.ends") + \ + rich.text.Text(t, style=f"color.{key}") + \ + rich.text.Text(self.tag_icons[1], style="color.tags.ends") \ ) g = rich.console.Group(*lst, fit = True) - # g = rich.console.Group(*[rich.text.Text(f"{self.tag_icons[0]}{t}{self.tag_icons[1]}", style=key) for t in val], fit = True) + # g = rich.console.Group(*[rich.text.Text(f"{self.tag_icons[0]}{t}{self.tag_icons[1]}", style=f"color.{key}") for t in val], fit = True) segments.append(g) else: - segments.append(rich.text.Text(segment+str(val), style=key)) + segments.append(rich.text.Text(segment+str(val), style=f"color.{key}")) # FIXME Columns does not fit. # cols = Columns(segments) @@ -145,7 +145,7 @@ class task: sid = str(task["id"]) if sid in self.touched: panel = rich.panel.Panel(body, title = title, - title_align="left", expand = False, padding = (0,1), border_style = 'touched', box = rich.box.DOUBLE_EDGE) + title_align="left", expand = False, padding = (0,1), border_style = "color.touched", box = rich.box.DOUBLE_EDGE) else: panel = rich.panel.Panel(body, title = title, title_align="left", expand = False, padding = (0,1)) @@ -160,15 +160,15 @@ class task: def __call__(self, task): title, body = self._make(task) - t = rich.text.Text(self.title_ends[0], style="short_description_ends") + \ + t = rich.text.Text(self.title_ends[0], style="color.description.short.ends") + \ title + \ - rich.text.Text(self.title_ends[1], style="short_description_ends") + rich.text.Text(self.title_ends[1], style="color.description.short.ends") sid = str(task["id"]) if sid in self.touched: - b = rich.panel.Panel(body, box = rich.box.SIMPLE_HEAD, style='touched') + b = rich.panel.Panel(body, box = rich.box.SIMPLE_HEAD, style="color.touched") else: - b = rich.panel.Panel(body, box = rich.box.SIMPLE_HEAD, style='description') + b = rich.panel.Panel(body, box = rich.box.SIMPLE_HEAD, style="color.description") sheet = rich.console.Group(t,b) return sheet @@ -219,11 +219,11 @@ class stack: def __call__(self, tasks): def p_value(task): - p_values = {'H': 0, 'M': 1, 'L': 2, '': 3} + p_values = {"H": 0, "M": 1, "L": 2, "": 3} if self.field in task: return p_values[task[self.field]] else: - return p_values[''] + return p_values[""] return sorted(tasks, key = p_value, reverse = self.reverse) class RawTable(Stacker): @@ -234,51 +234,59 @@ class stack: def __call__(self, tasks): keys = self.tasker.show_only - table = rich.table.Table(box = None, show_header = False, show_lines = True, expand = True, row_styles=['row_odd', 'row_even']) - table.add_column('H') + table = rich.table.Table(box = None, show_header = False, show_lines = True, expand = True, row_styles=["color.row.odd", "color.row.even"]) + table.add_column("H") for k in keys: table.add_column(k) for task in self.sorter(tasks): taskers = self.tasker(task) - if str(task['id']) in self.tasker.touched: - row = [rich.text.Text('▶', style = 'touched')] + if str(task["id"]) in self.tasker.touched: + row = [rich.text.Text("▶", style = "color.touched")] else: - row = [''] + row = [""] for k in keys: if k in task: val = taskers[k] + ##### String keys ##### if type(val) == str: - if k == 'description' and ':' in val: - short, desc = val.split(':') + # Description is a special case. + if k == "description" and ":" in val: + short, desc = val.split(":") # FIXME groups add a newline or hide what follows, no option to avoid it. # row.append( rich.console.Group( - # rich.text.Text(short+':', style='short_description', end='\n'), - # rich.text.Text(desc, style='description', end='\n') + # rich.text.Text(short+":", style="color.description.short", end="\n"), + # rich.text.Text(desc, style="color.description", end="\n") # )) # FIXME style leaks on all texts: - row.append( rich.text.Text(short, style='short_description', end='') + \ - rich.text.Text(':', style='default', end='') + \ - rich.text.Text(desc, style='long_description', end='') ) + # (Note that "default" is a special color for Rich.) + row.append( rich.text.Text(short, style="color.description.short", end="") + \ + rich.text.Text(":", style="default", end="") + \ + rich.text.Text(desc, style="color.description.long", end="") ) + # Strings, but not description. else: - row.append( rich.text.Text(val, style=k) ) + row.append( rich.text.Text(val, style=f"color.{k}") ) + ##### List keys. ##### elif type(val) == list: - if k == 'tags': + # Tags are a special case. + if k == "tags": tags = rich.text.Text("") for t in val: # FIXME use Columns if/when it does not expand. tags += \ - rich.text.Text(self.tag_icons[0], style="tags_ends") + \ - rich.text.Text(t, style=k) + \ - rich.text.Text(self.tag_icons[1], style="tags_ends") + \ + rich.text.Text(self.tag_icons[0], style="color.tags.ends") + \ + rich.text.Text(t, style=f"color.{k}") + \ + rich.text.Text(self.tag_icons[1], style="color.tags.ends") + \ " " row.append( tags ) + # List, but not tags. else: - row.append( rich.text.Text(" ".join(val), style=k) ) + row.append( rich.text.Text(" ".join(val), style=f"color.{k}") ) + ##### Other type of keys. ##### else: - row.append( rich.text.Text(str(val), style=k) ) + row.append( rich.text.Text(str(val), style=f"color.{k}") ) else: row.append("") table.add_row(*[t for t in row]) @@ -317,7 +325,7 @@ class sections: groups = self.group(tasks) for key in self.order(groups): if key in groups: - sections.append( rich.panel.Panel(self.stacker(groups[key]), title = rich.text.Text(str(key).upper(), style=key), title_align = "left", expand = True)) + sections.append( rich.panel.Panel(self.stacker(groups[key]), title = rich.text.Text(str(key).upper(), style=f"color.{key}"), title_align = "left", expand = True)) return rich.console.Group(*sections) class Horizontal(Sectioner): @@ -336,7 +344,7 @@ class sections: row = [] for k in keys: - row.append( rich.panel.Panel(self.stacker(groups[k]), title = rich.text.Text(k.upper(), style=k), title_align = "left", expand = True, border_style="title")) + row.append( rich.panel.Panel(self.stacker(groups[k]), title = rich.text.Text(k.upper(), style=f"color.{k}"), title_align = "left", expand = True, border_style="color.title")) table.add_row(*row) return table @@ -402,12 +410,12 @@ class group: return groups -def call_taskwarrior(args:list[str] = ['export'], taskfile = ".task") -> str: +def call_taskwarrior(args:list[str] = ["export"], taskfile = ".task") -> str: # Local file. env = os.environ.copy() env["TASKDATA"] = taskfile - cmd = ['task'] + args + cmd = ["task"] + args try: p = subprocess.Popen( " ".join(cmd), stdout=subprocess.PIPE, @@ -421,13 +429,13 @@ def call_taskwarrior(args:list[str] = ['export'], taskfile = ".task") -> str: print("ERROR:", exc.returncode, exc.output, err) sys.exit(exc.returncode) else: - return out.decode('utf-8') + return out.decode("utf-8") def get_data(taskfile, filter = None): if not filter: filter = [] - out = call_taskwarrior(filter+['export'], taskfile) + out = call_taskwarrior(filter+["export"], taskfile) try: jdata = json.loads(out) except json.decoder.JSONDecodeError as exc: @@ -437,91 +445,98 @@ def get_data(taskfile, filter = None): def parse_touched(out): - return re.findall('(?:Modifying|Created|Starting|Stopping)+ task ([0-9]+)', out) + return re.findall("(?:Modifying|Created|Starting|Stopping)+ task ([0-9]+)", out) def get_swatches(name = None): swatches = { "none": { - 'touched': '', - 'id': '', - 'title': '', - 'description': '', - 'short_description': '', - 'short_description_ends': '', - 'long_description': '', - 'entry': '', - 'modified': '', - 'started': '', - 'status': '', - 'uuid': '', - 'tags': '', - 'tags_ends': '', - 'urgency': '', - 'row_odd': '', - 'row_even' : '', + "color.touched": "", + "color.id": "", + "color.title": "", + "color.description": "", + "color.description.short": "", + "color.description.short.ends": "", + "color.description.long": "", + "color.entry": "", + "color.end": "", + "color.modified": "", + "color.started": "", + "color.status": "", + "color.uuid": "", + "color.tags": "", + "color.tags.ends": "", + "color.urgency": "", + "color.row.odd": "", + "color.row.even" : "", + "color.priority": "", }, "nojhan": { - 'touched': '#4E9A06', - 'id': 'color(214)', - 'title': '', - 'description': 'color(231)', - 'short_description': 'color(231)', - 'short_description_ends': '', - 'long_description': 'default', - 'entry': '', - 'modified': 'color(240)', - 'started': '', - 'status': 'bold italic white', - 'uuid': '', - 'tags': 'color(33)', - 'tags_ends': 'color(26)', - 'urgency': 'color(219)', - 'row_odd': 'on #262121', - 'row_even' : 'on #2d2929', - 'priority': 'color(105)', + "color.touched": "#4E9A06", + "color.id": "color(214)", + "color.title": "", + "color.description": "color(231)", + "color.description.short": "color(231)", + "color.description.short.ends": "", + "color.description.long": "default", + "color.entry": "", + "color.end": "", + "color.modified": "color(240)", + "color.started": "", + "color.status": "bold italic white", + "color.uuid": "", + "color.tags": "color(33)", + "color.tags.ends": "color(26)", + "color.urgency": "color(219)", + "color.row.odd": "on #262121", + "color.row.even" : "on #2d2929", + "color.priority": "color(105)", }, "chalky": { - 'touched': 'color(0) on color(15)', - 'id': 'bold color(160) on white', - 'title': '', - 'description': 'black on white', - 'short_description': 'bold black on white', - 'short_description_ends': 'white', - 'long_description': 'black on white', - 'entry': '', - 'modified': 'color(240)', - 'started': '', - 'status': 'bold italic white', - 'uuid': '', - 'tags': 'color(166) on white', - 'tags_ends': 'white', - 'urgency': 'color(219)', - 'row_odd': '', - 'row_even' : '', + "color.touched": "color(0) on color(15)", + "color.id": "bold color(160) on white", + "color.title": "", + "color.description": "black on white", + "color.description.short": "bold black on white", + "color.description.short.ends": "white", + "color.description.long": "black on white", + "color.entry": "", + "color.end": "", + "color.modified": "color(240)", + "color.started": "", + "color.status": "bold italic white", + "color.uuid": "", + "color.tags": "color(166) on white", + "color.tags.ends": "white", + "color.urgency": "color(219)", + "color.row.odd": "", + "color.row.even" : "", + "color.priority": "", }, "carbon": { - 'touched': 'color(15) on color(0)', - 'id': 'bold color(196) on color(236)', - 'title': '', - 'description': 'white on color(236)', - 'short_description': 'bold white on color(236)', - 'short_description_ends': 'color(236)', - 'long_description': 'white on color(236)', - 'entry': '', - 'modified': '', - 'started': '', - 'status': 'bold italic white', - 'uuid': '', - 'tags': 'bold black on color(88)', - 'tags_ends': 'color(88)', - 'urgency': 'color(219)', - 'row_odd': '', - 'row_even' : '', + "color.touched": "color(15) on color(0)", + "color.id": "bold color(196) on color(236)", + "color.title": "", + "color.description": "white on color(236)", + "color.description.short": "bold white on color(236)", + "color.description.short.ends": "color(236)", + "color.description.long": "white on color(236)", + "color.entry": "", + "color.end": "", + "color.modified": "", + "color.started": "", + "color.status": "bold italic white", + "color.uuid": "", + "color.tags": "bold black on color(88)", + "color.tags.ends": "color(88)", + "color.urgency": "color(219)", + "color.row.odd": "", + "color.row.even" : "", + "color.priority": "", }, } @@ -535,24 +550,24 @@ def get_icons(name=None): icons = { - 'none' : { - 'tag': ['', ''], - 'short': ['', ''], + "none" : { + "tag": ["", ""], + "short": ["", ""], }, - 'ascii' : { - 'tag': ['+', ''], - 'short': ['', ''], + "ascii" : { + "tag": ["+", ""], + "short": ["", ""], }, - 'emojis' : { - 'tag': ['🏷️ ', ''], - 'short': ['\n', ' '], + "emojis" : { + "tag": ["🏷️ ", ""], + "short": ["\n", " "], }, - 'power' : { - 'tag': ['', ''], - 'short': ['\n', ' '], + "power" : { + "tag": ["", ""], + "short": ["\n", " "], }, } @@ -586,24 +601,38 @@ def get_layouts(kind = None, name = None): elif not kind and not name: return available else: - raise KeyError("cannot get layouts with 'name' only") + raise KeyError("cannot get layouts with `name` only") + + +def tw_to_rich(color): + # color123 -> color(123) + color = re.sub(r"color([0-9]{0,3})", r"color(\1)", color) + # rgb123 -> #123 and rgb123abc -> #123abc + color = re.sub(r"rgb(([\da-f]{3}){1,2})", r"#\1", color) + # rgb123 -> rgb112233 + color = re.sub(r"#([\da-f])([\da-f])([\da-f])", r"#\1\1\2\2\3\3", color) + return color # We cannot use tomllib because strings are not quoted. -# We cannot use configparser because there is no section and because of the 'include' command. +# We cannot use configparser because there is no section and because of the "include" command. # FIXME handle possible values when possible. def parse_config(filename, current): config = current - with open(filename, 'r') as fd: + with open(filename, "r") as fd: for i,line in enumerate(fd.readlines()): - if line.strip() and line.strip()[0] != '#': - if '=' in line: - key,value = line.split('=') - if '#' in value: - value = value.split('#')[0] + if line.strip() and line.strip()[0] != "#": # Starting comment. + if "=" in line: + key,value = line.split("=") + if "#" in value: # Ending comments. + value = value.split("#")[0] + if "color" in key: # Colors conversion. + value = tw_to_rich(value.strip()) + if ".uda." in key: + key = re.sub(r"color\.uda\.", r"color.", key) config[key.strip()] = value.strip() elif "include" in line: _,path = line.split() - config['includes'].append(path.strip()) + config["includes"].append(path.strip()) else: print(f"Cannot parse line {i} of config file `{filename}`, I'll ignore it.") return config @@ -640,7 +669,7 @@ def find_config(fname, current): config = parse_config(p, config) # Second, user. - p = pathlib.Path(os.path.expanduser('~')) / pathlib.Path(fname) + p = pathlib.Path(os.path.expanduser("~")) / pathlib.Path(fname) if p.exists(): config = parse_config(p, config) @@ -685,7 +714,6 @@ def parse_filter(cmd): return filter return None - if __name__ == "__main__": default_conf = { @@ -709,7 +737,7 @@ if __name__ == "__main__": "list.filtered": "false", } - # First, taskwarrior's config... + # First, taskwarrior"s config... config = find_config(".taskrc", default_conf) # ... overwritten by TWD config. config = find_config(".twdrc", config) @@ -756,14 +784,16 @@ if __name__ == "__main__": swatch = rich.theme.Theme(get_swatches(config["design.swatch"])) layouts = get_layouts() + ##### Tasks ##### if config["layout.task"] == "Card": - tasker = layouts['task']['Card'](show_only, touched = touched, wrap_width = int(config["widget.card.wrap"]), tag_icons = get_icons(config["design.icons"])['tag']) + tasker = layouts["task"]["Card"](show_only, touched = touched, wrap_width = int(config["widget.card.wrap"]), tag_icons = get_icons(config["design.icons"])["tag"]) elif config["layout.task"] == "Sheet": icons = get_icons(config["design.icons"]) - tasker = layouts['task']['Sheet'](show_only, touched = touched, wrap_width = int(config["widget.card.wrap"]), tag_icons = icons['tag'], title_ends = icons['short']) + tasker = layouts["task"]["Sheet"](show_only, touched = touched, wrap_width = int(config["widget.card.wrap"]), tag_icons = icons["tag"], title_ends = icons["short"]) else: - tasker = layouts['task'][config["layout.task"]](show_only, touched = touched) + tasker = layouts["task"][config["layout.task"]](show_only, touched = touched) + ##### Stack ##### if config["layout.stack.sort"]: if config["layout.stack.sort"] == "priority": sorter = stack.sort.Priority(as_bool(config["layout.stack.sort.reverse"])) @@ -775,13 +805,14 @@ if __name__ == "__main__": sorter = None if config["layout.stack"] == "RawTable": - stacker = layouts['stack']["RawTable"](tasker, sorter = sorter, tag_icons = get_icons(config["design.icons"])['tag']) + stacker = layouts["stack"]["RawTable"](tasker, sorter = sorter, tag_icons = get_icons(config["design.icons"])["tag"]) else: - stacker = layouts['stack'][config["layout.stack"]](tasker, sorter = sorter) + stacker = layouts["stack"][config["layout.stack"]](tasker, sorter = sorter) + ##### Sections ##### if config["layout.sections.group"]: values = config["layout.sections.group.show"].split(list_separator) - if values == ['']: + if values == [""]: values = [] if config["layout.sections.group"].lower() == "status": group_by = group.Status() @@ -800,9 +831,10 @@ if __name__ == "__main__": group_by = group.Status() g_sort_on = group.sort.OnValues(["pending","started","completed"]) + ##### Subsections ##### if config["layout.subsections.group"]: values = config["layout.subsections.group.show"].split(list_separator) - if values == ['']: + if values == [""]: values = [] if config["layout.subsections.group"].lower() == "status": subgroup_by = group.Status() @@ -822,10 +854,10 @@ if __name__ == "__main__": g_subsort_on = None if config["layout.subsections"] and config["layout.subsections.group"]: - subsectioner = layouts['sections'][config["layout.subsections"]](stacker, g_subsort_on, subgroup_by) - sectioner = layouts['sections'][config["layout.sections"]](subsectioner, g_sort_on, group_by) + subsectioner = layouts["sections"][config["layout.subsections"]](stacker, g_subsort_on, subgroup_by) + sectioner = layouts["sections"][config["layout.sections"]](subsectioner, g_sort_on, group_by) else: - sectioner = layouts['sections'][config["layout.sections"]](stacker, g_sort_on, group_by) + sectioner = layouts["sections"][config["layout.sections"]](stacker, g_sort_on, group_by) console = Console(theme = swatch) # console.rule("taskwarrior-deluxe")