Skip to content

gh-128317: Highlight today in colour in calendar CLI output #128318

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jan 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 27 additions & 4 deletions Doc/library/calendar.rst
Original file line number Diff line number Diff line change
Expand Up @@ -146,46 +146,61 @@ interpreted as prescribed by the ISO 8601 standard. Year 0 is 1 BC, year -1 is
the specified width, representing an empty day. The *weekday* parameter
is unused.

.. method:: formatweek(theweek, w=0)
.. method:: formatweek(theweek, w=0, highlight_day=None)

Return a single week in a string with no newline. If *w* is provided, it
specifies the width of the date columns, which are centered. Depends
on the first weekday as specified in the constructor or set by the
:meth:`setfirstweekday` method.

.. versionchanged:: next
If *highlight_day* is given, this date is highlighted in color.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this say “using terminal escape sequences”?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I think it's more useful to describe the end result rather than the "how".

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was my intention: mention that you need to print the result to a terminal to see the highlight. If you don't, you'll see some garbage bytes.

This can be :ref:`controlled using environment variables
<using-on-controlling-color>`.


.. method:: formatweekday(weekday, width)

Return a string representing the name of a single weekday formatted to
the specified *width*. The *weekday* parameter is an integer representing
the day of the week, where ``0`` is Monday and ``6`` is Sunday.


.. method:: formatweekheader(width)

Return a string containing the header row of weekday names, formatted
with the given *width* for each column. The names depend on the locale
settings and are padded to the specified width.

.. method:: formatmonth(theyear, themonth, w=0, l=0)

.. method:: formatmonth(theyear, themonth, w=0, l=0, highlight_day=None)

Return a month's calendar in a multi-line string. If *w* is provided, it
specifies the width of the date columns, which are centered. If *l* is
given, it specifies the number of lines that each week will use. Depends
on the first weekday as specified in the constructor or set by the
:meth:`setfirstweekday` method.

.. versionchanged:: next
If *highlight_day* is given, this date is highlighted in color.
This can be :ref:`controlled using environment variables
<using-on-controlling-color>`.


.. method:: formatmonthname(theyear, themonth, width=0, withyear=True)

Return a string representing the month's name centered within the
specified *width*. If *withyear* is ``True``, include the year in the
output. The *theyear* and *themonth* parameters specify the year
and month for the name to be formatted respectively.


.. method:: prmonth(theyear, themonth, w=0, l=0)

Print a month's calendar as returned by :meth:`formatmonth`.


.. method:: formatyear(theyear, w=2, l=1, c=6, m=3)
.. method:: formatyear(theyear, w=2, l=1, c=6, m=3, highlight_day=None)

Return a *m*-column calendar for an entire year as a multi-line string.
Optional parameters *w*, *l*, and *c* are for date column width, lines per
Expand All @@ -194,6 +209,11 @@ interpreted as prescribed by the ISO 8601 standard. Year 0 is 1 BC, year -1 is
:meth:`setfirstweekday` method. The earliest year for which a calendar
can be generated is platform-dependent.

.. versionchanged:: next
If *highlight_day* is given, this date is highlighted in color.
This can be :ref:`controlled using environment variables
<using-on-controlling-color>`.


.. method:: pryear(theyear, w=2, l=1, c=6, m=3)

Expand Down Expand Up @@ -549,7 +569,7 @@ The :mod:`calendar` module defines the following exceptions:

.. _calendar-cli:

Command-Line Usage
Command-line usage
------------------

.. versionadded:: 2.5
Expand Down Expand Up @@ -687,6 +707,9 @@ The following options are accepted:
The number of months printed per row.
Defaults to 3.

.. versionchanged:: next
By default, today's date is highlighted in color and can be
:ref:`controlled using environment variables <using-on-controlling-color>`.

*HTML-mode options:*

Expand Down
13 changes: 13 additions & 0 deletions Doc/whatsnew/3.14.rst
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,19 @@ ast
* The ``repr()`` output for AST nodes now includes more information.
(Contributed by Tomas R in :gh:`116022`.)


calendar
--------

* By default, today's date is highlighted in color in :mod:`calendar`'s
:ref:`command-line <calendar-cli>` text output.
This can be controlled via the :envvar:`PYTHON_COLORS` environment
variable as well as the canonical |NO_COLOR|_
and |FORCE_COLOR|_ environment variables.
See also :ref:`using-on-controlling-color`.
(Contributed by Hugo van Kemenade in :gh:`128317`.)


concurrent.futures
------------------

Expand Down
2 changes: 2 additions & 0 deletions Lib/_colorize.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@


class ANSIColors:
BACKGROUND_YELLOW = "\x1b[43m"
BOLD_GREEN = "\x1b[1;32m"
BOLD_MAGENTA = "\x1b[1;35m"
BOLD_RED = "\x1b[1;31m"
BLACK = "\x1b[30m"
GREEN = "\x1b[32m"
GREY = "\x1b[90m"
MAGENTA = "\x1b[35m"
Expand Down
50 changes: 41 additions & 9 deletions Lib/calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -349,11 +349,27 @@ def formatday(self, day, weekday, width):
s = '%2i' % day # right-align single-digit days
return s.center(width)

def formatweek(self, theweek, width):
def formatweek(self, theweek, width, *, highlight_day=None):
"""
Returns a single week in a string (no newline).
"""
return ' '.join(self.formatday(d, wd, width) for (d, wd) in theweek)
if highlight_day:
from _colorize import get_colors

ansi = get_colors()
highlight = f"{ansi.BLACK}{ansi.BACKGROUND_YELLOW}"
reset = ansi.RESET
else:
highlight = reset = ""

return ' '.join(
(
f"{highlight}{self.formatday(d, wd, width)}{reset}"
if d == highlight_day
else self.formatday(d, wd, width)
)
for (d, wd) in theweek
)

def formatweekday(self, day, width):
"""
Expand Down Expand Up @@ -388,10 +404,11 @@ def prmonth(self, theyear, themonth, w=0, l=0):
"""
print(self.formatmonth(theyear, themonth, w, l), end='')

def formatmonth(self, theyear, themonth, w=0, l=0):
def formatmonth(self, theyear, themonth, w=0, l=0, *, highlight_day=None):
"""
Return a month's calendar string (multi-line).
"""
highlight_day = highlight_day.day if highlight_day else None
w = max(2, w)
l = max(1, l)
s = self.formatmonthname(theyear, themonth, 7 * (w + 1) - 1)
Expand All @@ -400,11 +417,11 @@ def formatmonth(self, theyear, themonth, w=0, l=0):
s += self.formatweekheader(w).rstrip()
s += '\n' * l
for week in self.monthdays2calendar(theyear, themonth):
s += self.formatweek(week, w).rstrip()
s += self.formatweek(week, w, highlight_day=highlight_day).rstrip()
s += '\n' * l
return s

def formatyear(self, theyear, w=2, l=1, c=6, m=3):
def formatyear(self, theyear, w=2, l=1, c=6, m=3, *, highlight_day=None):
"""
Returns a year's calendar as a multi-line string.
"""
Expand All @@ -428,15 +445,24 @@ def formatyear(self, theyear, w=2, l=1, c=6, m=3):
headers = (header for k in months)
a(formatstring(headers, colwidth, c).rstrip())
a('\n'*l)

if highlight_day and highlight_day.month in months:
month_pos = months.index(highlight_day.month)
else:
month_pos = None

# max number of weeks for this row
height = max(len(cal) for cal in row)
for j in range(height):
weeks = []
for cal in row:
for k, cal in enumerate(row):
if j >= len(cal):
weeks.append('')
else:
weeks.append(self.formatweek(cal[j], w))
day = highlight_day.day if k == month_pos else None
weeks.append(
self.formatweek(cal[j], w, highlight_day=day)
)
a(formatstring(weeks, colwidth, c).rstrip())
a('\n' * l)
return ''.join(v)
Expand Down Expand Up @@ -765,6 +791,7 @@ def main(args=None):
sys.exit(1)

locale = options.locale, options.encoding
today = datetime.date.today()

if options.type == "html":
if options.month:
Expand All @@ -781,7 +808,7 @@ def main(args=None):
optdict = dict(encoding=encoding, css=options.css)
write = sys.stdout.buffer.write
if options.year is None:
write(cal.formatyearpage(datetime.date.today().year, **optdict))
write(cal.formatyearpage(today.year, **optdict))
else:
write(cal.formatyearpage(options.year, **optdict))
else:
Expand All @@ -797,10 +824,15 @@ def main(args=None):
if options.month is not None:
_validate_month(options.month)
if options.year is None:
result = cal.formatyear(datetime.date.today().year, **optdict)
optdict["highlight_day"] = today
result = cal.formatyear(today.year, **optdict)
elif options.month is None:
if options.year == today.year:
optdict["highlight_day"] = today
result = cal.formatyear(options.year, **optdict)
else:
if options.year == today.year and options.month == today.month:
optdict["highlight_day"] = today
result = cal.formatmonth(options.year, options.month, **optdict)
write = sys.stdout.write
if options.encoding:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Highlight today in colour in :mod:`calendar`'s CLI output. Patch by Hugo van
Kemenade.
Loading