* 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
14 changed files with 801 additions and 514 deletions

623
README.md
View File

@@ -570,7 +570,6 @@ Another example: if you had `exiftool` installed and wanted to wipe all metadata
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 #### An example from an actual osxphotos user
Here's a comprehensive use case from an actual osxphotos user that integrates many of the concepts discussed in this tutorial (thank-you Philippe for contributing this!): Here's a comprehensive use case from an actual osxphotos user that integrates many of the concepts discussed in this tutorial (thank-you Philippe for contributing this!):
@@ -605,9 +604,14 @@ 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` `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 #### 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-TUTORIAL:END --> 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-TUTORIAL:END -->
### Command line reference: export ### Command line reference: export
@@ -1245,19 +1249,19 @@ Options:
'light' depending on system dark mode setting. 'light' depending on system dark mode setting.
-h, --help Show this message and exit. -h, --help Show this message and exit.
** Export ** Export
When exporting photos, osxphotos creates a database in the top-level export When exporting photos, osxphotos creates a database in the top-level export
folder called '.osxphotos_export.db'. This database preserves state information folder called '.osxphotos_export.db'. This database preserves state
used for determining which files need to be updated when run with --update. It information used for determining which files need to be updated when run with
is recommended that if you later move the export folder tree you also move the --update. It is recommended that if you later move the export folder tree you
database file. also move the database file.
The --update option will only copy new or updated files from the library to the The --update option will only copy new or updated files from the library to
export folder. If a file is changed in the export folder (for example, you the export folder. If a file is changed in the export folder (for example,
edited the exported image), osxphotos will detect this as a difference and re- you edited the exported image), osxphotos will detect this as a difference and
export the original image from the library thus overwriting the changes. If re-export the original image from the library thus overwriting the changes.
using --update, the exported library should be treated as a backup, not a If using --update, the exported library should be treated as a backup, not a
working copy where you intend to make changes. If you do edit or process the working copy where you intend to make changes. If you do edit or process the
exported files and do not want them to be overwritten withsubsequent --update, exported files and do not want them to be overwritten withsubsequent --update,
use --ignore-signature which will match filename but not file signature when use --ignore-signature which will match filename but not file signature when
@@ -1269,34 +1273,37 @@ are reported in the total photos exported.
Implementation note: To determine which files need to be updated, osxphotos Implementation note: To determine which files need to be updated, osxphotos
stores file signature information in the '.osxphotos_export.db' database. The stores file signature information in the '.osxphotos_export.db' database. The
signature includes size, modification time, and filename. In order to minimize signature includes size, modification time, and filename. In order to
run time, --update does not do a full comparison (diff) of the files nor does it minimize run time, --update does not do a full comparison (diff) of the files
compare hashes of the files. In normal usage, this is sufficient for updating nor does it compare hashes of the files. In normal usage, this is sufficient
the library. You can always run export without the --update option to re-export for updating the library. You can always run export without the --update
the entire library thus rebuilding the '.osxphotos_export.db' database. option to re-export the entire library thus rebuilding the
'.osxphotos_export.db' database.
** Extended Attributes ** Extended Attributes
Some options (currently '--finder-tag-template', '--finder-tag-keywords', Some options (currently '--finder-tag-template', '--finder-tag-keywords',
'-xattr-template') write additional metadata to extended attributes in the file. '-xattr-template') write additional metadata to extended attributes in the
These options will only work if the destination filesystem supports extended file. These options will only work if the destination filesystem supports
attributes (most do). For example, --finder-tag-keyword writes all keywords extended attributes (most do). For example, --finder-tag-keyword writes all
(including any specified by '--keyword-template' or other options) to Finder keywords (including any specified by '--keyword-template' or other options) to
tags that are searchable in Spotlight using the syntax: 'tag:tagname'. For Finder tags that are searchable in Spotlight using the syntax: 'tag:tagname'.
example, if you have images with keyword "Travel" then using '--finder-tag- For example, if you have images with keyword "Travel" then using '--finder-
keywords' you could quickly find those images in the Finder by typing tag-keywords' you could quickly find those images in the Finder by typing
'tag:Travel' in the Spotlight search bar. Finder tags are written to the 'tag:Travel' in the Spotlight search bar. Finder tags are written to the
'com.apple.metadata:_kMDItemUserTags' extended attribute. Unlike EXIF metadata, 'com.apple.metadata:_kMDItemUserTags' extended attribute. Unlike EXIF
extended attributes do not modify the actual file. Most cloud storage services metadata, extended attributes do not modify the actual file. Most cloud
do not synch extended attributes. Dropbox does sync them and any changes to a storage services do not synch extended attributes. Dropbox does sync them and
file's extended attributes will cause Dropbox to re-sync the files. any changes to a file's extended attributes will cause Dropbox to re-sync the
files.
The following attributes may be used with '--xattr-template': The following attributes may be used with '--xattr-template':
authors The author, or authors, of the contents of the file. A list of Attribute Description
strings. (com.apple.metadata:kMDItemAuthors) authors The author, or authors, of the contents of the file. A list
of strings. (com.apple.metadata:kMDItemAuthors)
comment A comment related to the file. This differs from the Finder comment A comment related to the file. This differs from the Finder
comment, kMDItemFinderComment. A string. comment, kMDItemFinderComment. A string.
(com.apple.metadata:kMDItemComment) (com.apple.metadata:kMDItemComment)
@@ -1305,69 +1312,70 @@ copyright The copyright owner of the file contents. A string.
creator Application used to create the document content (for example creator Application used to create the document content (for example
“Word”, “Pages”, and so on). A string. “Word”, “Pages”, and so on). A string.
(com.apple.metadata:kMDItemCreator) (com.apple.metadata:kMDItemCreator)
description A description of the content of the resource. The description description A description of the content of the resource. The
may include an abstract, table of contents, reference to a description may include an abstract, table of contents,
graphical representation of content or a free-text account of reference to a graphical representation of content or a free-
the content. A string. (com.apple.metadata:kMDItemDescription) text account of the content. A string.
(com.apple.metadata:kMDItemDescription)
findercomment Finder comments for this file. A string. findercomment Finder comments for this file. A string.
(com.apple.metadata:kMDItemFinderComment) (com.apple.metadata:kMDItemFinderComment)
headline A publishable entry providing a synopsis of the contents of the headline A publishable entry providing a synopsis of the contents of
file. A string. (com.apple.metadata:kMDItemHeadline) the file. A string. (com.apple.metadata:kMDItemHeadline)
keywords Keywords associated with this file. For example, “Birthday”, keywords Keywords associated with this file. For example, “Birthday”,
“Important”, etc. This differs from Finder tags “Important”, etc. This differs from Finder tags
(_kMDItemUserTags) which are keywords/tags shown in the Finder (_kMDItemUserTags) which are keywords/tags shown in the
and searchable in Spotlight using "tag:tag_name". A list of Finder and searchable in Spotlight using "tag:tag_name". A
strings. (com.apple.metadata:kMDItemKeywords) list of strings. (com.apple.metadata:kMDItemKeywords)
participants The list of people who are visible in an image or movie or participants The list of people who are visible in an image or movie or
written about in a document. A list of strings. written about in a document. A list of strings.
(com.apple.metadata:kMDItemParticipants) (com.apple.metadata:kMDItemParticipants)
projects The list of projects that this file is part of. For example, if projects The list of projects that this file is part of. For example,
you were working on a movie all of the files could be marked as if you were working on a movie all of the files could be
belonging to the project “My Movie”. A list of strings. marked as belonging to the project “My Movie”. A list of
(com.apple.metadata:kMDItemProjects) strings. (com.apple.metadata:kMDItemProjects)
rating User rating of this item. For example, the stars rating of an rating User rating of this item. For example, the stars rating of an
iTunes track. An integer. iTunes track. An integer.
(com.apple.metadata:kMDItemStarRating) (com.apple.metadata:kMDItemStarRating)
subject Subject of the this item. A string. subject Subject of the this item. A string.
(com.apple.metadata:kMDItemSubject) (com.apple.metadata:kMDItemSubject)
title The title of the file. For example, this could be the title of title The title of the file. For example, this could be the title
a document, the name of a song, or the subject of an email of a document, the name of a song, or the subject of an email
message. A string. (com.apple.metadata:kMDItemTitle) message. A string. (com.apple.metadata:kMDItemTitle)
version The version number of this file. A string. version The version number of this file. A string.
(com.apple.metadata:kMDItemVersion) (com.apple.metadata:kMDItemVersion)
For additional information on extended attributes see: https://developer.apple.c For additional information on extended attributes see: https://developer.apple
om/documentation/coreservices/file_metadata/mditem/common_metadata_attribute_key .com/documentation/coreservices/file_metadata/mditem/common_metadata_attribute
s _keys
** Templating System ** Templating System
The templating system converts one or template statements, written in osxphotos The templating system converts one or template statements, written in
metadata templating language, to one or more rendered values using information osxphotos metadata templating language, to one or more rendered values using
from the photo being processed. information from the photo being processed.
In its simplest form, a template statement has the form: "{template_field}", for In its simplest form, a template statement has the form: "{template_field}",
example "{title}" which would resolve to the title of the photo. for example "{title}" which would resolve to the title of the photo.
Template statements may contain one or more modifiers. The full syntax is: Template statements may contain one or more modifiers. The full syntax is:
"pretext{delim+template_field:subfield|filter(path_sep)[find,replace] "pretext{delim+template_field:subfield|filter(path_sep)[find,replace]
conditional?bool_value,default}posttext" conditional?bool_value,default}posttext"
Template statements are white-space sensitive meaning that white space (spaces, Template statements are white-space sensitive meaning that white space
tabs) changes the meaning of the template statement. (spaces, tabs) changes the meaning of the template statement.
pretext and posttext are free form text. For example, if a photo has title "My pretext and posttext are free form text. For example, if a photo has title
Photo Title". the template statement "The title of the photo is {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 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}. 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 delim: optional delimiter string to use when expanding multi-valued template
values in-place values in-place
+: If present before template name, expands the template in place. If delim not +: If present before template name, expands the template in place. If delim
provided, values are joined with no delimiter. not provided, values are joined with no delimiter.
e.g. if Photo keywords are ["foo","bar"]: e.g. if Photo keywords are ["foo","bar"]:
@@ -1382,9 +1390,9 @@ full list of template fields.
:subfield: Some templates have sub-fields, For example, {exiftool:IPTC:Make}; :subfield: Some templates have sub-fields, For example, {exiftool:IPTC:Make};
the template_field is exiftool and the sub-field is IPTC:Make. the template_field is exiftool and the sub-field is IPTC:Make.
|filter: You may optionally append one or more filter commands to the end of the |filter: You may optionally append one or more filter commands to the end of
template field using the vertical pipe ('|') symbol. Filters may be combined, the template field using the vertical pipe ('|') symbol. Filters may be
separated by '|' as in: {keyword|capitalize|parens}. combined, separated by '|' as in: {keyword|capitalize|parens}.
Valid filters are: Valid filters are:
@@ -1398,11 +1406,11 @@ Valid filters are:
• braces: Enclose value in curly braces, e.g. 'value => '{value}'. • braces: Enclose value in curly braces, e.g. 'value => '{value}'.
• parens: Enclose value in parentheses, e.g. 'value' => '(value') • parens: Enclose value in parentheses, e.g. 'value' => '(value')
• brackets: Enclose value in brackets, e.g. 'value' => '[value]' • brackets: Enclose value in brackets, e.g. 'value' => '[value]'
• shell_quote: Quotes the value for safe usage in the shell, e.g. My file.jpeg • shell_quote: Quotes the value for safe usage in the shell, e.g. My
=> 'My file.jpeg'; only adds quotes if needed. file.jpeg => 'My file.jpeg'; only adds quotes if needed.
• function: Run custom python function to filter value; use in format • function: Run custom python function to filter value; use in format
'function:/path/to/file.py::function_name'. See example at https://github.com 'function:/path/to/file.py::function_name'. See example at https://github.c
/RhetTbull/osxphotos/blob/master/examples/template_filter.py om/RhetTbull/osxphotos/blob/master/examples/template_filter.py
e.g. if Photo keywords are ["FOO","bar"]: e.g. if Photo keywords are ["FOO","bar"]:
@@ -1424,12 +1432,12 @@ e.g. If Photo is in Album1 in Folder1:
• "{folder_album(>)}" renders to ["Folder1>Album1"] • "{folder_album(>)}" renders to ["Folder1>Album1"]
• "{folder_album()}" renders to ["Folder1Album1"] • "{folder_album()}" renders to ["Folder1Album1"]
[find,replace]: optional text replacement to perform on rendered template value. [find,replace]: optional text replacement to perform on rendered template
For example, to replace "/" in an album name, you could use the template value. For example, to replace "/" in an album name, you could use the
"{album[/,-]}". Multiple replacements can be made by appending "|" and adding template "{album[/,-]}". Multiple replacements can be made by appending "|"
another find|replace pair. e.g. to replace both "/" and ":" in album name: and adding another find|replace pair. e.g. to replace both "/" and ":" in
"{album[/,-|:,-]}". find/replace pairs are not limited to single characters. album name: "{album[/,-|:,-]}". find/replace pairs are not limited to single
The "|" character cannot be used in a find/replace pair. characters. The "|" character cannot be used in a find/replace pair.
conditional: optional conditional expression that is evaluated as boolean conditional: optional conditional expression that is evaluated as boolean
(True/False) for use with the ?bool_value modifier. Conditional expressions (True/False) for use with the ?bool_value modifier. Conditional expressions
@@ -1450,9 +1458,9 @@ required if you use a conditional expression. Valid comparison operators are:
• !=: template field does not equal value • !=: template field does not equal value
The value part of the conditional expression is treated as a bare (unquoted) The value part of the conditional expression is treated as a bare (unquoted)
word/phrase. Multiple values may be separated by '|' (the pipe symbol). value word/phrase. Multiple values may be separated by '|' (the pipe symbol).
is itself a template statement so you can use one or more template fields in value is itself a template statement so you can use one or more template
value which will be resolved before the comparison occurs. fields in value which will be resolved before the comparison occurs.
For example: For example:
@@ -1460,8 +1468,8 @@ For example:
not match keyword 'BeachDay'. not match keyword 'BeachDay'.
• {keyword contains Beach} resolves to True if any keyword contains the word • {keyword contains Beach} resolves to True if any keyword contains the word
'Beach' so it would match both 'Beach' and 'BeachDay'. 'Beach' so it would match both 'Beach' and 'BeachDay'.
• {photo.score.overall > 0.7} resolves to True if the photo's overall aesthetic • {photo.score.overall > 0.7} resolves to True if the photo's overall
score is greater than 0.7. aesthetic score is greater than 0.7.
• {keyword|lower contains beach} uses the lower case filter to do • {keyword|lower contains beach} uses the lower case filter to do
case-insensitive matching to match any keyword that contains the word case-insensitive matching to match any keyword that contains the word
'beach'. 'beach'.
@@ -1475,24 +1483,25 @@ export command's --directory option:
--directory "{keyword|lower matches --directory "{keyword|lower matches
travel|vacation?Travel-Photos,Not-Travel-Photos}" travel|vacation?Travel-Photos,Not-Travel-Photos}"
This exports any photo that has keywords 'travel' or 'vacation' into a directory This exports any photo that has keywords 'travel' or 'vacation' into a
'Travel-Photos' and all other photos into directory 'Not-Travel-Photos'. directory 'Travel-Photos' and all other photos into directory
'Not-Travel-Photos'.
This can be used to rename files as well, for example: --filename This can be used to rename files as well, for example: --filename
"{favorite?Favorite-{original_name},{original_name}}" "{favorite?Favorite-{original_name},{original_name}}"
This renames any photo that is a favorite as 'Favorite-ImageName.jpg' (where This renames any photo that is a favorite as 'Favorite-ImageName.jpg' (where
'ImageName.jpg' is the original name of the photo) and all other photos with the 'ImageName.jpg' is the original name of the photo) and all other photos with
unmodified original name. the unmodified original name.
?bool_value: Template fields may be evaluated as boolean (True/False) by ?bool_value: Template fields may be evaluated as boolean (True/False) by
appending "?" after the field name (and following "(path_sep)" or appending "?" after the field name (and following "(path_sep)" or
"[find/replace]". If a field is True (e.g. photo is HDR and field is "{hdr}") "[find/replace]". If a field is True (e.g. photo is HDR and field is "{hdr}")
or has any value, the value following the "?" will be used to render the or has any value, the value following the "?" will be used to render the
template instead of the actual field value. If the template field evaluates to template instead of the actual field value. If the template field evaluates
False (e.g. in above example, photo is not HDR) or has no value (e.g. photo has to False (e.g. in above example, photo is not HDR) or has no value (e.g. photo
no title and field is "{title}") then the default value following a "," will be has no title and field is "{title}") then the default value following a ","
used. will be used.
e.g. if photo is an HDR image, e.g. if photo is an HDR image,
@@ -1502,10 +1511,10 @@ and if it is not an HDR image,
• "{hdr?ISHDR,NOTHDR}" renders to "NOTHDR" • "{hdr?ISHDR,NOTHDR}" renders to "NOTHDR"
,default: optional default value to use if the template name has no value. This ,default: optional default value to use if the template name has no value.
modifier is also used for the value if False for boolean-type fields (see above) This modifier is also used for the value if False for boolean-type fields (see
as well as to hold a sub-template for values like {created.strftime}. If no above) as well as to hold a sub-template for values like {created.strftime}.
default value provided, "_" is used. If no default value provided, "_" is used.
e.g., if photo has no title set, e.g., if photo has no title set,
@@ -1520,12 +1529,12 @@ e.g., if photo date is 4 February 2020, 19:07:38,
• "{created.strftime,%Y-%m-%d-%H%M%S}" renders to "2020-02-04-190738" • "{created.strftime,%Y-%m-%d-%H%M%S}" renders to "2020-02-04-190738"
Some template fields such as "{media_type}" use the default value to allow Some template fields such as "{media_type}" use the default value to allow
customization of the output. For example, "{media_type}" resolves to the special customization of the output. For example, "{media_type}" resolves to the
media type of the photo such as panorama or selfie. You may use the default special media type of the photo such as panorama or selfie. You may use the
value to override these in form: default value to override these in form:
"{media_type,video=vidéo;time_lapse=vidéo_accélérée}". In this example, if photo "{media_type,video=vidéo;time_lapse=vidéo_accélérée}". In this example, if
was a time_lapse photo, media_type would resolve to vidéo_accélérée instead of photo was a time_lapse photo, media_type would resolve to vidéo_accélérée
time_lapse. instead of time_lapse.
Either or both bool_value or default (False value) may be empty which would Either or both bool_value or default (False value) may be empty which would
result in empty string "" when rendered. result in empty string "" when rendered.
@@ -1539,23 +1548,24 @@ e.g. "{created.year}/{openbrace}{title}{closebrace}" would result in
With the --directory and --filename options you may specify a template for the With the --directory and --filename options you may specify a template for the
export directory or filename, respectively. The directory will be appended to export directory or filename, respectively. The directory will be appended to
the export path specified in the export DEST argument to export. For example, the export path specified in the export DEST argument to export. For example,
if template is '{created.year}/{created.month}', and export destination DEST is if template is '{created.year}/{created.month}', and export destination DEST
'/Users/maria/Pictures/export', the actual export directory for a photo would be is '/Users/maria/Pictures/export', the actual export directory for a photo
'/Users/maria/Pictures/export/2020/March' if the photo was created in March would be '/Users/maria/Pictures/export/2020/March' if the photo was created in
2020. March 2020.
The templating system may also be used with the --keyword-template option to set The templating system may also be used with the --keyword-template option to
keywords on export (with --exiftool or --sidecar), for example, to set a new set keywords on export (with --exiftool or --sidecar), for example, to set a
keyword in format 'folder/subfolder/album' to preserve the folder/album new keyword in format 'folder/subfolder/album' to preserve the folder/album
structure, you can use --keyword-template "{folder_album}" or in the structure, you can use --keyword-template "{folder_album}" or in the
'folder>subfolder>album' format used in Lightroom Classic, --keyword-template 'folder>subfolder>album' format used in Lightroom Classic, --keyword-template
"{folder_album(>)}". "{folder_album(>)}".
In the template, valid template substitutions will be replaced by the In the template, valid template substitutions will be replaced by the
corresponding value from the table below. Invalid substitutions will result in corresponding value from the table below. Invalid substitutions will result
a an error and the script will abort. in a an error and the script will abort.
** Template Substitutions **
Template Substitutions
Substitution Description Substitution Description
{name} Current filename of the photo {name} Current filename of the photo
@@ -1568,100 +1578,105 @@ Substitution Description
slow_mo, screenshot, portrait, live_photo, slow_mo, screenshot, portrait, live_photo,
burst, photo, video. Defaults to 'photo' or burst, photo, video. Defaults to 'photo' or
'video' if no special type. Customize one or 'video' if no special type. Customize one or
more media types using format: '{media_type,vi more media types using format: '{media_type,
deo=vidéo;time_lapse=vidéo_accélérée}' video=vidéo;time_lapse=vidéo_accélérée}'
{photo_or_video} 'photo' or 'video' depending on what type the {photo_or_video} 'photo' or 'video' depending on what type
image is. To customize, use default value as the image is. To customize, use default
in '{photo_or_video,photo=fotos;video=videos}' value as in
{hdr} Photo is HDR?; True/False value, use in format '{photo_or_video,photo=fotos;video=videos}'
'{hdr?VALUE_IF_TRUE,VALUE_IF_FALSE}' {hdr} Photo is HDR?; True/False value, use in
format '{hdr?VALUE_IF_TRUE,VALUE_IF_FALSE}'
{edited} True if photo has been edited (has {edited} True if photo has been edited (has
adjustments), otherwise False; use in format adjustments), otherwise False; use in format
'{edited?VALUE_IF_TRUE,VALUE_IF_FALSE}' '{edited?VALUE_IF_TRUE,VALUE_IF_FALSE}'
{edited_version} True if template is being rendered for the {edited_version} True if template is being rendered for the
edited version of a photo, otherwise False. edited version of a photo, otherwise False.
{favorite} Photo has been marked as favorite?; True/False {favorite} Photo has been marked as favorite?;
value, use in format True/False value, use in format
'{favorite?VALUE_IF_TRUE,VALUE_IF_FALSE}' '{favorite?VALUE_IF_TRUE,VALUE_IF_FALSE}'
{created.date} Photo's creation date in ISO format, e.g. {created.date} Photo's creation date in ISO format, e.g.
'2020-03-22' '2020-03-22'
{created.year} 4-digit year of photo creation time {created.year} 4-digit year of photo creation time
{created.yy} 2-digit year of photo creation time {created.yy} 2-digit year of photo creation time
{created.mm} 2-digit month of the photo creation time (zero {created.mm} 2-digit month of the photo creation time
padded) (zero padded)
{created.month} Month name in user's locale of the photo {created.month} Month name in user's locale of the photo
creation time creation time
{created.mon} Month abbreviation in the user's locale of the {created.mon} Month abbreviation in the user's locale of
photo creation time the photo creation time
{created.dd} 2-digit day of the month (zero padded) of {created.dd} 2-digit day of the month (zero padded) of
photo creation time photo creation time
{created.dow} Day of week in user's locale of the photo {created.dow} Day of week in user's locale of the photo
creation time creation time
{created.doy} 3-digit day of year (e.g Julian day) of photo {created.doy} 3-digit day of year (e.g Julian day) of
creation time, starting from 1 (zero padded) photo creation time, starting from 1 (zero
padded)
{created.hour} 2-digit hour of the photo creation time {created.hour} 2-digit hour of the photo creation time
{created.min} 2-digit minute of the photo creation time {created.min} 2-digit minute of the photo creation time
{created.sec} 2-digit second of the photo creation time {created.sec} 2-digit second of the photo creation time
{created.strftime} Apply strftime template to file creation {created.strftime} Apply strftime template to file creation
date/time. Should be used in form date/time. Should be used in form
{created.strftime,TEMPLATE} where TEMPLATE is {created.strftime,TEMPLATE} where TEMPLATE
a valid strftime template, e.g. is a valid strftime template, e.g.
{created.strftime,%Y-%U} would result in year- {created.strftime,%Y-%U} would result in
week number of year: '2020-23'. If used with year-week number of year: '2020-23'. If used
no template will return null value. See with no template will return null value. See
https://strftime.org/ for help on strftime https://strftime.org/ for help on strftime
templates. templates.
{modified.date} Photo's modification date in ISO format, e.g. {modified.date} Photo's modification date in ISO format,
'2020-03-22'; uses creation date if photo is e.g. '2020-03-22'; uses creation date if
not modified photo is not modified
{modified.year} 4-digit year of photo modification time; uses {modified.year} 4-digit year of photo modification time;
creation date if photo is not modified uses creation date if photo is not modified
{modified.yy} 2-digit year of photo modification time; uses {modified.yy} 2-digit year of photo modification time;
creation date if photo is not modified uses creation date if photo is not modified
{modified.mm} 2-digit month of the photo modification time {modified.mm} 2-digit month of the photo modification time
(zero padded); uses creation date if photo is (zero padded); uses creation date if photo
not modified is not modified
{modified.month} Month name in user's locale of the photo {modified.month} Month name in user's locale of the photo
modification time; uses creation date if photo modification time; uses creation date if
is not modified
{modified.mon} Month abbreviation in the user's locale of the
photo modification time; uses creation date if
photo is not modified
{modified.dd} 2-digit day of the month (zero padded) of the
photo modification time; uses creation date if
photo is not modified photo is not modified
{modified.mon} Month abbreviation in the user's locale of
the photo modification time; uses creation
date if photo is not modified
{modified.dd} 2-digit day of the month (zero padded) of
the photo modification time; uses creation
date if photo is not modified
{modified.dow} Day of week in user's locale of the photo {modified.dow} Day of week in user's locale of the photo
modification time; uses creation date if photo modification time; uses creation date if
photo is not modified
{modified.doy} 3-digit day of year (e.g Julian day) of
photo modification time, starting from 1
(zero padded); uses creation date if photo
is not modified is not modified
{modified.doy} 3-digit day of year (e.g Julian day) of photo
modification time, starting from 1 (zero
padded); uses creation date if photo is not
modified
{modified.hour} 2-digit hour of the photo modification time; {modified.hour} 2-digit hour of the photo modification time;
uses creation date if photo is not modified uses creation date if photo is not modified
{modified.min} 2-digit minute of the photo modification time; {modified.min} 2-digit minute of the photo modification
uses creation date if photo is not modified time; uses creation date if photo is not
{modified.sec} 2-digit second of the photo modification time; modified
uses creation date if photo is not modified {modified.sec} 2-digit second of the photo modification
time; uses creation date if photo is not
modified
{modified.strftime} Apply strftime template to file modification {modified.strftime} Apply strftime template to file modification
date/time. Should be used in form date/time. Should be used in form
{modified.strftime,TEMPLATE} where TEMPLATE is {modified.strftime,TEMPLATE} where TEMPLATE
a valid strftime template, e.g. is a valid strftime template, e.g.
{modified.strftime,%Y-%U} would result in {modified.strftime,%Y-%U} would result in
year-week number of year: '2020-23'. If used year-week number of year: '2020-23'. If used
with no template will return null value. Uses with no template will return null value.
creation date if photo is not modified. See Uses creation date if photo is not modified.
https://strftime.org/ for help on strftime See https://strftime.org/ for help on
templates. strftime templates.
{today.date} Current date in iso format, e.g. '2020-03-22' {today.date} Current date in iso format, e.g.
'2020-03-22'
{today.year} 4-digit year of current date {today.year} 4-digit year of current date
{today.yy} 2-digit year of current date {today.yy} 2-digit year of current date
{today.mm} 2-digit month of the current date (zero {today.mm} 2-digit month of the current date (zero
padded) padded)
{today.month} Month name in user's locale of the current {today.month} Month name in user's locale of the current
date date
{today.mon} Month abbreviation in the user's locale of the {today.mon} Month abbreviation in the user's locale of
current date the current date
{today.dd} 2-digit day of the month (zero padded) of {today.dd} 2-digit day of the month (zero padded) of
current date current date
{today.dow} Day of week in user's locale of the current {today.dow} Day of week in user's locale of the current
@@ -1671,10 +1686,10 @@ Substitution Description
{today.hour} 2-digit hour of the current date {today.hour} 2-digit hour of the current date
{today.min} 2-digit minute of the current date {today.min} 2-digit minute of the current date
{today.sec} 2-digit second of the current date {today.sec} 2-digit second of the current date
{today.strftime} Apply strftime template to current date/time. {today.strftime} Apply strftime template to current
Should be used in form date/time. Should be used in form
{today.strftime,TEMPLATE} where TEMPLATE is a {today.strftime,TEMPLATE} where TEMPLATE is
valid strftime template, e.g. a valid strftime template, e.g.
{today.strftime,%Y-%U} would result in year- {today.strftime,%Y-%U} would result in year-
week number of year: '2020-23'. If used with week number of year: '2020-23'. If used with
no template will return null value. See no template will return null value. See
@@ -1682,22 +1697,22 @@ Substitution Description
templates. templates.
{place.name} Place name from the photo's reverse {place.name} Place name from the photo's reverse
geolocation data, as displayed in Photos geolocation data, as displayed in Photos
{place.country_code} The ISO country code from the photo's reverse {place.country_code} The ISO country code from the photo's
geolocation data reverse geolocation data
{place.name.country} Country name from the photo's reverse {place.name.country} Country name from the photo's reverse
geolocation data geolocation data
{place.name.state_province} State or province name from the photo's {place.name.state_province} State or province name from the photo's
reverse geolocation data reverse geolocation data
{place.name.city} City or locality name from the photo's reverse {place.name.city} City or locality name from the photo's
reverse geolocation data
{place.name.area_of_interest} Area of interest name (e.g. landmark or
public place) from the photo's reverse
geolocation data geolocation data
{place.name.area_of_interest} Area of interest name (e.g. landmark or public
place) from the photo's reverse geolocation
data
{place.address} Postal address from the photo's reverse {place.address} Postal address from the photo's reverse
geolocation data, e.g. '2007 18th St NW, geolocation data, e.g. '2007 18th St NW,
Washington, DC 20009, United States' Washington, DC 20009, United States'
{place.address.street} Street part of the postal address, e.g. '2007 {place.address.street} Street part of the postal address, e.g.
18th St NW' '2007 18th St NW'
{place.address.city} City part of the postal address, e.g. {place.address.city} City part of the postal address, e.g.
'Washington' 'Washington'
{place.address.state_province} State/province part of the postal address, {place.address.state_province} State/province part of the postal address,
@@ -1710,8 +1725,8 @@ Substitution Description
'US' 'US'
{searchinfo.season} Season of the year associated with a photo, {searchinfo.season} Season of the year associated with a photo,
e.g. 'Summer'; (Photos 5+ only, applied e.g. 'Summer'; (Photos 5+ only, applied
automatically by Photos' image categorization automatically by Photos' image
algorithms). categorization algorithms).
{exif.camera_make} Camera make from original photo's EXIF {exif.camera_make} Camera make from original photo's EXIF
information as imported by Photos, e.g. information as imported by Photos, e.g.
'Apple' 'Apple'
@@ -1721,59 +1736,60 @@ Substitution Description
{exif.lens_model} Lens model from original photo's EXIF {exif.lens_model} Lens model from original photo's EXIF
information as imported by Photos, e.g. information as imported by Photos, e.g.
'iPhone 6s back camera 4.15mm f/2.2' 'iPhone 6s back camera 4.15mm f/2.2'
{uuid} Photo's internal universally unique identifier {uuid} Photo's internal universally unique
(UUID) for the photo, a 36-character string identifier (UUID) for the photo, a
unique to the photo, e.g. 36-character string unique to the photo,
'128FB4C6-0B16-4E7D-9108-FB2E90DA1546' e.g. '128FB4C6-0B16-4E7D-9108-FB2E90DA1546'
{id} A unique number for the photo based on its {id} A unique number for the photo based on its
primary key in the Photos database. A primary key in the Photos database. A
sequential integer, e.g. 1, 2, 3...etc. Each sequential integer, e.g. 1, 2, 3...etc.
asset associated with a photo (e.g. an image Each asset associated with a photo (e.g. an
and Live Photo preview) will share the same image and Live Photo preview) will share the
id. May be formatted using a python string same id. May be formatted using a python
format code. For example, to format as a string format code. For example, to format
5-digit integer and pad with zeros, use as a 5-digit integer and pad with zeros, use
'{id:05d}' which results in 00001, 00002, '{id:05d}' which results in 00001, 00002,
00003...etc. 00003...etc.
{album_seq} An integer, starting at 0, indicating the {album_seq} An integer, starting at 0, indicating the
photo's index (sequence) in the containing photo's index (sequence) in the containing
album. Only valid when used in a '--filename' album. Only valid when used in a '--
template and only when '{album}' or filename' template and only when '{album}'
'{folder_album}' is used in the '--directory' or '{folder_album}' is used in the '--
template. For example '--directory directory' template. For example '--
"{folder_album}" --filename directory "{folder_album}" --filename
"{album_seq}_{original_name}"'. To start "{album_seq}_{original_name}"'. To start
counting at a value other than 0, append counting at a value other than 0, append
append a period and the starting value to the append a period and the starting value to
field name. For example, to start counting at the field name. For example, to start
1 instead of 0: '{album_seq.1}'. May be counting at 1 instead of 0: '{album_seq.1}'.
formatted using a python string format code. May be formatted using a python string
For example, to format as a 5-digit integer format code. For example, to format as a
and pad with zeros, use '{album_seq:05d}' 5-digit integer and pad with zeros, use
which results in 00000, 00001, 00002...etc. '{album_seq:05d}' which results in 00000,
This may result in incorrect sequences if you 00001, 00002...etc. This may result in
have duplicate albums with the same name; see incorrect sequences if you have duplicate
also '{folder_album_seq}'. albums with the same name; see also
'{folder_album_seq}'.
{folder_album_seq} An integer, starting at 0, indicating the {folder_album_seq} An integer, starting at 0, indicating the
photo's index (sequence) in the containing photo's index (sequence) in the containing
album and folder path. Only valid when used in album and folder path. Only valid when used
a '--filename' template and only when in a '--filename' template and only when
'{folder_album}' is used in the '--directory' '{folder_album}' is used in the '--
template. For example '--directory directory' template. For example '--
"{folder_album}" --filename directory "{folder_album}" --filename
"{folder_album_seq}_{original_name}"'. To "{folder_album_seq}_{original_name}"'. To
start counting at a value other than 0, append start counting at a value other than 0,
append a period and the starting value to the append append a period and the starting
field name. For example, to start counting at value to the field name. For example, to
1 instead of 0: '{folder_album_seq.1}' May be start counting at 1 instead of 0:
formatted using a python string format code. '{folder_album_seq.1}' May be formatted
For example, to format as a 5-digit integer using a python string format code. For
and pad with zeros, use example, to format as a 5-digit integer and
'{folder_album_seq:05d}' which results in pad with zeros, use '{folder_album_seq:05d}'
00000, 00001, 00002...etc. This may result in which results in 00000, 00001, 00002...etc.
incorrect sequences if you have duplicate This may result in incorrect sequences if
albums with the same name in the same folder; you have duplicate albums with the same name
see also '{album_seq}'. in the same folder; see also '{album_seq}'.
{comma} A comma: ',' {comma} A comma: ','
{semicolon} A semicolon: ';' {semicolon} A semicolon: ';'
{questionmark} A question mark: '?' {questionmark} A question mark: '?'
@@ -1791,8 +1807,8 @@ Substitution Description
{osxphotos_version} The osxphotos version, e.g. '0.47.6' {osxphotos_version} The osxphotos version, e.g. '0.47.6'
{osxphotos_cmd_line} The full command line used to run osxphotos {osxphotos_cmd_line} The full command line used to run osxphotos
The following substitutions may result in multiple values. Thus if specified for The following substitutions may result in multiple values. Thus if specified
--directory these could result in multiple copies of a photo being being for --directory these could result in multiple copies of a photo being being
exported, one to each directory. For example: --directory exported, one to each directory. For example: --directory
'{created.year}/{album}' could result in the same photo being exported to each '{created.year}/{album}' could result in the same photo being exported to each
of the following directories if the photos were created in 2019 and were in of the following directories if the photos were created in 2019 and were in
@@ -1805,11 +1821,12 @@ Substitution Description
enclosing folder enclosing folder
{project} Project(s) photo is contained in (such as greeting {project} Project(s) photo is contained in (such as greeting
cards, calendars, slideshows) cards, calendars, slideshows)
{album_project} Album(s) and project(s) photo is contained in; treats {album_project} Album(s) and project(s) photo is contained in;
projects as regular albums treats projects as regular albums
{folder_album_project} Folder path + album (includes projects as albums) {folder_album_project} Folder path + album (includes projects as albums)
photo is contained in. e.g. 'Folder/Subfolder/Album' photo is contained in. e.g.
or just 'Album' if no enclosing folder 'Folder/Subfolder/Album' or just 'Album' if no
enclosing folder
{keyword} Keyword(s) assigned to photo {keyword} Keyword(s) assigned to photo
{person} Person(s) / face(s) in a photo {person} Person(s) / face(s) in a photo
{label} Image categorization label associated with a photo {label} Image categorization label associated with a photo
@@ -1819,17 +1836,17 @@ Substitution Description
{keyword} which refers to the user-defined {keyword} which refers to the user-defined
keywords/tags applied in Photos. keywords/tags applied in Photos.
{label_normalized} All lower case version of 'label' (Photos 5+ only) {label_normalized} All lower case version of 'label' (Photos 5+ only)
{comment} Comment(s) on shared Photos; format is 'Person name: {comment} Comment(s) on shared Photos; format is 'Person
comment text' (Photos 5+ only) name: comment text' (Photos 5+ only)
{exiftool} Format: '{exiftool:GROUP:TAGNAME}'; use exiftool {exiftool} Format: '{exiftool:GROUP:TAGNAME}'; use exiftool
(https://exiftool.org) to extract metadata, in form (https://exiftool.org) to extract metadata, in form
GROUP:TAGNAME, from image. E.g. GROUP:TAGNAME, from image. E.g.
'{exiftool:EXIF:Make}' to get camera make, or '{exiftool:EXIF:Make}' to get camera make, or
{exiftool:IPTC:Keywords} to extract keywords. See {exiftool:IPTC:Keywords} to extract keywords. See
https://exiftool.org/TagNames/ for list of valid tag https://exiftool.org/TagNames/ for list of valid
names. You must specify group (e.g. EXIF, IPTC, etc) tag names. You must specify group (e.g. EXIF,
as used in `exiftool -G`. exiftool must be installed IPTC, etc) as used in `exiftool -G`. exiftool must
in the path to use this template. be installed in the path to use this template.
{searchinfo.holiday} Holiday names associated with a photo, e.g. {searchinfo.holiday} Holiday names associated with a photo, e.g.
'Christmas Day'; (Photos 5+ only, applied 'Christmas Day'; (Photos 5+ only, applied
automatically by Photos' image categorization automatically by Photos' image categorization
@@ -1838,22 +1855,24 @@ Substitution Description
Event'; (Photos 5+ only, applied automatically by Event'; (Photos 5+ only, applied automatically by
Photos' image categorization algorithms). Photos' image categorization algorithms).
{searchinfo.venue} Venues associated with a photo, e.g. name of {searchinfo.venue} Venues associated with a photo, e.g. name of
restaurant; (Photos 5+ only, applied automatically by restaurant; (Photos 5+ only, applied automatically
Photos' image categorization algorithms).
{searchinfo.venue_type} Venue types associated with a photo, e.g.
'Restaurant'; (Photos 5+ only, applied automatically
by Photos' image categorization algorithms). by Photos' image categorization algorithms).
{searchinfo.venue_type} Venue types associated with a photo, e.g.
'Restaurant'; (Photos 5+ only, applied
automatically by Photos' image categorization
algorithms).
{photo} Provides direct access to the PhotoInfo object for {photo} Provides direct access to the PhotoInfo object for
the photo. Must be used in format '{photo.property}' the photo. Must be used in format
where 'property' represents a PhotoInfo property. For '{photo.property}' where 'property' represents a
example: '{photo.favorite}' is the same as PhotoInfo property. For example: '{photo.favorite}'
'{favorite}' and '{photo.place.name}' is the same as is the same as '{favorite}' and
'{place.name}'. '{photo}' provides access to '{photo.place.name}' is the same as '{place.name}'.
properties that are not available as separate '{photo}' provides access to properties that are
template fields but it assumes some knowledge of the not available as separate template fields but it
underlying PhotoInfo class. See assumes some knowledge of the underlying PhotoInfo
https://rhettbull.github.io/osxphotos/ for additional class. See https://rhettbull.github.io/osxphotos/
documentation on the PhotoInfo class. for additional documentation on the PhotoInfo
class.
{detected_text} List of text strings found in the image after {detected_text} List of text strings found in the image after
performing text detection. Using '{detected_text}' performing text detection. Using '{detected_text}'
will cause osxphotos to perform text detection on will cause osxphotos to perform text detection on
@@ -1862,30 +1881,33 @@ Substitution Description
results for each photo will be cached in the export results for each photo will be cached in the export
database so that future exports with '--update' do database so that future exports with '--update' do
not need to reprocess each photo. You may pass a not need to reprocess each photo. You may pass a
confidence threshold value between 0.0 and 1.0 after confidence threshold value between 0.0 and 1.0
a colon as in '{detected_text:0.5}'; The default after a colon as in '{detected_text:0.5}'; The
confidence threshold is 0.75. '{detected_text}' works default confidence threshold is 0.75.
only on macOS Catalina (10.15) or later. Note: this '{detected_text}' works only on macOS Catalina
feature is not the same thing as Live Text in macOS (10.15) or later. Note: this feature is not the
Monterey, which osxphotos does not yet support. same thing as Live Text in macOS Monterey, which
osxphotos does not yet support.
{shell_quote} Use in form '{shell_quote,TEMPLATE}'; quotes the {shell_quote} Use in form '{shell_quote,TEMPLATE}'; quotes the
rendered TEMPLATE value(s) for safe usage in the rendered TEMPLATE value(s) for safe usage in the
shell, e.g. My file.jpeg => 'My file.jpeg'; only adds shell, e.g. My file.jpeg => 'My file.jpeg'; only
quotes if needed. adds quotes if needed.
{strip} Use in form '{strip,TEMPLATE}'; strips whitespace {strip} Use in form '{strip,TEMPLATE}'; strips whitespace
from begining and end of rendered TEMPLATE value(s). from begining and end of rendered TEMPLATE
value(s).
{function} Execute a python function from an external file and {function} Execute a python function from an external file and
use return value as template substitution. Use in use return value as template substitution. Use in
format: {function:file.py::function_name} where format: {function:file.py::function_name} where
'file.py' is the name of the python file and 'file.py' is the name of the python file and
'function_name' is the name of the function to call. 'function_name' is the name of the function to
The function will be passed the PhotoInfo object for call. The function will be passed the PhotoInfo
the photo. See https://github.com/RhetTbull/osxphotos object for the photo. See https://github.com/RhetTb
/blob/master/examples/template_function.py for an ull/osxphotos/blob/master/examples/template_functio
example of how to implement a template function. n.py for an example of how to implement a template
function.
The following substitutions are file or directory paths. You can access various The following substitutions are file or directory paths. You can access
parts of the path using the following modifiers: various parts of the path using the following modifiers:
{path.parent}: the parent directory {path.parent}: the parent directory
{path.name}: the name of the file or final sub-directory {path.name}: the name of the file or final sub-directory
@@ -1906,33 +1928,35 @@ Substitution Description
{filepath} The full path to the exported file {filepath} The full path to the exported file
** Post Command ** Post Command
You can run commands on the exported photos for post-processing using the '-- 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. post-command' option. '--post-command' is passed a CATEGORY and a COMMAND.
COMMAND is an osxphotos template string which will be rendered and passed to the COMMAND is an osxphotos template string which will be rendered and passed to
shell for execution. CATEGORY is the category of file to pass to COMMAND. The the shell for execution. CATEGORY is the category of file to pass to COMMAND.
following categories are available: The following categories are available:
Category Description Category Description
exported All exported files exported All exported files
new When used with '--update', all newly exported files new When used with '--update', all newly exported
files
updated When used with '--update', all files which were updated When used with '--update', all files which were
previously exported but updated this time previously exported but updated this time
skipped When used with '--update', all files which were skipped When used with '--update', all files which were
skipped (because they were previously exported and skipped (because they were previously exported and
didn't change) didn't change)
missing All files which were not exported because they were missing All files which were not exported because they
missing from the Photos library were missing from the Photos library
exif_updated When used with '--exiftool', all files on which exif_updated When used with '--exiftool', all files on which
exiftool updated the metadata exiftool updated the metadata
touched When used with '--touch-file', all files where the touched When used with '--touch-file', all files where the
date was touched date was touched
converted_to_jpeg When used with '--convert-to-jpeg', all files which converted_to_jpeg When used with '--convert-to-jpeg', all files
were converted to jpeg which were converted to jpeg
sidecar_json_written When used with '--sidecar json', all JSON sidecar sidecar_json_written When used with '--sidecar json', all JSON sidecar
files which were written files which were written
sidecar_json_skipped When used with '--sidecar json' and '--update', all sidecar_json_skipped When used with '--sidecar json' and '--update',
JSON sidecar files which were skipped all JSON sidecar files which were skipped
sidecar_exiftool_written When used with '--sidecar exiftool', all exiftool sidecar_exiftool_written When used with '--sidecar exiftool', all exiftool
sidecar files which were written sidecar files which were written
sidecar_exiftool_skipped When used with '--sidecar exiftool' and '--update, sidecar_exiftool_skipped When used with '--sidecar exiftool' and '--update,
@@ -1943,42 +1967,43 @@ sidecar_xmp_skipped When used with '--sidecar xmp' and '--update', all
XMP sidecar files which were skipped XMP sidecar files which were skipped
error All files which produced an error during export error All files which produced an error during export
In addition to all normal template fields, the template fields '{filepath}' and In addition to all normal template fields, the template fields '{filepath}'
'{export_dir}' will be available to your command template. Both of these are and '{export_dir}' will be available to your command template. Both of these
path-type templates which means their various parts can be accessed using the are path-type templates which means their various parts can be accessed using
available properties, e.g. '{filepath.name}' provides just the file name without the available properties, e.g. '{filepath.name}' provides just the file name
path and '{filepath.suffix}' is the file extension (suffix) of the file. When without path and '{filepath.suffix}' is the file extension (suffix) of the
using paths in your command template, it is important to properly quote the file. When using paths in your command template, it is important to properly
paths as they will be passed to the shell and path names may contain spaces. quote the paths as they will be passed to the shell and path names may contain
Both the '{shell_quote}' template and the '|shell_quote' template filter are spaces. Both the '{shell_quote}' template and the '|shell_quote' template
available for this purpose. For example, the following command outputs the full filter are available for this purpose. For example, the following command
path of newly exported files to file 'new.txt': outputs the full path of newly exported files to file 'new.txt':
--post-command new "echo {filepath|shell_quote} >> {shell_quote,{export_dir}/exported.txt}" --post-command new "echo {filepath|shell_quote} >> {shell_quote,{export_dir}/exported.txt}"
In the above command, the 'shell_quote' filter is used to ensure '{filepath}' is In the above command, the 'shell_quote' filter is used to ensure '{filepath}'
properly quoted and the '{shell_quote}' template ensures the constructed path of is properly quoted and the '{shell_quote}' template ensures the constructed
'{exported_dir}/exported.txt' is properly quoted. If '{filepath}' is 'IMG path of '{exported_dir}/exported.txt' is properly quoted. If '{filepath}' is
1234.jpeg' and '{export_dir}' is '/Volumes/Photo Export', the command thus 'IMG 1234.jpeg' and '{export_dir}' is '/Volumes/Photo Export', the command
renders to: thus renders to:
echo 'IMG 1234.jpeg' >> '/Volumes/Photo Export/exported.txt' echo 'IMG 1234.jpeg' >> '/Volumes/Photo Export/exported.txt'
It is highly recommended that you run osxphotos with '--dry-run --verbose' first It is highly recommended that you run osxphotos with '--dry-run --verbose'
to ensure your commands are as expected. This will not actually run the commands first to ensure your commands are as expected. This will not actually run the
but will print out the exact command string which would be executed. commands but will print out the exact command string which would be executed.
** Post Function ** Post Function
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 You can run your own python functions on the exported photos for post-
python file and the name of the function in the file to call using format processing using the '--post-function' option. '--post-function' is passed the
'filename.py::function_name'. See the example function at name a python file and the name of the function in the file to call using
https://github.com/RhetTbull/osxphotos/blob/master/examples/post_function.py You format 'filename.py::function_name'. See the example function at
may specify multiple functions to run by repeating the --post-function option. https://github.com/RhetTbull/osxphotos/blob/master/examples/post_function.py
All post functions will be called immediately after export of each photo and You may specify multiple functions to run by repeating the --post-function
immediately before any --post-command commands. Post functions will not be option. All post functions will be called immediately after export of each
called if the --dry-run flag is set. photo and immediately before any --post-command commands. Post functions will
not be called if the --dry-run flag is set.
@@ -3481,7 +3506,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. 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 `delim`: optional delimiter string to use when expanding multi-valued template values in-place

View File

@@ -6,6 +6,8 @@ import os.path
from datetime import datetime from datetime import datetime
from enum import Enum from enum import Enum
APP_NAME = "osxphotos"
OSXPHOTOS_URL = "https://github.com/RhetTbull/osxphotos" OSXPHOTOS_URL = "https://github.com/RhetTbull/osxphotos"
# Time delta: add this to Photos times to get unix time # 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 .query import query
from .repl import repl from .repl import repl
from .snap_diff import diff, snap from .snap_diff import diff, snap
from .theme import theme
from .tutorial import tutorial from .tutorial import tutorial
from .uuid import uuid from .uuid import uuid
@@ -77,6 +78,7 @@ for command in [
repl, repl,
run, run,
snap, snap,
theme,
tutorial, tutorial,
uninstall, uninstall,
uuid, uuid,

View File

@@ -3,7 +3,6 @@
import inspect import inspect
import os import os
import typing as t import typing as t
from io import StringIO
import click import click
from rich.console import Console from rich.console import Console
@@ -213,11 +212,9 @@ def rich_click_echo(
# otherwise tests fail # otherwise tests fail
temp_console = Console() temp_console = Console()
width = temp_console.width if temp_console.is_terminal else 10_000 width = temp_console.width if temp_console.is_terminal else 10_000
output = StringIO()
console = Console( console = Console(
force_terminal=True, force_terminal=True,
theme=theme or get_rich_theme(), theme=theme or get_rich_theme(),
file=output,
width=width, width=width,
) )
if markdown: if markdown:
@@ -227,8 +224,9 @@ def rich_click_echo(
global _timestamp global _timestamp
if _timestamp: if _timestamp:
message = time_stamp() + message message = time_stamp() + message
console.print(message, end=end, highlight=highlight, **kwargs) with console.capture() as capture:
click.echo(output.getvalue(), **echo_args) console.print(message, end=end, highlight=highlight, **kwargs)
click.echo(capture.get(), **echo_args)
def rich_echo_via_pager( def rich_echo_via_pager(
@@ -259,11 +257,9 @@ def rich_echo_via_pager(
except TypeError: except TypeError:
text_or_generator = [text_or_generator] text_or_generator = [text_or_generator]
console = _console or Console(theme=theme) console = _console.console or Console(theme=theme)
color = kwargs.pop("color", None) color = kwargs.pop("color", True)
if color is None:
color = bool(console.color_system)
with console.pager(styles=color): with console.pager(styles=color):
for x in text_or_generator: 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.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 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 = { COLOR_THEMES = {
"dark": Theme( "dark": Theme(
{ name="dark",
description="Dark mode theme",
tags=["dark"],
styles={
# color pallette from https://github.com/dracula/dracula-theme # color pallette from https://github.com/dracula/dracula-theme
"color": Style(color="rgb(248,248,242)"), "color": Style(color="rgb(248,248,242)"),
"count": Style(color="rgb(139,233,253)"), "count": Style(color="rgb(139,233,253)"),
@@ -32,10 +65,15 @@ COLOR_THEMES = {
"progress.elapsed": Style(color="rgb(139,233,253)"), "progress.elapsed": Style(color="rgb(139,233,253)"),
"progress.percentage": Style(color="rgb(255,121,198)"), "progress.percentage": Style(color="rgb(255,121,198)"),
"progress.remaining": Style(color="rgb(139,233,253)"), "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( "light": Theme(
{ name="light",
description="Light mode theme",
styles={
"color": Style(color="#000000"), "color": Style(color="#000000"),
"count": Style(color="#005cc5", bold=True), "count": Style(color="#005cc5", bold=True),
"error": Style(color="#b31d28", bold=True, underline=True, italic=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.elapsed": Style(color="#032f62", bold=True),
"progress.percentage": Style(color="#6f42c1", bold=True), "progress.percentage": Style(color="#6f42c1", bold=True),
"progress.remaining": Style(color="#032f62", 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( "mono": Theme(
{ name="mono",
description="Monochromatic theme",
tags=["mono", "colorblind"],
styles={
"count": "bold", "count": "bold",
"error": "reverse italic", "error": "reverse italic",
"filename": "bold", "filename": "bold",
@@ -73,10 +117,16 @@ COLOR_THEMES = {
"progress.elapsed": "", "progress.elapsed": "",
"progress.percentage": "bold", "progress.percentage": "bold",
"progress.remaining": "bold", "progress.remaining": "bold",
} # "headers": "bold",
# "options": "bold",
# "metavar": "bold",
},
), ),
"plain": Theme( "plain": Theme(
{ name="plain",
description="Plain theme with no colors",
tags=["colorblind"],
styles={
"color": "", "color": "",
"count": "", "count": "",
"error": "", "error": "",
@@ -94,31 +144,51 @@ COLOR_THEMES = {
"progress.elapsed": "", "progress.elapsed": "",
"progress.percentage": "", "progress.percentage": "",
"progress.remaining": "", "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( def get_theme(
theme_name: Optional[str] = None, 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""" """Get theme by name, or default theme if no name is provided"""
if not verbose:
verbose = noop if theme_name is None:
# figure out which color theme to use return get_default_theme()
theme_name = theme_name or "default"
if theme_name == "default" and theme_file and theme_file.is_file(): theme_manager = get_theme_manager()
# load theme from file try:
verbose(f"Loading color theme from {theme_file}") return theme_manager.get(theme_name)
try: except ValueError as e:
theme = Theme.read(theme_file) raise click.ClickException(
except Exception as e: f"Theme '{theme_name}' not found. "
raise ValueError(f"Error reading theme file {theme_file}: {e}") f"Available themes: {', '.join(t.name for t in theme_manager.themes)}"
elif theme_name == "default": ) from e
# try to auto-detect dark/light mode
theme = COLOR_THEMES["dark"] if is_dark_mode() else COLOR_THEMES["light"]
else: def get_default_theme():
theme = COLOR_THEMES[theme_name] """Get the default color theme"""
return 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 click
import osxphotos import osxphotos
from osxphotos._constants import APP_NAME
from osxphotos._version import __version__ from osxphotos._version import __version__
from .param_types import * from .param_types import *
@@ -33,6 +34,7 @@ __all__ = [
"DELETED_OPTIONS", "DELETED_OPTIONS",
"JSON_OPTION", "JSON_OPTION",
"QUERY_OPTIONS", "QUERY_OPTIONS",
"THEME_OPTION",
"get_photos_db", "get_photos_db",
"load_uuid_from_file", "load_uuid_from_file",
"noop", "noop",
@@ -499,6 +501,16 @@ def DEBUG_OPTIONS(f):
return 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): def load_uuid_from_file(filename):
"""Load UUIDs from file. Does not validate UUIDs. """Load UUIDs from file. Does not validate UUIDs.
Format is 1 UUID per line, any line beginning with # is ignored. 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] != "#": if len(line) and line[0] != "#":
uuid.append(line) uuid.append(line)
return uuid 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_CRASH_LOG,
OSXPHOTOS_HIDDEN, OSXPHOTOS_HIDDEN,
QUERY_OPTIONS, QUERY_OPTIONS,
THEME_OPTION,
get_photos_db, get_photos_db,
load_uuid_from_file, load_uuid_from_file,
noop, 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}. " f"Can be specified multiple times. Valid options are: {PROFILE_SORT_KEYS}. "
"Default = 'cumulative'.", "Default = 'cumulative'.",
) )
@click.option( @THEME_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.",
)
@DEBUG_OPTIONS @DEBUG_OPTIONS
@DB_ARGUMENT @DB_ARGUMENT
@click.argument("dest", nargs=1, type=click.Path(exists=True)) @click.argument("dest", nargs=1, type=click.Path(exists=True))

View File

@@ -1,7 +1,6 @@
"""Help text helper class for osxphotos CLI """ """Help text helper class for osxphotos CLI """
import inspect import inspect
import io
import re import re
import typing as t import typing as t
@@ -23,8 +22,12 @@ from osxphotos.phototemplate import (
get_template_help, 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 .color_themes import get_theme
from .common import OSXPHOTOS_HIDDEN
HELP_WIDTH = 110
HIGHLIGHT_COLOR = "yellow"
__all__ = [ __all__ = [
"ExportCommand", "ExportCommand",
@@ -37,8 +40,6 @@ __all__ = [
"get_help_msg", "get_help_msg",
] ]
HIGHLIGHT_COLOR = "yellow"
def get_help_msg(command): def get_help_msg(command):
"""get help message for a Click command""" """get help message for a Click command"""
@@ -47,22 +48,50 @@ def get_help_msg(command):
@click.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("topic", default=None, required=False, nargs=1)
@click.argument("subtopic", default=None, required=False, nargs=1) @click.argument("subtopic", default=None, required=False, nargs=1)
@click.pass_context @click.pass_context
def help(ctx, topic, subtopic, **kw): def help(ctx, topic, subtopic, width, **kw):
"""Print help; for help on commands: help <command>.""" """Print help; for help on commands: help <command>."""
if topic is None: if topic is None:
click.echo(ctx.parent.get_help()) click.echo(ctx.parent.get_help())
return 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: if subtopic:
cmd = ctx.obj.group.commands[topic] cmd = ctx.obj.group.commands[topic]
theme = get_theme("light") rich_echo_via_pager(
rich_echo(
get_subtopic_help(cmd, ctx, subtopic), get_subtopic_help(cmd, ctx, subtopic),
theme=theme, theme=get_theme(),
width=click.HelpFormatter().width, width=HELP_WIDTH,
) )
return return
@@ -90,7 +119,7 @@ def get_subtopic_help(cmd: click.Command, ctx: click.Context, subtopic: str):
options = get_matching_options(cmd, ctx, subtopic) options = get_matching_options(cmd, ctx, subtopic)
# format help text and options # format help text and options
formatter = click.HelpFormatter() formatter = click.HelpFormatter(width=HELP_WIDTH)
formatter.write(usage_str) formatter.write(usage_str)
formatter.write_paragraph() formatter.write_paragraph()
format_help_text(help_str, formatter) format_help_text(help_str, formatter)
@@ -142,7 +171,7 @@ def format_options_help(
str with formatted 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] opt_help = [opt.get_help_record(ctx) for opt in options]
if highlight: if highlight:
# convert list of tuples to list of lists # convert list of tuples to list of lists
@@ -182,11 +211,9 @@ class ExportCommand(click.Command):
def get_help(self, ctx): def get_help(self, ctx):
help_text = super().get_help(ctx) help_text = super().get_help(ctx)
formatter = click.HelpFormatter() formatter = click.HelpFormatter(width=HELP_WIDTH)
# passed to click.HelpFormatter.write_dl for formatting formatter.write("\n")
formatter.write(rich_text("## Export", width=formatter.width, markdown=True))
formatter.write("\n\n")
formatter.write(rich_text("[bold]** Export **[/bold]", width=formatter.width))
formatter.write("\n") formatter.write("\n")
formatter.write_text( formatter.write_text(
"When exporting photos, osxphotos creates a database in the top-level " "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 " + "You can always run export without the --update option to re-export the entire library thus "
+ f"rebuilding the '{OSXPHOTOS_EXPORT_DB}' database." + f"rebuilding the '{OSXPHOTOS_EXPORT_DB}' database."
) )
formatter.write("\n\n") formatter.write("\n")
formatter.write( 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("\n")
formatter.write_text( 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, attr,
f"{osxmetadata.ATTRIBUTES[attr].help} ({osxmetadata.ATTRIBUTES[attr].constant})", f"{osxmetadata.ATTRIBUTES[attr].help} ({osxmetadata.ATTRIBUTES[attr].constant})",
) )
for attr in EXTENDED_ATTRIBUTE_NAMES for attr in EXTENDED_ATTRIBUTE_NAMES
] ],
) ]
formatter.write_dl(attr_tuples)
formatter.write("\n") formatter.write("\n")
formatter.write_text( formatter.write_text(
"For additional information on extended attributes see: https://developer.apple.com/documentation/coreservices/file_metadata/mditem/common_metadata_attribute_keys" "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( 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("\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("\n")
formatter.write_text( formatter.write_text(
"With the --directory and --filename options you may specify a template for the " "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("\n")
formatter.write( formatter.write(
rich_text( rich_text("## Template Substitutions", width=formatter.width, markdown=True)
"[bold]** Template Substitutions **[/bold]", width=formatter.width
)
) )
formatter.write("\n") 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()) templ_tuples.extend((k, v) for k, v in TEMPLATE_SUBSTITUTIONS.items())
formatter.write_dl(templ_tuples) formatter.write_dl(templ_tuples)
@@ -310,7 +348,12 @@ The following attributes may be used with '--xattr-template':
+ "2019/Vacation, 2019/Family" + "2019/Vacation, 2019/Family"
) )
formatter.write("\n") 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( templ_tuples.extend(
(k, v) for k, v in TEMPLATE_SUBSTITUTIONS_MULTI_VALUED.items() (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_dl(templ_tuples)
formatter.write("\n\n") formatter.write("\n")
formatter.write( 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( formatter.write_text(
"You can run commands on the exported photos for post-processing " "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. " + "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 " + "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." + "print out the exact command string which would be executed."
) )
formatter.write("\n\n") formatter.write("\n")
formatter.write( 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( formatter.write_text(
"You can run your own python functions on the exported photos for post-processing " "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 " + "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): def template_help(width=78):
"""Return formatted string for template system""" """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()) template_help_md = strip_md_header_and_links(get_template_help())
console.print(Markdown(template_help_md)) console = Console(force_terminal=True, width=width)
help_str = sio.getvalue() with console.capture() as capture:
sio.close() console.print(Markdown(template_help_md))
return help_str return capture.get()
def rich_text(text, width=78): def rich_text(text, width=78, markdown=False):
"""Return rich formatted text""" """Return rich formatted text"""
sio = io.StringIO() console = Console(force_terminal=True, width=width)
console = Console(file=sio, force_terminal=True, width=width) with console.capture() as capture:
console.print(text) console.print(Markdown(text) if markdown else text, end="")
rich_text = sio.getvalue() return capture.get()
sio.close()
return rich_text
def strip_md_header_and_links(md): 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. 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 `delim`: optional delimiter string to use when expanding multi-valued template values in-place

View File

@@ -355,7 +355,6 @@ Another example: if you had `exiftool` installed and wanted to wipe all metadata
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 ### An example from an actual osxphotos user
Here's a comprehensive use case from an actual osxphotos user that integrates many of the concepts discussed in this tutorial (thank-you Philippe for contributing this!): Here's a comprehensive use case from an actual osxphotos user that integrates many of the concepts discussed in this tutorial (thank-you Philippe for contributing this!):
@@ -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` `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 ### 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,6 +20,7 @@ pyobjc-framework-Quartz>=7.3,<9.0
pyobjc-framework-Vision>=7.3,<9.0 pyobjc-framework-Vision>=7.3,<9.0
PyYAML>=5.4.1,<6.0.0 PyYAML>=5.4.1,<6.0.0
rich>=11.2.0,<12.0.0 rich>=11.2.0,<12.0.0
rich_theme_manager>=0.7.0
textx>=2.3.0,<2.4.0 textx>=2.3.0,<2.4.0
toml>=0.10.2,<0.11.0 toml>=0.10.2,<0.11.0
wrapt>=1.13.3,<1.14.0 wrapt>=1.13.3,<1.14.0

View File

@@ -95,6 +95,7 @@ setup(
"pyobjc-framework-Quartz>=7.3,<9.0", "pyobjc-framework-Quartz>=7.3,<9.0",
"pyobjc-framework-Vision>=7.3,<9.0", "pyobjc-framework-Vision>=7.3,<9.0",
"rich>=11.2.0,<12.0.0", "rich>=11.2.0,<12.0.0",
"rich_theme_manager>=0.7.0",
"textx>=2.3.0,<3.0.0", "textx>=2.3.0,<3.0.0",
"toml>=0.10.2,<0.11.0", "toml>=0.10.2,<0.11.0",
"wrapt>=1.13.3,<1.14.0", "wrapt>=1.13.3,<1.14.0",

View File

@@ -73,11 +73,11 @@ def generate_help_text(command):
# get current help text # get current help text
with runner.isolated_filesystem(): with runner.isolated_filesystem():
result = runner.invoke(cli_main, ["help", command]) result = runner.invoke(cli_main, ["help", command, "--width", 78])
help_txt = result.output help_txt = result.output
# running the help command above doesn't output the full "Usage" line # 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 return help_txt