Theme (#664)
* Initial theme manager, not yet done * Added rich_theme_manager * Updated rich-theme-manager * Switched to rich_theme_manager for theme management * Updated dependencies * Added rich paging to subtopic help * Fixed clone to clone only styles specified in cloned theme * Added placeholder for help colors * Updated config dir, help methods
This commit is contained in:
parent
9c0b910046
commit
6c57fb2df9
@ -6,6 +6,8 @@ import os.path
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
|
||||
APP_NAME = "osxphotos"
|
||||
|
||||
OSXPHOTOS_URL = "https://github.com/RhetTbull/osxphotos"
|
||||
|
||||
# Time delta: add this to Photos times to get unix time
|
||||
|
||||
@ -24,6 +24,7 @@ from .places import places
|
||||
from .query import query
|
||||
from .repl import repl
|
||||
from .snap_diff import diff, snap
|
||||
from .theme import theme
|
||||
from .tutorial import tutorial
|
||||
from .uuid import uuid
|
||||
|
||||
@ -77,6 +78,7 @@ for command in [
|
||||
repl,
|
||||
run,
|
||||
snap,
|
||||
theme,
|
||||
tutorial,
|
||||
uninstall,
|
||||
uuid,
|
||||
|
||||
@ -3,7 +3,6 @@
|
||||
import inspect
|
||||
import os
|
||||
import typing as t
|
||||
from io import StringIO
|
||||
|
||||
import click
|
||||
from rich.console import Console
|
||||
@ -213,11 +212,9 @@ def rich_click_echo(
|
||||
# otherwise tests fail
|
||||
temp_console = Console()
|
||||
width = temp_console.width if temp_console.is_terminal else 10_000
|
||||
output = StringIO()
|
||||
console = Console(
|
||||
force_terminal=True,
|
||||
theme=theme or get_rich_theme(),
|
||||
file=output,
|
||||
width=width,
|
||||
)
|
||||
if markdown:
|
||||
@ -227,8 +224,9 @@ def rich_click_echo(
|
||||
global _timestamp
|
||||
if _timestamp:
|
||||
message = time_stamp() + message
|
||||
console.print(message, end=end, highlight=highlight, **kwargs)
|
||||
click.echo(output.getvalue(), **echo_args)
|
||||
with console.capture() as capture:
|
||||
console.print(message, end=end, highlight=highlight, **kwargs)
|
||||
click.echo(capture.get(), **echo_args)
|
||||
|
||||
|
||||
def rich_echo_via_pager(
|
||||
@ -259,11 +257,9 @@ def rich_echo_via_pager(
|
||||
except TypeError:
|
||||
text_or_generator = [text_or_generator]
|
||||
|
||||
console = _console or Console(theme=theme)
|
||||
console = _console.console or Console(theme=theme)
|
||||
|
||||
color = kwargs.pop("color", None)
|
||||
if color is None:
|
||||
color = bool(console.color_system)
|
||||
color = kwargs.pop("color", True)
|
||||
|
||||
with console.pager(styles=color):
|
||||
for x in text_or_generator:
|
||||
|
||||
@ -1,19 +1,52 @@
|
||||
"""Support for colorized output for photos_time_warp"""
|
||||
"""Support for colorized output for osxphotos cli using rich"""
|
||||
|
||||
from typing import Optional
|
||||
import pathlib
|
||||
from typing import List, Optional
|
||||
|
||||
import click
|
||||
from rich.style import Style
|
||||
from rich.themes import Theme
|
||||
from rich_theme_manager import Theme, ThemeManager
|
||||
|
||||
from .common import noop
|
||||
from .common import get_config_dir, noop
|
||||
from .darkmode import is_dark_mode
|
||||
|
||||
__all__ = ["get_theme"]
|
||||
DEFAULT_THEME_NAME = "default"
|
||||
|
||||
__all__ = [
|
||||
"get_default_theme",
|
||||
"get_theme",
|
||||
"get_theme_dir",
|
||||
"get_theme_manager",
|
||||
DEFAULT_THEME_NAME,
|
||||
]
|
||||
|
||||
|
||||
THEME_STYLES = [
|
||||
"color",
|
||||
"count",
|
||||
"error",
|
||||
"filename",
|
||||
"filepath",
|
||||
"highlight",
|
||||
"num",
|
||||
"time",
|
||||
"uuid",
|
||||
"warning",
|
||||
"bar.back",
|
||||
"bar.complete",
|
||||
"bar.finished",
|
||||
"bar.pulse",
|
||||
"progress.elapsed",
|
||||
"progress.percentage",
|
||||
"progress.remaining",
|
||||
]
|
||||
|
||||
COLOR_THEMES = {
|
||||
"dark": Theme(
|
||||
{
|
||||
name="dark",
|
||||
description="Dark mode theme",
|
||||
tags=["dark"],
|
||||
styles={
|
||||
# color pallette from https://github.com/dracula/dracula-theme
|
||||
"color": Style(color="rgb(248,248,242)"),
|
||||
"count": Style(color="rgb(139,233,253)"),
|
||||
@ -32,10 +65,15 @@ COLOR_THEMES = {
|
||||
"progress.elapsed": Style(color="rgb(139,233,253)"),
|
||||
"progress.percentage": Style(color="rgb(255,121,198)"),
|
||||
"progress.remaining": Style(color="rgb(139,233,253)"),
|
||||
}
|
||||
# "headers": Style(color="rgb(165,194,97)"),
|
||||
# "options": Style(color="rgb(255,198,109)"),
|
||||
# "metavar": Style(color="rgb(12,125,157)"),
|
||||
},
|
||||
),
|
||||
"light": Theme(
|
||||
{
|
||||
name="light",
|
||||
description="Light mode theme",
|
||||
styles={
|
||||
"color": Style(color="#000000"),
|
||||
"count": Style(color="#005cc5", bold=True),
|
||||
"error": Style(color="#b31d28", bold=True, underline=True, italic=True),
|
||||
@ -53,10 +91,16 @@ COLOR_THEMES = {
|
||||
"progress.elapsed": Style(color="#032f62", bold=True),
|
||||
"progress.percentage": Style(color="#6f42c1", bold=True),
|
||||
"progress.remaining": Style(color="#032f62", bold=True),
|
||||
}
|
||||
# "headers": Style(color="rgb(254,212,66)"),
|
||||
# "options": Style(color="rgb(227,98,9)"),
|
||||
# "metavar": Style(color="rgb(111,66,193)"),
|
||||
},
|
||||
),
|
||||
"mono": Theme(
|
||||
{
|
||||
name="mono",
|
||||
description="Monochromatic theme",
|
||||
tags=["mono", "colorblind"],
|
||||
styles={
|
||||
"count": "bold",
|
||||
"error": "reverse italic",
|
||||
"filename": "bold",
|
||||
@ -73,10 +117,16 @@ COLOR_THEMES = {
|
||||
"progress.elapsed": "",
|
||||
"progress.percentage": "bold",
|
||||
"progress.remaining": "bold",
|
||||
}
|
||||
# "headers": "bold",
|
||||
# "options": "bold",
|
||||
# "metavar": "bold",
|
||||
},
|
||||
),
|
||||
"plain": Theme(
|
||||
{
|
||||
name="plain",
|
||||
description="Plain theme with no colors",
|
||||
tags=["colorblind"],
|
||||
styles={
|
||||
"color": "",
|
||||
"count": "",
|
||||
"error": "",
|
||||
@ -94,31 +144,51 @@ COLOR_THEMES = {
|
||||
"progress.elapsed": "",
|
||||
"progress.percentage": "",
|
||||
"progress.remaining": "",
|
||||
}
|
||||
# "headers": "",
|
||||
# "options": "",
|
||||
# "metavar": "",
|
||||
},
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def get_theme_dir() -> pathlib.Path:
|
||||
"""Return the theme config dir, creating it if necessary"""
|
||||
theme_dir = get_config_dir() / "themes"
|
||||
if not theme_dir.exists():
|
||||
theme_dir.mkdir()
|
||||
return theme_dir
|
||||
|
||||
|
||||
def get_theme_manager() -> ThemeManager:
|
||||
"""Return theme manager instance"""
|
||||
return ThemeManager(theme_dir=str(get_theme_dir()), themes=COLOR_THEMES.values())
|
||||
|
||||
|
||||
def get_theme(
|
||||
theme_name: Optional[str] = None,
|
||||
theme_file: Optional[str] = None,
|
||||
verbose=None,
|
||||
):
|
||||
"""Get the color theme based on the color flags or load from config file"""
|
||||
if not verbose:
|
||||
verbose = noop
|
||||
# figure out which color theme to use
|
||||
theme_name = theme_name or "default"
|
||||
if theme_name == "default" and theme_file and theme_file.is_file():
|
||||
# load theme from file
|
||||
verbose(f"Loading color theme from {theme_file}")
|
||||
try:
|
||||
theme = Theme.read(theme_file)
|
||||
except Exception as e:
|
||||
raise ValueError(f"Error reading theme file {theme_file}: {e}")
|
||||
elif theme_name == "default":
|
||||
# try to auto-detect dark/light mode
|
||||
theme = COLOR_THEMES["dark"] if is_dark_mode() else COLOR_THEMES["light"]
|
||||
else:
|
||||
theme = COLOR_THEMES[theme_name]
|
||||
return theme
|
||||
"""Get theme by name, or default theme if no name is provided"""
|
||||
|
||||
if theme_name is None:
|
||||
return get_default_theme()
|
||||
|
||||
theme_manager = get_theme_manager()
|
||||
try:
|
||||
return theme_manager.get(theme_name)
|
||||
except ValueError as e:
|
||||
raise click.ClickException(
|
||||
f"Theme '{theme_name}' not found. "
|
||||
f"Available themes: {', '.join(t.name for t in theme_manager.themes)}"
|
||||
) from e
|
||||
|
||||
|
||||
def get_default_theme():
|
||||
"""Get the default color theme"""
|
||||
theme_manager = get_theme_manager()
|
||||
try:
|
||||
return theme_manager.get(DEFAULT_THEME_NAME)
|
||||
except ValueError:
|
||||
return (
|
||||
theme_manager.get("dark") if is_dark_mode() else theme_manager.get("light")
|
||||
)
|
||||
|
||||
@ -8,6 +8,7 @@ from datetime import datetime
|
||||
import click
|
||||
|
||||
import osxphotos
|
||||
from osxphotos._constants import APP_NAME
|
||||
from osxphotos._version import __version__
|
||||
|
||||
from .param_types import *
|
||||
@ -33,6 +34,7 @@ __all__ = [
|
||||
"DELETED_OPTIONS",
|
||||
"JSON_OPTION",
|
||||
"QUERY_OPTIONS",
|
||||
"THEME_OPTION",
|
||||
"get_photos_db",
|
||||
"load_uuid_from_file",
|
||||
"noop",
|
||||
@ -499,6 +501,16 @@ def DEBUG_OPTIONS(f):
|
||||
return f
|
||||
|
||||
|
||||
THEME_OPTION = click.option(
|
||||
"--theme",
|
||||
metavar="THEME",
|
||||
type=click.Choice(["dark", "light", "mono", "plain"], case_sensitive=False),
|
||||
help="Specify the color theme to use for --verbose output. "
|
||||
"Valid themes are 'dark', 'light', 'mono', and 'plain'. "
|
||||
"Defaults to 'dark' or 'light' depending on system dark mode setting.",
|
||||
)
|
||||
|
||||
|
||||
def load_uuid_from_file(filename):
|
||||
"""Load UUIDs from file. Does not validate UUIDs.
|
||||
Format is 1 UUID per line, any line beginning with # is ignored.
|
||||
@ -524,3 +536,11 @@ def load_uuid_from_file(filename):
|
||||
if len(line) and line[0] != "#":
|
||||
uuid.append(line)
|
||||
return uuid
|
||||
|
||||
|
||||
def get_config_dir() -> pathlib.Path:
|
||||
"""Get the directory where config files are stored."""
|
||||
config_dir = pathlib.Path.home() / ".config" / APP_NAME
|
||||
if not config_dir.is_dir():
|
||||
config_dir.mkdir(parents=True)
|
||||
return config_dir
|
||||
|
||||
@ -77,6 +77,7 @@ from .common import (
|
||||
OSXPHOTOS_CRASH_LOG,
|
||||
OSXPHOTOS_HIDDEN,
|
||||
QUERY_OPTIONS,
|
||||
THEME_OPTION,
|
||||
get_photos_db,
|
||||
load_uuid_from_file,
|
||||
noop,
|
||||
@ -642,14 +643,7 @@ from .verbose import get_verbose_console, time_stamp, verbose_print
|
||||
f"Can be specified multiple times. Valid options are: {PROFILE_SORT_KEYS}. "
|
||||
"Default = 'cumulative'.",
|
||||
)
|
||||
@click.option(
|
||||
"--theme",
|
||||
metavar="THEME",
|
||||
type=click.Choice(["dark", "light", "mono", "plain"], case_sensitive=False),
|
||||
help="Specify the color theme to use for --verbose output. "
|
||||
"Valid themes are 'dark', 'light', 'mono', and 'plain'. "
|
||||
"Defaults to 'dark' or 'light' depending on system dark mode setting.",
|
||||
)
|
||||
@THEME_OPTION
|
||||
@DEBUG_OPTIONS
|
||||
@DB_ARGUMENT
|
||||
@click.argument("dest", nargs=1, type=click.Path(exists=True))
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
"""Help text helper class for osxphotos CLI """
|
||||
|
||||
import inspect
|
||||
import io
|
||||
import re
|
||||
import typing as t
|
||||
|
||||
@ -23,8 +22,12 @@ from osxphotos.phototemplate import (
|
||||
get_template_help,
|
||||
)
|
||||
|
||||
from .click_rich_echo import rich_echo
|
||||
from .click_rich_echo import rich_echo_via_pager
|
||||
from .color_themes import get_theme
|
||||
from .common import OSXPHOTOS_HIDDEN
|
||||
|
||||
HELP_WIDTH = 110
|
||||
HIGHLIGHT_COLOR = "yellow"
|
||||
|
||||
__all__ = [
|
||||
"ExportCommand",
|
||||
@ -37,8 +40,6 @@ __all__ = [
|
||||
"get_help_msg",
|
||||
]
|
||||
|
||||
HIGHLIGHT_COLOR = "yellow"
|
||||
|
||||
|
||||
def get_help_msg(command):
|
||||
"""get help message for a Click command"""
|
||||
@ -47,22 +48,50 @@ def get_help_msg(command):
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option(
|
||||
"--width",
|
||||
default=HELP_WIDTH,
|
||||
help="Width of help text",
|
||||
hidden=OSXPHOTOS_HIDDEN,
|
||||
)
|
||||
@click.argument("topic", default=None, required=False, nargs=1)
|
||||
@click.argument("subtopic", default=None, required=False, nargs=1)
|
||||
@click.pass_context
|
||||
def help(ctx, topic, subtopic, **kw):
|
||||
def help(ctx, topic, subtopic, width, **kw):
|
||||
"""Print help; for help on commands: help <command>."""
|
||||
if topic is None:
|
||||
click.echo(ctx.parent.get_help())
|
||||
return
|
||||
|
||||
global HELP_WIDTH
|
||||
HELP_WIDTH = width
|
||||
|
||||
wrap_text_original = click.formatting.wrap_text
|
||||
|
||||
def wrap_text(
|
||||
text: str,
|
||||
width: int = HELP_WIDTH,
|
||||
initial_indent: str = "",
|
||||
subsequent_indent: str = "",
|
||||
preserve_paragraphs: bool = False,
|
||||
) -> str:
|
||||
return wrap_text_original(
|
||||
text,
|
||||
width=width,
|
||||
initial_indent=initial_indent,
|
||||
subsequent_indent=subsequent_indent,
|
||||
preserve_paragraphs=preserve_paragraphs,
|
||||
)
|
||||
|
||||
click.formatting.wrap_text = wrap_text
|
||||
click.wrap_text = wrap_text
|
||||
|
||||
if subtopic:
|
||||
cmd = ctx.obj.group.commands[topic]
|
||||
theme = get_theme("light")
|
||||
rich_echo(
|
||||
rich_echo_via_pager(
|
||||
get_subtopic_help(cmd, ctx, subtopic),
|
||||
theme=theme,
|
||||
width=click.HelpFormatter().width,
|
||||
theme=get_theme(),
|
||||
width=HELP_WIDTH,
|
||||
)
|
||||
return
|
||||
|
||||
@ -90,7 +119,7 @@ def get_subtopic_help(cmd: click.Command, ctx: click.Context, subtopic: str):
|
||||
options = get_matching_options(cmd, ctx, subtopic)
|
||||
|
||||
# format help text and options
|
||||
formatter = click.HelpFormatter()
|
||||
formatter = click.HelpFormatter(width=HELP_WIDTH)
|
||||
formatter.write(usage_str)
|
||||
formatter.write_paragraph()
|
||||
format_help_text(help_str, formatter)
|
||||
@ -142,7 +171,7 @@ def format_options_help(
|
||||
str with formatted help
|
||||
|
||||
"""
|
||||
formatter = click.HelpFormatter()
|
||||
formatter = click.HelpFormatter(width=HELP_WIDTH)
|
||||
opt_help = [opt.get_help_record(ctx) for opt in options]
|
||||
if highlight:
|
||||
# convert list of tuples to list of lists
|
||||
@ -182,11 +211,9 @@ class ExportCommand(click.Command):
|
||||
|
||||
def get_help(self, ctx):
|
||||
help_text = super().get_help(ctx)
|
||||
formatter = click.HelpFormatter()
|
||||
# passed to click.HelpFormatter.write_dl for formatting
|
||||
|
||||
formatter.write("\n\n")
|
||||
formatter.write(rich_text("[bold]** Export **[/bold]", width=formatter.width))
|
||||
formatter = click.HelpFormatter(width=HELP_WIDTH)
|
||||
formatter.write("\n")
|
||||
formatter.write(rich_text("## Export", width=formatter.width, markdown=True))
|
||||
formatter.write("\n")
|
||||
formatter.write_text(
|
||||
"When exporting photos, osxphotos creates a database in the top-level "
|
||||
@ -221,9 +248,9 @@ class ExportCommand(click.Command):
|
||||
+ "You can always run export without the --update option to re-export the entire library thus "
|
||||
+ f"rebuilding the '{OSXPHOTOS_EXPORT_DB}' database."
|
||||
)
|
||||
formatter.write("\n\n")
|
||||
formatter.write("\n")
|
||||
formatter.write(
|
||||
rich_text("[bold]** Extended Attributes **[/bold]", width=formatter.width)
|
||||
rich_text("## Extended Attributes", width=formatter.width, markdown=True)
|
||||
)
|
||||
formatter.write("\n")
|
||||
formatter.write_text(
|
||||
@ -244,25 +271,33 @@ The following attributes may be used with '--xattr-template':
|
||||
|
||||
"""
|
||||
)
|
||||
formatter.write_dl(
|
||||
[
|
||||
attr_tuples = [
|
||||
(
|
||||
rich_text("[bold]Attribute[/bold]", width=formatter.width),
|
||||
rich_text("[bold]Description[/bold]", width=formatter.width),
|
||||
),
|
||||
*[
|
||||
(
|
||||
attr,
|
||||
f"{osxmetadata.ATTRIBUTES[attr].help} ({osxmetadata.ATTRIBUTES[attr].constant})",
|
||||
)
|
||||
for attr in EXTENDED_ATTRIBUTE_NAMES
|
||||
]
|
||||
)
|
||||
],
|
||||
]
|
||||
formatter.write_dl(attr_tuples)
|
||||
formatter.write("\n")
|
||||
formatter.write_text(
|
||||
"For additional information on extended attributes see: https://developer.apple.com/documentation/coreservices/file_metadata/mditem/common_metadata_attribute_keys"
|
||||
)
|
||||
formatter.write("\n\n")
|
||||
formatter.write("\n")
|
||||
formatter.write(
|
||||
rich_text("[bold]** Templating System **[/bold]", width=formatter.width)
|
||||
rich_text("## Templating System", width=formatter.width, markdown=True)
|
||||
)
|
||||
formatter.write("\n")
|
||||
formatter.write(template_help(width=formatter.width))
|
||||
help_text += formatter.getvalue()
|
||||
help_text += template_help(width=formatter.width)
|
||||
formatter = click.HelpFormatter(width=HELP_WIDTH)
|
||||
|
||||
formatter.write("\n")
|
||||
formatter.write_text(
|
||||
"With the --directory and --filename options you may specify a template for the "
|
||||
@ -290,12 +325,15 @@ The following attributes may be used with '--xattr-template':
|
||||
)
|
||||
formatter.write("\n")
|
||||
formatter.write(
|
||||
rich_text(
|
||||
"[bold]** Template Substitutions **[/bold]", width=formatter.width
|
||||
)
|
||||
rich_text("## Template Substitutions", width=formatter.width, markdown=True)
|
||||
)
|
||||
formatter.write("\n")
|
||||
templ_tuples = [("Substitution", "Description")]
|
||||
templ_tuples = [
|
||||
(
|
||||
rich_text("[bold]Substitution[/bold]", width=formatter.width),
|
||||
rich_text("[bold]Description[/bold]", width=formatter.width),
|
||||
)
|
||||
]
|
||||
templ_tuples.extend((k, v) for k, v in TEMPLATE_SUBSTITUTIONS.items())
|
||||
formatter.write_dl(templ_tuples)
|
||||
|
||||
@ -310,7 +348,12 @@ The following attributes may be used with '--xattr-template':
|
||||
+ "2019/Vacation, 2019/Family"
|
||||
)
|
||||
formatter.write("\n")
|
||||
templ_tuples = [("Substitution", "Description")]
|
||||
templ_tuples = [
|
||||
(
|
||||
rich_text("[bold]Substitution[/bold]", width=formatter.width),
|
||||
rich_text("[bold]Description[/bold]", width=formatter.width),
|
||||
)
|
||||
]
|
||||
templ_tuples.extend(
|
||||
(k, v) for k, v in TEMPLATE_SUBSTITUTIONS_MULTI_VALUED.items()
|
||||
)
|
||||
@ -348,10 +391,11 @@ The following attributes may be used with '--xattr-template':
|
||||
|
||||
formatter.write_dl(templ_tuples)
|
||||
|
||||
formatter.write("\n\n")
|
||||
formatter.write("\n")
|
||||
formatter.write(
|
||||
rich_text("[bold]** Post Command **[/bold]", width=formatter.width)
|
||||
rich_text("## Post Command", width=formatter.width, markdown=True)
|
||||
)
|
||||
formatter.write("\n")
|
||||
formatter.write_text(
|
||||
"You can run commands on the exported photos for post-processing "
|
||||
+ "using the '--post-command' option. '--post-command' is passed a CATEGORY and a COMMAND. "
|
||||
@ -394,10 +438,11 @@ The following attributes may be used with '--xattr-template':
|
||||
+ "first to ensure your commands are as expected. This will not actually run the commands but will "
|
||||
+ "print out the exact command string which would be executed."
|
||||
)
|
||||
formatter.write("\n\n")
|
||||
formatter.write("\n")
|
||||
formatter.write(
|
||||
rich_text("[bold]** Post Function **[/bold]", width=formatter.width)
|
||||
rich_text("## Post Function", width=formatter.width, markdown=True)
|
||||
)
|
||||
formatter.write("\n")
|
||||
formatter.write_text(
|
||||
"You can run your own python functions on the exported photos for post-processing "
|
||||
+ "using the '--post-function' option. '--post-function' is passed the name a python file "
|
||||
@ -415,23 +460,19 @@ The following attributes may be used with '--xattr-template':
|
||||
|
||||
def template_help(width=78):
|
||||
"""Return formatted string for template system"""
|
||||
sio = io.StringIO()
|
||||
console = Console(file=sio, force_terminal=True, width=width)
|
||||
template_help_md = strip_md_header_and_links(get_template_help())
|
||||
console.print(Markdown(template_help_md))
|
||||
help_str = sio.getvalue()
|
||||
sio.close()
|
||||
return help_str
|
||||
console = Console(force_terminal=True, width=width)
|
||||
with console.capture() as capture:
|
||||
console.print(Markdown(template_help_md))
|
||||
return capture.get()
|
||||
|
||||
|
||||
def rich_text(text, width=78):
|
||||
def rich_text(text, width=78, markdown=False):
|
||||
"""Return rich formatted text"""
|
||||
sio = io.StringIO()
|
||||
console = Console(file=sio, force_terminal=True, width=width)
|
||||
console.print(text)
|
||||
rich_text = sio.getvalue()
|
||||
sio.close()
|
||||
return rich_text
|
||||
console = Console(force_terminal=True, width=width)
|
||||
with console.capture() as capture:
|
||||
console.print(Markdown(text) if markdown else text, end="")
|
||||
return capture.get()
|
||||
|
||||
|
||||
def strip_md_header_and_links(md):
|
||||
|
||||
132
osxphotos/cli/theme.py
Normal file
132
osxphotos/cli/theme.py
Normal file
@ -0,0 +1,132 @@
|
||||
"""theme command for osxphotos for managing color themes"""
|
||||
|
||||
import pathlib
|
||||
|
||||
import click
|
||||
from rich.console import Console
|
||||
from rich_theme_manager import Theme
|
||||
|
||||
from .click_rich_echo import rich_click_echo
|
||||
from .color_themes import get_default_theme, get_theme, get_theme_dir, get_theme_manager
|
||||
from .help import get_help_msg
|
||||
|
||||
|
||||
@click.command(name="theme")
|
||||
@click.pass_obj
|
||||
@click.pass_context
|
||||
@click.option("--default", is_flag=True, help="Show default theme.")
|
||||
@click.option("--list", "list_", is_flag=True, help="List all themes.")
|
||||
@click.option(
|
||||
"--config",
|
||||
metavar="[THEME]",
|
||||
is_flag=False,
|
||||
flag_value="_DEFAULT_",
|
||||
default=None,
|
||||
help="Print configuration for THEME (or default theme if not specified).",
|
||||
)
|
||||
@click.option(
|
||||
"--preview",
|
||||
metavar="[THEME]",
|
||||
is_flag=False,
|
||||
flag_value="_DEFAULT_",
|
||||
default=None,
|
||||
help="Preview THEME (or default theme if not specified).",
|
||||
)
|
||||
@click.option(
|
||||
"--edit",
|
||||
metavar="[THEME]",
|
||||
is_flag=False,
|
||||
flag_value="_DEFAULT_",
|
||||
default=None,
|
||||
help="Edit THEME (or default theme if not specified).",
|
||||
)
|
||||
@click.option(
|
||||
"--clone",
|
||||
metavar="THEME NEW_THEME",
|
||||
nargs=2,
|
||||
type=str,
|
||||
help="Clone THEME to NEW_THEME.",
|
||||
)
|
||||
@click.option("--delete", metavar="THEME", help="Delete THEME.")
|
||||
def theme(ctx, cli_obj, default, list_, config, preview, edit, clone, delete):
|
||||
"""Manage osxphotos color themes."""
|
||||
|
||||
subcommands = [default, list_, config, preview, edit, clone, delete]
|
||||
subcommand_names = (
|
||||
"--default, --list, --config, --preview, --edit, --clone, --delete"
|
||||
)
|
||||
if not any(subcommands):
|
||||
click.echo(
|
||||
f"Must specify exactly one of: {subcommand_names}\n",
|
||||
err=True,
|
||||
)
|
||||
rich_click_echo(get_help_msg(theme), err=True)
|
||||
return
|
||||
|
||||
if sum(bool(cmd) for cmd in subcommands) != 1:
|
||||
# only a single subcommand may be specified
|
||||
raise click.ClickException(f"Must specify exactly one of: {subcommand_names}")
|
||||
|
||||
theme_manager = get_theme_manager()
|
||||
console = Console(theme=get_default_theme())
|
||||
|
||||
if default:
|
||||
default = get_default_theme()
|
||||
theme_manager.list_themes(theme_names=[default.name])
|
||||
return
|
||||
|
||||
if list_:
|
||||
theme_manager.list_themes()
|
||||
return
|
||||
|
||||
if config:
|
||||
if config == "_DEFAULT_":
|
||||
print(get_default_theme().config)
|
||||
else:
|
||||
print(get_theme(config).config)
|
||||
return
|
||||
|
||||
if preview:
|
||||
theme_ = get_default_theme() if preview == "_DEFAULT_" else get_theme(preview)
|
||||
theme_manager.preview_theme(theme_)
|
||||
return
|
||||
|
||||
if edit:
|
||||
theme_ = get_default_theme() if edit == "_DEFAULT_" else get_theme(edit)
|
||||
config_file = pathlib.Path(theme_.path)
|
||||
console.print(f"Opening [filepath]{config_file}[/] in $EDITOR")
|
||||
click.edit(filename=str(config_file))
|
||||
return
|
||||
|
||||
if clone:
|
||||
src_theme = get_theme(clone[0])
|
||||
dest_path = get_theme_dir() / f"{clone[1]}.theme"
|
||||
if dest_path.exists():
|
||||
raise click.ClickException(
|
||||
f"Theme '{clone[1]}' already exists at {dest_path}"
|
||||
)
|
||||
dest_theme = Theme(
|
||||
name=clone[1],
|
||||
description=src_theme.description,
|
||||
inherit=src_theme.inherit,
|
||||
tags=src_theme.tags,
|
||||
styles={
|
||||
style_name: src_theme.styles[style_name]
|
||||
for style_name in src_theme.style_names
|
||||
},
|
||||
)
|
||||
theme_manager = get_theme_manager()
|
||||
theme_manager.add(dest_theme)
|
||||
theme_ = get_theme(dest_theme.name)
|
||||
console.print(
|
||||
f"Cloned theme '[filename]{clone[0]}[/]' to '[filename]{clone[1]}[/]' "
|
||||
f"at [filepath]{theme_.path}[/]"
|
||||
)
|
||||
return
|
||||
|
||||
if delete:
|
||||
theme_ = get_theme(delete)
|
||||
click.confirm(f"Are you sure you want to delete theme {delete}?", abort=True)
|
||||
theme_manager.remove(theme_)
|
||||
console.print(f"Deleted theme [filepath]{theme_.path}[/]")
|
||||
return
|
||||
@ -8,7 +8,7 @@ Template statements may contain one or more modifiers. The full syntax is:
|
||||
|
||||
Template statements are white-space sensitive meaning that white space (spaces, tabs) changes the meaning of the template statement.
|
||||
|
||||
`pretext` and `posttext` are free form text. For example, if a photo has title "My Photo Title". the template statement `"The title of the photo is {title}"`, resolves to `"The title of the photo is My Photo Title"`. The `pretext` in this example is `"The title if the photo is "` and the template_field is `{title}`.
|
||||
`pretext` and `posttext` are free form text. For example, if a photo has title "My Photo Title" the template statement `"The title of the photo is {title}"`, resolves to `"The title of the photo is My Photo Title"`. The `pretext` in this example is `"The title if the photo is "` and the template_field is `{title}`.
|
||||
|
||||
|
||||
`delim`: optional delimiter string to use when expanding multi-valued template values in-place
|
||||
|
||||
@ -224,7 +224,7 @@ You can use the `--report` option to create a report, in comma-separated values
|
||||
|
||||
### Exporting only certain photos
|
||||
|
||||
By default, osxphotos will export your entire Photos library. If you want to export only certain photos, osxphotos provides a rich set of "query options" that allow you to query the Photos database to filter out only certain photos that match your query criteria. The tutorial does not cover all the query options as there are over 50 of them--read the help text (`osxphotos help export`) to better understand the available query options. No matter which subset of photos you would like to export, there is almost certainly a way for osxphotos to filter these. For example, you can filter for only images that contain certain keywords or images without a title, images from a specific time of day or specific date range, images contained in specific albums, etc.
|
||||
By default, osxphotos will export your entire Photos library. If you want to export only certain photos, osxphotos provides a rich set of "query options" that allow you to query the Photos database to filter out only certain photos that match your query criteria. The tutorial does not cover all the query options as there are over 50 of them--read the help text (`osxphotos help export`) to better understand the available query options. No matter which subset of photos you would like to export, there is almost certainly a way for osxphotos to filter these. For example, you can filter for only images that contain certain keywords or images without a title, images from a specific time of day or specific date range, images contained in specific albums, etc.
|
||||
|
||||
For example, to export only photos with keyword `Travel`:
|
||||
|
||||
@ -266,13 +266,13 @@ osxphotos can, for example, write any keywords in the image to Finder tags so th
|
||||
|
||||
`osxphotos export /path/to/export --finder-tag-keywords`
|
||||
|
||||
`--finder-tag-keywords` also works with `--keyword-template` as described above in the section on `exiftool`:
|
||||
`--finder-tag-keywords` also works with `--keyword-template` as described above in the section on `exiftool`:
|
||||
|
||||
`osxphotos export /path/to/export --finder-tag-keywords --keyword-template "{label}"`
|
||||
|
||||
The `--xattr-template` option allows you to set a variety of other extended attributes. It is used in the format `--xattr-template ATTRIBUTE TEMPLATE` where ATTRIBUTE is one of 'authors','comment', 'copyright', 'description', 'findercomment', 'headline', 'keywords'.
|
||||
The `--xattr-template` option allows you to set a variety of other extended attributes. It is used in the format `--xattr-template ATTRIBUTE TEMPLATE` where ATTRIBUTE is one of 'authors','comment', 'copyright', 'description', 'findercomment', 'headline', 'keywords'.
|
||||
|
||||
For example, to set Finder comment to the photo's title and description:
|
||||
For example, to set Finder comment to the photo's title and description:
|
||||
|
||||
`osxphotos export /path/to/export --xattr-template findercomment "{title}{newline}{descr}"`
|
||||
|
||||
@ -310,7 +310,7 @@ See Extended Attributes section in the help for `osxphotos export` for additiona
|
||||
|
||||
### Saving and loading options
|
||||
|
||||
If you repeatedly run a complex osxphotos export command (for example, to regularly back-up your Photos library), you can save all the options to a configuration file for future use (`--save-config FILE`) and then load them (`--load-config FILE`) instead of repeating each option on the command line.
|
||||
If you repeatedly run a complex osxphotos export command (for example, to regularly back-up your Photos library), you can save all the options to a configuration file for future use (`--save-config FILE`) and then load them (`--load-config FILE`) instead of repeating each option on the command line.
|
||||
|
||||
To save the configuration:
|
||||
|
||||
@ -320,7 +320,7 @@ Then the next to you run osxphotos, you can simply do this:
|
||||
|
||||
`osxphotos export /path/to/export --load-config osxphotos.toml`
|
||||
|
||||
The configuration file is a plain text file in [TOML](https://toml.io/en/) format so the `.toml` extension is standard but you can name the file anything you like.
|
||||
The configuration file is a plain text file in [TOML](https://toml.io/en/) format so the `.toml` extension is standard but you can name the file anything you like.
|
||||
|
||||
### Run commands on exported photos for post-processing
|
||||
|
||||
@ -353,8 +353,7 @@ Another example: if you had `exiftool` installed and wanted to wipe all metadata
|
||||
|
||||
`osxphotos export /path/to/export --post-command exported "/usr/local/bin/exiftool -all= {filepath|shell_quote}"`
|
||||
|
||||
This command uses the `|shell_quote` template filter instead of the `{shell_quote}` template because the only thing that needs to be quoted is the path to the exported file. Template filters filter the value of the rendered template field. A number of other filters are available and are described in the help text.
|
||||
|
||||
This command uses the `|shell_quote` template filter instead of the `{shell_quote}` template because the only thing that needs to be quoted is the path to the exported file. Template filters filter the value of the rendered template field. A number of other filters are available and are described in the help text.
|
||||
|
||||
### An example from an actual osxphotos user
|
||||
|
||||
@ -390,6 +389,10 @@ Here's a comprehensive use case from an actual osxphotos user that integrates ma
|
||||
|
||||
`osxphotos export ~/Desktop/folder for exported videos/ --keyword Quik --only-movies --db /path to my.photoslibrary --touch-file --finder-tag-keywords --person-keyword --xattr-template findercomment "{title}{title?{descr?{newline},},}{descr}" --exiftool-merge-keywords --exiftool-merge-persons --exiftool --strip`
|
||||
|
||||
### Color Themes
|
||||
|
||||
Some osxphotos commands such as export use color themes to colorize the output to make it more legible. The theme may be specified with the `--theme` option. For example: `osxphotos export /path/to/export --verbose --theme dark` uses a theme suited for dark terminals. If you don't specify the color theme, osxphotos will select a default theme based on the current terminal settings. You can also specify your own default theme. See `osxphotos help theme` for more information on themes and for commands to help manage themes. Themes are defined in `.theme` files in the `~/.osxphotos/themes` directory and use style specifications compatible with the [rich](https://rich.readthedocs.io/en/stable/style.html) library.
|
||||
|
||||
### Conclusion
|
||||
|
||||
osxphotos is very flexible. If you merely want to backup your Photos library, then spending a few minutes to understand the `--directory` option is likely all you need and you can be up and running in minutes. However, if you have a more complex workflow, osxphotos likely provides options to implement your workflow. This tutorial does not attempt to cover every option offered by osxphotos but hopefully it provides a good understanding of what kinds of things are possible and where to explore if you want to learn more.
|
||||
osxphotos is very flexible. If you merely want to backup your Photos library, then spending a few minutes to understand the `--directory` option is likely all you need and you can be up and running in minutes. However, if you have a more complex workflow, osxphotos likely provides options to implement your workflow. This tutorial does not attempt to cover every option offered by osxphotos but hopefully it provides a good understanding of what kinds of things are possible and where to explore if you want to learn more.
|
||||
|
||||
@ -20,7 +20,8 @@ pyobjc-framework-Quartz>=7.3,<9.0
|
||||
pyobjc-framework-Vision>=7.3,<9.0
|
||||
PyYAML>=5.4.1,<6.0.0
|
||||
rich>=11.2.0,<12.0.0
|
||||
rich_theme_manager>=0.7.0
|
||||
textx>=2.3.0,<2.4.0
|
||||
toml>=0.10.2,<0.11.0
|
||||
wrapt>=1.13.3,<1.14.0
|
||||
wurlitzer>=2.1.0,<2.2.0
|
||||
wurlitzer>=2.1.0,<2.2.0
|
||||
|
||||
1
setup.py
1
setup.py
@ -95,6 +95,7 @@ setup(
|
||||
"pyobjc-framework-Quartz>=7.3,<9.0",
|
||||
"pyobjc-framework-Vision>=7.3,<9.0",
|
||||
"rich>=11.2.0,<12.0.0",
|
||||
"rich_theme_manager>=0.7.0",
|
||||
"textx>=2.3.0,<3.0.0",
|
||||
"toml>=0.10.2,<0.11.0",
|
||||
"wrapt>=1.13.3,<1.14.0",
|
||||
|
||||
@ -73,11 +73,11 @@ def generate_help_text(command):
|
||||
|
||||
# get current help text
|
||||
with runner.isolated_filesystem():
|
||||
result = runner.invoke(cli_main, ["help", command])
|
||||
result = runner.invoke(cli_main, ["help", command, "--width", 78])
|
||||
help_txt = result.output
|
||||
|
||||
# running the help command above doesn't output the full "Usage" line
|
||||
help_txt = help_txt.replace(f"Usage: cli-main", f"Usage: osxphotos")
|
||||
help_txt = help_txt.replace("Usage: cli-main", "Usage: osxphotos")
|
||||
return help_txt
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user