<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Brian Wisti</title>
    <description>The latest articles on DEV Community by Brian Wisti (@brianwisti).</description>
    <link>https://dev.to/brianwisti</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F258059%2Fef62b279-be6c-439c-be82-d62b4685f783.jpeg</url>
      <title>DEV Community: Brian Wisti</title>
      <link>https://dev.to/brianwisti</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/brianwisti"/>
    <language>en</language>
    <item>
      <title>Added RSS forwarding here</title>
      <dc:creator>Brian Wisti</dc:creator>
      <pubDate>Sat, 20 Feb 2021 01:37:22 +0000</pubDate>
      <link>https://dev.to/brianwisti/added-rss-forwarding-here-1le5</link>
      <guid>https://dev.to/brianwisti/added-rss-forwarding-here-1le5</guid>
      <description>&lt;p&gt;This account will start seeing some activity now as I mess with the RSS forwarding features — syndicate everything from &lt;a href="https://randomgeekery.org"&gt;my blog&lt;/a&gt; to here.&lt;/p&gt;

&lt;p&gt;Well, almost everything. My blog's only mostly technical. Sketches, knitting, and late night stream of consciousness also show up. I'll probably stick to syndicating the more tech-specific stuff, though for now I'm including bookmarks, posts, and notes within that range.&lt;/p&gt;

&lt;p&gt;I figure this is a form of &lt;a href="https://indieweb.org"&gt;IndieWeb&lt;/a&gt;'s &lt;a href="https://indieweb.org/POSSE"&gt;POSSE&lt;/a&gt; — Publish on your Own Site, Syndicate Elsewhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Update
&lt;/h2&gt;

&lt;p&gt;Okay, so this "AsciiDoctor -&amp;gt; HTML -&amp;gt; RSS XML -&amp;gt; Dev.to Markdown" flow takes a lot of handholding and manual adjustment. Until I figure it out well enough to automate the process, better restrict the feed to full posts and not &lt;em&gt;ALL THE THINGS&lt;/em&gt;. No fun writing every note twice.&lt;/p&gt;

</description>
      <category>indieweb</category>
      <category>posse</category>
      <category>syndication</category>
    </item>
    <item>
      <title>Note: Just noticed Okular PDF is on Windows</title>
      <dc:creator>Brian Wisti</dc:creator>
      <pubDate>Fri, 19 Feb 2021 19:37:37 +0000</pubDate>
      <link>https://dev.to/brianwisti/note-just-noticed-okular-pdf-is-on-windows-31jh</link>
      <guid>https://dev.to/brianwisti/note-just-noticed-okular-pdf-is-on-windows-31jh</guid>
      <description>&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--biN0c3dR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://randomgeekery.org/note/2021/02/just-noticed-okular-pdf-is-on-windows/cover_hu7c9721bdce4c9c20f27d59ab6cb92c99_151508_600x0_resize_box_2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--biN0c3dR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://randomgeekery.org/note/2021/02/just-noticed-okular-pdf-is-on-windows/cover_hu7c9721bdce4c9c20f27d59ab6cb92c99_151508_600x0_resize_box_2.png" alt="Just noticed Okular PDF is on Windows"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://okular.kde.org"&gt;Okular&lt;/a&gt;, the &lt;a href="https://kde.org"&gt;KDE&lt;/a&gt; document viewer, works on Windows. Not only that, but it’s available in the &lt;a href="https://www.microsoft.com/en-us/p/okular/9n41msq1wnm8?activetab=pivot:overviewtab"&gt;Microsoft Store&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Free, obviously.&lt;/p&gt;

&lt;p&gt;This should help keep annotations in order as I work through my TOREAD stack — three chapters down, only several hundred books to go! — regardless of current operating system.&lt;/p&gt;

</description>
      <category>note</category>
      <category>windows</category>
      <category>tools</category>
    </item>
    <item>
      <title>Note: Tweaking my tools</title>
      <dc:creator>Brian Wisti</dc:creator>
      <pubDate>Tue, 16 Feb 2021 22:40:03 +0000</pubDate>
      <link>https://dev.to/brianwisti/note-tweaking-my-tools-3f6f</link>
      <guid>https://dev.to/brianwisti/note-tweaking-my-tools-3f6f</guid>
      <description>&lt;p&gt;Playing a little more with &lt;a href="https://ttytoolkit.org"&gt;TTY Toolkit&lt;/a&gt; for the site workflow. I wanted to say I’m tightening focus, but with a &lt;code&gt;require&lt;/code&gt; list like this for one tool?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;require 'pastel'
require 'ruby-slugify'
require 'tty-editor'
require 'tty-exit'
require 'tty-logger'
require 'tty-option'
require 'tty-prompt'
require 'tty-screen'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"Tightening focus" would be a lie.&lt;/p&gt;

&lt;p&gt;Anyways, it seems to function correctly. Huzzah! Now back to work.&lt;/p&gt;

</description>
      <category>note</category>
      <category>ruby</category>
      <category>site</category>
    </item>
    <item>
      <title>Bookmark: Joplin</title>
      <dc:creator>Brian Wisti</dc:creator>
      <pubDate>Tue, 16 Feb 2021 01:18:07 +0000</pubDate>
      <link>https://dev.to/brianwisti/bookmark-joplin-2o61</link>
      <guid>https://dev.to/brianwisti/bookmark-joplin-2o61</guid>
      <description>&lt;p&gt;&lt;a href="https://joplinapp.org"&gt;Joplin&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This one’s worth bookmarking because it’s &lt;a href="https://github.com/laurent22/joplin/blob/dev/LICENSE"&gt;open source&lt;/a&gt;, multi-platform including mobile, extensible, and can be synchronized via multiple services.&lt;/p&gt;

&lt;p&gt;Still haven’t decided if it’s my one true note-taking resource, or if there ever will be such a thing. I wish there was setting synchronization too, but maybe there’s a &lt;a href="https://discourse.joplinapp.org/c/plugins/18"&gt;plugin&lt;/a&gt; for that.&lt;/p&gt;

&lt;h3&gt;
  
  
  General usage
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;nodes can be notes or tasks, and you can toggle between them&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://joplinapp.org/markdown/"&gt;note format&lt;/a&gt; is slightly tweaked variant of Github-Flavored Markdown&lt;/li&gt;
&lt;li&gt;default split pane approach w/editor on one side, working preview on the other&lt;/li&gt;
&lt;li&gt;has a rich editor, but that’s not my style so I couldn’t tell you how well it works&lt;/li&gt;
&lt;li&gt;drag and drop to create subnotebooks&lt;/li&gt;
&lt;li&gt;note links are via UUID-style hashes, so they’ll persist when you move a linked note&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Plugins
&lt;/h3&gt;

&lt;p&gt;Check the &lt;a href="https://github.com/joplin/plugins/blob/master/README.md"&gt;master list&lt;/a&gt; of plugins. Check the &lt;a href="https://discourse.joplinapp.org/c/plugins/18"&gt;forum&lt;/a&gt; for discussions and announcements.&lt;/p&gt;

&lt;p&gt;Here’s what I’m using so far:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://discourse.joplinapp.org/t/plugin-inline-tags/14192"&gt;inline tags&lt;/a&gt;: autocomplete existing tag links with &lt;code&gt;#&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://discourse.joplinapp.org/t/quick-links-plugin/14214"&gt;quick links&lt;/a&gt;: autocomplete note links with &lt;code&gt;@@&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://discourse.joplinapp.org/t/automatic-backlinks-with-manual-insert-option/13632"&gt;automatic backlinks&lt;/a&gt;: inserts list of links to notes that reference the &lt;em&gt;current&lt;/em&gt; note&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://discourse.joplinapp.org/t/create-note-from-highlighted-text/12511"&gt;convert text to new note&lt;/a&gt;: select, right click, bang. new note&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>bookmark</category>
      <category>tools</category>
      <category>secondbrain</category>
    </item>
    <item>
      <title>Note: testing a thing</title>
      <dc:creator>Brian Wisti</dc:creator>
      <pubDate>Mon, 15 Feb 2021 02:29:10 +0000</pubDate>
      <link>https://dev.to/brianwisti/note-testing-a-thing-ad3</link>
      <guid>https://dev.to/brianwisti/note-testing-a-thing-ad3</guid>
      <description>&lt;p&gt;Sometime &lt;a href="https://randomgeekery.org/post/2020/05/letting-ruby-build-asciidoctor-files-for-hugo/"&gt;last year&lt;/a&gt; I had half of a great idea for better &lt;a href="https://asciidoctor.org/"&gt;Asciidoctor&lt;/a&gt; handling in &lt;a href="https://gohugo.io"&gt;Hugo&lt;/a&gt;. I &lt;em&gt;might&lt;/em&gt; have the other half now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;keep my content in the content folder.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;adoc.txt&lt;/code&gt; for the extension so Hugo ignores it.&lt;/li&gt;
&lt;li&gt;Point my &lt;code&gt;build-adoc&lt;/code&gt; script there instead of a neighboring &lt;code&gt;adoc&lt;/code&gt; folder.&lt;/li&gt;
&lt;li&gt;profit?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Would work for &lt;a href="https://randomgeekery.org/tags/rst"&gt;reStructuredText&lt;/a&gt; too.&lt;/p&gt;

&lt;p&gt;Need to get through a few post cycles to see how it works.&lt;/p&gt;

</description>
      <category>note</category>
      <category>asciidoctor</category>
      <category>hugo</category>
      <category>site</category>
    </item>
    <item>
      <title>Post: Pretty File Summaries with Rich and ExifTool</title>
      <dc:creator>Brian Wisti</dc:creator>
      <pubDate>Sat, 06 Feb 2021 00:00:00 +0000</pubDate>
      <link>https://dev.to/brianwisti/post-pretty-file-summaries-with-rich-and-exiftool-2afn</link>
      <guid>https://dev.to/brianwisti/post-pretty-file-summaries-with-rich-and-exiftool-2afn</guid>
      <description>&lt;h2&gt;
  
  
  Want to see something cool?
&lt;/h2&gt;

&lt;p&gt;A while back I &lt;a href="https://randomgeekery.org/post/2020/04/getting-file-info-from-the-shell/"&gt;shared&lt;/a&gt; how I use &lt;a href="https://exiftool.org"&gt;ExifTool&lt;/a&gt; to get extensive metadata for any file. I want to make that info dump pretty with &lt;a href="https://rich.readthedocs.io/en/stable/introduction.html"&gt;Rich&lt;/a&gt;, a text formatting library for &lt;a href="https://randomgeekery.org/tags/python"&gt;Python&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;“But Brian,” I hear you cry. “ExifTool is &lt;a href="https://randomgeekery.org/tags/perl"&gt;Perl&lt;/a&gt;. Why would I want to use both Perl and Python?”&lt;/p&gt;

&lt;p&gt;Because it’s fun, obviously.&lt;/p&gt;

&lt;p&gt;You want a “real” reason? Okay fine. I haven’t found anything that can get the depth of file information I get from ExifTool. I haven’t found a formatting library that’s as pleasant to use as Rich — maybe &lt;a href="https://ttytoolkit.org"&gt;TTY Toolkit&lt;/a&gt;?&lt;/p&gt;

&lt;p&gt;Besides — ExifTool is a standalone command line tool. We don’t need to write any Perl to &lt;em&gt;use&lt;/em&gt; it. Heck, we don’t even need to figure out the system calls.&lt;a href="https://github.com/smarnach"&gt;Sven Marnach&lt;/a&gt; is way ahead of us with the extremely helpful &lt;a href="https://smarnach.github.io/pyexiftool/"&gt;pyexiftool&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Rich and pyexiftool make Python an easy choice for this task.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up
&lt;/h2&gt;

&lt;p&gt;If you want to play along at home, make sure you have the dependencies.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;exiftool
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;pyexiftool rich typer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://typer.tiangolo.com"&gt;Typer&lt;/a&gt; simplifies turning this random idea into a useful command line tool.&lt;/p&gt;



    &lt;p&gt;&lt;b&gt;Note&lt;/b&gt;&lt;/p&gt;
  &lt;p&gt;If you’re already a fan of Perl, consider &lt;a href="https://metacpan.org/pod/App::cpanminus"&gt;&lt;code&gt;cpanm&lt;/code&gt;&lt;/a&gt; instead of &lt;a href="https://brew.sh"&gt;Homebrew&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang="shell"&gt;$ cpanm Image::ExifTool
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now you can use &lt;a href="https://metacpan.org/pod/distribution/Image-ExifTool/lib/Image/ExifTool.pod"&gt;Image::ExifTool&lt;/a&gt; in your own Perl projects.&lt;/p&gt;



&lt;h2&gt;
  
  
  Some scaffolding
&lt;/h2&gt;

&lt;p&gt;Even though I’m the only user, I still need to figure out how I plan to use it. At minimum? I hand my script a filename. It hands me metadata.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;richexif FILENAME [OPTIONS]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I can hook some &lt;a href="https://typer.tiangolo.com/tutorial/first-steps/#add-a-cli-argument"&gt;minimal&lt;/a&gt; Typer argument handling around that flow.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env python
&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;logging&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;rich.logging&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;RichHandler&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;typer&lt;/span&gt;

&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;basicConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DEBUG&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"%(message)s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;datefmt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"[%X]"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;handlers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;RichHandler&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="s"&gt;"""Display nicely-formatted file metadata."""&lt;/span&gt;
    &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"filename: %s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Can I run it?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;❯ chmod 755 richexif.py
❯ ./richexif.py hoku-hopes-for-snacksjpg.jpg

[14:33:30] DEBUG filename: hoku-hopes-for-snacks.jpg richexif.py:13
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I can! What happens if I use it wrong?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;❯ ./richexif.py
Usage: richexif.py [OPTIONS] FILENAME
Try 'richexif.py --help' for help.

Error: Missing argument 'FILENAME'.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I get an error message telling me what &lt;code&gt;richexif.py&lt;/code&gt; needs to do its thing. Nice.&lt;/p&gt;

&lt;p&gt;I confirmed that Typer handles the CLI bits, and Rich handles the formatting. Now for pyexiftool.&lt;/p&gt;

&lt;p&gt;Oh and I’ll skip logging output from here on. Rich’s &lt;a href="https://rich.readthedocs.io/en/latest/logging.html"&gt;logging handler&lt;/a&gt; output is a joy to look at, but really that stuff is for me. For you it’ll just add noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Some metadata
&lt;/h2&gt;

&lt;p&gt;I need exiftool, of course. Plus a Rich &lt;a href="https://rich.readthedocs.io/en/latest/console.html"&gt;Console&lt;/a&gt; object, masterminding the display details for my terminal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;exiftool&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;rich.console&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Console&lt;/span&gt;

&lt;span class="n"&gt;console&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;exiftool’s &lt;a href="https://smarnach.github.io/pyexiftool/#exiftool.ExifTool.get_metadata"&gt;&lt;code&gt;get_metadata&lt;/code&gt;&lt;/a&gt; grabs everything ExifTool sees about a file. It also provides methods for ExifTool &lt;a href="https://exiftool.org/TagNames/index.html"&gt;tags&lt;/a&gt;, but I won’t mess with them today. Tags — the official name for our metadata keys — are most useful when you already know what you’re looking for. We’re just checking stuff out.&lt;/p&gt;

&lt;p&gt;For now, a little abstraction layer over pyexiftool’s &lt;code&gt;ExifTool&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_metadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="s"&gt;"""Return a dictionary of file metadata."""&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;exiftool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ExifTool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;et&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;et&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_metadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;main&lt;/code&gt; gets the metadata and asks &lt;code&gt;console&lt;/code&gt; to print it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="s"&gt;"""Display nicely-formatted file metadata."""&lt;/span&gt;
    &lt;span class="n"&gt;metadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;get_metadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here’s what that looks like.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;'SourceFile'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;'hoku-hopes-for-snacks.jpg'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;'ExifTool:ExifToolVersion'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;12.15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;'File:FileName'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;'hoku-hopes-for-snacks.jpg'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;'File:Directory'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;'.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;'File:FileSize'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;918330&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;'File:FileModifyDate'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;'2021:02:06 00:54:29-08:00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;'File:FileAccessDate'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;'2021:02:06 11:30:33-08:00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;'File:FileInodeChangeDate'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;'2021:02:06 11:30:33-08:00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;'File:FilePermissions'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;775&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;'File:FileType'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;'JPEG'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="err"&gt;…&lt;/span&gt;&lt;span class="n"&gt;skipping&lt;/span&gt; &lt;span class="mi"&gt;62&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="err"&gt;…&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;
    &lt;span class="s"&gt;'Composite:ScaleFactor35efl'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;6.04651162790698&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;'Composite:ShutterSpeed'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;'Composite:GPSLatitude'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;47.5750857997222&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;'Composite:GPSLongitude'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;122.386441&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;'Composite:CircleOfConfusion'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;'0.00496918925785101'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;'Composite:FOV'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;69.3903656740024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;'Composite:FocalLength35efl'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;'Composite:GPSPosition'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;'47.5750857997222 -122.386441'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;'Composite:HyperfocalDistance'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;2.48061927751922&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;'Composite:LightValue'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;3.81378119121704&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Holy crap that’s a lot. Some of it could be considered sensitive information — unless you read my &lt;a href="https://randomgeekery.org/now"&gt;/now&lt;/a&gt; page. But it’s all there! Even in the snipped version you can learn a lot. Hello from my Windows partition in West Seattle during February of 2021!&lt;/p&gt;



    &lt;p&gt;Note&lt;/p&gt;
  Uncomfortable sharing that much with every photo you upload?
You can scrub those tags right out.
&lt;a href="https://www.linux-magazine.com/Online/Blogs/Productivity-Sauce/Remove-EXIF-Metadata-from-Photos-with-exiftool"&gt;With ExifTool&lt;/a&gt;, of course.


&lt;p&gt;But back to the other gripe about all this metadata. It’s way too much for me to take in all at once. I need some kind of filter!&lt;/p&gt;

&lt;h3&gt;
  
  
  Filtering the firehose
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;filter_metadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="s"&gt;"""Return a copy of the metadata where fields contain the substring `filter`."""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;filter&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There’s no kind of transformation here. If a field constains the exact substring described in &lt;code&gt;filter&lt;/code&gt;, use it.&lt;/p&gt;

&lt;p&gt;Adding a Typer &lt;a href="https://typer.tiangolo.com/tutorial/options/"&gt;Option&lt;/a&gt; lets us ask for a filter from the command line.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nb"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;typer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Substring to restrict displayed fields"&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="s"&gt;"""Display nicely-formatted file metadata."""&lt;/span&gt;
    &lt;span class="n"&gt;metadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;get_metadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;metadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;filter_metadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If use &lt;code&gt;--filter&lt;/code&gt;, we should only get matching tags. Leaving out the filter gets us everything.&lt;/p&gt;

&lt;p&gt;Try it out!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ./richexif.py hoku-hopes-for-snacks.jpg --filter=Image

{
    'File:ImageWidth': 3672,
    'File:ImageHeight': 2066,
    'EXIF:ImageWidth': 4032,
    'EXIF:ImageHeight': 2268,
    'EXIF:ExifImageWidth': 4032,
    'EXIF:ExifImageHeight': 2268,
    'EXIF:ImageUniqueID': 'J12LLKL00SM',
    'EXIF:ThumbnailImage': '(Binary data 6788 bytes, use -b option to extract)',
    'Composite:ImageSize': '3672 2066'
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now that I’m not overwhelmed by the quantity of output, I’m a little underwhelmed by the quality.&lt;/p&gt;

&lt;p&gt;It’s nice. Don’t get me wrong. But all we’ve added to default &lt;code&gt;exiftool&lt;/code&gt; behavior is some color.&lt;/p&gt;

&lt;p&gt;I’ve played with Rich a bit. I know we can do better.&lt;/p&gt;

&lt;h2&gt;
  
  
  A metadata table!
&lt;/h2&gt;

&lt;p&gt;Rich lets us create and display &lt;a href="https://rich.readthedocs.io/en/stable/tables.html"&gt;tables&lt;/a&gt; in the terminal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;rich.table&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Table&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We need to &lt;em&gt;build&lt;/em&gt; the table, defining columns and adding values row by row.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def file_table(filename, metadata):
    """Return a Rich Table showing the metadata for a file."""
    table = Table("Field", "Value", title=filename)

    for key, value in metadata.items():
        table.add_row(key, str(value))

    return table
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





    &lt;h3&gt;Warning&lt;/h3&gt;
  

Hey, don’t miss that `str(value)`! Rich tables need strings, and take nothing for granted with the values you give it. Numeric values won’t necessarily convert straight to strings without a little help.




&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(...):&lt;/span&gt;
    &lt;span class="s"&gt;"""Display nicely-formatted file metadata."""&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;metadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;filter_metadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;file_table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ./richexif.py hoku-hopes-for-snacksjpg.jpg --filter=Image


 hoku-hopes-for-snacksjpg.jpg 
┏━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Field ┃ Value ┃
┡━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ File:ImageWidth │ 3672 │
│ File:ImageHeight │ 2066 │
│ EXIF:ImageWidth │ 4032 │
│ EXIF:ImageHeight │ 2268 │
│ EXIF:ExifImageWidth │ 4032 │
│ EXIF:ExifImageHeight │ 2268 │
│ EXIF:ImageUniqueID │ J12LLKL00SM │
│ EXIF:ThumbnailImage │ (Binary data 6788 bytes, use -b option to extract) │
│ Composite:ImageSize │ 3672 2066 │
└──────────────────────┴────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pretty nifty.&lt;/p&gt;

&lt;h2&gt;
  
  
  A metadata tree!
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;rich.tree&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Tree&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;file_tree&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;tree&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Tree&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s"&gt;"[bold]&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;branches&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="n"&gt;tagged_values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;":"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tagged_values&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;root_tag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;root_tag&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;branches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;branches&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;root_tag&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tree&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s"&gt;"[bold]&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;root_tag&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;branches&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;root_tag&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s"&gt;"[italic]&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:[/italic] &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;branches&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]].&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;tree&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Except now we have to ways to display metadata. Three, if you count the dictionary we started with. Well. Maybe later.&lt;/p&gt;

&lt;p&gt;For now, a callback table that says what to call for each of the options.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;rich.tree&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Tree&lt;/span&gt;

&lt;span class="n"&gt;DISPLAYS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"table"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;file_table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s"&gt;"tree"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;file_tree&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We don’t &lt;em&gt;need&lt;/em&gt; to use lambdas here. Functions can be passed around same as any other value. But if I wrap them in a lambda I can build my constant table before Python knows the functions exist.&lt;/p&gt;

&lt;p&gt;Typer uses &lt;a href="https://typer.tiangolo.com/tutorial/options/callback-and-context/"&gt;callback&lt;/a&gt; functions to validate options. They do any processing or checks they need to, then return the supplied value if everything goes well.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;validate_display&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="s"&gt;"""Return value if valid, or panic if it isn't."""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;DISPLAYS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="n"&gt;typer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BadParameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s"&gt;"Format must be one of: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;DISPLAYS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the &lt;code&gt;--display&lt;/code&gt; Option, making sure to point Typer at the callback.&lt;code&gt;main&lt;/code&gt; itself knows the value is safe, or the script never would have reached it. So I can grab the displayer and call it without fear of consequence.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;
    &lt;span class="n"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;typer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"table"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"How to display the metadata"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;callback&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;validate_display&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="s"&gt;"""Display nicely-formatted file metadata."""&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;

    &lt;span class="n"&gt;displayer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FORMATS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;display&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;displayer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Okay! What do we have now?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt; ./richexif.py hoku-hopes-for-snacks.jpg --filter=Image --display=tree


hoku-hopes-for-snacks.jpg                                                       
├── File                                                                        
│ ├── ImageWidth: 3672                                                        
│ └── ImageHeight: 2066                                                       
├── EXIF                                                                        
│ ├── ImageWidth: 4032                                                        
│ ├── ImageHeight: 2268                                                       
│ ├── ExifImageWidth: 4032                                                    
│ ├── ExifImageHeight: 2268                                                   
│ ├── ImageUniqueID: J12LLKL00SM                                              
│ └── ThumbnailImage: (Binary data 6788 bytes, use -b option to extract)      
└── Composite                                                                   
    └── ImageSize: 3672 2066                                                    
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Oooooh.&lt;/p&gt;

&lt;p&gt;Anyways, that’s what I wanted to show you. Got plenty more ideas for mashing ExifTool and Rich together, as I’m sure you can imagine.&lt;/p&gt;

</description>
      <category>post</category>
      <category>tools</category>
      <category>files</category>
      <category>python</category>
    </item>
  </channel>
</rss>
