mirror of
https://github.com/jrnl-org/jrnl.git
synced 2025-06-29 22:16:13 +02:00
Pretty print journal entries (#692)
* Pretty print journal entry titles and dates. Changes appearance of all jrnl viewing commands, such as $ jrnl --short and $ jrnl -n {NUM}. Fix #508 * Removed extra newline at end of title * Use ansiwrap to properly wrap strings with ANSI escapes * Add ansiwrap to pyproject.toml * Allow configuration of colors - Replaced raw escapes with colorama - Added colors key to config - Add checks for validity of color values * Add color configuration documentation * Fix broken tests due to config change * Add tests for colors in configs - Identifying invalid color configs - Upgrading config from no colors -> colors * Add colorama dependency for all platforms * Allow users to disable colorization of output * Update poetry.lock * Add tag and body color customization options * Fix colorization of tags in title and body * Updated tests to use no color by default * Change pass to continue in verify_config() * Better style in Entry.py * Reduce code duplication for tag highlighting - Breaks "unreadable date" regression test for unknown reason * Properly colorize tags and print body * Reformatting and clean up * Replace list comprehension with generator * Handle invalid colors by not using a color * Process ANSI escapes properly with behave * Fixed the 'spaces after tags directly next to punctuation' bug Broke processing of tags next to any punctuation at all * Closer to working tag colorization but not perfect * Add tests printing for multiline journals Fix #717 * Correctly indent first line of multiline entry * Add test for multiline entries with tags * Remove redundant UNICODE flag * Progress towards proper tag colorization and body formatting * Fix newline colorization bug Debug code left intact since there are more bugs to fix :/ * And now the space just ends up before the tag instead of after it * Fix assertion syntax warning * Moved tag test to tagging.feature file * Strip out debug code and clean up * Bold datetimes in title * Bold all titles Fix #720 * Remove PY2 and PY3 constants * Fix regression in features/steps/core.py * Fix tag_regex * Remove redundant re.UNICODE flag * Remove extraneous code
This commit is contained in:
parent
6985de2844
commit
9e5d160bbd
24 changed files with 835 additions and 119 deletions
|
@ -1,9 +1,9 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import re
|
||||
import textwrap
|
||||
import ansiwrap
|
||||
from datetime import datetime
|
||||
from .util import split_title
|
||||
from .util import split_title, colorize, highlight_tags_with_background_color
|
||||
|
||||
|
||||
class Entry:
|
||||
|
@ -49,7 +49,7 @@ class Entry:
|
|||
|
||||
@staticmethod
|
||||
def tag_regex(tagsymbols):
|
||||
pattern = fr"(?u)(?:^|\s)([{tagsymbols}][-+*#/\w]+)"
|
||||
pattern = fr"(?<!\S)([{tagsymbols}][-+*#/\w]+)"
|
||||
return re.compile(pattern)
|
||||
|
||||
def _parse_tags(self):
|
||||
|
@ -73,31 +73,77 @@ class Entry:
|
|||
def pprint(self, short=False):
|
||||
"""Returns a pretty-printed version of the entry.
|
||||
If short is true, only print the title."""
|
||||
date_str = self.date.strftime(self.journal.config["timeformat"])
|
||||
# Handle indentation
|
||||
if self.journal.config["indent_character"]:
|
||||
indent = self.journal.config["indent_character"].rstrip() + " "
|
||||
else:
|
||||
indent = ""
|
||||
|
||||
date_str = colorize(
|
||||
self.date.strftime(self.journal.config["timeformat"]),
|
||||
self.journal.config["colors"]["date"],
|
||||
bold=True,
|
||||
)
|
||||
|
||||
if not short and self.journal.config["linewrap"]:
|
||||
title = textwrap.fill(
|
||||
date_str + " " + self.title, self.journal.config["linewrap"]
|
||||
# Color date / title and bold title
|
||||
title = ansiwrap.fill(
|
||||
date_str
|
||||
+ " "
|
||||
+ highlight_tags_with_background_color(
|
||||
self,
|
||||
self.title,
|
||||
self.journal.config["colors"]["title"],
|
||||
is_title=True,
|
||||
),
|
||||
self.journal.config["linewrap"],
|
||||
)
|
||||
body = "\n".join(
|
||||
[
|
||||
textwrap.fill(
|
||||
body = highlight_tags_with_background_color(
|
||||
self, self.body.rstrip(" \n"), self.journal.config["colors"]["body"]
|
||||
)
|
||||
body_text = [
|
||||
colorize(
|
||||
ansiwrap.fill(
|
||||
line,
|
||||
self.journal.config["linewrap"],
|
||||
initial_indent=indent,
|
||||
subsequent_indent=indent,
|
||||
drop_whitespace=True,
|
||||
)
|
||||
or indent
|
||||
for line in self.body.rstrip(" \n").splitlines()
|
||||
),
|
||||
self.journal.config["colors"]["body"],
|
||||
)
|
||||
or indent
|
||||
for line in body.rstrip(" \n").splitlines()
|
||||
]
|
||||
|
||||
# ansiwrap doesn't handle lines with only the "\n" character and some
|
||||
# ANSI escapes properly, so we have this hack here to make sure the
|
||||
# beginning of each line has the indent character and it's colored
|
||||
# properly. textwrap doesn't have this issue, however, it doesn't wrap
|
||||
# the strings properly as it counts ANSI escapes as literal characters.
|
||||
# TL;DR: I'm sorry.
|
||||
body = "\n".join(
|
||||
[
|
||||
colorize(indent, self.journal.config["colors"]["body"]) + line
|
||||
if not ansiwrap.strip_color(line).startswith(indent)
|
||||
else line
|
||||
for line in body_text
|
||||
]
|
||||
)
|
||||
else:
|
||||
title = date_str + " " + self.title.rstrip("\n ")
|
||||
body = self.body.rstrip("\n ")
|
||||
title = (
|
||||
date_str
|
||||
+ " "
|
||||
+ highlight_tags_with_background_color(
|
||||
self,
|
||||
self.title.rstrip("\n"),
|
||||
self.journal.config["colors"]["title"],
|
||||
is_title=True,
|
||||
)
|
||||
)
|
||||
body = highlight_tags_with_background_color(
|
||||
self, self.body.rstrip("\n "), self.journal.config["colors"]["body"]
|
||||
)
|
||||
|
||||
# Suppress bodies that are just blanks and new lines.
|
||||
has_body = len(self.body) > 20 or not all(
|
||||
|
|
|
@ -153,20 +153,7 @@ class Journal:
|
|||
|
||||
def pprint(self, short=False):
|
||||
"""Prettyprints the journal's entries"""
|
||||
sep = "\n"
|
||||
pp = sep.join([e.pprint(short=short) for e in self.entries])
|
||||
if self.config["highlight"]: # highlight tags
|
||||
if self.search_tags:
|
||||
for tag in self.search_tags:
|
||||
tagre = re.compile(re.escape(tag), re.IGNORECASE)
|
||||
pp = re.sub(tagre, lambda match: util.colorize(match.group(0)), pp)
|
||||
else:
|
||||
pp = re.sub(
|
||||
Entry.Entry.tag_regex(self.config["tagsymbols"]),
|
||||
lambda match: util.colorize(match.group(0)),
|
||||
pp,
|
||||
)
|
||||
return pp
|
||||
return "\n".join([e.pprint(short=short) for e in self.entries])
|
||||
|
||||
def __str__(self):
|
||||
return self.pprint()
|
||||
|
|
|
@ -9,7 +9,7 @@ from . import upgrade
|
|||
from . import __version__
|
||||
from .Journal import PlainJournal
|
||||
from .EncryptedJournal import EncryptedJournal
|
||||
from .util import UserAbort
|
||||
from .util import UserAbort, verify_config
|
||||
import yaml
|
||||
import logging
|
||||
import sys
|
||||
|
@ -47,7 +47,7 @@ def module_exists(module_name):
|
|||
|
||||
default_config = {
|
||||
"version": __version__,
|
||||
"journals": {DEFAULT_JOURNAL_KEY: JOURNAL_FILE_PATH},
|
||||
"journals": {"default": JOURNAL_FILE_PATH},
|
||||
"editor": os.getenv("VISUAL") or os.getenv("EDITOR") or "",
|
||||
"encrypt": False,
|
||||
"template": False,
|
||||
|
@ -58,6 +58,7 @@ default_config = {
|
|||
"highlight": True,
|
||||
"linewrap": 79,
|
||||
"indent_character": "|",
|
||||
"colors": {"date": "none", "title": "none", "body": "none", "tags": "none",},
|
||||
}
|
||||
|
||||
|
||||
|
@ -114,6 +115,7 @@ def load_or_install_jrnl():
|
|||
sys.exit(1)
|
||||
|
||||
upgrade_config(config)
|
||||
verify_config(config)
|
||||
|
||||
return config
|
||||
else:
|
||||
|
|
107
jrnl/util.py
107
jrnl/util.py
|
@ -4,24 +4,24 @@ import sys
|
|||
import os
|
||||
import getpass as gp
|
||||
import yaml
|
||||
import colorama
|
||||
|
||||
if "win32" in sys.platform:
|
||||
import colorama
|
||||
|
||||
colorama.init()
|
||||
import re
|
||||
import tempfile
|
||||
import subprocess
|
||||
import unicodedata
|
||||
import shlex
|
||||
from string import punctuation, whitespace
|
||||
import logging
|
||||
from typing import Optional, Callable
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
WARNING_COLOR = "\033[33m"
|
||||
ERROR_COLOR = "\033[31m"
|
||||
RESET_COLOR = "\033[0m"
|
||||
WARNING_COLOR = colorama.Fore.YELLOW
|
||||
ERROR_COLOR = colorama.Fore.RED
|
||||
RESET_COLOR = colorama.Fore.RESET
|
||||
|
||||
# Based on Segtok by Florian Leitner
|
||||
# https://github.com/fnl/segtok
|
||||
|
@ -140,6 +140,27 @@ def scope_config(config, journal_name):
|
|||
return config
|
||||
|
||||
|
||||
def verify_config(config):
|
||||
"""
|
||||
Ensures the keys set for colors are valid colorama.Fore attributes, or "None"
|
||||
:return: True if all keys are set correctly, False otherwise
|
||||
"""
|
||||
all_valid_colors = True
|
||||
for key, color in config["colors"].items():
|
||||
upper_color = color.upper()
|
||||
if upper_color == "NONE":
|
||||
continue
|
||||
if not getattr(colorama.Fore, upper_color, None):
|
||||
print(
|
||||
"[{2}ERROR{3}: {0} set to invalid color: {1}]".format(
|
||||
key, color, ERROR_COLOR, RESET_COLOR
|
||||
),
|
||||
file=sys.stderr,
|
||||
)
|
||||
all_valid_colors = False
|
||||
return all_valid_colors
|
||||
|
||||
|
||||
def get_text_from_editor(config, template=""):
|
||||
filehandle, tmpfile = tempfile.mkstemp(prefix="jrnl", text=True, suffix=".txt")
|
||||
os.close(filehandle)
|
||||
|
@ -165,9 +186,79 @@ def get_text_from_editor(config, template=""):
|
|||
return raw
|
||||
|
||||
|
||||
def colorize(string):
|
||||
"""Returns the string wrapped in cyan ANSI escape"""
|
||||
return f"\033[36m{string}\033[39m"
|
||||
def colorize(string, color, bold=False):
|
||||
"""Returns the string colored with colorama.Fore.color. If the color set by
|
||||
the user is "NONE" or the color doesn't exist in the colorama.Fore attributes,
|
||||
it returns the string without any modification."""
|
||||
color_escape = getattr(colorama.Fore, color.upper(), None)
|
||||
if not color_escape:
|
||||
return string
|
||||
elif not bold:
|
||||
return color_escape + string + colorama.Fore.RESET
|
||||
else:
|
||||
return colorama.Style.BRIGHT + color_escape + string + colorama.Style.RESET_ALL
|
||||
|
||||
|
||||
def highlight_tags_with_background_color(entry, text, color, is_title=False):
|
||||
"""
|
||||
Takes a string and colorizes the tags in it based upon the config value for
|
||||
color.tags, while colorizing the rest of the text based on `color`.
|
||||
:param entry: Entry object, for access to journal config
|
||||
:param text: Text to be colorized
|
||||
:param color: Color for non-tag text, passed to colorize()
|
||||
:param is_title: Boolean flag indicating if the text is a title or not
|
||||
:return: Colorized str
|
||||
"""
|
||||
|
||||
def colorized_text_generator(fragments):
|
||||
"""Efficiently generate colorized tags / text from text fragments.
|
||||
Taken from @shobrook. Thanks, buddy :)
|
||||
:param fragments: List of strings representing parts of entry (tag or word).
|
||||
:rtype: List of tuples
|
||||
:returns [(colorized_str, original_str)]"""
|
||||
for part in fragments:
|
||||
if part and part[0] not in config["tagsymbols"]:
|
||||
yield (colorize(part, color, bold=is_title), part)
|
||||
elif part:
|
||||
yield (colorize(part, config["colors"]["tags"], bold=True), part)
|
||||
|
||||
config = entry.journal.config
|
||||
if config["highlight"]: # highlight tags
|
||||
if entry.journal.search_tags:
|
||||
text_fragments = []
|
||||
for tag in entry.search_tags:
|
||||
text_fragments.extend(
|
||||
re.split(
|
||||
re.compile(re.escape(tag), re.IGNORECASE),
|
||||
text,
|
||||
flags=re.UNICODE,
|
||||
)
|
||||
)
|
||||
else:
|
||||
text_fragments = re.split(entry.tag_regex(config["tagsymbols"]), text)
|
||||
|
||||
# Colorizing tags inside of other blocks of text
|
||||
final_text = ""
|
||||
previous_piece = ""
|
||||
for colorized_piece, piece in colorized_text_generator(text_fragments):
|
||||
# If this piece is entirely punctuation or whitespace or the start
|
||||
# of a line or the previous piece was a tag or this piece is a tag,
|
||||
# then add it to the final text without a leading space.
|
||||
if (
|
||||
all(char in punctuation + whitespace for char in piece)
|
||||
or previous_piece.endswith("\n")
|
||||
or (previous_piece and previous_piece[0] in config["tagsymbols"])
|
||||
or piece[0] in config["tagsymbols"]
|
||||
):
|
||||
final_text += colorized_piece
|
||||
else:
|
||||
# Otherwise add a leading space and then append the piece.
|
||||
final_text += " " + colorized_piece
|
||||
|
||||
previous_piece = piece
|
||||
return final_text.lstrip()
|
||||
else:
|
||||
return text
|
||||
|
||||
|
||||
def slugify(string):
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue