Skip to content

Commit 586c0c2

Browse files
author
Hugo Osvaldo Barrera
committed
Use a fluid layout for TODOs
Stop drawing a table as done previously, and use a fluid layout. This reduces internal whitespace, which makes the ids for todos easier to recognise in may scenarios. Also makes the description and location blocks easier to copy-paste verbatim without additional indentation. Due to dropping the table rendering, this should slightly improve performance, though that's not a main goal here.
1 parent c2962a0 commit 586c0c2

File tree

5 files changed

+70
-71
lines changed

5 files changed

+70
-71
lines changed

CHANGELOG.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ Changelog
44
This file contains a brief summary of new features and dependency changes or
55
releases, in reverse chronological order.
66

7+
v4.1.0
8+
------
9+
10+
* The "table" layout has been dropped in favour of a simpler, fluid layout. As
11+
such, ``tabulate`` is not longer a required dependency.
12+
713
v4.0.1
814
------
915

setup.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
"parsedatetime",
2121
"python-dateutil",
2222
"pyxdg",
23-
"tabulate",
2423
"urwid",
2524
],
2625
long_description=open("README.rst").read(),

tests/test_cli.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -401,12 +401,14 @@ def test_color_due_dates(tmpdir, runner, create, hours):
401401
assert not result.exception
402402
due_str = due.strftime("%Y-%m-%d")
403403
if hours == 72:
404-
assert result.output == f"1 [ ] {due_str} aaa @default\x1b[0m\n"
404+
expected = (
405+
f"[ ] 1 \x1b[35m\x1b[0m \x1b[37m{due_str}\x1b[0m aaa @default\x1b[0m\n"
406+
)
405407
else:
406-
assert (
407-
result.output
408-
== f"1 [ ] \x1b[31m{due_str}\x1b[0m aaa @default\x1b[0m\n"
408+
expected = (
409+
f"[ ] 1 \x1b[35m\x1b[0m \x1b[31m{due_str}\x1b[0m aaa @default\x1b[0m\n"
409410
)
411+
assert result.output == expected
410412

411413

412414
def test_color_flag(runner, todo_factory):
@@ -415,16 +417,16 @@ def test_color_flag(runner, todo_factory):
415417
result = runner.invoke(cli, ["--color", "always"], color=True)
416418
assert (
417419
result.output.strip()
418-
== "1 [ ] \x1b[31m2007-03-22\x1b[0m YARR! @default\x1b[0m"
420+
== "[ ] 1 \x1b[35m\x1b[0m \x1b[31m2007-03-22\x1b[0m YARR! @default\x1b[0m"
419421
)
420422
result = runner.invoke(cli, color=True)
421423
assert (
422424
result.output.strip()
423-
== "1 [ ] \x1b[31m2007-03-22\x1b[0m YARR! @default\x1b[0m"
425+
== "[ ] 1 \x1b[35m\x1b[0m \x1b[31m2007-03-22\x1b[0m YARR! @default\x1b[0m"
424426
)
425427

426428
result = runner.invoke(cli, ["--color", "never"], color=True)
427-
assert result.output.strip() == "1 [ ] 2007-03-22 YARR! @default"
429+
assert result.output.strip() == "[ ] 1 2007-03-22 YARR! @default"
428430

429431

430432
def test_flush(tmpdir, runner, create, todo_factory, todos):
@@ -740,7 +742,7 @@ def test_cancel(runner, todo_factory, todos):
740742
def test_id_printed_for_new(runner):
741743
result = runner.invoke(cli, ["new", "-l", "default", "show me an id"])
742744
assert not result.exception
743-
assert result.output.strip().startswith("1")
745+
assert result.output.strip().startswith("[ ] 1")
744746

745747

746748
def test_repl(runner):

tests/test_formatter.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -91,15 +91,19 @@ def test_detailed_format(runner, todo_factory):
9191

9292
# TODO:use formatter instead of runner?
9393
result = runner.invoke(cli, ["show", "1"])
94-
expected = (
95-
"1 [ ] YARR! @default\n\n"
96-
"Description Test detailed formatting\n"
97-
" This includes multiline descriptions\n"
98-
" Blah!\n"
99-
"Location Over the hills, and far away"
100-
)
94+
expected = [
95+
"[ ] 1 (no due date) YARR! @default",
96+
"",
97+
"Description:",
98+
"Test detailed formatting",
99+
"This includes multiline descriptions",
100+
"Blah!",
101+
"",
102+
"Location: Over the hills, and far away",
103+
]
104+
101105
assert not result.exception
102-
assert result.output.strip() == expected
106+
assert result.output.strip().splitlines() == expected
103107

104108

105109
def test_parse_time(default_formatter):
@@ -168,7 +172,7 @@ def test_format_multiple_with_list(default_formatter, todo_factory):
168172
assert todo.list
169173
assert (
170174
default_formatter.compact_multiple([todo])
171-
== "1 [ ] YARR! @default\x1b[0m"
175+
== "[ ] 1 \x1b[35m\x1b[0m \x1b[37m(no due date)\x1b[0m YARR! @default\x1b[0m"
172176
)
173177

174178

todoman/formatters.py

Lines changed: 41 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,17 @@
11
import json
22
from datetime import date
33
from datetime import datetime
4+
from datetime import timedelta
45
from time import mktime
56
from typing import Iterable
6-
from typing import List
77
from typing import Optional
8-
from typing import Tuple
98
from typing import Union
109

1110
import click
1211
import humanize
1312
import parsedatetime
1413
import pytz
1514
from dateutil.tz import tzlocal
16-
from tabulate import tabulate
1715

1816
from todoman.model import Todo
1917
from todoman.model import TodoList
@@ -63,18 +61,36 @@ def compact(self, todo: Todo) -> str:
6361
return self.compact_multiple([todo])
6462

6563
def compact_multiple(self, todos: Iterable[Todo], hide_list=False) -> str:
64+
# TODO: format lines fuidly and drop the table
65+
# it can end up being more readable when too many columns are empty.
66+
# show dates that are in the future in yellow (in 24hs) or grey (future)
6667
table = []
6768
for todo in todos:
6869
completed = "X" if todo.is_completed else " "
6970
percent = todo.percent_complete or ""
7071
if percent:
7172
percent = f" ({percent}%)"
72-
priority = self.format_priority_compact(todo.priority)
73+
priority = click.style(
74+
self.format_priority_compact(todo.priority),
75+
fg="magenta",
76+
)
7377

74-
due = self.format_datetime(todo.due)
78+
due = self.format_datetime(todo.due) or "(no due date)"
7579
now = self.now if isinstance(todo.due, datetime) else self.now.date()
76-
if todo.due and todo.due <= now and not todo.is_completed:
77-
due = click.style(str(due), fg="red")
80+
81+
due_colour = None
82+
if todo.due:
83+
if todo.due <= now and not todo.is_completed:
84+
due_colour = "red"
85+
elif todo.due >= now + timedelta(hours=24):
86+
due_colour = "white"
87+
elif todo.due >= now:
88+
due_colour = "yellow"
89+
else:
90+
due_colour = "white"
91+
92+
if due_colour:
93+
due = click.style(str(due), fg=due_colour)
7894

7995
recurring = "⟳" if todo.is_recurring else ""
8096

@@ -93,64 +109,36 @@ def compact_multiple(self, todos: Iterable[Todo], hide_list=False) -> str:
93109
percent,
94110
)
95111

112+
# TODO: add spaces on the left based on max todos"
113+
114+
# FIXME: double space when no priority
96115
table.append(
97-
[
98-
todo.id,
99-
f"[{completed}]",
100-
priority,
101-
f"{due} {recurring}",
102-
summary,
103-
]
116+
f"[{completed}] {todo.id} {priority} {due} {recurring}{summary}"
104117
)
105118

106-
return tabulate(table, tablefmt="plain")
119+
return "\n".join(table)
107120

108-
def _columnize_text(
109-
self,
110-
label: str,
111-
text: Optional[str],
112-
) -> List[Tuple[Optional[str], str]]:
113-
"""Display text, split text by line-endings, on multiple colums.
114-
115-
Do nothing if text is empty or None.
116-
"""
117-
lines = text.splitlines() if text else None
121+
def _format_multiline(self, title: str, value: str) -> str:
122+
formatted_title = click.style(title, fg="white")
118123

119-
return self._columnize_list(label, lines)
120-
121-
def _columnize_list(
122-
self,
123-
label: str,
124-
lst: Optional[List[str]],
125-
) -> List[Tuple[Optional[str], str]]:
126-
"""Display list on multiple columns.
127-
128-
Do nothing if list is empty or None.
129-
"""
130-
131-
rows: List[Tuple[Optional[str], str]] = []
132-
133-
if lst:
134-
rows.append((label, lst[0]))
135-
for line in lst[1:]:
136-
rows.append((None, line))
137-
138-
return rows
124+
if value.strip().count("\n") == 0:
125+
return f"\n\n{formatted_title}: {value}"
126+
else:
127+
return f"\n\n{formatted_title}:\n{value}"
139128

140129
def detailed(self, todo: Todo) -> str:
141130
"""Returns a detailed representation of a task.
142131
143132
:param todo: The todo component.
144133
"""
145-
extra_rows = []
146-
extra_rows += self._columnize_text("Description", todo.description)
147-
extra_rows += self._columnize_text("Location", todo.location)
134+
extra_lines = []
135+
if todo.description:
136+
extra_lines.append(self._format_multiline("Description", todo.description))
148137

149-
if extra_rows:
150-
return "{}\n\n{}".format(
151-
self.compact(todo), tabulate(extra_rows, tablefmt="plain")
152-
)
153-
return self.compact(todo)
138+
if todo.location:
139+
extra_lines.append(self._format_multiline("Location", todo.location))
140+
141+
return f"{self.compact(todo)}{''.join(extra_lines)}"
154142

155143
def format_datetime(self, dt: Optional[date]) -> Union[str, int, None]:
156144
if not dt:

0 commit comments

Comments
 (0)