* Initial theme manager, not yet done

* Added rich_theme_manager

* Updated rich-theme-manager

* Switched to rich_theme_manager for theme management

* Updated dependencies

* Added rich paging to subtopic help

* Fixed clone to clone only styles specified in cloned theme

* Added placeholder for help colors

* Updated config dir, help methods
This commit is contained in:
Rhet Turnbull
2022-04-17 23:53:42 -06:00
committed by GitHub
parent 9c0b910046
commit 6c57fb2df9
14 changed files with 801 additions and 514 deletions

623
README.md
View File

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

View File

@@ -6,6 +6,8 @@ import os.path
from datetime import datetime
from enum import Enum
APP_NAME = "osxphotos"
OSXPHOTOS_URL = "https://github.com/RhetTbull/osxphotos"
# Time delta: add this to Photos times to get unix time

View File

@@ -24,6 +24,7 @@ from .places import places
from .query import query
from .repl import repl
from .snap_diff import diff, snap
from .theme import theme
from .tutorial import tutorial
from .uuid import uuid
@@ -77,6 +78,7 @@ for command in [
repl,
run,
snap,
theme,
tutorial,
uninstall,
uuid,

View File

@@ -3,7 +3,6 @@
import inspect
import os
import typing as t
from io import StringIO
import click
from rich.console import Console
@@ -213,11 +212,9 @@ def rich_click_echo(
# otherwise tests fail
temp_console = Console()
width = temp_console.width if temp_console.is_terminal else 10_000
output = StringIO()
console = Console(
force_terminal=True,
theme=theme or get_rich_theme(),
file=output,
width=width,
)
if markdown:
@@ -227,8 +224,9 @@ def rich_click_echo(
global _timestamp
if _timestamp:
message = time_stamp() + message
console.print(message, end=end, highlight=highlight, **kwargs)
click.echo(output.getvalue(), **echo_args)
with console.capture() as capture:
console.print(message, end=end, highlight=highlight, **kwargs)
click.echo(capture.get(), **echo_args)
def rich_echo_via_pager(
@@ -259,11 +257,9 @@ def rich_echo_via_pager(
except TypeError:
text_or_generator = [text_or_generator]
console = _console or Console(theme=theme)
console = _console.console or Console(theme=theme)
color = kwargs.pop("color", None)
if color is None:
color = bool(console.color_system)
color = kwargs.pop("color", True)
with console.pager(styles=color):
for x in text_or_generator:

View File

@@ -1,19 +1,52 @@
"""Support for colorized output for photos_time_warp"""
"""Support for colorized output for osxphotos cli using rich"""
from typing import Optional
import pathlib
from typing import List, Optional
import click
from rich.style import Style
from rich.themes import Theme
from rich_theme_manager import Theme, ThemeManager
from .common import noop
from .common import get_config_dir, noop
from .darkmode import is_dark_mode
__all__ = ["get_theme"]
DEFAULT_THEME_NAME = "default"
__all__ = [
"get_default_theme",
"get_theme",
"get_theme_dir",
"get_theme_manager",
DEFAULT_THEME_NAME,
]
THEME_STYLES = [
"color",
"count",
"error",
"filename",
"filepath",
"highlight",
"num",
"time",
"uuid",
"warning",
"bar.back",
"bar.complete",
"bar.finished",
"bar.pulse",
"progress.elapsed",
"progress.percentage",
"progress.remaining",
]
COLOR_THEMES = {
"dark": Theme(
{
name="dark",
description="Dark mode theme",
tags=["dark"],
styles={
# color pallette from https://github.com/dracula/dracula-theme
"color": Style(color="rgb(248,248,242)"),
"count": Style(color="rgb(139,233,253)"),
@@ -32,10 +65,15 @@ COLOR_THEMES = {
"progress.elapsed": Style(color="rgb(139,233,253)"),
"progress.percentage": Style(color="rgb(255,121,198)"),
"progress.remaining": Style(color="rgb(139,233,253)"),
}
# "headers": Style(color="rgb(165,194,97)"),
# "options": Style(color="rgb(255,198,109)"),
# "metavar": Style(color="rgb(12,125,157)"),
},
),
"light": Theme(
{
name="light",
description="Light mode theme",
styles={
"color": Style(color="#000000"),
"count": Style(color="#005cc5", bold=True),
"error": Style(color="#b31d28", bold=True, underline=True, italic=True),
@@ -53,10 +91,16 @@ COLOR_THEMES = {
"progress.elapsed": Style(color="#032f62", bold=True),
"progress.percentage": Style(color="#6f42c1", bold=True),
"progress.remaining": Style(color="#032f62", bold=True),
}
# "headers": Style(color="rgb(254,212,66)"),
# "options": Style(color="rgb(227,98,9)"),
# "metavar": Style(color="rgb(111,66,193)"),
},
),
"mono": Theme(
{
name="mono",
description="Monochromatic theme",
tags=["mono", "colorblind"],
styles={
"count": "bold",
"error": "reverse italic",
"filename": "bold",
@@ -73,10 +117,16 @@ COLOR_THEMES = {
"progress.elapsed": "",
"progress.percentage": "bold",
"progress.remaining": "bold",
}
# "headers": "bold",
# "options": "bold",
# "metavar": "bold",
},
),
"plain": Theme(
{
name="plain",
description="Plain theme with no colors",
tags=["colorblind"],
styles={
"color": "",
"count": "",
"error": "",
@@ -94,31 +144,51 @@ COLOR_THEMES = {
"progress.elapsed": "",
"progress.percentage": "",
"progress.remaining": "",
}
# "headers": "",
# "options": "",
# "metavar": "",
},
),
}
def get_theme_dir() -> pathlib.Path:
"""Return the theme config dir, creating it if necessary"""
theme_dir = get_config_dir() / "themes"
if not theme_dir.exists():
theme_dir.mkdir()
return theme_dir
def get_theme_manager() -> ThemeManager:
"""Return theme manager instance"""
return ThemeManager(theme_dir=str(get_theme_dir()), themes=COLOR_THEMES.values())
def get_theme(
theme_name: Optional[str] = None,
theme_file: Optional[str] = None,
verbose=None,
):
"""Get the color theme based on the color flags or load from config file"""
if not verbose:
verbose = noop
# figure out which color theme to use
theme_name = theme_name or "default"
if theme_name == "default" and theme_file and theme_file.is_file():
# load theme from file
verbose(f"Loading color theme from {theme_file}")
try:
theme = Theme.read(theme_file)
except Exception as e:
raise ValueError(f"Error reading theme file {theme_file}: {e}")
elif theme_name == "default":
# try to auto-detect dark/light mode
theme = COLOR_THEMES["dark"] if is_dark_mode() else COLOR_THEMES["light"]
else:
theme = COLOR_THEMES[theme_name]
return theme
"""Get theme by name, or default theme if no name is provided"""
if theme_name is None:
return get_default_theme()
theme_manager = get_theme_manager()
try:
return theme_manager.get(theme_name)
except ValueError as e:
raise click.ClickException(
f"Theme '{theme_name}' not found. "
f"Available themes: {', '.join(t.name for t in theme_manager.themes)}"
) from e
def get_default_theme():
"""Get the default color theme"""
theme_manager = get_theme_manager()
try:
return theme_manager.get(DEFAULT_THEME_NAME)
except ValueError:
return (
theme_manager.get("dark") if is_dark_mode() else theme_manager.get("light")
)

View File

@@ -8,6 +8,7 @@ from datetime import datetime
import click
import osxphotos
from osxphotos._constants import APP_NAME
from osxphotos._version import __version__
from .param_types import *
@@ -33,6 +34,7 @@ __all__ = [
"DELETED_OPTIONS",
"JSON_OPTION",
"QUERY_OPTIONS",
"THEME_OPTION",
"get_photos_db",
"load_uuid_from_file",
"noop",
@@ -499,6 +501,16 @@ def DEBUG_OPTIONS(f):
return f
THEME_OPTION = click.option(
"--theme",
metavar="THEME",
type=click.Choice(["dark", "light", "mono", "plain"], case_sensitive=False),
help="Specify the color theme to use for --verbose output. "
"Valid themes are 'dark', 'light', 'mono', and 'plain'. "
"Defaults to 'dark' or 'light' depending on system dark mode setting.",
)
def load_uuid_from_file(filename):
"""Load UUIDs from file. Does not validate UUIDs.
Format is 1 UUID per line, any line beginning with # is ignored.
@@ -524,3 +536,11 @@ def load_uuid_from_file(filename):
if len(line) and line[0] != "#":
uuid.append(line)
return uuid
def get_config_dir() -> pathlib.Path:
"""Get the directory where config files are stored."""
config_dir = pathlib.Path.home() / ".config" / APP_NAME
if not config_dir.is_dir():
config_dir.mkdir(parents=True)
return config_dir

View File

@@ -77,6 +77,7 @@ from .common import (
OSXPHOTOS_CRASH_LOG,
OSXPHOTOS_HIDDEN,
QUERY_OPTIONS,
THEME_OPTION,
get_photos_db,
load_uuid_from_file,
noop,
@@ -642,14 +643,7 @@ from .verbose import get_verbose_console, time_stamp, verbose_print
f"Can be specified multiple times. Valid options are: {PROFILE_SORT_KEYS}. "
"Default = 'cumulative'.",
)
@click.option(
"--theme",
metavar="THEME",
type=click.Choice(["dark", "light", "mono", "plain"], case_sensitive=False),
help="Specify the color theme to use for --verbose output. "
"Valid themes are 'dark', 'light', 'mono', and 'plain'. "
"Defaults to 'dark' or 'light' depending on system dark mode setting.",
)
@THEME_OPTION
@DEBUG_OPTIONS
@DB_ARGUMENT
@click.argument("dest", nargs=1, type=click.Path(exists=True))

View File

@@ -1,7 +1,6 @@
"""Help text helper class for osxphotos CLI """
import inspect
import io
import re
import typing as t
@@ -23,8 +22,12 @@ from osxphotos.phototemplate import (
get_template_help,
)
from .click_rich_echo import rich_echo
from .click_rich_echo import rich_echo_via_pager
from .color_themes import get_theme
from .common import OSXPHOTOS_HIDDEN
HELP_WIDTH = 110
HIGHLIGHT_COLOR = "yellow"
__all__ = [
"ExportCommand",
@@ -37,8 +40,6 @@ __all__ = [
"get_help_msg",
]
HIGHLIGHT_COLOR = "yellow"
def get_help_msg(command):
"""get help message for a Click command"""
@@ -47,22 +48,50 @@ def get_help_msg(command):
@click.command()
@click.option(
"--width",
default=HELP_WIDTH,
help="Width of help text",
hidden=OSXPHOTOS_HIDDEN,
)
@click.argument("topic", default=None, required=False, nargs=1)
@click.argument("subtopic", default=None, required=False, nargs=1)
@click.pass_context
def help(ctx, topic, subtopic, **kw):
def help(ctx, topic, subtopic, width, **kw):
"""Print help; for help on commands: help <command>."""
if topic is None:
click.echo(ctx.parent.get_help())
return
global HELP_WIDTH
HELP_WIDTH = width
wrap_text_original = click.formatting.wrap_text
def wrap_text(
text: str,
width: int = HELP_WIDTH,
initial_indent: str = "",
subsequent_indent: str = "",
preserve_paragraphs: bool = False,
) -> str:
return wrap_text_original(
text,
width=width,
initial_indent=initial_indent,
subsequent_indent=subsequent_indent,
preserve_paragraphs=preserve_paragraphs,
)
click.formatting.wrap_text = wrap_text
click.wrap_text = wrap_text
if subtopic:
cmd = ctx.obj.group.commands[topic]
theme = get_theme("light")
rich_echo(
rich_echo_via_pager(
get_subtopic_help(cmd, ctx, subtopic),
theme=theme,
width=click.HelpFormatter().width,
theme=get_theme(),
width=HELP_WIDTH,
)
return
@@ -90,7 +119,7 @@ def get_subtopic_help(cmd: click.Command, ctx: click.Context, subtopic: str):
options = get_matching_options(cmd, ctx, subtopic)
# format help text and options
formatter = click.HelpFormatter()
formatter = click.HelpFormatter(width=HELP_WIDTH)
formatter.write(usage_str)
formatter.write_paragraph()
format_help_text(help_str, formatter)
@@ -142,7 +171,7 @@ def format_options_help(
str with formatted help
"""
formatter = click.HelpFormatter()
formatter = click.HelpFormatter(width=HELP_WIDTH)
opt_help = [opt.get_help_record(ctx) for opt in options]
if highlight:
# convert list of tuples to list of lists
@@ -182,11 +211,9 @@ class ExportCommand(click.Command):
def get_help(self, ctx):
help_text = super().get_help(ctx)
formatter = click.HelpFormatter()
# passed to click.HelpFormatter.write_dl for formatting
formatter.write("\n\n")
formatter.write(rich_text("[bold]** Export **[/bold]", width=formatter.width))
formatter = click.HelpFormatter(width=HELP_WIDTH)
formatter.write("\n")
formatter.write(rich_text("## Export", width=formatter.width, markdown=True))
formatter.write("\n")
formatter.write_text(
"When exporting photos, osxphotos creates a database in the top-level "
@@ -221,9 +248,9 @@ class ExportCommand(click.Command):
+ "You can always run export without the --update option to re-export the entire library thus "
+ f"rebuilding the '{OSXPHOTOS_EXPORT_DB}' database."
)
formatter.write("\n\n")
formatter.write("\n")
formatter.write(
rich_text("[bold]** Extended Attributes **[/bold]", width=formatter.width)
rich_text("## Extended Attributes", width=formatter.width, markdown=True)
)
formatter.write("\n")
formatter.write_text(
@@ -244,25 +271,33 @@ The following attributes may be used with '--xattr-template':
"""
)
formatter.write_dl(
[
attr_tuples = [
(
rich_text("[bold]Attribute[/bold]", width=formatter.width),
rich_text("[bold]Description[/bold]", width=formatter.width),
),
*[
(
attr,
f"{osxmetadata.ATTRIBUTES[attr].help} ({osxmetadata.ATTRIBUTES[attr].constant})",
)
for attr in EXTENDED_ATTRIBUTE_NAMES
]
)
],
]
formatter.write_dl(attr_tuples)
formatter.write("\n")
formatter.write_text(
"For additional information on extended attributes see: https://developer.apple.com/documentation/coreservices/file_metadata/mditem/common_metadata_attribute_keys"
)
formatter.write("\n\n")
formatter.write("\n")
formatter.write(
rich_text("[bold]** Templating System **[/bold]", width=formatter.width)
rich_text("## Templating System", width=formatter.width, markdown=True)
)
formatter.write("\n")
formatter.write(template_help(width=formatter.width))
help_text += formatter.getvalue()
help_text += template_help(width=formatter.width)
formatter = click.HelpFormatter(width=HELP_WIDTH)
formatter.write("\n")
formatter.write_text(
"With the --directory and --filename options you may specify a template for the "
@@ -290,12 +325,15 @@ The following attributes may be used with '--xattr-template':
)
formatter.write("\n")
formatter.write(
rich_text(
"[bold]** Template Substitutions **[/bold]", width=formatter.width
)
rich_text("## Template Substitutions", width=formatter.width, markdown=True)
)
formatter.write("\n")
templ_tuples = [("Substitution", "Description")]
templ_tuples = [
(
rich_text("[bold]Substitution[/bold]", width=formatter.width),
rich_text("[bold]Description[/bold]", width=formatter.width),
)
]
templ_tuples.extend((k, v) for k, v in TEMPLATE_SUBSTITUTIONS.items())
formatter.write_dl(templ_tuples)
@@ -310,7 +348,12 @@ The following attributes may be used with '--xattr-template':
+ "2019/Vacation, 2019/Family"
)
formatter.write("\n")
templ_tuples = [("Substitution", "Description")]
templ_tuples = [
(
rich_text("[bold]Substitution[/bold]", width=formatter.width),
rich_text("[bold]Description[/bold]", width=formatter.width),
)
]
templ_tuples.extend(
(k, v) for k, v in TEMPLATE_SUBSTITUTIONS_MULTI_VALUED.items()
)
@@ -348,10 +391,11 @@ The following attributes may be used with '--xattr-template':
formatter.write_dl(templ_tuples)
formatter.write("\n\n")
formatter.write("\n")
formatter.write(
rich_text("[bold]** Post Command **[/bold]", width=formatter.width)
rich_text("## Post Command", width=formatter.width, markdown=True)
)
formatter.write("\n")
formatter.write_text(
"You can run commands on the exported photos for post-processing "
+ "using the '--post-command' option. '--post-command' is passed a CATEGORY and a COMMAND. "
@@ -394,10 +438,11 @@ The following attributes may be used with '--xattr-template':
+ "first to ensure your commands are as expected. This will not actually run the commands but will "
+ "print out the exact command string which would be executed."
)
formatter.write("\n\n")
formatter.write("\n")
formatter.write(
rich_text("[bold]** Post Function **[/bold]", width=formatter.width)
rich_text("## Post Function", width=formatter.width, markdown=True)
)
formatter.write("\n")
formatter.write_text(
"You can run your own python functions on the exported photos for post-processing "
+ "using the '--post-function' option. '--post-function' is passed the name a python file "
@@ -415,23 +460,19 @@ The following attributes may be used with '--xattr-template':
def template_help(width=78):
"""Return formatted string for template system"""
sio = io.StringIO()
console = Console(file=sio, force_terminal=True, width=width)
template_help_md = strip_md_header_and_links(get_template_help())
console.print(Markdown(template_help_md))
help_str = sio.getvalue()
sio.close()
return help_str
console = Console(force_terminal=True, width=width)
with console.capture() as capture:
console.print(Markdown(template_help_md))
return capture.get()
def rich_text(text, width=78):
def rich_text(text, width=78, markdown=False):
"""Return rich formatted text"""
sio = io.StringIO()
console = Console(file=sio, force_terminal=True, width=width)
console.print(text)
rich_text = sio.getvalue()
sio.close()
return rich_text
console = Console(force_terminal=True, width=width)
with console.capture() as capture:
console.print(Markdown(text) if markdown else text, end="")
return capture.get()
def strip_md_header_and_links(md):

132
osxphotos/cli/theme.py Normal file
View File

@@ -0,0 +1,132 @@
"""theme command for osxphotos for managing color themes"""
import pathlib
import click
from rich.console import Console
from rich_theme_manager import Theme
from .click_rich_echo import rich_click_echo
from .color_themes import get_default_theme, get_theme, get_theme_dir, get_theme_manager
from .help import get_help_msg
@click.command(name="theme")
@click.pass_obj
@click.pass_context
@click.option("--default", is_flag=True, help="Show default theme.")
@click.option("--list", "list_", is_flag=True, help="List all themes.")
@click.option(
"--config",
metavar="[THEME]",
is_flag=False,
flag_value="_DEFAULT_",
default=None,
help="Print configuration for THEME (or default theme if not specified).",
)
@click.option(
"--preview",
metavar="[THEME]",
is_flag=False,
flag_value="_DEFAULT_",
default=None,
help="Preview THEME (or default theme if not specified).",
)
@click.option(
"--edit",
metavar="[THEME]",
is_flag=False,
flag_value="_DEFAULT_",
default=None,
help="Edit THEME (or default theme if not specified).",
)
@click.option(
"--clone",
metavar="THEME NEW_THEME",
nargs=2,
type=str,
help="Clone THEME to NEW_THEME.",
)
@click.option("--delete", metavar="THEME", help="Delete THEME.")
def theme(ctx, cli_obj, default, list_, config, preview, edit, clone, delete):
"""Manage osxphotos color themes."""
subcommands = [default, list_, config, preview, edit, clone, delete]
subcommand_names = (
"--default, --list, --config, --preview, --edit, --clone, --delete"
)
if not any(subcommands):
click.echo(
f"Must specify exactly one of: {subcommand_names}\n",
err=True,
)
rich_click_echo(get_help_msg(theme), err=True)
return
if sum(bool(cmd) for cmd in subcommands) != 1:
# only a single subcommand may be specified
raise click.ClickException(f"Must specify exactly one of: {subcommand_names}")
theme_manager = get_theme_manager()
console = Console(theme=get_default_theme())
if default:
default = get_default_theme()
theme_manager.list_themes(theme_names=[default.name])
return
if list_:
theme_manager.list_themes()
return
if config:
if config == "_DEFAULT_":
print(get_default_theme().config)
else:
print(get_theme(config).config)
return
if preview:
theme_ = get_default_theme() if preview == "_DEFAULT_" else get_theme(preview)
theme_manager.preview_theme(theme_)
return
if edit:
theme_ = get_default_theme() if edit == "_DEFAULT_" else get_theme(edit)
config_file = pathlib.Path(theme_.path)
console.print(f"Opening [filepath]{config_file}[/] in $EDITOR")
click.edit(filename=str(config_file))
return
if clone:
src_theme = get_theme(clone[0])
dest_path = get_theme_dir() / f"{clone[1]}.theme"
if dest_path.exists():
raise click.ClickException(
f"Theme '{clone[1]}' already exists at {dest_path}"
)
dest_theme = Theme(
name=clone[1],
description=src_theme.description,
inherit=src_theme.inherit,
tags=src_theme.tags,
styles={
style_name: src_theme.styles[style_name]
for style_name in src_theme.style_names
},
)
theme_manager = get_theme_manager()
theme_manager.add(dest_theme)
theme_ = get_theme(dest_theme.name)
console.print(
f"Cloned theme '[filename]{clone[0]}[/]' to '[filename]{clone[1]}[/]' "
f"at [filepath]{theme_.path}[/]"
)
return
if delete:
theme_ = get_theme(delete)
click.confirm(f"Are you sure you want to delete theme {delete}?", abort=True)
theme_manager.remove(theme_)
console.print(f"Deleted theme [filepath]{theme_.path}[/]")
return

View File

@@ -8,7 +8,7 @@ Template statements may contain one or more modifiers. The full syntax is:
Template statements are white-space sensitive meaning that white space (spaces, tabs) changes the meaning of the template statement.
`pretext` and `posttext` are free form text. For example, if a photo has title "My Photo Title". the template statement `"The title of the photo is {title}"`, resolves to `"The title of the photo is My Photo Title"`. The `pretext` in this example is `"The title if the photo is "` and the template_field is `{title}`.
`pretext` and `posttext` are free form text. For example, if a photo has title "My Photo Title" the template statement `"The title of the photo is {title}"`, resolves to `"The title of the photo is My Photo Title"`. The `pretext` in this example is `"The title if the photo is "` and the template_field is `{title}`.
`delim`: optional delimiter string to use when expanding multi-valued template values in-place

View File

@@ -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.
### 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!):
@@ -390,6 +389,10 @@ Here's a comprehensive use case from an actual osxphotos user that integrates ma
`osxphotos export ~/Desktop/folder for exported videos/ --keyword Quik --only-movies --db /path to my.photoslibrary --touch-file --finder-tag-keywords --person-keyword --xattr-template findercomment "{title}{title?{descr?{newline},},}{descr}" --exiftool-merge-keywords --exiftool-merge-persons --exiftool --strip`
### Color Themes
Some osxphotos commands such as export use color themes to colorize the output to make it more legible. The theme may be specified with the `--theme` option. For example: `osxphotos export /path/to/export --verbose --theme dark` uses a theme suited for dark terminals. If you don't specify the color theme, osxphotos will select a default theme based on the current terminal settings. You can also specify your own default theme. See `osxphotos help theme` for more information on themes and for commands to help manage themes. Themes are defined in `.theme` files in the `~/.osxphotos/themes` directory and use style specifications compatible with the [rich](https://rich.readthedocs.io/en/stable/style.html) library.
### Conclusion
osxphotos is very flexible. If you merely want to backup your Photos library, then spending a few minutes to understand the `--directory` option is likely all you need and you can be up and running in minutes. However, if you have a more complex workflow, osxphotos likely provides options to implement your workflow. This tutorial does not attempt to cover every option offered by osxphotos but hopefully it provides a good understanding of what kinds of things are possible and where to explore if you want to learn more.

View File

@@ -20,6 +20,7 @@ pyobjc-framework-Quartz>=7.3,<9.0
pyobjc-framework-Vision>=7.3,<9.0
PyYAML>=5.4.1,<6.0.0
rich>=11.2.0,<12.0.0
rich_theme_manager>=0.7.0
textx>=2.3.0,<2.4.0
toml>=0.10.2,<0.11.0
wrapt>=1.13.3,<1.14.0

View File

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

View File

@@ -73,11 +73,11 @@ def generate_help_text(command):
# get current help text
with runner.isolated_filesystem():
result = runner.invoke(cli_main, ["help", command])
result = runner.invoke(cli_main, ["help", command, "--width", 78])
help_txt = result.output
# running the help command above doesn't output the full "Usage" line
help_txt = help_txt.replace(f"Usage: cli-main", f"Usage: osxphotos")
help_txt = help_txt.replace("Usage: cli-main", "Usage: osxphotos")
return help_txt