* 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:
Rhet Turnbull 2022-04-17 23:53:42 -06:00 committed by GitHub
parent 9c0b910046
commit 6c57fb2df9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 801 additions and 514 deletions

833
README.md

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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,

View File

@ -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:

View File

@ -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")
)

View File

@ -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

View File

@ -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))

View File

@ -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
View 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

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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",

View File

@ -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