Theme (#664)
* Initial theme manager, not yet done * Added rich_theme_manager * Updated rich-theme-manager * Switched to rich_theme_manager for theme management * Updated dependencies * Added rich paging to subtopic help * Fixed clone to clone only styles specified in cloned theme * Added placeholder for help colors * Updated config dir, help methods
This commit is contained in:
623
README.md
623
README.md
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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")
|
||||||
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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
132
osxphotos/cli/theme.py
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
"""theme command for osxphotos for managing color themes"""
|
||||||
|
|
||||||
|
import pathlib
|
||||||
|
|
||||||
|
import click
|
||||||
|
from rich.console import Console
|
||||||
|
from rich_theme_manager import Theme
|
||||||
|
|
||||||
|
from .click_rich_echo import rich_click_echo
|
||||||
|
from .color_themes import get_default_theme, get_theme, get_theme_dir, get_theme_manager
|
||||||
|
from .help import get_help_msg
|
||||||
|
|
||||||
|
|
||||||
|
@click.command(name="theme")
|
||||||
|
@click.pass_obj
|
||||||
|
@click.pass_context
|
||||||
|
@click.option("--default", is_flag=True, help="Show default theme.")
|
||||||
|
@click.option("--list", "list_", is_flag=True, help="List all themes.")
|
||||||
|
@click.option(
|
||||||
|
"--config",
|
||||||
|
metavar="[THEME]",
|
||||||
|
is_flag=False,
|
||||||
|
flag_value="_DEFAULT_",
|
||||||
|
default=None,
|
||||||
|
help="Print configuration for THEME (or default theme if not specified).",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--preview",
|
||||||
|
metavar="[THEME]",
|
||||||
|
is_flag=False,
|
||||||
|
flag_value="_DEFAULT_",
|
||||||
|
default=None,
|
||||||
|
help="Preview THEME (or default theme if not specified).",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--edit",
|
||||||
|
metavar="[THEME]",
|
||||||
|
is_flag=False,
|
||||||
|
flag_value="_DEFAULT_",
|
||||||
|
default=None,
|
||||||
|
help="Edit THEME (or default theme if not specified).",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--clone",
|
||||||
|
metavar="THEME NEW_THEME",
|
||||||
|
nargs=2,
|
||||||
|
type=str,
|
||||||
|
help="Clone THEME to NEW_THEME.",
|
||||||
|
)
|
||||||
|
@click.option("--delete", metavar="THEME", help="Delete THEME.")
|
||||||
|
def theme(ctx, cli_obj, default, list_, config, preview, edit, clone, delete):
|
||||||
|
"""Manage osxphotos color themes."""
|
||||||
|
|
||||||
|
subcommands = [default, list_, config, preview, edit, clone, delete]
|
||||||
|
subcommand_names = (
|
||||||
|
"--default, --list, --config, --preview, --edit, --clone, --delete"
|
||||||
|
)
|
||||||
|
if not any(subcommands):
|
||||||
|
click.echo(
|
||||||
|
f"Must specify exactly one of: {subcommand_names}\n",
|
||||||
|
err=True,
|
||||||
|
)
|
||||||
|
rich_click_echo(get_help_msg(theme), err=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
if sum(bool(cmd) for cmd in subcommands) != 1:
|
||||||
|
# only a single subcommand may be specified
|
||||||
|
raise click.ClickException(f"Must specify exactly one of: {subcommand_names}")
|
||||||
|
|
||||||
|
theme_manager = get_theme_manager()
|
||||||
|
console = Console(theme=get_default_theme())
|
||||||
|
|
||||||
|
if default:
|
||||||
|
default = get_default_theme()
|
||||||
|
theme_manager.list_themes(theme_names=[default.name])
|
||||||
|
return
|
||||||
|
|
||||||
|
if list_:
|
||||||
|
theme_manager.list_themes()
|
||||||
|
return
|
||||||
|
|
||||||
|
if config:
|
||||||
|
if config == "_DEFAULT_":
|
||||||
|
print(get_default_theme().config)
|
||||||
|
else:
|
||||||
|
print(get_theme(config).config)
|
||||||
|
return
|
||||||
|
|
||||||
|
if preview:
|
||||||
|
theme_ = get_default_theme() if preview == "_DEFAULT_" else get_theme(preview)
|
||||||
|
theme_manager.preview_theme(theme_)
|
||||||
|
return
|
||||||
|
|
||||||
|
if edit:
|
||||||
|
theme_ = get_default_theme() if edit == "_DEFAULT_" else get_theme(edit)
|
||||||
|
config_file = pathlib.Path(theme_.path)
|
||||||
|
console.print(f"Opening [filepath]{config_file}[/] in $EDITOR")
|
||||||
|
click.edit(filename=str(config_file))
|
||||||
|
return
|
||||||
|
|
||||||
|
if clone:
|
||||||
|
src_theme = get_theme(clone[0])
|
||||||
|
dest_path = get_theme_dir() / f"{clone[1]}.theme"
|
||||||
|
if dest_path.exists():
|
||||||
|
raise click.ClickException(
|
||||||
|
f"Theme '{clone[1]}' already exists at {dest_path}"
|
||||||
|
)
|
||||||
|
dest_theme = Theme(
|
||||||
|
name=clone[1],
|
||||||
|
description=src_theme.description,
|
||||||
|
inherit=src_theme.inherit,
|
||||||
|
tags=src_theme.tags,
|
||||||
|
styles={
|
||||||
|
style_name: src_theme.styles[style_name]
|
||||||
|
for style_name in src_theme.style_names
|
||||||
|
},
|
||||||
|
)
|
||||||
|
theme_manager = get_theme_manager()
|
||||||
|
theme_manager.add(dest_theme)
|
||||||
|
theme_ = get_theme(dest_theme.name)
|
||||||
|
console.print(
|
||||||
|
f"Cloned theme '[filename]{clone[0]}[/]' to '[filename]{clone[1]}[/]' "
|
||||||
|
f"at [filepath]{theme_.path}[/]"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if delete:
|
||||||
|
theme_ = get_theme(delete)
|
||||||
|
click.confirm(f"Are you sure you want to delete theme {delete}?", abort=True)
|
||||||
|
theme_manager.remove(theme_)
|
||||||
|
console.print(f"Deleted theme [filepath]{theme_.path}[/]")
|
||||||
|
return
|
||||||
@@ -8,7 +8,7 @@ Template statements may contain one or more modifiers. The full syntax is:
|
|||||||
|
|
||||||
Template statements are white-space sensitive meaning that white space (spaces, tabs) changes the meaning of the template statement.
|
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
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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
|
||||||
|
|||||||
1
setup.py
1
setup.py
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user