<rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0"><channel><title>Josh Duncan - Blog</title><atom:link href="https://joshbduncan.com/rss.xml" rel="self" type="application/rss+xml" /><link>https://joshbduncan.com/</link><description>just some random thoughts published on the internet...</description><lastBuildDate>Wed, 12 Aug 2026 00:48:42 -0400</lastBuildDate><language>en-US</language><item><title>Printing PDFs to the Dropzone Drop Bar</title><link>https://joshbduncan.com/printing-pdfs-to-dropzone-drop-bar.html</link><pubDate>Wed, 10 Sep 2025 00:00:00 -0400</pubDate><guid>https://joshbduncan.com/printing-pdfs-to-dropzone-drop-bar.html</guid><description><![CDATA[Skip all of the clicking-and-dragging by printing directly to the Dropzone Drop Bar right from the macOS print dialog window.]]></description><content:encoded><![CDATA[<p>"I often use macOS’s “Print to PDF” option and have Automator Print Plugins for tasks like saving receipts, statements, and order confirmations. The problem comes when I do not know where to file a PDF right away. In the past, I would drop it on the Desktop 😱, send it to Downloads, or leave it open in Preview until I had time to deal with it. None of these options are great, so I started looking for a better way.</p>
<h2>An Aha Moment</h2>
<p>That changed when I spotted a new Command Line Integration (CLI) in the release notes for Dropzone. Suddenly it clicked...</p>
<blockquote>
<p>What if I could print directly to the Drop Bar in Dropzone?</p>
</blockquote>
<h2>macOS Print Plugins</h2>
<p>On macOS, Automator Print Plugins let you do more with the PDF button in the Print dialog. Instead of just saving a file, you can set up quick actions like automatically naming, moving, or filing PDFs—right when you print. It’s a handy way to speed up routine tasks.</p>
<p><img alt="macOs Print Dialog PDF Menu" src="https://joshbduncan.com/static/images/macos-print-dialog-pdf-menu.png"/></p>
<h2>Setting Up a Print Plugin</h2>
<p>Upon launching Automator, <a href="https://support.apple.com/guide/automator/create-a-workflow-aut7cac58839/mac">choose the "Print Plugin" workflow</a>.</p>
<p><img alt="Automator Startup Screen" src="https://joshbduncan.com/static/images/automator-startup-screen-print-plugin.png"/></p>
<p>Since we will be using the Dropzone CLI via the <code>dz</code> command, you'll only need the <a href="https://support.apple.com/guide/automator/use-a-shell-script-action-in-a-workflow-autbbd4cc11c/2.10/mac/15.0"><code>Run Shell Script</code></a> action for this workflow.</p>
<p><img alt="Automator Print Plugin Workflow" src="https://joshbduncan.com/static/images/automator-print-plugin-workflow.png"/></p>
<h2>Code Explanation</h2>
<p>The shell code is pretty simple, but here is a breakdown of the steps.</p>
<div class="codehilite"><pre><span></span><code><span class="c1"># 1. Set the directory (`base_dir`) for saving the PDF file.</span>
<span class="nv">base_dir</span><span class="o">=</span><span class="s2">"</span><span class="nv">$HOME</span><span class="s2">/Downloads/"</span>
<span class="c1"># 2. Move the PDF to the `base_dir`.</span>
mv<span class="w"> </span><span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span><span class="w"> </span><span class="s2">"</span><span class="nv">$base_dir</span><span class="s2">"</span>
<span class="c1"># 3. Set the final path of the PDF file.</span>
<span class="nv">file_path</span><span class="o">=</span><span class="s2">"</span><span class="nv">$base_dir</span><span class="s2">/</span><span class="k">$(</span>basename<span class="w"> </span><span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span><span class="k">)</span><span class="s2">"</span>
<span class="c1"># 4. Send the PDF file to Dropzone Drop Bar via the `dz add` command.</span>
/usr/local/bin/dz<span class="w"> </span>add<span class="w"> </span><span class="s2">"</span><span class="nv">$file_path</span><span class="s2">"</span>
</code></pre></div>
<h3>A Few Notes</h3>
<ol>
<li>
<p>So, how do you get your new Automator Print Plugin to show up in the Print dialog’s PDF menu? When you save the workflow in Automator as a Print Plugin, it will appear automatically in the PDF dropdown the next time you open the Print dialog. For reference, all of your custom items from that menu are stored in your user’s Library folder at <code>~/Library/PDF Services/</code>.</p>
</li>
<li>
<p>Full path to dz – Automator doesn’t always use your shell’s PATH, so call /usr/local/bin/dz explicitly.</p>
</li>
<li>
<p>By default, macOS prints PDFs to a temp directory. If you don’t need to move the file, you can send it straight to Dropzone with:</p>
</li>
</ol>
<div class="codehilite"><pre><span></span><code>/usr/local/bin/dz<span class="w"> </span>add<span class="w"> </span><span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span>
</code></pre></div>
<h2>Extra Credit</h2>
<p>You can trigger Dropzone actions too 🙌!</p>
<div class="codehilite"><pre><span></span><code>/usr/local/bin/dz<span class="w"> </span>run<span class="w"> </span><span class="s2">"File Receipt"</span><span class="w"> </span>dragged<span class="w"> </span><span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span>
</code></pre></div>
<h2>Final Thoughts</h2>
<p>With this setup, I no longer clutter my Desktop or Downloads with stray PDFs and everything goes right into Dropzone, ready to be filed where it belongs.</p>
<p>🤖 Happy PDF filing!</p>]]></content:encoded></item><item><title>Saving JSON &quot;like&quot; Preferences Files With Adobe ExtendScript</title><link>https://joshbduncan.com/saving-preferences-in-adobe-extendscript.html</link><pubDate>Thu, 07 Jul 2022 00:00:00 -0400</pubDate><guid>https://joshbduncan.com/saving-preferences-in-adobe-extendscript.html</guid><description><![CDATA[Some of my scripts for Adobe products have the ability to save user preferences. Here's how I save those preferences to a JSON like file on disk using ExtendScript and the Adobe Bridge Messaging API.]]></description><content:encoded><![CDATA[<p>Any time I'm writing an <a href="https://extendscript.docsforadobe.dev/introduction/extendscript-overview.html">Adobe ExtendScript</a> that includes a dialog box with more than a few options, I like to give the user the ability to save their dialog selections to a preference file. That way, when they run the script again their defaults (or last used settings) are preloaded, saving them time.</p>
<p>I hope to go deeper into this subject in a later post, but for now I'll show you how I write the users last used settings out to a JSON "like" file on their hard disk.</p>
<h2>What is JSON "like"?</h2>
<p>Adobe ExtendScript doesn't include a JSON library, so to easily read and write JSON I'll be using the <a href="https://extendscript.docsforadobe.dev/interapplication-communication/communicating-through-messages.html">Adobe Bridge Messaging API</a>. This API provides two key functions.</p>
<ul>
<li><code>toSource()</code> to serialize/encode JavaScript <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Working_with_Objects">objects</a> and <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array">arrays</a></li>
<li><code>eval()</code> to reconstitute/decode JavaScript objects and arrays</li>
</ul>
<p>The reason I say JSON "like" is because the <code>toSource()</code> function doesn't encode valid JSON, it serializes objects or arrays into a special string. There are a few funky aspects of this serialized string that make it invalid JSON, but since I'll be reconstituting it using the <code>eval()</code> function, it works perfect for my purposes.</p>
<div class="codehilite"><pre><span></span><code><span class="c1">// valid json</span>
<span class="p">{</span>
<span class="w">    </span><span class="s2">"a"</span><span class="o">:</span><span class="w"> </span><span class="mf">1</span><span class="p">,</span>
<span class="w">    </span><span class="s2">"b"</span><span class="o">:</span><span class="w"> </span><span class="mf">2</span>
<span class="p">}</span>

<span class="c1">// invalid `toSource()` serialized JSON string</span>
<span class="p">({</span><span class="s2">"a"</span><span class="o">:</span><span class="w"> </span><span class="mf">1</span><span class="p">,</span><span class="w"> </span><span class="s2">"b"</span><span class="o">:</span><span class="w"> </span><span class="mf">2</span><span class="p">})</span>
</code></pre></div>
<p><em>Going forward, I'll just use the term JSON.</em></p>
<h2>Script Setup</h2>
<p>First, I'll setup some option variables that will be used to create the dialog window, and I'll also set defaults to be used on the first run (or when a preferences file isn't found).</p>
<div class="codehilite"><pre><span></span><code><span class="kd">var</span><span class="w"> </span><span class="nx">locations</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="s2">"Location 1"</span><span class="p">,</span><span class="w"> </span><span class="s2">"Location 2"</span><span class="p">,</span><span class="w"> </span><span class="s2">"Location 3"</span><span class="p">];</span>
<span class="kd">var</span><span class="w"> </span><span class="nx">products</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="s2">"Product 1"</span><span class="p">,</span><span class="w"> </span><span class="s2">"Product 2"</span><span class="p">,</span><span class="w"> </span><span class="s2">"Product 3"</span><span class="p">];</span>
<span class="kd">var</span><span class="w"> </span><span class="nx">defaults</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">location</span><span class="o">:</span><span class="w"> </span><span class="s2">"Location 1"</span><span class="p">,</span><span class="w"> </span><span class="nx">product</span><span class="o">:</span><span class="w"> </span><span class="s2">"Product 3"</span><span class="w"> </span><span class="p">};</span>
</code></pre></div>
<p>Next, I'll check to see if a preferences file already exists. To do this, I make a new <a href="https://extendscript.docsforadobe.dev/file-system-access/file-object.html">File object</a> pointing at the user’s default document folder path provided by ExtendScript's <a href="https://extendscript.docsforadobe.dev/file-system-access/folder-object.html?highlight=folder#folder-class-properties">Folder object</a> class properties <code>Folder.myDocuments</code>.</p>
<div class="codehilite"><pre><span></span><code><span class="kd">var</span><span class="w"> </span><span class="nx">prefsFile</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nx">File</span><span class="p">(</span><span class="nx">Folder</span><span class="p">.</span><span class="nx">myDocuments</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s2">"/prefs.json"</span><span class="p">);</span>
<span class="kd">var</span><span class="w"> </span><span class="nx">prefsData</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">prefsFile</span><span class="p">.</span><span class="nx">exists</span><span class="w"> </span><span class="o">?</span><span class="w"> </span><span class="nx">readJSONData</span><span class="p">(</span><span class="nx">prefsFile</span><span class="p">)</span><span class="w"> </span><span class="o">:</span><span class="w"> </span><span class="nx">defaults</span><span class="p">;</span>
</code></pre></div>
<p>Using a <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Conditional_Operator">Conditional (ternary) operator</a> I'll set <code>prefsData</code> to the data read from <code>prefsFile</code> (if it exists) using the <code>readJSONData()</code> function below, or to the defaults I set above.</p>
<h2>Reading JSON Files With ExtendScript</h2>
<p>Reading the JSON data from the preferences file is pretty straight forward. First, I try opening the file and reading its data to the variable <code>json</code>. If all is successful, I'll reconstitute/decode that data back into a JavaScript object (using the <a href="https://extendscript.docsforadobe.dev/interapplication-communication/communicating-through-messages.html?highlight=toSource#passing-an-object-with-tosource-and-eval"><code>eval()</code></a> function mentioned above) and return it for use in the dialog window.</p>
<div class="codehilite"><pre><span></span><code><span class="kd">function</span><span class="w"> </span><span class="nx">readJSONData</span><span class="p">(</span><span class="nx">file</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="kd">var</span><span class="w"> </span><span class="nx">obj</span><span class="p">,</span><span class="w"> </span><span class="nx">json</span><span class="p">;</span>
<span class="w">  </span><span class="k">try</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">file</span><span class="p">.</span><span class="nx">encoding</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"UTF-8"</span><span class="p">;</span>
<span class="w">    </span><span class="nx">file</span><span class="p">.</span><span class="nx">open</span><span class="p">(</span><span class="s2">"r"</span><span class="p">);</span>
<span class="w">    </span><span class="nx">json</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">file</span><span class="p">.</span><span class="nx">read</span><span class="p">();</span>
<span class="w">    </span><span class="nx">file</span><span class="p">.</span><span class="nx">close</span><span class="p">();</span>
<span class="w">  </span><span class="p">}</span><span class="w"> </span><span class="k">catch</span><span class="w"> </span><span class="p">(</span><span class="nx">e</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">alert</span><span class="p">(</span><span class="s2">"Error loading "</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nx">file</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s2">" file!"</span><span class="p">);</span>
<span class="w">  </span><span class="p">}</span>
<span class="w">  </span><span class="nx">obj</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">eval</span><span class="p">(</span><span class="nx">json</span><span class="p">);</span>
<span class="w">  </span><span class="k">return</span><span class="w"> </span><span class="nx">obj</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>
<h2>Presenting The Dialog</h2>
<p>I won't go into too much detail in this post on the dialog creation code, but basically I create a simple dialog full of radio buttons determined by the <code>locations</code> and <code>products</code> arrays from above.</p>
<p>Now, the reason for all of this work is that while creating the radio buttons (or most any UI element), I can check to see if a particular element (radio button) is set in the <code>prefsData</code> object (be it either loaded from a file or the defaults), and if so, preselect if for the user. As you can see, this can be a huge timesaver if a dialog has lots of options or if the user needs the ability to save multiple presets for different uses.</p>
<p><img alt="Save Preferences From Dialog Example" src="https://joshbduncan.com/static/images/save-preferences-adobe-extendscript-dialog.png"/></p>
<div class="codehilite"><pre><span></span><code><span class="c1">// show the settings dialog</span>
<span class="kd">var</span><span class="w"> </span><span class="nx">settings</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">settingsWin</span><span class="p">(</span><span class="nx">prefsData</span><span class="p">);</span>
<span class="c1">// parse the dialog settings</span>
<span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">settings</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="nx">alert</span><span class="p">(</span>
<span class="w">    </span><span class="s2">"User Settings Saved!\nSelected Location: "</span><span class="w"> </span><span class="o">+</span>
<span class="w">      </span><span class="nx">settings</span><span class="p">.</span><span class="nx">location</span><span class="w"> </span><span class="o">+</span>
<span class="w">      </span><span class="s2">"\nSelected Product: "</span><span class="w"> </span><span class="o">+</span>
<span class="w">      </span><span class="nx">settings</span><span class="p">.</span><span class="nx">product</span>
<span class="w">  </span><span class="p">);</span>
<span class="p">}</span>

<span class="c1">// get selected radio button from array</span>
<span class="kd">function</span><span class="w"> </span><span class="nx">captureRBSelection</span><span class="p">(</span><span class="nx">rbs</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="kd">var</span><span class="w"> </span><span class="nx">i</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mf">0</span><span class="p">;</span><span class="w"> </span><span class="nx">i</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="nx">rbs</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span><span class="w"> </span><span class="nx">i</span><span class="o">++</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">rbs</span><span class="p">[</span><span class="nx">i</span><span class="p">].</span><span class="nx">value</span><span class="p">)</span><span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">rbs</span><span class="p">[</span><span class="nx">i</span><span class="p">].</span><span class="nx">text</span><span class="p">;</span>
<span class="w">  </span><span class="p">}</span>
<span class="w">  </span><span class="k">return</span><span class="w"> </span><span class="kc">null</span><span class="p">;</span>
<span class="p">}</span>

<span class="c1">// setup the dialog window</span>
<span class="kd">function</span><span class="w"> </span><span class="nx">settingsWin</span><span class="p">(</span><span class="nx">prefsData</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="kd">var</span><span class="w"> </span><span class="nx">win</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nx">Window</span><span class="p">(</span><span class="s2">"dialog"</span><span class="p">);</span>
<span class="w">  </span><span class="nx">win</span><span class="p">.</span><span class="nx">text</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Save Preferences From Dialog Example"</span><span class="p">;</span>

<span class="w">  </span><span class="c1">// create a radio button for each location</span>
<span class="w">  </span><span class="nx">win</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="s2">"statictext"</span><span class="p">,</span><span class="w"> </span><span class="kc">undefined</span><span class="p">,</span><span class="w"> </span><span class="s2">"Select Your Location"</span><span class="p">);</span>
<span class="w">  </span><span class="kd">var</span><span class="w"> </span><span class="nx">gLocation</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">win</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="s2">"group"</span><span class="p">);</span>
<span class="w">  </span><span class="kd">var</span><span class="w"> </span><span class="nx">locationRBs</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[];</span>
<span class="w">  </span><span class="kd">var</span><span class="w"> </span><span class="nx">rb</span><span class="p">;</span>
<span class="w">  </span><span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="kd">var</span><span class="w"> </span><span class="nx">i</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mf">0</span><span class="p">;</span><span class="w"> </span><span class="nx">i</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="nx">locations</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span><span class="w"> </span><span class="nx">i</span><span class="o">++</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">rb</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">gLocation</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="s2">"radiobutton"</span><span class="p">,</span><span class="w"> </span><span class="kc">undefined</span><span class="p">,</span><span class="w"> </span><span class="nx">locations</span><span class="p">[</span><span class="nx">i</span><span class="p">]);</span>
<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">prefsData</span><span class="p">[</span><span class="s2">"location"</span><span class="p">]</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="nx">locations</span><span class="p">[</span><span class="nx">i</span><span class="p">])</span><span class="w"> </span><span class="nx">rb</span><span class="p">.</span><span class="nx">value</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">true</span><span class="p">;</span>
<span class="w">    </span><span class="nx">locationRBs</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">rb</span><span class="p">);</span>
<span class="w">  </span><span class="p">}</span>

<span class="w">  </span><span class="c1">// create a radio button for each product</span>
<span class="w">  </span><span class="nx">win</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="s2">"statictext"</span><span class="p">,</span><span class="w"> </span><span class="kc">undefined</span><span class="p">,</span><span class="w"> </span><span class="s2">"Select Your Product Type"</span><span class="p">);</span>
<span class="w">  </span><span class="kd">var</span><span class="w"> </span><span class="nx">gProducts</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">win</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="s2">"group"</span><span class="p">);</span>
<span class="w">  </span><span class="kd">var</span><span class="w"> </span><span class="nx">productRBs</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[];</span>
<span class="w">  </span><span class="kd">var</span><span class="w"> </span><span class="nx">rb</span><span class="p">;</span>
<span class="w">  </span><span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="kd">var</span><span class="w"> </span><span class="nx">i</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mf">0</span><span class="p">;</span><span class="w"> </span><span class="nx">i</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="nx">products</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span><span class="w"> </span><span class="nx">i</span><span class="o">++</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">rb</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">gProducts</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="s2">"radiobutton"</span><span class="p">,</span><span class="w"> </span><span class="kc">undefined</span><span class="p">,</span><span class="w"> </span><span class="nx">products</span><span class="p">[</span><span class="nx">i</span><span class="p">]);</span>
<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">prefsData</span><span class="p">[</span><span class="s2">"product"</span><span class="p">]</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="nx">products</span><span class="p">[</span><span class="nx">i</span><span class="p">])</span><span class="w"> </span><span class="nx">rb</span><span class="p">.</span><span class="nx">value</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">true</span><span class="p">;</span>
<span class="w">    </span><span class="nx">productRBs</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">rb</span><span class="p">);</span>
<span class="w">  </span><span class="p">}</span>

<span class="w">  </span><span class="c1">// setup window buttons</span>
<span class="w">  </span><span class="kd">var</span><span class="w"> </span><span class="nx">gWindowButtons</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">win</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="s2">"group"</span><span class="p">,</span><span class="w"> </span><span class="kc">undefined</span><span class="p">);</span>
<span class="w">  </span><span class="kd">var</span><span class="w"> </span><span class="nx">btOK</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">gWindowButtons</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="s2">"button"</span><span class="p">,</span><span class="w"> </span><span class="kc">undefined</span><span class="p">,</span><span class="w"> </span><span class="s2">"OK"</span><span class="p">);</span>
<span class="w">  </span><span class="kd">var</span><span class="w"> </span><span class="nx">btCancel</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">gWindowButtons</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="s2">"button"</span><span class="p">,</span><span class="w"> </span><span class="kc">undefined</span><span class="p">,</span><span class="w"> </span><span class="s2">"Cancel"</span><span class="p">);</span>

<span class="w">  </span><span class="c1">// if "ok" button clicked then return inputs</span>
<span class="w">  </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">win</span><span class="p">.</span><span class="nx">show</span><span class="p">()</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="mf">1</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="c1">// check to see which location and product was selected</span>
<span class="w">    </span><span class="kd">var</span><span class="w"> </span><span class="nx">selectedSettings</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span>
<span class="w">      </span><span class="nx">location</span><span class="o">:</span><span class="w"> </span><span class="nx">captureRBSelection</span><span class="p">(</span><span class="nx">locationRBs</span><span class="p">),</span>
<span class="w">      </span><span class="nx">product</span><span class="o">:</span><span class="w"> </span><span class="nx">captureRBSelection</span><span class="p">(</span><span class="nx">productRBs</span><span class="p">),</span>
<span class="w">    </span><span class="p">};</span>
<span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="nx">selectedSettings</span><span class="p">;</span>
<span class="w">  </span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="k">return</span><span class="p">;</span>
<span class="w">  </span><span class="p">}</span>
<span class="p">}</span>
</code></pre></div>
<p>The main thing to understand from the code above are the final few lines. If the user clicks the "OK" button <code>if (win.show() == 1)</code>, I gather their dialog selections (radio buttons in this case) and construct an object from them that I can return for use in the rest of the script.</p>
<div class="callout">
<p>You may notice a utility function above called <code>captureRBSelection()</code>. It's not necessarily needed, it just provides an easy way to determine which radio button from a UI group is selected. </p>
</div>
<h2>Writing JSON Files With ExtendScript</h2>
<p>Now that I have the user selected settings from the dialog stored as an object, I can write that data to the preferences file.</p>
<div class="codehilite"><pre><span></span><code><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">settings</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="nx">writeJSONData</span><span class="p">(</span><span class="nx">settings</span><span class="p">,</span><span class="w"> </span><span class="nx">prefsFile</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div>
<p>Writing the JSON data to a file is not that dissimilar from reading it. First, I serialize the JSON data using the <a href="https://extendscript.docsforadobe.dev/interapplication-communication/communicating-through-messages.html?highlight=toSource#passing-an-object-with-tosource-and-eval"><code>toSource()</code></a> function mentioned above. Then, I try writing the data to disk using the <a href="https://extendscript.docsforadobe.dev/file-system-access/file-object.html#write"><code>write()</code></a> function provided by the file object.</p>
<div class="codehilite"><pre><span></span><code><span class="kd">function</span><span class="w"> </span><span class="nx">writeJSONData</span><span class="p">(</span><span class="nx">obj</span><span class="p">,</span><span class="w"> </span><span class="nx">file</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="kd">var</span><span class="w"> </span><span class="nx">data</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">obj</span><span class="p">.</span><span class="nx">toSource</span><span class="p">();</span>
<span class="w">  </span><span class="k">try</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">file</span><span class="p">.</span><span class="nx">encoding</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"UTF-8"</span><span class="p">;</span>
<span class="w">    </span><span class="nx">file</span><span class="p">.</span><span class="nx">open</span><span class="p">(</span><span class="s2">"w"</span><span class="p">);</span>
<span class="w">    </span><span class="nx">file</span><span class="p">.</span><span class="nx">write</span><span class="p">(</span><span class="nx">data</span><span class="p">);</span>
<span class="w">    </span><span class="nx">file</span><span class="p">.</span><span class="nx">close</span><span class="p">();</span>
<span class="w">  </span><span class="p">}</span><span class="w"> </span><span class="k">catch</span><span class="w"> </span><span class="p">(</span><span class="nx">e</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">alert</span><span class="p">(</span><span class="s2">"Error writing file "</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nx">file</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s2">"!"</span><span class="p">);</span>
<span class="w">  </span><span class="p">}</span>
<span class="p">}</span>
</code></pre></div>
<div class="callout">
<p>Normally, a script would do more than just write a preferences file. Usually, the selection(s) made by the user direct the next steps the script takes. In this case, that only means showing the selected preferences in an alert.</p>
</div>
<div class="codehilite"><pre><span></span><code><span class="nx">alert</span><span class="p">(</span>
<span class="w">  </span><span class="s2">"User Settings Saved!\nSelected Location: "</span><span class="w"> </span><span class="o">+</span>
<span class="w">    </span><span class="nx">settings</span><span class="p">.</span><span class="nx">location</span><span class="w"> </span><span class="o">+</span>
<span class="w">    </span><span class="s2">"\nSelected Product: "</span><span class="w"> </span><span class="o">+</span>
<span class="w">    </span><span class="nx">settings</span><span class="p">.</span><span class="nx">product</span>
<span class="p">);</span>
</code></pre></div>
<p><img alt="Save Preferences Alert" src="https://joshbduncan.com/static/images/save-preferences-adobe-extendscript-dialog-alert.png"/></p>
<h2>Final Thoughts</h2>
<p>This setup is just the base for more advanced functionality like I implemented in my <a href="https://github.com/joshbduncan/adobe-scripts/blob/main/ScreenSepMarks.jsx">Screen Print Separation Marks</a> script shown below. I hope to do a more in-depth post on this in the future.</p>
<p><img alt="Save Preferences From Dialog Advanced" src="https://joshbduncan.com/static/images/save-preferences-adobe-extendscript-dialog-advanced.png"/></p>
<p>View the <a href="https://github.com/joshbduncan/adobe-scripts/blob/main/snippets/savePrefsFromDialog.jsx">full script</a> on Github. ✌️</p>]]></content:encoded></item><item><title>Progress Spinners Using Python Generators and Context Managers</title><link>https://joshbduncan.com/python-progress-spinners-using-generators.html</link><pubDate>Fri, 01 Apr 2022 00:00:00 -0400</pubDate><guid>https://joshbduncan.com/python-progress-spinners-using-generators.html</guid><description><![CDATA[Making neat little progress spinners that let you know things are working in your cli apps is quite simple using Python generators and context manager.]]></description><content:encoded><![CDATA[<p>Often with long running scripts, it's nice to know that everything is working as it should. Now, I could easily just print to stdout...</p>
<div class="codehilite"><pre><span></span><code><span class="gp">&gt;&gt;&gt; </span><span class="k">for</span> <span class="n">step</span> <span class="ow">in</span> <span class="n">steps</span><span class="p">:</span>
<span class="gp">... </span>   <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">"processing </span><span class="si">{</span><span class="n">step</span><span class="si">}</span><span class="s2"> of </span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">steps</span><span class="p">)</span><span class="si">}</span><span class="s2">..."</span><span class="p">)</span>
<span class="gp">... </span>   <span class="n">long_running_process</span><span class="p">()</span>
<span class="go">processing step 1 of 300 steps</span>
<span class="go">processing step 2 of 300 steps</span>
<span class="go">...</span>
<span class="go">processing step 300 of 300 steps</span>
</code></pre></div>
<p>But that mucks up my terminal and causes a lot of scrolling.</p>
<p>So, below are three ways I've found to do this, from simple to over engineered... They all write to <a href="https://docs.python.org/3/library/sys.html#sys.stdout">stdout</a> using the <code>\r</code> carriage return so that they keep updating the same line and keep my terminal clean.</p>
<h2>Quick and Simple</h2>
<p>My first attempt at this utilized a basic generator that yields defined "ticks" which simulate a spinning line.</p>
<div class="codehilite"><pre><span></span><code><span class="kn">import</span><span class="w"> </span><span class="nn">sys</span>

<span class="k">def</span><span class="w"> </span><span class="nf">spinner</span><span class="p">(</span><span class="n">msg</span><span class="o">=</span><span class="s2">"working..."</span><span class="p">):</span>
    <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span>
    <span class="n">ticks</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"-"</span><span class="p">,</span> <span class="s2">"</span><span class="se">\\</span><span class="s2">"</span><span class="p">,</span> <span class="s2">"|"</span><span class="p">,</span> <span class="s2">"/"</span><span class="p">]</span>
    <span class="k">while</span> <span class="kc">True</span><span class="p">:</span>
        <span class="n">tick</span> <span class="o">=</span> <span class="n">ticks</span><span class="p">[</span><span class="n">i</span> <span class="o">%</span> <span class="nb">len</span><span class="p">(</span><span class="n">ticks</span><span class="p">)]</span>
        <span class="k">yield</span> <span class="n">sys</span><span class="o">.</span><span class="n">stdout</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="sa">f</span><span class="s2">"</span><span class="se">\r</span><span class="si">{</span><span class="n">tick</span><span class="si">}</span><span class="s2"> </span><span class="si">{</span><span class="n">msg</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
        <span class="n">i</span> <span class="o">+=</span> <span class="mi">1</span>
</code></pre></div>
<p>And I would setup and invoke the generator like below.</p>
<div class="codehilite"><pre><span></span><code><span class="n">jobs</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"sample job"</span><span class="p">,</span> <span class="s2">"another job"</span><span class="p">]</span>
<span class="k">for</span> <span class="n">job</span> <span class="ow">in</span> <span class="n">jobs</span><span class="p">:</span>
    <span class="n">s</span> <span class="o">=</span> <span class="n">spinner</span><span class="p">(</span><span class="sa">f</span><span class="s2">"processing </span><span class="si">{</span><span class="n">job</span><span class="si">}</span><span class="s2">..."</span><span class="p">)</span>
    <span class="k">try</span><span class="p">:</span>
        <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="n">long_running_iteration</span><span class="p">:</span>
            <span class="nb">next</span><span class="p">(</span><span class="n">s</span><span class="p">)</span>
    <span class="k">finally</span><span class="p">:</span>
        <span class="n">s</span><span class="o">.</span><span class="n">close</span><span class="p">()</span>
</code></pre></div>
<p><img alt="Simple Python Spinner" src="https://joshbduncan.com/static/images/spinner-simple-1.gif"/></p>
<p>This works, but I see two problems. First, the second job overwrites the first job, and once a job is done, the spinner just ends at the last yielded "tick" which isn't a good look.</p>
<h3>Exiting The Generator</h3>
<p>To fix both issues, I'm going to catch the <a href="https://docs.python.org/3/library/exceptions.html#GeneratorExit">GeneratorExit</a> exception, then place a "✔" before the completed job and jump down a line so that any following output doesn't overwrite the previous output.</p>
<div class="codehilite"><pre><span></span><code><span class="kn">import</span><span class="w"> </span><span class="nn">sys</span>

<span class="k">def</span><span class="w"> </span><span class="nf">spinner</span><span class="p">(</span><span class="n">msg</span><span class="o">=</span><span class="s2">"working..."</span><span class="p">):</span>
    <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span>
    <span class="n">ticks</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"-"</span><span class="p">,</span> <span class="s2">"</span><span class="se">\\</span><span class="s2">"</span><span class="p">,</span> <span class="s2">"|"</span><span class="p">,</span> <span class="s2">"/"</span><span class="p">]</span>
    <span class="k">try</span><span class="p">:</span>
        <span class="k">while</span> <span class="kc">True</span><span class="p">:</span>
            <span class="n">tick</span> <span class="o">=</span> <span class="n">ticks</span><span class="p">[</span><span class="n">i</span> <span class="o">%</span> <span class="nb">len</span><span class="p">(</span><span class="n">ticks</span><span class="p">)]</span>
            <span class="k">yield</span> <span class="n">sys</span><span class="o">.</span><span class="n">stdout</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="sa">f</span><span class="s2">"</span><span class="se">\r</span><span class="si">{</span><span class="n">tick</span><span class="si">}</span><span class="s2"> </span><span class="si">{</span><span class="n">msg</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
            <span class="n">i</span> <span class="o">+=</span> <span class="mi">1</span>
    <span class="k">except</span> <span class="ne">GeneratorExit</span><span class="p">:</span>
        <span class="n">sys</span><span class="o">.</span><span class="n">stdout</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="s2">"</span><span class="se">\r</span><span class="s2">✔</span><span class="se">\n</span><span class="s2">"</span><span class="p">)</span>
    <span class="k">finally</span><span class="p">:</span>
        <span class="n">sys</span><span class="o">.</span><span class="n">stdout</span><span class="o">.</span><span class="n">flush</span><span class="p">()</span>
</code></pre></div>
<p><img alt="Simple Python Spinner" src="https://joshbduncan.com/static/images/spinner-simple-2.gif"/></p>
<h3>Catching Errors</h3>
<p>But what happens when there is an exception?</p>
<div class="codehilite"><pre><span></span><code><span class="gp">$ </span>python<span class="w"> </span>spinner_simple.py
<span class="go">✔ processing sample job...</span>
<span class="go">✔ processing another job...</span>
<span class="go">✔ processing job with an exception...</span>
<span class="go">Traceback (most recent call last):</span>
<span class="go">  File "/Users/jbd/Dropbox/DEV/projects/progress-spinner/spinner_simple.py", line 31, in &lt;module&gt;</span>
<span class="go">    raise ValueError("test exception")</span>
<span class="go">ValueError: test exception</span>
</code></pre></div>
<p>As you can see, even though there was an exception while processing the third job, my spinner still put a "✔" and that's not right.</p>
<p>To fix this, I need to now catch any Exception that is not a GeneratorExit. This allows me to put an "𝘅" at the start of the line for the job that errored and still provide the exception information.</p>
<div class="codehilite"><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">spinner</span><span class="p">(</span><span class="n">msg</span><span class="o">=</span><span class="s2">"working..."</span><span class="p">):</span>
    <span class="o">...</span>
    <span class="k">except</span> <span class="ne">Exception</span><span class="p">:</span>
        <span class="n">sys</span><span class="o">.</span><span class="n">stdout</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="s2">"</span><span class="se">\r</span><span class="s2">𝘅</span><span class="se">\n</span><span class="s2">"</span><span class="p">)</span>
        <span class="k">raise</span>
    <span class="k">except</span> <span class="ne">GeneratorExit</span><span class="p">:</span>
        <span class="n">sys</span><span class="o">.</span><span class="n">stdout</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="s2">"</span><span class="se">\r</span><span class="s2">✔</span><span class="se">\n</span><span class="s2">"</span><span class="p">)</span>
    <span class="o">...</span>
</code></pre></div>
<p><img alt="Simple Python Spinner" src="https://joshbduncan.com/static/images/spinner-simple-3.gif"/></p>
<p>That looks much better!</p>
<p>Checkout the full <a href="https://gist.github.com/joshbduncan/a6a01583292cc7564236a3a1898ce173">Simple Spinner</a> script on Github.</p>
<h2>Context Manager</h2>
<p>Building on the simple example, I can easily create a <a href="https://docs.python.org/3/library/contextlib.html#contextlib.contextmanager">context manager</a> that generates a progress spinner while being used.</p>
<p>First, I can pull the generator out as a separate function...</p>
<div class="codehilite"><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">spinner_indicator</span><span class="p">(</span><span class="n">msg</span><span class="p">):</span>
    <span class="n">ticks</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"-"</span><span class="p">,</span> <span class="s2">"</span><span class="se">\\</span><span class="s2">"</span><span class="p">,</span> <span class="s2">"|"</span><span class="p">,</span> <span class="s2">"/"</span><span class="p">]</span>
    <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span>
    <span class="k">while</span> <span class="kc">True</span><span class="p">:</span>
        <span class="n">tick</span> <span class="o">=</span> <span class="n">ticks</span><span class="p">[</span><span class="n">i</span> <span class="o">%</span> <span class="nb">len</span><span class="p">(</span><span class="n">ticks</span><span class="p">)]</span>
        <span class="k">yield</span> <span class="n">sys</span><span class="o">.</span><span class="n">stdout</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="sa">f</span><span class="s2">"</span><span class="se">\r</span><span class="si">{</span><span class="n">tick</span><span class="si">}</span><span class="s2"> </span><span class="si">{</span><span class="n">msg</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
        <span class="n">i</span> <span class="o">+=</span> <span class="mi">1</span>
</code></pre></div>
<p>Then, I can and handle all of the setup, exception catching, and tear down inside of the context manager and only call the generator when I need it.</p>
<div class="codehilite"><pre><span></span><code><span class="nd">@contextmanager</span>
<span class="k">def</span><span class="w"> </span><span class="nf">spinner</span><span class="p">(</span><span class="n">msg</span><span class="o">=</span><span class="s2">"working..."</span><span class="p">):</span>
    <span class="n">error</span> <span class="o">=</span> <span class="kc">False</span>
    <span class="k">try</span><span class="p">:</span>
        <span class="k">yield</span> <span class="n">spinner_indicator</span><span class="p">(</span><span class="n">msg</span><span class="p">)</span>
    <span class="k">except</span> <span class="ne">Exception</span><span class="p">:</span>
        <span class="n">error</span> <span class="o">=</span> <span class="kc">True</span>
        <span class="k">raise</span>
    <span class="k">except</span> <span class="ne">GeneratorExit</span><span class="p">:</span>
        <span class="n">sys</span><span class="o">.</span><span class="n">stdout</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="s2">"</span><span class="se">\r</span><span class="s2">✔</span><span class="se">\n</span><span class="s2">"</span><span class="p">)</span>
    <span class="k">finally</span><span class="p">:</span>
        <span class="k">if</span> <span class="n">error</span><span class="p">:</span>
            <span class="n">sys</span><span class="o">.</span><span class="n">stdout</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="s2">"</span><span class="se">\r</span><span class="s2">𝘅</span><span class="se">\n</span><span class="s2">"</span><span class="p">)</span>
        <span class="k">else</span><span class="p">:</span>
            <span class="n">sys</span><span class="o">.</span><span class="n">stdout</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="s2">"</span><span class="se">\r</span><span class="s2">✔</span><span class="se">\n</span><span class="s2">"</span><span class="p">)</span>
        <span class="n">sys</span><span class="o">.</span><span class="n">stdout</span><span class="o">.</span><span class="n">flush</span><span class="p">()</span>
</code></pre></div>
<p>This version allows me to do something like below.</p>
<div class="codehilite"><pre><span></span><code><span class="n">jobs</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"sample job"</span><span class="p">,</span> <span class="s2">"another job"</span><span class="p">,</span> <span class="s2">"job with an exception"</span><span class="p">]</span>
<span class="k">for</span> <span class="n">job</span> <span class="ow">in</span> <span class="n">jobs</span><span class="p">:</span>
    <span class="k">with</span> <span class="n">spinner</span><span class="p">(</span><span class="sa">f</span><span class="s2">"processing </span><span class="si">{</span><span class="n">job</span><span class="si">}</span><span class="s2">..."</span><span class="p">)</span> <span class="k">as</span> <span class="n">s</span><span class="p">:</span>
        <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="n">long_running_iteration</span><span class="p">:</span>
            <span class="nb">next</span><span class="p">(</span><span class="n">s</span><span class="p">)</span>
</code></pre></div>
<p>Not necessarily any better than the simple option, just a different solution.</p>
<p><img alt="Spinner Context Manager" src="https://joshbduncan.com/static/images/spinner-ctx-manager.gif"/></p>
<p>Checkout the full <a href="https://gist.github.com/joshbduncan/d47726aa351e467d849748e3de607943">Spinner Context Manager </a> script on Github.</p>
<h2>Over The Top</h2>
<p>Now, to get extra fancy, I'm going combine what I did above into a Python <a href="https://docs.python.org/3/tutorial/classes.html">Class</a>. The Spinner class will allow me to choose different spinners and also allow me to update the progress message while running.</p>
<h3>Spinners Galore</h3>
<p>There are tons of spinners you can come up with but here are the four I'm going to have available for use.</p>
<div class="codehilite"><pre><span></span><code><span class="n">spinners</span> <span class="o">=</span> <span class="p">{</span>
    <span class="s2">"angles"</span><span class="p">:</span> <span class="p">[</span><span class="s2">"◢"</span><span class="p">,</span> <span class="s2">"◣"</span><span class="p">,</span> <span class="s2">"◤"</span><span class="p">,</span> <span class="s2">"◥"</span><span class="p">],</span>
    <span class="s2">"circle"</span><span class="p">:</span> <span class="p">[</span><span class="s2">"◜"</span><span class="p">,</span> <span class="s2">"◠"</span><span class="p">,</span> <span class="s2">"◝"</span><span class="p">,</span> <span class="s2">"◞"</span><span class="p">,</span> <span class="s2">"◡"</span><span class="p">,</span> <span class="s2">"◟"</span><span class="p">],</span>
    <span class="s2">"dots"</span><span class="p">:</span> <span class="p">[</span><span class="s2">"⠋"</span><span class="p">,</span> <span class="s2">"⠙"</span><span class="p">,</span> <span class="s2">"⠹"</span><span class="p">,</span> <span class="s2">"⠸"</span><span class="p">,</span> <span class="s2">"⠼"</span><span class="p">,</span> <span class="s2">"⠴"</span><span class="p">,</span> <span class="s2">"⠦"</span><span class="p">,</span> <span class="s2">"⠧"</span><span class="p">,</span> <span class="s2">"⠇"</span><span class="p">,</span> <span class="s2">"⠏"</span><span class="p">],</span>
    <span class="s2">"ticks"</span><span class="p">:</span> <span class="p">[</span><span class="s2">"-"</span><span class="p">,</span> <span class="s2">"</span><span class="se">\\</span><span class="s2">"</span><span class="p">,</span> <span class="s2">"|"</span><span class="p">,</span> <span class="s2">"/"</span><span class="p">],</span>
<span class="p">}</span>
</code></pre></div>
<h3>Class Structure</h3>
<p>I have setup the Spinner class so that it can be used both as a standard generator or a context manager.</p>
<div class="codehilite"><pre><span></span><code><span class="k">class</span><span class="w"> </span><span class="nc">Spinner</span><span class="p">:</span>
    <span class="k">def</span><span class="w"> </span><span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">style</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">text</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span>
        <span class="k">if</span> <span class="n">style</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
            <span class="n">style</span> <span class="o">=</span> <span class="s2">"ticks"</span>
        <span class="k">if</span> <span class="n">text</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
            <span class="n">text</span> <span class="o">=</span> <span class="s2">"Processing..."</span>
        <span class="bp">self</span><span class="o">.</span><span class="n">style</span> <span class="o">=</span> <span class="n">style</span>
        <span class="bp">self</span><span class="o">.</span><span class="n">text</span> <span class="o">=</span> <span class="n">text</span>
        <span class="bp">self</span><span class="o">.</span><span class="n">_error</span> <span class="o">=</span> <span class="kc">False</span>
        <span class="bp">self</span><span class="o">.</span><span class="n">spinner</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">frame_generator</span><span class="p">()</span>

    <span class="k">def</span><span class="w"> </span><span class="fm">__enter__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="k">return</span> <span class="bp">self</span>

    <span class="k">def</span><span class="w"> </span><span class="fm">__exit__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">exc_type</span><span class="p">,</span> <span class="n">exc_value</span><span class="p">,</span> <span class="n">traceback</span><span class="p">):</span>
        <span class="k">if</span> <span class="n">exc_type</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
            <span class="n">sys</span><span class="o">.</span><span class="n">stdout</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="s2">"</span><span class="se">\r</span><span class="s2">𝘅</span><span class="se">\n</span><span class="s2">"</span><span class="p">)</span>
        <span class="k">else</span><span class="p">:</span>
            <span class="n">sys</span><span class="o">.</span><span class="n">stdout</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="s2">"</span><span class="se">\r</span><span class="s2">✔</span><span class="se">\n</span><span class="s2">"</span><span class="p">)</span>
        <span class="bp">self</span><span class="o">.</span><span class="n">spinner</span><span class="o">.</span><span class="n">close</span><span class="p">()</span>
        <span class="bp">self</span><span class="o">.</span><span class="n">_cleanup</span><span class="p">()</span>

    <span class="k">def</span><span class="w"> </span><span class="nf">frame_generator</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="n">frames</span> <span class="o">=</span> <span class="n">spinners</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">style</span><span class="p">]</span>
        <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span>
        <span class="k">while</span> <span class="kc">True</span><span class="p">:</span>
            <span class="k">yield</span> <span class="n">frames</span><span class="p">[</span><span class="n">i</span> <span class="o">%</span> <span class="nb">len</span><span class="p">(</span><span class="n">frames</span><span class="p">)]</span>
            <span class="n">i</span> <span class="o">+=</span> <span class="mi">1</span>

    <span class="k">def</span><span class="w"> </span><span class="nf">update</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">text</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span>
        <span class="k">if</span> <span class="n">text</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
            <span class="bp">self</span><span class="o">.</span><span class="n">text</span> <span class="o">=</span> <span class="n">text</span>
        <span class="n">sys</span><span class="o">.</span><span class="n">stdout</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="sa">f</span><span class="s2">"</span><span class="se">\r</span><span class="si">{</span><span class="nb">next</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">spinner</span><span class="p">)</span><span class="si">}</span><span class="s2"> </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">text</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>

    <span class="k">def</span><span class="w"> </span><span class="nf">error</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">error</span><span class="p">):</span>
        <span class="bp">self</span><span class="o">.</span><span class="n">_error</span> <span class="o">=</span> <span class="kc">True</span>
        <span class="n">sys</span><span class="o">.</span><span class="n">stdout</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="s2">"</span><span class="se">\r</span><span class="s2">𝘅</span><span class="se">\n</span><span class="s2">"</span><span class="p">)</span>
        <span class="k">raise</span>

    <span class="k">def</span><span class="w"> </span><span class="nf">close</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="k">if</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">_error</span><span class="p">:</span>
            <span class="n">sys</span><span class="o">.</span><span class="n">stdout</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="s2">"</span><span class="se">\r</span><span class="s2">✔</span><span class="se">\n</span><span class="s2">"</span><span class="p">)</span>
        <span class="bp">self</span><span class="o">.</span><span class="n">spinner</span><span class="o">.</span><span class="n">close</span><span class="p">()</span>
        <span class="bp">self</span><span class="o">.</span><span class="n">_cleanup</span><span class="p">()</span>

    <span class="nd">@staticmethod</span>
    <span class="k">def</span><span class="w"> </span><span class="nf">_cleanup</span><span class="p">():</span>
        <span class="n">sys</span><span class="o">.</span><span class="n">stdout</span><span class="o">.</span><span class="n">flush</span><span class="p">()</span>
</code></pre></div>
<div class="callout">
<p>You'll notice a few special dunder methods above, like <code>__enter__</code> and <code>__exit__</code>. These allow the Spinner class to be used as a <a href="https://docs.python.org/3/reference/datamodel.html#with-statement-context-managers">context manager</a>.</p>
</div>
<h4>Using Spinner As A Simple Generator</h4>
<p>The most basic usage of the Spinner class is to create an instance of it and then update it each iteration. You can specify the spinner style and the spinner text.</p>
<div class="codehilite"><pre><span></span><code><span class="kn">import</span><span class="w"> </span><span class="nn">time</span>

<span class="n">jobs</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"dots spinner"</span><span class="p">,</span> <span class="s2">"spinner with exception"</span><span class="p">]</span>
<span class="k">for</span> <span class="n">job</span> <span class="ow">in</span> <span class="n">jobs</span><span class="p">:</span>
    <span class="n">style</span> <span class="o">=</span> <span class="n">job</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">" "</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span> <span class="k">if</span> <span class="s2">"exception"</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">job</span> <span class="k">else</span> <span class="kc">None</span>
    <span class="n">s</span> <span class="o">=</span> <span class="n">Spinner</span><span class="p">(</span><span class="n">style</span><span class="o">=</span><span class="n">style</span><span class="p">,</span> <span class="n">text</span><span class="o">=</span><span class="n">job</span><span class="p">)</span>
    <span class="n">steps</span> <span class="o">=</span> <span class="mi">10</span>
    <span class="k">try</span><span class="p">:</span>
        <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">steps</span><span class="p">):</span>
            <span class="n">s</span><span class="o">.</span><span class="n">update</span><span class="p">()</span>
            <span class="n">time</span><span class="o">.</span><span class="n">sleep</span><span class="p">(</span><span class="mf">0.25</span><span class="p">)</span>
            <span class="k">if</span> <span class="s2">"exception"</span> <span class="ow">in</span> <span class="n">job</span> <span class="ow">and</span> <span class="n">i</span> <span class="o">==</span> <span class="mi">6</span><span class="p">:</span>
                <span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="s2">"test exception"</span><span class="p">)</span>
    <span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
        <span class="n">s</span><span class="o">.</span><span class="n">error</span><span class="p">(</span><span class="n">e</span><span class="p">)</span>
    <span class="k">finally</span><span class="p">:</span>
        <span class="n">s</span><span class="o">.</span><span class="n">close</span><span class="p">()</span>
</code></pre></div>
<p><img alt="Python Spinner Class" src="https://joshbduncan.com/static/images/spinner-object-3.gif"/></p>
<p>You could even update the text at a special place in your job progress.</p>
<div class="codehilite"><pre><span></span><code><span class="o">...</span>
<span class="k">if</span> <span class="n">i</span> <span class="o">==</span> <span class="n">steps</span> <span class="o">//</span> <span class="mi">2</span><span class="p">:</span>
    <span class="n">s</span><span class="o">.</span><span class="n">update</span><span class="p">(</span><span class="s2">"half way through"</span><span class="p">)</span>
<span class="o">...</span>
</code></pre></div>
<h4>Using Spinner As A Context Manager</h4>
<p>The Spinner class can also be implemented as a context manager using the standard <code>with</code> syntax.</p>
<div class="codehilite"><pre><span></span><code><span class="k">with</span> <span class="n">Spinner</span><span class="p">()</span> <span class="k">as</span> <span class="n">spinner</span><span class="p">:</span>
    <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="n">long_running_iteration</span><span class="p">:</span>
        <span class="n">spinner</span><span class="o">.</span><span class="n">update</span><span class="p">()</span>
</code></pre></div>
<p><img alt="Python Spinner Class" src="https://joshbduncan.com/static/images/spinner-object-1.gif"/></p>
<p>And, just as above, you can update the spinner text as you go...</p>
<div class="codehilite"><pre><span></span><code><span class="n">jobs</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"angles spinner"</span><span class="p">,</span> <span class="s2">"circle spinner"</span><span class="p">]</span>
<span class="k">for</span> <span class="n">job</span> <span class="ow">in</span> <span class="n">jobs</span><span class="p">:</span>
    <span class="n">style</span> <span class="o">=</span> <span class="n">job</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">" "</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span>
    <span class="k">with</span> <span class="n">Spinner</span><span class="p">(</span><span class="n">style</span><span class="o">=</span><span class="n">style</span><span class="p">)</span> <span class="k">as</span> <span class="n">spinner</span><span class="p">:</span>
        <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="n">long_running_iteration</span><span class="p">:</span>
            <span class="n">spinner</span><span class="o">.</span><span class="n">update</span><span class="p">(</span><span class="sa">f</span><span class="s2">"processing </span><span class="si">{</span><span class="n">job</span><span class="si">}</span><span class="s2"> (step </span><span class="si">{</span><span class="n">i</span><span class="si">}</span><span class="s2"> of </span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">long_running_iteration</span><span class="p">)</span><span class="si">}</span><span class="s2">)..."</span><span class="p">)</span>
</code></pre></div>
<p><img alt="Python Spinner Class" src="https://joshbduncan.com/static/images/spinner-object-2.gif"/></p>
<p>Checkout the <a href="https://gist.github.com/joshbduncan/0df382217b6ab761f106e45479708b6c">full script</a> on Github.</p>
<h2>Final Thoughts</h2>
<p>There are much better solutions to this problem like <a href="https://github.com/tqdm/tqdm">tqdm</a>, but since I'm still learning, I like to try and figure out how to do things myself. I definitely learn something each time I do.</p>
<p>Cheers 🍻</p>]]></content:encoded></item><item><title>Search &amp; Filter HTML Table Data With JavaScript</title><link>https://joshbduncan.com/search-and-filter-html-table-data-with-javascript.html</link><pubDate>Sun, 06 Mar 2022 00:00:00 -0500</pubDate><guid>https://joshbduncan.com/search-and-filter-html-table-data-with-javascript.html</guid><description><![CDATA[Searching through HTML tables is super simple using  Vanilla JavaScript, a few query selectors, and some neat methods.]]></description><content:encoded><![CDATA[<p>Recently, after building a quick and dirty database reporting portal, a few users requested the ability to easily search/filter the table live on the webpage. Turns out, this is super simple using  Vanilla JavaScript, a few query selectors, and some neat JavaScript methods.</p>
<h2>The Problem</h2>
<p>When working with HTML tables that have lots of rows, finding something particular can be a struggle that involves a lot of scrolling. And, not everyone knows you can do Find within the browser.</p>
<h2>The Simple Solution</h2>
<p>My first attempt was basic. I typically  start all projects and scripts this way... Start simple, get it working, then add features and/or complexity.</p>
<p>Here's the sample HTML table I'll work from...</p>
<div class="codehilite"><pre><span></span><code><span class="p">&lt;</span><span class="nt">table</span> <span class="na">id</span><span class="o">=</span><span class="s">"search-table"</span><span class="p">&gt;</span>
  <span class="p">&lt;</span><span class="nt">thead</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">tr</span><span class="p">&gt;</span>
      <span class="p">&lt;</span><span class="nt">th</span><span class="p">&gt;</span>Company<span class="p">&lt;/</span><span class="nt">th</span><span class="p">&gt;</span>
      <span class="p">&lt;</span><span class="nt">th</span><span class="p">&gt;</span>Contact<span class="p">&lt;/</span><span class="nt">th</span><span class="p">&gt;</span>
      <span class="p">&lt;</span><span class="nt">th</span><span class="p">&gt;</span>Country<span class="p">&lt;/</span><span class="nt">th</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="nt">tr</span><span class="p">&gt;</span>
  <span class="p">&lt;/</span><span class="nt">thead</span><span class="p">&gt;</span>
  <span class="p">&lt;</span><span class="nt">tbody</span><span class="p">&gt;</span>
   <span class="p">&lt;</span><span class="nt">tr</span><span class="p">&gt;</span>
      <span class="p">&lt;</span><span class="nt">td</span><span class="p">&gt;</span>Apple<span class="p">&lt;/</span><span class="nt">td</span><span class="p">&gt;</span>
      <span class="p">&lt;</span><span class="nt">td</span><span class="p">&gt;</span>Steve Jobs<span class="p">&lt;/</span><span class="nt">td</span><span class="p">&gt;</span>
      <span class="p">&lt;</span><span class="nt">td</span><span class="p">&gt;</span>USA<span class="p">&lt;/</span><span class="nt">td</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="nt">tr</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">tr</span><span class="p">&gt;</span>
      <span class="p">&lt;</span><span class="nt">td</span><span class="p">&gt;</span>Microsoft<span class="p">&lt;/</span><span class="nt">td</span><span class="p">&gt;</span>
      <span class="p">&lt;</span><span class="nt">td</span><span class="p">&gt;</span>Bill Gates<span class="p">&lt;/</span><span class="nt">td</span><span class="p">&gt;</span>
      <span class="p">&lt;</span><span class="nt">td</span><span class="p">&gt;</span>USA<span class="p">&lt;/</span><span class="nt">td</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="nt">tr</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">tr</span><span class="p">&gt;</span>
      <span class="p">&lt;</span><span class="nt">td</span><span class="p">&gt;</span>Tesla<span class="p">&lt;/</span><span class="nt">td</span><span class="p">&gt;</span>
      <span class="p">&lt;</span><span class="nt">td</span><span class="p">&gt;</span>Elon Musk<span class="p">&lt;/</span><span class="nt">td</span><span class="p">&gt;</span>
      <span class="p">&lt;</span><span class="nt">td</span><span class="p">&gt;</span>Mars<span class="p">&lt;/</span><span class="nt">td</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="nt">tr</span><span class="p">&gt;</span>
  <span class="p">&lt;/</span><span class="nt">tbody</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">table</span><span class="p">&gt;</span>
</code></pre></div>
<div class="callout">
<p>Need a refresher on HTML tables? <a href="https://www.w3schools.com/html/html_tables.asp">Check this out</a>.</p>
</div>
<h3>Setup</h3>
<p>First, I'll need an input field with the id <code>search-input</code> for the user query. Then I'll add the id <code>search-input</code> to the table I want to filter. These ids make it easy to select the correct elements in my script. </p>
<div class="codehilite"><pre><span></span><code><span class="p">&lt;</span><span class="nt">input</span> <span class="na">class</span><span class="o">=</span><span class="s">"form-control"</span> <span class="na">type</span><span class="o">=</span><span class="s">"text"</span> <span class="na">id</span><span class="o">=</span><span class="s">"search-input"</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">table</span> <span class="na">id</span><span class="o">=</span><span class="s">"search-table"</span><span class="p">&gt;</span>
  ...
<span class="p">&lt;/</span><span class="nt">table</span><span class="p">&gt;</span>
</code></pre></div>
<p>Now, I can select those two elements inside of my script along with the rows in the table.</p>
<div class="codehilite"><pre><span></span><code><span class="kd">const</span><span class="w"> </span><span class="nx">searchInput</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="s2">"search-input"</span><span class="p">);</span>
<span class="kd">const</span><span class="w"> </span><span class="nx">searchTable</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="s2">"search-table"</span><span class="p">);</span>
<span class="kd">const</span><span class="w"> </span><span class="nx">rows</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">searchTable</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s2">"tbody tr"</span><span class="p">);</span>
</code></pre></div>
<p>I will also go ahead and setup a custom CSS selector to make it easy to hide any rows that don't match the search query.</p>
<div class="codehilite"><pre><span></span><code><span class="p">.</span><span class="nc">hide</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="k">display</span><span class="p">:</span><span class="w"> </span><span class="kc">none</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>
<h3>Input Listener</h3>
<p>Now that I have everything set up and all key elements selected, I need to listen for input in the <code>search-input</code> field.</p>
<div class="codehilite"><pre><span></span><code><span class="nx">searchInput</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s2">"input"</span><span class="p">,</span><span class="w"> </span><span class="kd">function</span><span class="w"> </span><span class="p">(</span><span class="nx">e</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="kd">const</span><span class="w"> </span><span class="nx">q</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">value</span><span class="p">.</span><span class="nx">toLowerCase</span><span class="p">();</span><span class="w">  </span>
<span class="p">}</span>
</code></pre></div>
<p>And, to make searching through the table easier, I converted the user query to lower case using <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toLowerCase">toLowerCase()</a>.</p>
<h3>Iterate Over Every Row</h3>
<p>With the user input set to variable <code>q</code>, I can iterate over each row using the <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach">.forEach()</a> function to check and see if it contains <code>q</code>.</p>
<div class="codehilite"><pre><span></span><code><span class="nx">rows</span><span class="p">.</span><span class="nx">forEach</span><span class="p">((</span><span class="nx">row</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="c1">// do something with each row</span>
<span class="p">});</span>
</code></pre></div>
<h3>Checking The Text</h3>
<p>How do I check all of the text in the row? The simplest solution I could come up with was to combine the text from each cell in the row into a single string. To do that, I grab each <code>td</code> element in the row using <code>row.querySelectorAll("td")</code>.</p>
<p>Now that I have a NodeList of each cell in the row, I can <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map">map</a> the textContent of each cell to a new array called <code>matchString</code>.</p>
<div class="codehilite"><pre><span></span><code><span class="nx">rows</span><span class="p">.</span><span class="nx">forEach</span><span class="p">((</span><span class="nx">row</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="kd">const</span><span class="w"> </span><span class="nx">cells</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">Array</span><span class="p">.</span><span class="kr">from</span><span class="p">(</span><span class="nx">row</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s2">"td"</span><span class="p">));</span>
<span class="w">  </span><span class="kd">const</span><span class="w"> </span><span class="nx">matchString</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">cells</span><span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">n</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="nx">n</span><span class="p">.</span><span class="nx">textContent</span><span class="p">.</span><span class="nx">toLowerCase</span><span class="p">()).</span><span class="nx">join</span><span class="p">(</span><span class="s2">" "</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div>
<div class="callout">
<p>I used the <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from">Array.from()</a> method to convert the NodeList into an array that JavaScript can apply <code>map()</code> to. This was a new trick I learned during this project.</p>
</div>
<p>You'll notice I did two extra things that I didn't mention above. First, I converted the entire string to lower case to match what I did on the user input. Second I <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/join">joined</a> the resulting array so that I get a single string of all the content separated by the space character.</p>
<p>Now, I can use the <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/includes">includes()</a> method to check whether the user input is found anywhere in the row.</p>
<div class="codehilite"><pre><span></span><code><span class="nx">rows</span><span class="p">.</span><span class="nx">forEach</span><span class="p">((</span><span class="nx">row</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="kd">const</span><span class="w"> </span><span class="nx">cells</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">Array</span><span class="p">.</span><span class="kr">from</span><span class="p">(</span><span class="nx">row</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s2">"td"</span><span class="p">));</span>
<span class="w">  </span><span class="kd">const</span><span class="w"> </span><span class="nx">matchString</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">cells</span><span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">n</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="nx">n</span><span class="p">.</span><span class="nx">textContent</span><span class="p">.</span><span class="nx">toLowerCase</span><span class="p">()).</span><span class="nx">join</span><span class="p">(</span><span class="s2">" "</span><span class="p">);</span>
<span class="w">  </span><span class="kd">const</span><span class="w"> </span><span class="nx">match</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">matchString</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="nx">q</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div>
<h3>CSS To The Rescue</h3>
<p>Last step, is to hide the rows that don't match using the <code>.hide</code> class I created in the beginning.</p>
<p>Using the <a href="https://developer.mozilla.org/en-US/docs/Web/API/DOMTokenList/toggle">toggle()</a> method, I can add or remove "toggle" a class on any element based off of a variable. I am using the <code>match</code> variable here. So, if the row doesn't match the query (<code>!match</code>) then toggle on the <code>.hide</code> class, if it does match, then toggle off the <code>.hide</code> class.</p>
<p>Best part is, this helps me remove the class when the user query changes and a row now matches that previously didn't.</p>
<div class="codehilite"><pre><span></span><code><span class="nx">rows</span><span class="p">.</span><span class="nx">forEach</span><span class="p">((</span><span class="nx">row</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="kd">const</span><span class="w"> </span><span class="nx">cells</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">Array</span><span class="p">.</span><span class="kr">from</span><span class="p">(</span><span class="nx">row</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s2">"td"</span><span class="p">));</span>
<span class="w">  </span><span class="kd">const</span><span class="w"> </span><span class="nx">matchString</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">cells</span><span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">n</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="nx">n</span><span class="p">.</span><span class="nx">textContent</span><span class="p">.</span><span class="nx">toLowerCase</span><span class="p">()).</span><span class="nx">join</span><span class="p">(</span><span class="s2">" "</span><span class="p">);</span>
<span class="w">  </span><span class="kd">const</span><span class="w"> </span><span class="nx">match</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">matchString</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="nx">q</span><span class="p">);</span>
<span class="w">  </span><span class="nx">row</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nx">toggle</span><span class="p">(</span><span class="s2">"hide"</span><span class="p">,</span><span class="w"> </span><span class="o">!</span><span class="nx">match</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div>
<p>As is, this script works great for a simple page. Just embed the script at the bottom of a webpage and go on about your day.</p>
<p><a href="https://codepen.io/joshbduncan/pen/wvPYRJb">Try it out.</a></p>
<p>But I needed a little more control...</p>
<h2>The Fancy Solution</h2>
<p>The portal I was building had lots of pages, and posed two main challenges to the simple script above.</p>
<ol>
<li>Some pages didn't have any tables or an input causing a console error.</li>
<li>Some tables had columns that needed to be ignored when filtering.</li>
</ol>
<h3>No Errors For Me</h3>
<p>Getting rid of the script querySelector error is simple. Just wrap everything except the first line in an if statement.</p>
<div class="codehilite"><pre><span></span><code><span class="kd">const</span><span class="w"> </span><span class="nx">searchInput</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="s2">"search-input"</span><span class="p">);</span>
<span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">searchInput</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">null</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="p">...</span>
<span class="p">}</span>
</code></pre></div>
<p>So, if there is no search input just stop, as there is no need to continue.</p>
<h3>Ignoring Columns</h3>
<p>Ignoring columns isn't a hard feature addition, I just couldn't decide how I wanted to go about it. I definitely knew I wanted to use <a href="https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes">data-attributes</a>...</p>
<p>So, I decided that <code>data-attribute-search-ignore-cols</code> would be a comma separated list of index ids (<a href="https://en.wikipedia.org/wiki/Zero-based_numbering">zero based</a>) for the columns that should be ignored (e.g. <code>data-attribute-search-ignore-cols="0,2,7"</code>)</p>
<div class="callout">
<p>FYI, JavaScript removes the "-" in data-attributes names and coverts the string to camel case <a href="https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes#javascript_access">read more</a>. So <code>data-attribute-sample-name-with-dashes</code> becomes <code>sampleNameWithDashes</code>.</p>
</div>
<div class="codehilite"><pre><span></span><code><span class="kd">const</span><span class="w"> </span><span class="nx">ignoreCols</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">searchTable</span><span class="p">.</span><span class="nx">dataset</span><span class="p">.</span><span class="nx">searchIgnoreCols</span>
<span class="w">  </span><span class="o">?</span><span class="w"> </span><span class="nx">searchTable</span><span class="p">.</span><span class="nx">dataset</span><span class="p">.</span><span class="nx">searchIgnoreCols</span><span class="p">.</span><span class="nx">split</span><span class="p">(</span><span class="s2">","</span><span class="p">).</span><span class="nx">map</span><span class="p">(</span><span class="nb">Number</span><span class="p">)</span>
<span class="w">  </span><span class="o">:</span><span class="w"> </span><span class="kc">false</span><span class="p">;</span>
</code></pre></div>
<p>The above allows me to grab the data-attribute if it is present (doesn't have to be if I want all columns searchable), and convert each column id to an integer using the <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map">map</a> method.</p>
<p>Now, while iterating over each row and grabbing the textContent, I can check and make sure if any columns should be ignored using the same <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from">Array.from()</a> method as above and also paring it with the <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter">filter()</a> method this time.</p>
<div class="codehilite"><pre><span></span><code><span class="kd">const</span><span class="w"> </span><span class="nx">filteredCols</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">ignoreCols</span>
<span class="w">  </span><span class="o">?</span><span class="w"> </span><span class="nb">Array</span><span class="p">.</span><span class="kr">from</span><span class="p">(</span><span class="nx">cols</span><span class="p">).</span><span class="nx">filter</span><span class="p">((</span><span class="nx">_</span><span class="p">,</span><span class="w"> </span><span class="nx">n</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="nx">ignoreCols</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="o">!</span><span class="nx">ignoreCols</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="nx">n</span><span class="p">))</span>
<span class="w">  </span><span class="o">:</span><span class="w"> </span><span class="nb">Array</span><span class="p">.</span><span class="kr">from</span><span class="p">(</span><span class="nx">cols</span><span class="p">);</span>
</code></pre></div>
<div class="callout">
<p>I mostly program in Python, so maps and filters are a bit new to me but they are very similar to list comprehensions once you get your head around them.</p>
</div>
<p>This filter says, if data-attribute-search-ignore-cols was specified and if the column id <code>n</code> is in that array, don't include it in the <code>filteredCols</code> array.</p>
<h3>The Home Stretch</h3>
<p>With "non-ignored" searchable text now in the array <code>filteredCols</code> we can join everything into a string just as before.</p>
<div class="codehilite"><pre><span></span><code><span class="kd">const</span><span class="w"> </span><span class="nx">colsText</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">filteredCols</span>
<span class="w">  </span><span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">n</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="nx">n</span><span class="p">.</span><span class="nx">textContent</span><span class="p">.</span><span class="nx">toLowerCase</span><span class="p">())</span>
<span class="w">  </span><span class="p">.</span><span class="nx">join</span><span class="p">(</span><span class="s2">" "</span><span class="p">);</span>
</code></pre></div>
<p>And, then check to see if the query is contained within the string.</p>
<div class="codehilite"><pre><span></span><code><span class="nx">row</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nx">toggle</span><span class="p">(</span><span class="s2">"hide"</span><span class="p">,</span><span class="w"> </span><span class="o">!</span><span class="nx">colsText</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="nx">q</span><span class="p">));</span>
</code></pre></div>
<h3>Putting It All Together</h3>
<p>I setup a <a href="https://codepen.io/joshbduncan/pen/ZEaqeBp">full working example</a> if you want to check it out.</p>
<div class="codehilite"><pre><span></span><code><span class="kd">const</span><span class="w"> </span><span class="nx">searchInput</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="s2">"search-input"</span><span class="p">);</span>
<span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">searchInput</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">null</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="kd">const</span><span class="w"> </span><span class="nx">searchTable</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="s2">"search-table"</span><span class="p">);</span>
<span class="w">  </span><span class="kd">const</span><span class="w"> </span><span class="nx">ignoreCols</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">searchTable</span><span class="p">.</span><span class="nx">dataset</span><span class="p">.</span><span class="nx">searchIgnoreCols</span><span class="w"> </span><span class="o">?</span><span class="w"> </span><span class="nx">searchTable</span><span class="p">.</span><span class="nx">dataset</span><span class="p">.</span><span class="nx">searchIgnoreCols</span><span class="p">.</span><span class="nx">split</span><span class="p">(</span><span class="s2">","</span><span class="p">).</span><span class="nx">map</span><span class="p">(</span><span class="nb">Number</span><span class="p">)</span><span class="w"> </span><span class="o">:</span><span class="w"> </span><span class="kc">false</span><span class="p">;</span>
<span class="w">  </span><span class="kd">const</span><span class="w"> </span><span class="nx">rows</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">searchTable</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s2">"tbody tr"</span><span class="p">);</span>
<span class="w">  </span><span class="nx">searchInput</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s2">"input"</span><span class="p">,</span><span class="w"> </span><span class="kd">function</span><span class="w"> </span><span class="p">(</span><span class="nx">e</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="kd">const</span><span class="w"> </span><span class="nx">q</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">value</span><span class="p">.</span><span class="nx">toLowerCase</span><span class="p">();</span>
<span class="w">    </span><span class="nx">rows</span><span class="p">.</span><span class="nx">forEach</span><span class="p">((</span><span class="nx">row</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span>
<span class="w">      </span><span class="kd">const</span><span class="w"> </span><span class="nx">cols</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">row</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s2">"td"</span><span class="p">);</span>
<span class="w">      </span><span class="kd">const</span><span class="w"> </span><span class="nx">filteredCols</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">ignoreCols</span><span class="w"> </span><span class="o">?</span><span class="w"> </span><span class="nb">Array</span><span class="p">.</span><span class="kr">from</span><span class="p">(</span><span class="nx">cols</span><span class="p">).</span><span class="nx">filter</span><span class="p">((</span><span class="nx">_</span><span class="p">,</span><span class="w"> </span><span class="nx">n</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="nx">ignoreCols</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="o">!</span><span class="nx">ignoreCols</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="nx">n</span><span class="p">))</span><span class="w"> </span><span class="o">:</span><span class="w"> </span><span class="nb">Array</span><span class="p">.</span><span class="kr">from</span><span class="p">(</span><span class="nx">cols</span><span class="p">);</span>
<span class="w">      </span><span class="kd">const</span><span class="w"> </span><span class="nx">colsText</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">filteredCols</span><span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">n</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="nx">n</span><span class="p">.</span><span class="nx">textContent</span><span class="p">.</span><span class="nx">toLowerCase</span><span class="p">()).</span><span class="nx">join</span><span class="p">(</span><span class="s2">" "</span><span class="p">);</span>
<span class="w">      </span><span class="nx">row</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nx">toggle</span><span class="p">(</span><span class="s2">"hide"</span><span class="p">,</span><span class="w"> </span><span class="o">!</span><span class="nx">colsText</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="nx">q</span><span class="p">));</span>
<span class="w">    </span><span class="p">});</span>
<span class="w">  </span><span class="p">});</span>
<span class="p">}</span>
</code></pre></div>
<h2>Final Thoughts</h2>
<p>I'm no JavaScript wizard but this little script has been very handy lately. Hopefully, you can find use for it in one of your projects.</p>
<p><a href="https://gist.github.com/joshbduncan/87df03c4108c7572155d605a1da00960">View the full script with comments on Github.</a></p>]]></content:encoded></item><item><title>Autofilling Two Factor Authentication (2FA) Codes in macOS Chrome</title><link>https://joshbduncan.com/macos_chrome_autofill_security_codes.html</link><pubDate>Tue, 02 Nov 2021 00:00:00 -0400</pubDate><guid>https://joshbduncan.com/macos_chrome_autofill_security_codes.html</guid><description><![CDATA[If you use Chrome on macOS and are jealous of Safari's ability to autofill Two Factor Authentication (2FA) security codes checkout this sweet Keyboard Maestro macro.]]></description><content:encoded><![CDATA[<p>Looking to escape the constant battery drain of Chrome I've tried switching to Safari a few times this past year but it never sticks. Multiple times a day I had to open Chrome for (decent) Developer Tools and I'm just not interested in using two browsers.</p>
<div class="callout">
<p>Not long after posting this I saw a <a href="https://tyler.io/a-better-way-to-copy-two-factor-codes-on-macos">cleaner solution</a> from Tyler Hall.</p>
</div>
<h2>The Problem</h2>
<p>One thing I miss from Safari though is autofilling Two Factor Authentication (2FA) codes. If you use Safari or an iPhone you know how awesome this feature is.</p>
<p>😢 Sadly, Chrome doesn't have this feature...</p>
<p>So, unless you have great eyesight and can quickly read and remember the code before the notification disappears you have to do a multi-step process to retrieve it.</p>
<h2>The Solution</h2>
<p>If I can work out how to read the notification with <a href="https://en.wikipedia.org/wiki/Optical_character_recognition">OCR</a> (Optical Character Recognition) and extract the code I should be able to recreate 90% of the functionality from Safari.</p>
<p>Enter <a href="https://www.keyboardmaestro.com/main/">Keyboard Maestro</a>, the Swiss Army Knife of Mac automation.</p>
<div class="callout">
<p><a href="https://www.stairways.com/press/2021-11-02">Version 10</a> of Keyboard Maestro was just released and it now allows you to OCR an exact area of the screen. Previously, this Macro used a screenshot and cropping but now it's much more simplified.</p>
</div>
<h3>Step 1.</h3>
<p>The first thing I need to do is read the screen using the <a href="https://wiki.keyboardmaestro.com/action/OCR_Image?redirect=1">OCR Screen action</a>. But, I don't want to OCR the entire screen as that's a waste of resources and could potentially introduce errors later in the macro.</p>
<p>As you can see below, Keyboard Maestro now allows you to define an exact area of the screen to OCR. You'll need to specify how large of an area should be read (Width and Height), and where on the screen Keyboard Maestro should start reading at (Left and Down).</p>
<p><img alt="OCR Area" src="https://joshbduncan.com/static/images/keyboard-maestro-2fa-step-1.png"/></p>
<p>How did I figure out the Width and Height inputs (and Left offset) for the Crop Image function? I sent myself a test notification...</p>
<p><img alt="Test macOS Two Factor Authentication (2FA) Notification" src="https://joshbduncan.com/static/images/test-2fa-notification.png"/></p>
<p>Then, while the notification was still showing, I took a screenshot (Shift-Command-3), opened it in Photoshop, and measured how far the notification was from the top-right corner. It was 361 pixels from the left and 134 pixels from the right so I padded those numbers just a bit to 375 and 150.</p>
<p>You probably also noticed that I used the <a href="https://wiki.keyboardmaestro.com/function/SCREEN?redirect=1">SCREEN</a> function to calculate the Left input. I often use my MacBook Pro connected to an external monitor so this ensures I provide the correct Left position no matter what monitor I'm using. The <code>- 375</code> part offsets the area to the left by the width I determined above since we are working from the top-right corner of the screen.</p>
<p>Also note, I'm saving the captured text to the Keyboard Maestro variable <code>OCR</code> so that I can easily access it in later steps.</p>
<h3>Step 2.</h3>
<p>Below is the text captured during the OCR process. As you can see, it contains the code I'm looking for (934919) from the test notification but it also captured stuff I don't want.</p>
<div class="codehilite"><pre><span></span><code>QQ © B&amp;H m4 ZB Tue Nov2 9:56 AM
/ KEYBOARD MAESTRO ENGINE now
Test 2FA Notification
Example: Your 2FA Sign In Code is 934919. Reply
HELP if you didn't request it.
</code></pre></div>
<p>To clean this up I'm using the <a href="https://wiki.keyboardmaestro.com/action/Search_using_Regular_Expression?redirect=1">Search using Regular Expression action</a> on the variable <code>OCR</code>.</p>
<div class="callout">
<p><a href="https://en.wikipedia.org/wiki/Regular_expression">Regular Expressions (regex)</a> are a deep topic so I won't cover them in detail here but you can learn tons at <a href="https://regexr.com/">RegExr.com</a>.</p>
</div>
<p><img alt="Search Variable Using Regular Expression" src="https://joshbduncan.com/static/images/keyboard-maestro-2fa-step-2.png"/></p>
<p>This regex is quite simple. The <code>\d</code> matches any digit character (0-9), and the <code>{5,7}</code> is a quantifier for <code>\d</code> that instructs the search to only match groups of consecutive digits between 5 and 7 digits long. The first match the regex finds will be stored in the variable <code>TwoFactorCode</code>.</p>
<div class="callout">
<p>To figure out the best regex to capture the 2FA codes, I dumped the last fifteen 2FA codes I had received into (names changed to protect to innocent) RegExr and started experimenting. You can also <a href="https://regexr.com/68lnv">experiment here</a> and see exactly how this regex works on my list of <a href="https://joshbduncan.com/static/downloads/sample-2fa-codes.txt">example notifications</a>.</p>
</div>
<p>Why 5 and 7? It's a pretty narrow range and that's to try and minimize errors. Lots of 2FA notifications include the sender phone number in one of two formats, ###-###-#### or ##########. So, if the lower bound of the quantifier range is less than 5, it would capture the parts of the segmented phone number, and if the upper bound of the quantifier range greater than 8 it would capture the non-segmented phone number.</p>
<p>So, why not use 8 for the upper bound then? The quick answer is that I have yet to receive a notification over 7 digits so I'm just trying to eliminate as many potential errors as possible.</p>
<h3>Step 3.</h3>
<p>Now that I have captured the code, I place it on the system clipboard so it can easily be pasted into a webpage.</p>
<p><img alt="Set System Clipboard" src="https://joshbduncan.com/static/images/keyboard-maestro-2fa-step-3.png"/></p>
<h3>Step 4.</h3>
<p>Finally, I display the captured code really large just in case I can't paste it directly into the webpage. This is much easier to read than the small notification and stays on the screen even while you type the code.</p>
<p><img alt="Display Large Text" src="https://joshbduncan.com/static/images/keyboard-maestro-2fa-step-4.png"/></p>
<p>Eventually I may turn this particular action off as it can be annoying at times 🤷‍♂️...</p>
<h2>Potential Problems</h2>
<p>So, what happens if a code isn't found?</p>
<p>If the regex can't find a matching code in the OCR'd text the macro gets canceled and Keyboard Maestro sends a notification like below.</p>
<p><img alt="Macro OCR Error" src="https://joshbduncan.com/static/images/keyboard-maestro-2fa-error.png"/></p>
<p>If you find this happening often you may need to adjust your OCR Area placement from Step 1 or you may adjust your lower and upper bounds of the regex quantifier in Step 2.</p>
<h2>Final Macro</h2>
<p>Here's the final macro in all its glory 🙌.</p>
<p><img alt="Final Keyboard Maestro Macro" src="https://joshbduncan.com/static/images/keyboard-maestro-2fa-step-final.png"/></p>
<p>It's really simple but quite useful. You can <a href="https://joshbduncan.com/static/downloads/OCR-macOS-2FA-Notification-Codes.kmmacros">download it here</a> if you think it could help make your life a bit easier. It's set up to be triggered by the <a href="https://brettterpstra.com/2017/06/15/a-hyper-key-with-karabiner-elements-full-instructions/">Hyper Key</a> + 2 (Shift-Control-Option-Command-2). The number 2 is easy to remember for "2FA" and also simple to press quickly with your left hand. ✌️</p>
<div class="callout">
<p>Pro Tip: If you completely miss the notification you can still OCR it by opening Notification Center and running the macro. The 2FA notification does need to be at the top of the list or it won't be in the capture area Keyboard Maestro is looking at.</p>
</div>]]></content:encoded></item><item><title>Custom Sorting in Python</title><link>https://joshbduncan.com/python-custom-sorting.html</link><pubDate>Tue, 17 Aug 2021 00:00:00 -0400</pubDate><guid>https://joshbduncan.com/python-custom-sorting.html</guid><description><![CDATA[I recently found myself needing to sort a list by cardinal directions. Here's a super simple solution to do custom sorting in Python.]]></description><content:encoded><![CDATA[<p>While working on my new Python project <a href="https://github.com/joshbduncan/word-search-generator">Word-Search-Generator</a>, I needed a way to do a custom sort based on <a href="https://en.wikipedia.org/wiki/cardinal_direction">cardinal directions</a> (think compass). I knew I could do it with a custom function and some looping but I wanted to use a Python <a href="https://docs.python.org/3/library/functions.html#built-in-functions">built-in function</a> if possible.</p>
<h2>The Problem</h2>
<p>Depending on the level of the puzzle, words can go in two, four, or eight cardinal directions... But, due to the number of words provided and how they fit into the puzzle, not all directions for each level are always used.</p>
<p>I needed a way to sort all of the directions actually used by words during puzzle generation so that they display in the same order as a compass.</p>
<h2>What I'm Working With</h2>
<p>So, I have an answer key for each puzzle that's a dictionary of all contained words along with their start location and direction.</p>
<div class="codehilite"><pre><span></span><code><span class="gp">&gt;&gt;&gt; </span><span class="n">key</span>
<span class="go">{"CAT": {"start": (1, 34), "direction": "SE"},</span>
<span class="go"> "CHICKEN": {"start": (46, 26), "direction": "E"},</span>
<span class="go"> "DOG": {"start": (32, 45), "direction": "NE"},</span>
<span class="go"> "ROOSTER": {"start": (32, 12), "direction": "N"},</span>
<span class="go"> "SHEEP": {"start": (30, 2), "direction": "S"},</span>
<span class="go"> "GOAT": {"start": (29, 48), "direction": "W"},</span>
<span class="go"> "HORSE": {"start": (16, 26), "direction": "SE"},</span>
<span class="go"> "LAMB": {"start": (25, 10), "direction": "N"}}</span>
</code></pre></div>
<h2>The Solution</h2>
<p>First, I need to get all the directions from the dictionary.</p>
<div class="codehilite"><pre><span></span><code><span class="gp">&gt;&gt;&gt; </span><span class="n">dirs</span> <span class="o">=</span> <span class="p">{</span><span class="n">v</span><span class="p">[</span><span class="s2">"direction"</span><span class="p">]</span> <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">key</span><span class="o">.</span><span class="n">values</span><span class="p">()}</span>
<span class="go">["NE", "N", "S", "W", "SE", "E"]</span>
</code></pre></div>
<p>You'll notice that only six of the eight possible cardinal directions were used in this particular puzzle, and if I use the default <code>sorted()</code> method they aren't in the order I need them.</p>
<div class="codehilite"><pre><span></span><code><span class="gp">&gt;&gt;&gt; </span><span class="nb">sorted</span><span class="p">(</span><span class="n">dirs</span><span class="p">)</span>
<span class="go">["E", "N", "NE", "S", "SE", "W"]</span>
</code></pre></div>
<h3>A Little Helper</h3>
<p>To help out, I setup a new variable which has all eight possible cardinal directions in the order they are on a compass (starting at the top and going clockwise).</p>
<div class="codehilite"><pre><span></span><code><span class="gp">&gt;&gt;&gt; </span><span class="n">sort_by</span> <span class="o">=</span> <span class="p">(</span><span class="s2">"N"</span><span class="p">,</span> <span class="s2">"NE"</span><span class="p">,</span> <span class="s2">"E"</span><span class="p">,</span> <span class="s2">"SE"</span><span class="p">,</span> <span class="s2">"S"</span><span class="p">,</span> <span class="s2">"SW"</span><span class="p">,</span> <span class="s2">"W"</span><span class="p">,</span> <span class="s2">"NW"</span><span class="p">)</span>
</code></pre></div>
<p>Now, when we I call <code>sorted()</code>, I can provide the <a href="https://docs.python.org/3/howto/sorting.html#key-functions">key parameter</a> that lets Python know how I want the sorting performed. But I can't just provide <code>sort_by</code> as they key. Python requires a "function or other callable".</p>
<h3>Enter Lambdas</h3>
<p>The key I'm using to sort by is the <a href="https://docs.python.org/3/tutorial/controlflow.html#lambda-expressions">lambda function</a> <code>key=lambda d: sort_by.index(d)</code>.</p>
<div class="callout">
<p>Lambdas are "small anonymous functions" that run a single expression against a provided argument.</p>
</div>
<p>My lambda takes each direction from <code>dirs</code> as variable <code>d</code>, looks up its <a href="https://docs.python.org/3/tutorial/datastructures.html#data-structures">index</a> in <code>sort_by</code> and returns that number. Python then uses that number to determine the sorting placement.</p>
<h2>The Result</h2>
<p>Finally, if I supply Python's <code>sorted()</code> function with my new <code>key</code> I get exactly what I was looking for.</p>
<div class="codehilite"><pre><span></span><code><span class="gp">&gt;&gt;&gt; </span><span class="nb">sorted</span><span class="p">(</span><span class="n">dirs</span><span class="p">,</span> <span class="n">key</span><span class="o">=</span><span class="k">lambda</span> <span class="n">d</span><span class="p">:</span> <span class="n">sort_by</span><span class="o">.</span><span class="n">index</span><span class="p">(</span><span class="n">d</span><span class="p">))</span>
<span class="go">["N", "NE", "E", "SE", "S", "W"]</span>
</code></pre></div>
<p>👌 Perfecto! If you follow a compass clockwise, you'll find each direction in the correct order.</p>
<h1>Extra Credit</h1>
<p>Need a weighted sort based on how many times an item is present?</p>
<div class="codehilite"><pre><span></span><code><span class="go"># list to sort by count</span>
<span class="gp">&gt;&gt;&gt; </span><span class="n">dirs</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"W"</span><span class="p">,</span> <span class="s2">"N"</span><span class="p">,</span> <span class="s2">"E"</span><span class="p">,</span> <span class="s2">"W"</span><span class="p">,</span> <span class="s2">"E"</span><span class="p">,</span> <span class="s2">"W"</span><span class="p">]</span>
<span class="go"># ascending</span>
<span class="gp">&gt;&gt;&gt; </span><span class="nb">sorted</span><span class="p">(</span><span class="nb">set</span><span class="p">(</span><span class="n">dirs</span><span class="p">),</span> <span class="n">key</span><span class="o">=</span><span class="k">lambda</span> <span class="n">d</span><span class="p">:</span> <span class="n">dirs</span><span class="o">.</span><span class="n">count</span><span class="p">(</span><span class="n">d</span><span class="p">))</span>
<span class="go">["N", "E", "W"]</span>
<span class="go"># descending</span>
<span class="gp">&gt;&gt;&gt; </span><span class="nb">sorted</span><span class="p">(</span><span class="nb">set</span><span class="p">(</span><span class="n">dirs</span><span class="p">),</span> <span class="n">key</span><span class="o">=</span><span class="k">lambda</span> <span class="n">d</span><span class="p">:</span> <span class="n">dirs</span><span class="o">.</span><span class="n">count</span><span class="p">(</span><span class="n">d</span><span class="p">),</span> <span class="n">reverse</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="go">["W", "E", "N"]</span>
</code></pre></div>
<p>How about sorting by word length?</p>
<div class="codehilite"><pre><span></span><code><span class="gp">&gt;&gt;&gt; </span><span class="n">words</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"nine"</span><span class="p">,</span> <span class="s2">"one"</span><span class="p">,</span> <span class="s2">"twelve"</span><span class="p">,</span> <span class="s2">"seventeen"</span><span class="p">]</span>
<span class="gp">&gt;&gt;&gt; </span><span class="nb">sorted</span><span class="p">(</span><span class="nb">set</span><span class="p">(</span><span class="n">words</span><span class="p">),</span> <span class="n">key</span><span class="o">=</span><span class="k">lambda</span> <span class="n">word</span><span class="p">:</span> <span class="nb">len</span><span class="p">(</span><span class="n">word</span><span class="p">))</span>
<span class="go">["one", "nine", "twelve", "seventeen"]</span>
</code></pre></div>
<p>And, if your information is formatted correctly you can use the <a href="https://docs.python.org/3/howto/sorting.html#operator-module-functions">Operator Module Functions</a> <code>itemgetter</code> and <code>attrgetter</code>.</p>
<div class="codehilite"><pre><span></span><code><span class="gp">&gt;&gt;&gt; </span><span class="kn">from</span><span class="w"> </span><span class="nn">operator</span><span class="w"> </span><span class="kn">import</span> <span class="n">itemgetter</span><span class="p">,</span> <span class="n">attrgetter</span>
<span class="gp">&gt;&gt;&gt; </span><span class="n">student_grades</span> <span class="o">=</span> <span class="p">((</span><span class="s2">"Ella"</span><span class="p">,</span> <span class="mi">76</span><span class="p">),</span> <span class="p">(</span><span class="s2">"Dave"</span><span class="p">,</span> <span class="mi">92</span><span class="p">),</span> <span class="p">(</span><span class="s2">"Jess"</span><span class="p">,</span> <span class="mi">97</span><span class="p">),</span> <span class="p">(</span><span class="s2">"Bob"</span><span class="p">,</span> <span class="mi">84</span><span class="p">))</span>
<span class="go"># sort students by grade from highest to lowest using itemgetter</span>
<span class="gp">&gt;&gt;&gt; </span><span class="nb">sorted</span><span class="p">(</span><span class="n">student_grades</span><span class="p">,</span> <span class="n">key</span><span class="o">=</span><span class="n">itemgetter</span><span class="p">(</span><span class="mi">1</span><span class="p">),</span> <span class="n">reverse</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="go">[("Jess", 97), ("Dave", 92), ("Bob", 84), ("Ella", 76)]</span>
</code></pre></div>
<p>Now, using <code>attrgetter</code> to sort by a specific object attribute.</p>
<div class="codehilite"><pre><span></span><code><span class="go"># setup a Pet class to structure the objects</span>
<span class="gp">&gt;&gt;&gt; </span><span class="k">class</span><span class="w"> </span><span class="nc">Pet</span><span class="p">:</span>
<span class="gp">... </span>    <span class="k">def</span><span class="w"> </span><span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">name</span><span class="p">,</span> <span class="n">age</span><span class="p">,</span> <span class="n">weight</span><span class="p">):</span>
<span class="gp">... </span>        <span class="bp">self</span><span class="o">.</span><span class="n">name</span> <span class="o">=</span> <span class="n">name</span>
<span class="gp">... </span>        <span class="bp">self</span><span class="o">.</span><span class="n">age</span> <span class="o">=</span> <span class="n">age</span>
<span class="gp">... </span>        <span class="bp">self</span><span class="o">.</span><span class="n">weight</span> <span class="o">=</span> <span class="n">weight</span>
<span class="gp">... </span>    <span class="k">def</span><span class="w"> </span><span class="fm">__repr__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="gp">... </span>        <span class="k">return</span> <span class="nb">repr</span><span class="p">((</span><span class="bp">self</span><span class="o">.</span><span class="n">name</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">age</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">weight</span><span class="p">))</span>
<span class="go"># create some pets</span>
<span class="gp">&gt;&gt;&gt; </span><span class="n">pets</span> <span class="o">=</span> <span class="p">[</span>
<span class="gp">... </span>    <span class="n">Pet</span><span class="p">(</span><span class="s2">"Maddie"</span><span class="p">,</span> <span class="mi">8</span><span class="p">,</span> <span class="mi">62</span><span class="p">),</span>
<span class="gp">... </span>    <span class="n">Pet</span><span class="p">(</span><span class="s2">"Harper"</span><span class="p">,</span> <span class="mi">13</span><span class="p">,</span> <span class="mi">15</span><span class="p">),</span>
<span class="gp">... </span>    <span class="n">Pet</span><span class="p">(</span><span class="s2">"Greta"</span><span class="p">,</span> <span class="mi">10</span><span class="p">,</span> <span class="mi">9</span><span class="p">),</span>
<span class="gp">&gt;&gt;&gt; </span><span class="p">]</span>
<span class="go"># sort the pets by weight</span>
<span class="gp">&gt;&gt;&gt; </span><span class="nb">sorted</span><span class="p">(</span><span class="n">pets</span><span class="p">,</span> <span class="n">key</span><span class="o">=</span><span class="n">attrgetter</span><span class="p">(</span><span class="s2">"weight"</span><span class="p">))</span>
<span class="go">[("Greta", "10", 9), ("Harper", "13", 15), ("Maddie", "8", 62)]</span>
<span class="go"># sort the pets from oldest to youngest</span>
<span class="gp">&gt;&gt;&gt; </span><span class="nb">sorted</span><span class="p">(</span><span class="n">pets</span><span class="p">,</span> <span class="n">key</span><span class="o">=</span><span class="n">attrgetter</span><span class="p">(</span><span class="s2">"age"</span><span class="p">),</span> <span class="n">reverse</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="go">[("Harper", 13, 15), ("Greta", 10, 9), ("Maddie", 8, 62)]</span>
</code></pre></div>
<h1>Final Thoughts</h1>
<p>There are many other ways to do sorting in Python but I think these few are pretty slick. Check out the <a href="https://docs.python.org/3/howto/sorting.html">Python Docs on Sorting</a> for more information and examples. ✌️</p>]]></content:encoded></item><item><title>Checking off Linked Records in an Airtable Script</title><link>https://joshbduncan.com/checking-linked-records-in-airtable.html</link><pubDate>Tue, 03 Aug 2021 00:00:00 -0400</pubDate><guid>https://joshbduncan.com/checking-linked-records-in-airtable.html</guid><description><![CDATA[I needed a quick way to check off a bunch of linked records in Airtable so I worked up this little script to do it from an Airtable button.]]></description><content:encoded><![CDATA[<p>A few years back I built a custom job tracker in <a href="https://www.airtable.com/">Airtable</a> that helps me keep up with all of my freelance projects. The base is pretty simple and only consists of three tables, "Jobs", "Clients", and "Contacts". As you would expect, each "Job" has a "Client", which in-turn has a "Contact" (some of my clients have multiple contacts).</p>
<h2>Backstory</h2>
<p>Previously, I tracked overall time for each project in a "duration" field that I would manually update every time I did any work. And, up until recently non of my time-tracked projects lasted more than week so this method worked just fine... Well, until last month.</p>
<p>See, I bill my clients monthly and some their projects started spanning more than one month, making it hard to determine what I time had already billed when the next month comes around.</p>
<p>To fix this, I created a new table titled "Time Entries" which links back to individual jobs. Every time I work on a project now, I create a new time entry with the date and time that I worked. Each time entry also has a "checkbox" field titled "Invoiced". Now, when the end of the month comes, I can just bill for the time entries that are not checked as "Invoiced".</p>
<h2>The Problem</h2>
<p>This new method works well, but I got annoyed today when I had to click off sixty-four linked time entries for one client. I do my invoicing from a custom view in the "Jobs" table so this process takes way too long... You have to click the linked record, scroll down and click the "Invoiced" checkbox, then click out of the linked record pop-up.</p>
<p>I know I could create a filtered view in the "Time Entries" table that's grouped by customer but that's not ideal for me.</p>
<h2>The Solution</h2>
<p>I've recently been learning some JavaScript and knew Airtable had scripting capabilities so I figured I could whip up something to help me out.</p>
<p>Before I started scripting I switched to my custom "Invoicing" view in the "Jobs" table that I mentioned above. This view only shows jobs with "uninvoiced" time entries and has all records grouped by client so things are easier to see.</p>
<p>To this view, I added a new <a href="https://support.airtable.com/hc/en-us/articles/360048496693-Button-field">button field</a> that will run my script on the current record when clicked.</p>
<h2>Time to Script</h2>
<p>Before I start scripting I like to make a list of everything I'm trying to accomplish. This time the list is short but I find it helps me stay on track...</p>
<p><strong>Scripting Steps:</strong></p>
<ol>
<li>Get all time entries for a particular job</li>
<li>Loop through all matching time entries</li>
<li>Check to make sure the time entry hasn't been invoiced</li>
<li>Mark it as invoiced</li>
</ol>
<p>If you are new to Airtable Scripting like me, here's their <a href="https://www.airtable.com/developers/scripting/api">Scripting Documentation</a> for reference.</p>
<h3>Getting Setup</h3>
<p>First I'll need to get the two tables I'm working with as objects so that I can reference and query them. And, I'll also go ahead and query all of the time entries so I can update the "uninvoiced" ones that match the job I'm invoicing.</p>
<div class="codehilite"><pre><span></span><code><span class="kd">let</span><span class="w"> </span><span class="nx">jobsTable</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">base</span><span class="p">.</span><span class="nx">getTable</span><span class="p">(</span><span class="s2">"Jobs"</span><span class="p">);</span>
<span class="kd">let</span><span class="w"> </span><span class="nx">timeEntriesTable</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">base</span><span class="p">.</span><span class="nx">getTable</span><span class="p">(</span><span class="s2">"Time Entries"</span><span class="p">);</span>
<span class="kd">let</span><span class="w"> </span><span class="nx">timeEntriesQuery</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">timeEntriesTable</span><span class="p">.</span><span class="nx">selectRecordsAsync</span><span class="p">();</span>
</code></pre></div>
<p>I'll be running this script via a button like I mentioned above, but it can also be ran right from the scripting menu so I need to have a way to choose a record in that case.</p>
<div class="codehilite"><pre><span></span><code><span class="kd">let</span><span class="w"> </span><span class="nx">record</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">input</span><span class="p">.</span><span class="nx">recordAsync</span><span class="p">(</span><span class="s2">"Choose a record"</span><span class="p">,</span><span class="w"> </span><span class="nx">jobsTable</span><span class="p">);</span>
</code></pre></div>
<h3>1. Get all time entries for a particular job</h3>
<p>Airtable makes it easy to get all of the linked entries for a record by getting its cell value. This provides me with a list of each linked record as an object.</p>
<div class="codehilite"><pre><span></span><code><span class="kd">let</span><span class="w"> </span><span class="nx">linkedTimeEntries</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">record</span><span class="p">.</span><span class="nx">getCellValue</span><span class="p">(</span><span class="s2">"Time Entries"</span><span class="p">)</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="p">[];</span>
<span class="nx">output</span><span class="p">.</span><span class="nx">text</span><span class="p">(</span><span class="s2">"Found "</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nx">linkedTimeEntries</span><span class="p">.</span><span class="nx">length</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s2">" total time entries"</span><span class="p">);</span>
</code></pre></div>
<h3>2. Loop through all matching time entries</h3>
<p>Now that I have each linked time entry, I can loop through them and make sure I'm getting the correct info.</p>
<div class="codehilite"><pre><span></span><code><span class="c1">// loop over all time entries for record</span>
<span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">linkedTimeEntries</span><span class="p">.</span><span class="nx">length</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mf">0</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="kd">let</span><span class="w"> </span><span class="nx">i</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mf">0</span><span class="p">;</span><span class="w"> </span><span class="nx">i</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="nx">linkedTimeEntries</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span><span class="w"> </span><span class="nx">i</span><span class="o">++</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="kd">let</span><span class="w"> </span><span class="nx">record</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">timeEntriesQuery</span><span class="p">.</span><span class="nx">getRecord</span><span class="p">(</span><span class="nx">linkedTimeEntries</span><span class="p">[</span><span class="nx">i</span><span class="p">].</span><span class="nx">id</span><span class="p">);</span>
<span class="w">    </span><span class="nx">output</span><span class="p">.</span><span class="nx">text</span><span class="p">(</span><span class="nx">record</span><span class="p">.</span><span class="nx">getCellValue</span><span class="p">(</span><span class="s2">"Date"</span><span class="p">));</span>
<span class="w">  </span><span class="p">}</span>
<span class="p">}</span>
</code></pre></div>
<h3>3. Check to make sure the time entry hasn't been invoiced</h3>
<p>While looping, I'll need to make sure that each time entry hasn't already been invoiced, If the boolean value of the "Invoiced" field is <code>false</code> then I'll output the date of the entry as a check.</p>
<div class="codehilite"><pre><span></span><code><span class="c1">// loop over all time entries for record</span>
<span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">linkedTimeEntries</span><span class="p">.</span><span class="nx">length</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mf">0</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="kd">let</span><span class="w"> </span><span class="nx">i</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mf">0</span><span class="p">;</span><span class="w"> </span><span class="nx">i</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="nx">linkedTimeEntries</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span><span class="w"> </span><span class="nx">i</span><span class="o">++</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="kd">let</span><span class="w"> </span><span class="nx">record</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">timeEntriesQuery</span><span class="p">.</span><span class="nx">getRecord</span><span class="p">(</span><span class="nx">linkedTimeEntries</span><span class="p">[</span><span class="nx">i</span><span class="p">].</span><span class="nx">id</span><span class="p">);</span>
<span class="w">    </span><span class="c1">// if time entry not invoiced</span>
<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="nx">record</span><span class="p">.</span><span class="nx">getCellValue</span><span class="p">(</span><span class="s2">"Invoiced"</span><span class="p">))</span><span class="w"> </span><span class="p">{</span>
<span class="w">      </span><span class="nx">output</span><span class="p">.</span><span class="nx">text</span><span class="p">(</span><span class="nx">record</span><span class="p">.</span><span class="nx">getCellValue</span><span class="p">(</span><span class="s2">"Date"</span><span class="p">)</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s2">" not invoiced"</span><span class="p">);</span>
<span class="w">    </span><span class="p">}</span>
<span class="w">  </span><span class="p">}</span>
<span class="p">}</span>
</code></pre></div>
<h3>4. Mark it as invoiced</h3>
<p>If a time entry hasn't been "invoiced", I will check it off using the <a href="https://www.airtable.com/developers/scripting/api/table#update-record-async"><code>updateRecordAsync()</code></a> function.</p>
<div class="codehilite"><pre><span></span><code><span class="k">await</span><span class="w"> </span><span class="nx">timeEntriesTable</span><span class="p">.</span><span class="nx">updateRecordAsync</span><span class="p">(</span><span class="nx">record</span><span class="p">.</span><span class="nx">id</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="nx">Invoiced</span><span class="o">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
<span class="p">});</span>
</code></pre></div>
<h2>Putting It All Together</h2>
<p>Now that all of the steps I laid out above are covered, I can put everything together and give it a test run.</p>
<div class="codehilite"><pre><span></span><code><span class="c1">// get the two tables I need to work on</span>
<span class="kd">let</span><span class="w"> </span><span class="nx">jobsTable</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">base</span><span class="p">.</span><span class="nx">getTable</span><span class="p">(</span><span class="s2">"Jobs"</span><span class="p">);</span>
<span class="kd">let</span><span class="w"> </span><span class="nx">timeEntriesTable</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">base</span><span class="p">.</span><span class="nx">getTable</span><span class="p">(</span><span class="s2">"Time Entries"</span><span class="p">);</span>
<span class="c1">// get all time entries for later reference</span>
<span class="kd">let</span><span class="w"> </span><span class="nx">timeEntriesQuery</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">timeEntriesTable</span><span class="p">.</span><span class="nx">selectRecordsAsync</span><span class="p">();</span>
<span class="c1">// when run via button, automatically use that record otherwise choose</span>
<span class="kd">let</span><span class="w"> </span><span class="nx">record</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">input</span><span class="p">.</span><span class="nx">recordAsync</span><span class="p">(</span><span class="s2">"Choose a record"</span><span class="p">,</span><span class="w"> </span><span class="nx">jobsTable</span><span class="p">);</span>
<span class="c1">// get all linked time entries for record</span>
<span class="kd">let</span><span class="w"> </span><span class="nx">linkedTimeEntries</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">record</span><span class="p">.</span><span class="nx">getCellValue</span><span class="p">(</span><span class="s2">"Time Entries"</span><span class="p">)</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="p">[];</span>
<span class="nx">output</span><span class="p">.</span><span class="nx">text</span><span class="p">(</span><span class="s2">"Found "</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nx">linkedTimeEntries</span><span class="p">.</span><span class="nx">length</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s2">" total time entries"</span><span class="p">);</span>
<span class="c1">// loop over all time entries for record</span>
<span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">linkedTimeEntries</span><span class="p">.</span><span class="nx">length</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mf">0</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="kd">let</span><span class="w"> </span><span class="nx">i</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mf">0</span><span class="p">;</span><span class="w"> </span><span class="nx">i</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="nx">linkedTimeEntries</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span><span class="w"> </span><span class="nx">i</span><span class="o">++</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="kd">let</span><span class="w"> </span><span class="nx">record</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">timeEntriesQuery</span><span class="p">.</span><span class="nx">getRecord</span><span class="p">(</span><span class="nx">linkedTimeEntries</span><span class="p">[</span><span class="nx">i</span><span class="p">].</span><span class="nx">id</span><span class="p">);</span>
<span class="w">    </span><span class="c1">// if time entry not invoiced</span>
<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="nx">record</span><span class="p">.</span><span class="nx">getCellValue</span><span class="p">(</span><span class="s2">"Invoiced"</span><span class="p">))</span><span class="w"> </span><span class="p">{</span>
<span class="w">      </span><span class="c1">// check "Invoiced"</span>
<span class="w">      </span><span class="k">await</span><span class="w"> </span><span class="nx">timeEntriesTable</span><span class="p">.</span><span class="nx">updateRecordAsync</span><span class="p">(</span><span class="nx">record</span><span class="p">.</span><span class="nx">id</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="nx">Invoiced</span><span class="o">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
<span class="w">      </span><span class="p">});</span>
<span class="w">      </span><span class="nx">output</span><span class="p">.</span><span class="nx">text</span><span class="p">(</span><span class="s2">"✅ "</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nx">record</span><span class="p">.</span><span class="nx">getCellValue</span><span class="p">(</span><span class="s2">"Date"</span><span class="p">));</span>
<span class="w">    </span><span class="p">}</span>
<span class="w">  </span><span class="p">}</span>
<span class="p">}</span>
</code></pre></div>
<p>If you are trying something similar, I would highly recommend using a copy of your production Airtable base, or at least make a test "job" with test "time entries".</p>
<p>So, here it is in action.</p>
<p><img alt="Airtable Checkbox Script in Action" src="https://joshbduncan.com/static/images/airtable-checkbox-script.gif"/></p>
<p>As you can see, when I want to mark all time entries for a job as "invoiced" I just click the big blue button and the script goes to work. #satisfied</p>
<p><a href="https://gist.github.com/joshbduncan/10b7f448e984e238fdbfa3fc95ebc1e6">View the full script here</a></p>]]></content:encoded></item><item><title>JavaScript Search Token Highlighter</title><link>https://joshbduncan.com/javascript-search-token-highlighter.html</link><pubDate>Tue, 04 May 2021 00:00:00 -0400</pubDate><guid>https://joshbduncan.com/javascript-search-token-highlighter.html</guid><description><![CDATA[Building a search results page? Thinking about highlighting the search terms so they standout? Here's how I did it using vanilla JavaScript.]]></description><content:encoded><![CDATA[<p>While building a full-text search engine for a <a href="https://flask.palletsprojects.com/">Flask</a> project I'm working on, I wanted a way to highlight all of the search terms on the search results page. After dabbling in JavaScript for a bit, here's what I come up with.</p>
<div class="callout">
<p>Just to be clear, I'm not a JavaScript programmer. This is something I was able to hack together for my purposes and it seems to work well. Your milage may vary...</p>
</div>
<h2>TL;DR</h2>
<p>If you just want the final script, here it is with all of the specifics of my project removed. Supply a query string however you want and the script will highlight those tokens in each article element on the page.</p>
<div class="codehilite"><pre><span></span><code><span class="c1">// the query of tokens to highlight</span>
<span class="kd">var</span><span class="w"> </span><span class="nx">query</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"life social page pursuit"</span><span class="p">;</span>
<span class="c1">// add regex '|' "or" character in place of spaces</span>
<span class="kd">var</span><span class="w"> </span><span class="nx">tokens</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">query</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="sr">/ /g</span><span class="p">,</span><span class="w"> </span><span class="s2">"|"</span><span class="p">);</span>
<span class="c1">// regex to find all matching tokens</span>
<span class="kd">var</span><span class="w"> </span><span class="nx">match_tokens</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nb">RegExp</span><span class="p">(</span><span class="nx">tokens</span><span class="p">,</span><span class="w"> </span><span class="s2">"gi"</span><span class="p">);</span>
<span class="c1">// find all articles on the page</span>
<span class="kd">var</span><span class="w"> </span><span class="nx">articles</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">document</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s2">"[id=article]"</span><span class="p">);</span>
<span class="c1">// loop through all found articles elements (articles)</span>
<span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="kd">var</span><span class="w"> </span><span class="nx">i</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mf">0</span><span class="p">;</span><span class="w"> </span><span class="nx">i</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="nx">articles</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span><span class="w"> </span><span class="nx">i</span><span class="o">++</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="c1">// grab the article quote content</span>
<span class="w">  </span><span class="kd">var</span><span class="w"> </span><span class="nx">content</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">articles</span><span class="p">[</span><span class="nx">i</span><span class="p">].</span><span class="nx">innerHTML</span><span class="p">;</span>
<span class="w">  </span><span class="c1">// loop over the all token matches in content</span>
<span class="w">  </span><span class="nx">content</span><span class="p">.</span><span class="nx">match</span><span class="p">(</span><span class="nx">match_tokens</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(</span><span class="kd">function</span><span class="w"> </span><span class="p">(</span><span class="nx">match</span><span class="p">,</span><span class="w"> </span><span class="nx">i</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="c1">// replace regex for all whole word token matches</span>
<span class="w">    </span><span class="kd">var</span><span class="w"> </span><span class="nx">replace_tokens</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nb">RegExp</span><span class="p">(</span><span class="nx">match</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s2">"\\b"</span><span class="p">,</span><span class="w"> </span><span class="s2">"gi"</span><span class="p">);</span>
<span class="w">    </span><span class="c1">// actually replace all token matches in content variable</span>
<span class="w">    </span><span class="nx">content</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">content</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="nx">replace_tokens</span><span class="p">,</span><span class="w"> </span><span class="s2">"&lt;mark&gt;"</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nx">match</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s2">"&lt;/mark&gt;"</span><span class="p">);</span>
<span class="w">  </span><span class="p">});</span>
<span class="w">  </span><span class="c1">// set the innerHTML of the article to the update content</span>
<span class="w">  </span><span class="nx">articles</span><span class="p">[</span><span class="nx">i</span><span class="p">].</span><span class="nx">innerHTML</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">content</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>
<h2>From Python To JavaScript</h2>
<p>After a visitor successfully submits a search query to my Flask app, I'm capturing their input, sanitizing it, and then passing that query to the search results page as the variable <code>tokens</code>.</p>
<p>Once, the search results page is rendered, I can access that variable via <a href="https://jinja.palletsprojects.com/">Jinja</a> inside of my JavaScript script.</p>
<div class="codehilite"><pre><span></span><code><span class="kd">var</span><span class="w"> </span><span class="nx">tokens</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"{{ tokens }}"</span><span class="p">;</span>
</code></pre></div>
<h2>Setting Up For Regex</h2>
<p>Since, I'll be utilizing <a href="https://en.wikipedia.org/wiki/Regular_expression">Regular Expressions (regex)</a> to find the tokens, I'm going to add regex "or" separators between my tokens using the Jinja <a href="https://jinja.palletsprojects.com/en/2.11.x/templates/?highlight=replace#replace">Replace Filter</a>.</p>
<div class="codehilite"><pre><span></span><code><span class="kd">var</span><span class="w"> </span><span class="nx">tokens</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"{{ tokens|replace(' ', '|') }}"</span><span class="p">;</span><span class="w"> </span><span class="c1">// add regex "|' "or" character</span>
</code></pre></div>
<p>This filter is just like the built-in Python string.replace function, in where it replaces each occurrence of the first argument with the second argument.</p>
<p>You could also do this with the JavaScript <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace">string replace method</a> if you are not using Jinja templates.</p>
<div class="codehilite"><pre><span></span><code><span class="kd">var</span><span class="w"> </span><span class="nx">tokens</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">query</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="sr">/ /g</span><span class="p">,</span><span class="w"> </span><span class="s2">"|"</span><span class="p">);</span>
</code></pre></div>
<p>Original query string from Jinja:</p>
<div class="codehilite"><pre><span></span><code><span class="nx">$</span><span class="w"> </span><span class="kd">var</span><span class="w"> </span><span class="nx">tokens</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"{{ tokens }}"</span><span class="p">;</span>
<span class="nx">$</span><span class="w"> </span><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">tokens</span><span class="p">)</span>
<span class="nx">$</span><span class="w"> </span><span class="s2">"this is a query"</span>
</code></pre></div>
<p>Update query string with regex "or" separators added in:</p>
<div class="codehilite"><pre><span></span><code><span class="nx">$</span><span class="w"> </span><span class="kd">var</span><span class="w"> </span><span class="nx">tokens</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"{{ tokens|replace(' ', '|') }}"</span><span class="p">;</span>
<span class="nx">$</span><span class="w"> </span><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">tokens</span><span class="p">)</span>
<span class="nx">$</span><span class="w"> </span><span class="s2">"this|is|a|query"</span>
</code></pre></div>
<div class="callout">
<p>So, regex is an entire language that I'm definitely not going to even scratch the surface of here. If you want to learn more, I suggest the great website <a href="https://regexr.com/">regexr.com</a>.</p>
</div>
<h2>Cleaning Up The Query</h2>
<p>The first issue I encountered were HTML encodings in my string that were added when everything was passed to Jinja.</p>
<p>To remove these, I found a nifty little function from <a href="https://gomakethings.com/decoding-html-entities-with-vanilla-javascript/">Rob Wu on StackOverflow</a> that strips all HTML encoding so I have a raw string to match.</p>
<div class="codehilite"><pre><span></span><code><span class="c1">// remove html encoding from the flask search tokens</span>
<span class="c1">// hat tip to https://gomakethings.com/decoding-html-entities-with-vanilla-javascript/</span>
<span class="kd">var</span><span class="w"> </span><span class="nx">decodeHTML</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kd">function</span><span class="w"> </span><span class="p">(</span><span class="nx">html</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="kd">var</span><span class="w"> </span><span class="nx">txt</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="s2">"textarea"</span><span class="p">);</span>
<span class="w">  </span><span class="nx">txt</span><span class="p">.</span><span class="nx">innerHTML</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">html</span><span class="p">;</span>
<span class="w">  </span><span class="k">return</span><span class="w"> </span><span class="nx">txt</span><span class="p">.</span><span class="nx">value</span><span class="p">;</span>
<span class="p">};</span>
</code></pre></div>
<h2>Define Regex For Matching Tokens</h2>
<p>Since I'll be using regex to find all matching tokens on the page, I'm going to setup a new regular expression that I can apply to my content.</p>
<div class="codehilite"><pre><span></span><code><span class="kd">var</span><span class="w"> </span><span class="nx">re_quotes</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nb">RegExp</span><span class="p">(</span><span class="nx">tokens</span><span class="p">,</span><span class="w"> </span><span class="s2">"gi"</span><span class="p">);</span>
</code></pre></div>
<div class="callout">
<p>The "gi" options are for "global" search, and "case-insensitive" search". This ensures I find every instance of the search tokens.</p>
</div>
<h2>Limit The Highlights</h2>
<p>To make sure I only highlight the search tokens in the areas I want, I'll select all "article" objects on the page.</p>
<div class="codehilite"><pre><span></span><code><span class="kd">var</span><span class="w"> </span><span class="nx">quotes</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">document</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s2">"[id=article]"</span><span class="p">);</span>
</code></pre></div>
<h2>Time To Loop</h2>
<p>Now that I have all the elements I want to apply my highlighter to, I'll need to iterate over them, find the matches, and apply the <code>&lt;mark&gt;</code> tag.</p>
<h3>Iterating Over Any Found Matches</h3>
<p>Now that I know I have the correct elements selected, I can iterate over each of them, apply the regex I created earlier to find all matching tokens, and then iterate over those matches.</p>
<div class="codehilite"><pre><span></span><code><span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="kd">var</span><span class="w"> </span><span class="nx">i</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mf">0</span><span class="p">;</span><span class="w"> </span><span class="nx">i</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="nx">quotes</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span><span class="w"> </span><span class="nx">i</span><span class="o">++</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="kd">var</span><span class="w"> </span><span class="nx">content</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">quotes</span><span class="p">[</span><span class="nx">i</span><span class="p">].</span><span class="nx">innerHTML</span><span class="p">;</span>
<span class="w">  </span><span class="nx">content</span><span class="p">.</span><span class="nx">match</span><span class="p">(</span><span class="nx">re_quotes</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(</span><span class="kd">function</span><span class="w"> </span><span class="p">(</span><span class="nx">match</span><span class="p">,</span><span class="w"> </span><span class="nx">i</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">match</span><span class="p">);</span>
<span class="w">  </span><span class="p">});</span>
<span class="p">}</span>
</code></pre></div>
<h3>Setting Up The Replacement Regex</h3>
<p>To update the found tokens with the correct html tags, I need to setup a replacement regex that I can apply inside of the string replace function.</p>
<div class="codehilite"><pre><span></span><code><span class="kd">var</span><span class="w"> </span><span class="nx">re_matches</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nb">RegExp</span><span class="p">(</span><span class="nx">match</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s2">"\\b"</span><span class="p">,</span><span class="w"> </span><span class="s2">"gi"</span><span class="p">);</span>
</code></pre></div>
<p>The "\\b" addition to the token string makes sure I only replace whole word matches.</p>
<h3>Updating The Tokens</h3>
<p>With my replacement regex setup, I can apply the string.replace method to my content. This adds the opening <code>&lt;mark&gt;</code> and closing <code>&lt;/mark&gt;</code> html tags to each token match.</p>
<div class="codehilite"><pre><span></span><code><span class="nx">content</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">content</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="nx">re_matches</span><span class="p">,</span><span class="w"> </span><span class="s2">"&lt;mark&gt;"</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nx">match</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s2">"&lt;/mark&gt;"</span><span class="p">);</span>
</code></pre></div>
<h3>Applying The Changes On The Page</h3>
<p>Lastly, I need to update the page with the newly highlighted content. I can do this by setting the <code>innerHTML</code> of the current quote object of the iteration to the <code>content</code> variable.</p>
<div class="codehilite"><pre><span></span><code><span class="nx">quotes</span><span class="p">[</span><span class="nx">i</span><span class="p">].</span><span class="nx">innerHTML</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">content</span><span class="p">;</span>
</code></pre></div>
<h2>The Final Script</h2>
<p>Once again, I'm no JavaScript programmer but here's the final working script. It could be shortened a bit by combining some lines and functions, but I find this easier to read and keep up with exactly what's going on.</p>
<div class="codehilite"><pre><span></span><code><span class="c1">// remove html encoding from the flask search tokens</span>
<span class="c1">// hat tip to https://gomakethings.com/decoding-html-entities-with-vanilla-javascript/</span>
<span class="kd">var</span><span class="w"> </span><span class="nx">decodeHTML</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kd">function</span><span class="w"> </span><span class="p">(</span><span class="nx">html</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="kd">var</span><span class="w"> </span><span class="nx">txt</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="s2">"textarea"</span><span class="p">);</span>
<span class="w">  </span><span class="nx">txt</span><span class="p">.</span><span class="nx">innerHTML</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">html</span><span class="p">;</span>
<span class="w">  </span><span class="k">return</span><span class="w"> </span><span class="nx">txt</span><span class="p">.</span><span class="nx">value</span><span class="p">;</span>
<span class="p">};</span>

<span class="c1">// grab all search query tokens from flask</span>
<span class="c1">// add regex '|' "or" character in place of spaces</span>
<span class="kd">var</span><span class="w"> </span><span class="nx">tokens</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"{{ query| replace(' ', '|')}}"</span><span class="p">;</span>
<span class="c1">// remove html encoding mess from tokens using function above</span>
<span class="kd">var</span><span class="w"> </span><span class="nx">tokens</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">decodeHTML</span><span class="p">(</span><span class="nx">tokens</span><span class="p">);</span>
<span class="c1">// setup regex to find all matching tokens</span>
<span class="kd">var</span><span class="w"> </span><span class="nx">re_quotes</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nb">RegExp</span><span class="p">(</span><span class="nx">tokens</span><span class="p">,</span><span class="w"> </span><span class="s2">"gi"</span><span class="p">);</span>
<span class="c1">// find all articles/quotes on the page</span>
<span class="kd">var</span><span class="w"> </span><span class="nx">quotes</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">document</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s2">"[id=article]"</span><span class="p">);</span>

<span class="c1">// loop through all found articles elements (quotes)</span>
<span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="kd">var</span><span class="w"> </span><span class="nx">i</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mf">0</span><span class="p">;</span><span class="w"> </span><span class="nx">i</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="nx">quotes</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span><span class="w"> </span><span class="nx">i</span><span class="o">++</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="c1">// grab the article quote content</span>
<span class="w">  </span><span class="kd">var</span><span class="w"> </span><span class="nx">content</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">quotes</span><span class="p">[</span><span class="nx">i</span><span class="p">].</span><span class="nx">innerHTML</span><span class="p">;</span>
<span class="w">  </span><span class="nx">content</span><span class="p">.</span><span class="nx">match</span><span class="p">(</span><span class="nx">re_quotes</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(</span><span class="kd">function</span><span class="w"> </span><span class="p">(</span><span class="nx">match</span><span class="p">,</span><span class="w"> </span><span class="nx">i</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="c1">// loop over the all token matches in content</span>
<span class="w">    </span><span class="c1">// replace regex for all whole word token matches</span>
<span class="w">    </span><span class="kd">var</span><span class="w"> </span><span class="nx">re_matches</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nb">RegExp</span><span class="p">(</span><span class="nx">match</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s2">"\\b"</span><span class="p">,</span><span class="w"> </span><span class="s2">"gi"</span><span class="p">);</span>
<span class="w">    </span><span class="c1">// actually replace all token matches</span>
<span class="w">    </span><span class="nx">content</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">content</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="nx">re_matches</span><span class="p">,</span><span class="w"> </span><span class="s2">"&lt;mark&gt;"</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nx">match</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s2">"&lt;/mark&gt;"</span><span class="p">);</span>
<span class="w">  </span><span class="p">});</span>
<span class="w">  </span><span class="c1">// set quote content to with updated highlighting</span>
<span class="w">  </span><span class="nx">quotes</span><span class="p">[</span><span class="nx">i</span><span class="p">].</span><span class="nx">innerHTML</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">content</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>
<p>If you would like to give this a try, you can download the script and accompanying files at the gist below.</p>
<p><a href="https://gist.github.com/joshbduncan/fc230670837c91dad41683d73cc67d09">View the full script here</a></p>]]></content:encoded></item><item><title>Trying Out Things 3</title><link>https://joshbduncan.com/trying-out-things-3.html</link><pubDate>Mon, 01 Feb 2021 00:00:00 -0500</pubDate><guid>https://joshbduncan.com/trying-out-things-3.html</guid><description><![CDATA[I decided to switch my to-do list to Things 3 and needed to move a lot of to-dos over from Todoist. Seemed like a great project to explore their API and save myself a bunch of copying and pasting.]]></description><content:encoded><![CDATA[<p>I'm always on the search for the best to-do list app and I've been pretty happy with <a href="https://todoist.com/">Todoist</a> since 2015 as a premium member. Their input and date recognition are the best, and they are always adding and updating features. What's not to like, right?</p>
<p>Well, recently I've just grown tired of the look. There is nothing wrong with the look, I'm just tired of it.</p>
<h2>Enter Things 3</h2>
<p><a href="https://culturedcode.com/things/">Things 3</a> is gorgeous, and it's a native Mac app, so a few years back, I purchased it from Cultured Code for all of my devices on a whim 🤦‍♂️.</p>
<p>But after trying it out for a few days I wasn't 100% sold on the to-do entry mechanism or their repeating feature, so I never made the switch from Todoist.</p>
<div class="callout">
<p>If I'm being honest, I've always felt like I wasted my money on the Things 3 purchase and that's why I'm giving the app a real chance this time and I'm hoping it sticks.</p>
</div>
<h2>The Problem</h2>
<p>I often use Todoist as a read-it-later service, so I have a lot of to-dos that need to be moved over to Things 3.</p>
<p>To move over these to-dos correctly, I've have a hurdles to overcome...</p>
<p>Since I enter most of the read-it-later to-dos via the iOS share sheet, the to-do name in Todoist is actually a link in the <a href="https://daringfireball.net/projects/markdown/syntax#link">Markdown link format</a> like below.</p>
<div class="codehilite"><pre><span></span><code>Todoist To-do Item With Clickable Link:
[To-do Name](https://website.com)
</code></pre></div>
<p><img alt="Todoist To-do Item With Clickable Link" src="https://joshbduncan.com/static/images/todoist-link-to-do.png"/></p>
<p>This looks nice, and also opens the url when you click the to-do name.</p>
<p>If I copy and paste this over in Things 3 it looks crappy.</p>
<p><img alt="Things To-do Item With Link in Title" src="https://joshbduncan.com/static/images/things-link-to-do.png"/></p>
<p>The better looking way is to put the to-do name first (less the brackets) and then put the url link (less the parentheses) in the notes.</p>
<p><img alt="Things To-do Item With Link in Notes" src="https://joshbduncan.com/static/images/things-link-to-do-2.png"/></p>
<p>But this way would require a lots of copying and pasting, would be prone to error, and would be a complete pain to do for over 100 items.</p>
<h2>The Solution</h2>
<p>Now, I could probably hack something together using the Mac Swiss Army knife <a href="https://www.keyboardmaestro.com/">Keyboard Maestro</a>, but I know that Todoist has an awesome <a href="https://github.com/Doist/todoist-python">Python API Library</a> and Things 3 has great support for <a href="https://culturedcode.com/things/blog/2018/02/hey-things/">URL Schemes</a> so I should be able to whip something up with Python.</p>
<h3>Getting Setup</h3>
<p>First, I'll create a new folder for this project and create a virtual environment to work in.</p>
<div class="codehilite"><pre><span></span><code>$ mkdir todoist-grabber
$ cd todoist-grabber
$ python3 -m venv venv
</code></pre></div>
<p>Now, I can activate my new virtual environment.</p>
<div class="codehilite"><pre><span></span><code>$ source venv/bin/activate
</code></pre></div>
<div class="callout">
<p>If you are new to Python Virtual Environments, you can learn more about them <a href="https://docs.python.org/3/tutorial/venv.html">here</a>.</p>
</div>
<p>Lastly, I'll create a Python file to hold my code.</p>
<div class="codehilite"><pre><span></span><code>$ touch app.py
</code></pre></div>
<h3>Todoist API Token</h3>
<p>Now I'll need to find my Todoist User API Token which is located in the Todoist app under Settings &gt; Integrations.</p>
<p>I'll save my token as an environment variable in the terminal.</p>
<div class="codehilite"><pre><span></span><code>$ export TODOIST_KEY=abunchofgibberish0123456789
</code></pre></div>
<p>Saving environment variables this way is only temporary. As soon as you deactivate your virtual environment, they are lost.</p>
<p>An easier option is to use <a href="https://github.com/theskumar/python-dotenv">python-dotenv</a>.</p>
<div class="codehilite"><pre><span></span><code>$ pip install python-dotenv
</code></pre></div>
<p>Then, I can load my Todoist API Token into a new '.env' file.</p>
<div class="codehilite"><pre><span></span><code>$ echo "TODOIST_KEY=abunchofgibberish0123456789" &gt; .env
</code></pre></div>
<p>Now, whenever I activate the virtual environment for this project my Todoist API Token will automatically be loaded into the environment and accessible in my app.</p>
<h3>Todoist Python API library</h3>
<p>Doist, the company behind the Todoist app has an official <a href="https://github.com/Doist/todoist-python">Python API Library</a>. I can install the package using pip.</p>
<div class="codehilite"><pre><span></span><code>$ pip install todoist-python
</code></pre></div>
<p>This module will make it easier to interact with the Todoist API, saving me time and effort.</p>
<h2>The Code</h2>
<p>I'll start off using the <a href="https://docs.python.org/3/tutorial/interpreter.html">Python Interpreter</a> to make sure everything is working correctly with the Todoist API.</p>
<div class="codehilite"><pre><span></span><code><span class="gp">&gt;&gt;&gt; </span><span class="kn">import</span><span class="w"> </span><span class="nn">os</span>
<span class="gp">&gt;&gt;&gt; </span><span class="kn">from</span><span class="w"> </span><span class="nn">dotenv</span><span class="w"> </span><span class="kn">import</span> <span class="n">load_dotenv</span>
<span class="gp">&gt;&gt;&gt; </span><span class="kn">from</span><span class="w"> </span><span class="nn">todoist.api</span><span class="w"> </span><span class="kn">import</span> <span class="n">TodoistAPI</span>
</code></pre></div>
<p>Now that I have everything imported, I'm going to try and get my Todoist token from the environment variable I set earlier.</p>
<p>Next, I need to instantiate python-dotenv to load the environment variables I have setup.</p>
<div class="codehilite"><pre><span></span><code><span class="gp">&gt;&gt;&gt; </span><span class="n">load_dotenv</span><span class="p">()</span>
</code></pre></div>
<p>Then I can set the variable 'api_key' to my Todoist token.</p>
<div class="codehilite"><pre><span></span><code><span class="gp">&gt;&gt;&gt; </span><span class="n">api_key</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">environ</span><span class="p">[</span><span class="s2">"TODOIST_KEY"</span><span class="p">]</span>
</code></pre></div>
<p>And, to test out the connection, I'll print the name associated with my token.</p>
<div class="codehilite"><pre><span></span><code><span class="gp">&gt;&gt;&gt; </span><span class="n">api</span> <span class="o">=</span> <span class="n">TodoistAPI</span><span class="p">(</span><span class="n">api_key</span><span class="p">)</span>
<span class="gp">&gt;&gt;&gt; </span><span class="n">api</span><span class="o">.</span><span class="n">sync</span><span class="p">()</span>
<span class="gp">&gt;&gt;&gt; </span><span class="nb">print</span><span class="p">(</span><span class="n">api</span><span class="o">.</span><span class="n">state</span><span class="p">[</span><span class="s2">"user"</span><span class="p">][</span><span class="s2">"full_name"</span><span class="p">])</span>
<span class="go">Josh D.</span>
</code></pre></div>
<p>Hey, that's me!</p>
<p>Since this is working, I'll put what I have so far into app.py and start trying to extract my to-dos.</p>
<h3>Getting Every To-do from Todoist</h3>
<p>I have a lot of to-dos in Todoist, so I'm going to make a function that grabs them all and then stores them in the variable 'todoist_tasks'.</p>
<div class="codehilite"><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">get_all_tasks</span><span class="p">():</span>
    <span class="k">return</span> <span class="n">api</span><span class="o">.</span><span class="n">state</span><span class="p">[</span><span class="s2">"items"</span><span class="p">]</span>

<span class="n">todoist_tasks</span> <span class="o">=</span> <span class="n">get_all_tasks</span><span class="p">()</span>
</code></pre></div>
<p>This way, I don't have to keep reaching out to the API and wasting resources.</p>
<h3>My Todoist Projects</h3>
<p>Since, I'm only interested in the to-dos inside of my "Review" project, I need to grab all of my projects...</p>
<div class="codehilite"><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">get_all_projects</span><span class="p">():</span>
    <span class="k">return</span> <span class="n">api</span><span class="o">.</span><span class="n">state</span><span class="p">[</span><span class="s2">"projects"</span><span class="p">]</span>

<span class="n">todoist_projects</span> <span class="o">=</span> <span class="n">get_all_projects</span><span class="p">()</span>
</code></pre></div>
<p>And then present a list to pick from...</p>
<div class="codehilite"><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">pick_project</span><span class="p">(</span><span class="n">todoist_projects</span><span class="p">):</span>
    <span class="n">projects</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="nb">print</span><span class="p">(</span><span class="s2">"todoist projects"</span><span class="p">)</span>
    <span class="k">for</span> <span class="n">n</span><span class="p">,</span> <span class="n">project</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">todoist_projects</span><span class="p">):</span>
        <span class="n">project_name</span> <span class="o">=</span> <span class="n">project</span><span class="p">[</span><span class="s2">"name"</span><span class="p">]</span><span class="o">.</span><span class="n">encode</span><span class="p">(</span>
            <span class="s2">"ascii"</span><span class="p">,</span> <span class="s2">"ignore"</span><span class="p">)</span><span class="o">.</span><span class="n">decode</span><span class="p">(</span><span class="s2">"ascii"</span><span class="p">)</span>  <span class="c1"># remove emojis</span>
        <span class="n">project_id</span> <span class="o">=</span> <span class="n">project</span><span class="p">[</span><span class="s2">"id"</span><span class="p">]</span>
        <span class="n">projects</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">project_id</span><span class="p">)</span>
        <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="n">n</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="mi">1</span><span class="si">}</span><span class="s2">: </span><span class="si">{</span><span class="n">project_name</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
    <span class="n">project</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="nb">input</span><span class="p">(</span><span class="s2">"which project id? "</span><span class="p">))</span>
    <span class="k">return</span> <span class="n">projects</span><span class="p">[</span><span class="n">project</span> <span class="o">-</span> <span class="mi">1</span><span class="p">]</span>

<span class="n">todoist_project</span> <span class="o">=</span> <span class="n">pick_project</span><span class="p">(</span><span class="n">todoist_projects</span><span class="p">)</span>
</code></pre></div>
<p>I'll need this todoist_project variable later as it's the 'id' I will use to find the correct to-dos to move over to Things #.</p>
<h2>Finding My Read Later To-dos</h2>
<p>This next function will iterate over all of my to-dos and only grab the ones from the "Review" project.</p>
<div class="codehilite"><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">find_tasks</span><span class="p">(</span><span class="n">todoist_tasks</span><span class="p">,</span> <span class="n">todoist_project</span><span class="p">):</span>
    <span class="n">tasks</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">task</span> <span class="ow">in</span> <span class="n">todoist_tasks</span><span class="p">:</span>
        <span class="k">if</span> <span class="n">task</span><span class="o">.</span><span class="n">data</span><span class="p">[</span><span class="s2">"project_id"</span><span class="p">]</span> <span class="o">==</span> <span class="n">todoist_project</span><span class="p">:</span>
            <span class="n">content</span> <span class="o">=</span> <span class="n">task</span><span class="o">.</span><span class="n">data</span><span class="p">[</span><span class="s2">"content"</span><span class="p">]</span>
            <span class="n">when</span> <span class="o">=</span> <span class="n">task</span><span class="o">.</span><span class="n">data</span><span class="p">[</span><span class="s2">"due"</span><span class="p">][</span><span class="s2">"date"</span><span class="p">]</span> <span class="k">if</span> <span class="n">task</span><span class="o">.</span><span class="n">data</span><span class="p">[</span><span class="s2">"due"</span><span class="p">]</span> <span class="k">else</span> <span class="s2">""</span>
            <span class="n">creation_date</span> <span class="o">=</span> <span class="n">task</span><span class="o">.</span><span class="n">data</span><span class="p">[</span><span class="s2">"date_added"</span><span class="p">]</span>
            <span class="c1"># if markdown link task then separate out</span>
            <span class="k">if</span> <span class="n">content</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">==</span> <span class="s2">"["</span> <span class="ow">and</span> <span class="n">content</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span> <span class="o">==</span> <span class="s2">")"</span><span class="p">:</span>
                <span class="n">title</span> <span class="o">=</span> <span class="n">content</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">"]("</span><span class="p">)[</span><span class="mi">0</span><span class="p">][</span><span class="mi">1</span><span class="p">:]</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
                <span class="n">notes</span> <span class="o">=</span> <span class="n">content</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">"]("</span><span class="p">)[</span><span class="mi">1</span><span class="p">][:</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
            <span class="k">else</span><span class="p">:</span>
                <span class="n">title</span> <span class="o">=</span> <span class="n">content</span>
                <span class="n">notes</span> <span class="o">=</span> <span class="s2">""</span>
            <span class="n">tasks</span><span class="o">.</span><span class="n">append</span><span class="p">({</span><span class="s2">"title"</span><span class="p">:</span> <span class="n">title</span><span class="p">,</span> <span class="s2">"notes"</span><span class="p">:</span> <span class="n">notes</span><span class="p">,</span>
                            <span class="s2">"when"</span><span class="p">:</span> <span class="n">when</span><span class="p">,</span> <span class="s2">"creation_date"</span><span class="p">:</span> <span class="n">creation_date</span><span class="p">})</span>
    <span class="k">return</span> <span class="n">tasks</span>

<span class="n">things_tasks</span> <span class="o">=</span> <span class="n">find_tasks</span><span class="p">(</span><span class="n">todoist_tasks</span><span class="p">,</span> <span class="n">todoist_project</span><span class="p">)</span>
</code></pre></div>
<p>When the function finds a to-do from the "Review" project, it will save the to-do information I need as a dictionary to the list 'tasks'.</p>
<p>Before, appends to the tasks list, the function checks to see if the to-do includes one of the Markdown links I described earlier.</p>
<p>Basically, if the to-do 'content' starts with an opening bracket '[' and ends with a closing parenthesis ')', it's a Markdown link for the purposes of this program.</p>
<div class="codehilite"><pre><span></span><code><span class="k">if</span> <span class="n">content</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">==</span> <span class="s2">"["</span> <span class="ow">and</span> <span class="n">content</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span> <span class="o">==</span> <span class="s2">")"</span><span class="p">:</span>
    <span class="n">title</span> <span class="o">=</span> <span class="n">content</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">"]("</span><span class="p">)[</span><span class="mi">0</span><span class="p">][</span><span class="mi">1</span><span class="p">:]</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
    <span class="n">notes</span> <span class="o">=</span> <span class="n">content</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">"]("</span><span class="p">)[</span><span class="mi">1</span><span class="p">][:</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
<span class="k">else</span><span class="p">:</span>
    <span class="n">title</span> <span class="o">=</span> <span class="n">content</span>
    <span class="n">notes</span> <span class="o">=</span> <span class="s2">""</span>
</code></pre></div>
<p>If the function finds a match for that pattern, it splits the string at the '](' and takes the two parts, title and notes (url).</p>
<h2>Things 3 URL Schemes</h2>
<p>Now, that I have all of the to-dos that need to be moved, I can use URL Schemes to send the information to Things 3.</p>
<p>To "visit" these special urls, I'll make use of Python's Web-browser controller which will need to be imported into my app.</p>
<div class="codehilite"><pre><span></span><code><span class="kn">import</span><span class="w"> </span><span class="nn">webbrowser</span>
</code></pre></div>
<h3>Building the Urls</h3>
<p>Cultured Code has an awesome URL Scheme <a href="https://culturedcode.com/things/support/articles/2803573/#link-builder">Link Builder</a> on it's website. This makes it really easy to figure out the exact url I'll need to create each to-do in Things 3.</p>
<p>Using their Link Builder, I created placeholders for everything I would need to enter and ended up with this url.</p>
<div class="codehilite"><pre><span></span><code>things:///add?title=title&amp;notes=notes&amp;when=when&amp;tags=tags&amp;list=list_title&amp;creation-date=creation_date
</code></pre></div>
<p>And with the magic of Python 3's <a href="https://docs.python.org/3/reference/lexical_analysis.html#f-strings">f-Strings</a> I can easily make a custom url for each to-do, I'll just need to have each variable with the curly braces '{}' defined.</p>
<div class="codehilite"><pre><span></span><code><span class="sa">f</span><span class="s2">"things:///add?title=</span><span class="si">{</span><span class="n">title</span><span class="si">}</span><span class="s2">&amp;notes=</span><span class="si">{</span><span class="n">notes</span><span class="si">}</span><span class="s2">&amp;when=</span><span class="si">{</span><span class="n">when</span><span class="si">}</span><span class="s2">&amp;tags=</span><span class="si">{</span><span class="n">tags</span><span class="si">}</span><span class="s2">&amp;list=</span><span class="si">{</span><span class="n">list_title</span><span class="si">}</span><span class="s2">&amp;creation-date=</span><span class="si">{</span><span class="n">creation_date</span><span class="si">}</span><span class="s2">"</span>
</code></pre></div>
<div class="callout">
<p>You can leave the value for a placeholder variable blank if it's not needed for a specific to-do.</p>
</div>
<h3>Covering my A$$</h3>
<p>Before I import 100+ to-dos, maybe I should make it easy to find the imported to-dos just in case something goes wrong.</p>
<p>So, I plan to create a new project in Things 3 to hold all of the imported to-dos, which should make them easier to find.</p>
<p>Using the same <a href="https://culturedcode.com/things/support/articles/2803573/#link-builder">Link Builder</a> from above, I was able to create this url.</p>
<div class="codehilite"><pre><span></span><code>things:///add-project?title=things_project&amp;reveal=true
</code></pre></div>
<p>Visiting this url will create a new project in Things 3 and then reveal that project in the app.</p>
<div class="codehilite"><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">make_project</span><span class="p">():</span>
    <span class="n">things_project</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">"IMPORT </span><span class="si">{</span><span class="n">datetime</span><span class="o">.</span><span class="n">now</span><span class="p">()</span><span class="o">.</span><span class="n">strftime</span><span class="p">(</span><span class="s1">'%m-%y </span><span class="si">%X</span><span class="s1">'</span><span class="p">)</span><span class="si">}</span><span class="s2">"</span>
    <span class="n">purl</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">"things:///add-project?title=</span><span class="si">{</span><span class="n">things_project</span><span class="si">}</span><span class="s2">&amp;reveal=true"</span>
    <span class="n">webbrowser</span><span class="o">.</span><span class="n">open</span><span class="p">(</span><span class="n">purl</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">things_project</span>
</code></pre></div>
<p>As you can see, this function does use the datetime module so I added that to the top of my app.py file.</p>
<div class="codehilite"><pre><span></span><code><span class="kn">from</span><span class="w"> </span><span class="nn">datetime</span><span class="w"> </span><span class="kn">import</span> <span class="n">datetime</span>
</code></pre></div>
<p>Next, the function creates the Things 3 url using f-Strings again and visits the url using the webbrowser.open(purl).</p>
<p>The last line returns the name of the new project so that I can use that value when adding my to-dos.</p>
<h2>Adding To-dos to Things 3</h2>
<p>I have everything in place, now I just need to make a function that creates a custom url for each to-do that needs to be moved.</p>
<div class="codehilite"><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">add_to_things</span><span class="p">(</span><span class="n">things_tasks</span><span class="p">):</span>
    <span class="c1"># create a new project/list to hold the imported tasks</span>
    <span class="n">list_title</span> <span class="o">=</span> <span class="n">make_project</span><span class="p">()</span>
    <span class="c1"># tags must already exist, comma separated</span>
    <span class="n">tags</span> <span class="o">=</span> <span class="s2">"todoist"</span>
    <span class="k">for</span> <span class="n">task</span> <span class="ow">in</span> <span class="n">things_tasks</span><span class="p">:</span>
        <span class="n">title</span> <span class="o">=</span> <span class="n">task</span><span class="p">[</span><span class="s2">"title"</span><span class="p">]</span>
        <span class="n">notes</span> <span class="o">=</span> <span class="n">task</span><span class="p">[</span><span class="s2">"notes"</span><span class="p">]</span>
        <span class="n">when</span> <span class="o">=</span> <span class="n">task</span><span class="p">[</span><span class="s2">"when"</span><span class="p">]</span>
        <span class="n">creation_date</span> <span class="o">=</span> <span class="n">task</span><span class="p">[</span><span class="s2">"creation_date"</span><span class="p">]</span>
        <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">"...moving </span><span class="si">{</span><span class="n">title</span><span class="p">[:</span><span class="mi">30</span><span class="p">]</span><span class="si">}</span><span class="s2">..."</span><span class="p">)</span>
        <span class="n">turl</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">"things:///add?title=</span><span class="si">{</span><span class="n">title</span><span class="si">}</span><span class="s2">&amp;notes=</span><span class="si">{</span><span class="n">notes</span><span class="si">}</span><span class="s2">&amp;when=</span><span class="si">{</span><span class="n">when</span><span class="si">}</span><span class="s2">&amp;tags=</span><span class="si">{</span><span class="n">tags</span><span class="si">}</span><span class="s2">&amp;list=</span><span class="si">{</span><span class="n">list_title</span><span class="si">}</span><span class="s2">&amp;creation-date=</span><span class="si">{</span><span class="n">creation_date</span><span class="si">}</span><span class="s2">"</span>
        <span class="n">webbrowser</span><span class="o">.</span><span class="n">open</span><span class="p">(</span><span class="n">turl</span><span class="p">)</span>
    <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">"tasks moved to things. find them in project </span><span class="si">{</span><span class="n">list_title</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
</code></pre></div>
<p>First, I'll call the make_project() function to create and open a new project in Things 3.</p>
<p>Next, I can set whatever tags (must already exist in Things 3) I want to applied to the imported to-dos.</p>
<p>Now the fun starts!</p>
<p>The function iterates over my list of to-dos, grabbing all of the values I need from the dictionary in the things_tasks list to create a custom Things 3 url.</p>
<p>Once, the url is formatted. I visit it using webbrowser.open(turl) and things happen fast.</p>
<p><img alt="To-dos Added to Things 3" src="https://joshbduncan.com/static/images/to-dos-added-to-things.gif"/></p>
<p>As you can see, all of the to-dos come flooding in to my new project. Success!</p>
<h2>Cleaning Up</h2>
<p>Now, I just need to clean up my code and add the <code>if name == main</code> clause so I can use this standalone or on import if need be.</p>
<div class="codehilite"><pre><span></span><code><span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s2">"__main__"</span><span class="p">:</span>
    <span class="n">todoist_tasks</span> <span class="o">=</span> <span class="n">get_all_tasks</span><span class="p">()</span>
    <span class="n">todoist_projects</span> <span class="o">=</span> <span class="n">get_all_projects</span><span class="p">()</span>
    <span class="n">todoist_project</span> <span class="o">=</span> <span class="n">pick_project</span><span class="p">(</span><span class="n">todoist_projects</span><span class="p">)</span>
    <span class="n">things_tasks</span> <span class="o">=</span> <span class="n">find_tasks</span><span class="p">(</span><span class="n">todoist_tasks</span><span class="p">,</span> <span class="n">todoist_project</span><span class="p">)</span>
    <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">"found </span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">things_tasks</span><span class="p">)</span><span class="si">}</span><span class="s2"> matching tasks..."</span><span class="p">)</span>
    <span class="n">cont</span> <span class="o">=</span> <span class="nb">input</span><span class="p">(</span><span class="s2">"continue? (y/n) "</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">cont</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span> <span class="o">!=</span> <span class="s2">"y"</span><span class="p">:</span>
        <span class="nb">print</span><span class="p">(</span><span class="s2">"no tasks were moved."</span><span class="p">)</span>
        <span class="n">exit</span><span class="p">()</span>
    <span class="k">else</span><span class="p">:</span>
        <span class="nb">print</span><span class="p">(</span><span class="s2">"starting to move your tasks..."</span><span class="p">)</span>
        <span class="n">add_to_things</span><span class="p">(</span><span class="n">things_tasks</span><span class="p">)</span>
</code></pre></div>
<p><a href="https://gist.github.com/joshbduncan/9e88e8ccdc22e32cac844da80671274e">View the full script here</a></p>]]></content:encoded></item><item><title>Python Type Hints and Fancy Banners</title><link>https://joshbduncan.com/python-type-hints-and-banners.html</link><pubDate>Fri, 22 Jan 2021 00:00:00 -0500</pubDate><guid>https://joshbduncan.com/python-type-hints-and-banners.html</guid><description><![CDATA[While leaning about Python Type Hints, I wrote a nifty little function to display cool banners and help remind me about the concepts of type hinting.]]></description><content:encoded><![CDATA[<p>When learning new things in Python, I like to write snippets that I can refer to later when I need a quick refresher.</p>
<p>Recently, while reading about <a href="https://docs.python.org/3/library/typing.html">Python Type Hints</a>, I wrote a nifty little function to display banners in the terminal.</p>
<h2>Backstory</h2>
<p>Back in December, while working on the <a href="https://adventofcode.com/">2020 Advent of Code</a> challenge, I needed an easy way to print what I'm calling "banners" while debugging.</p>
<p>If you've participated in an Advent of Code challenge, you know that while solving the challenges, you print lots of information out to the terminal. And, sometimes you need easily recognizable breaks (what I'm calling "banners") in the output (e.g. new branches in a recursion tree).</p>
<p>I usually do something like this...</p>
<div class="codehilite"><pre><span></span><code><span class="gp">&gt;&gt;&gt; </span><span class="nb">print</span><span class="p">(</span><span class="s2">"*"</span> <span class="o">*</span> <span class="mi">40</span><span class="p">)</span>
<span class="gp">&gt;&gt;&gt; </span><span class="nb">print</span><span class="p">(</span><span class="s2">"Thing I'm looking for"</span><span class="p">)</span>
<span class="gp">&gt;&gt;&gt; </span><span class="nb">print</span><span class="p">(</span><span class="s2">"*"</span> <span class="o">*</span> <span class="mi">40</span><span class="p">)</span>
<span class="go">****************************************</span>
<span class="go">Thing I'm looking for</span>
<span class="go">****************************************</span>
</code></pre></div>
<p>Now, there's nothing wrong with this, but it's certainly not pretty.</p>
<h2>Let's Make a Function</h2>
<p>First, I'll make a new function 'make_banner()' so it's easy to print new banners when needed.</p>
<div class="codehilite"><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">make_banner</span><span class="p">(</span><span class="n">txt</span><span class="p">):</span>
    <span class="nb">print</span><span class="p">(</span><span class="s2">"*"</span> <span class="o">*</span> <span class="mi">40</span><span class="p">)</span>
    <span class="nb">print</span><span class="p">(</span><span class="n">txt</span><span class="p">)</span>
    <span class="nb">print</span><span class="p">(</span><span class="s2">"*"</span> <span class="o">*</span> <span class="mi">40</span><span class="p">)</span>
</code></pre></div>
<p>Now, you just call the function make_banner() and supply your banner text as an argument.</p>
<div class="codehilite"><pre><span></span><code><span class="gp">&gt;&gt;&gt; </span><span class="n">make_banner</span><span class="p">(</span><span class="s2">"Provided as an argument"</span><span class="p">)</span>
<span class="go">****************************************</span>
<span class="go">Provided as an argument</span>
<span class="go">****************************************</span>
</code></pre></div>
<h2>Adding Styling</h2>
<p>Wouldn't it be nice to have the text centered? That's simple enough...</p>
<div class="codehilite"><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">make_banner</span><span class="p">(</span><span class="n">txt</span><span class="p">):</span>
    <span class="nb">print</span><span class="p">(</span><span class="s2">"*"</span> <span class="o">*</span> <span class="mi">40</span><span class="p">)</span>
    <span class="nb">print</span><span class="p">(</span><span class="s2">" "</span> <span class="o">*</span> <span class="p">((</span><span class="mi">40</span> <span class="o">-</span> <span class="nb">len</span><span class="p">(</span><span class="n">txt</span><span class="p">))</span> <span class="o">//</span> <span class="mi">2</span><span class="p">)</span> <span class="o">+</span> <span class="n">txt</span><span class="p">)</span>
    <span class="nb">print</span><span class="p">(</span><span class="s2">"*"</span> <span class="o">*</span> <span class="mi">40</span><span class="p">)</span>
</code></pre></div>
<p>Here, I've subtracted the length of the provided text from 40 (the amount of stars), divided that by 2, and added that many spaces before printing the text.</p>
<div class="codehilite"><pre><span></span><code><span class="gp">&gt;&gt;&gt; </span><span class="n">make_banner</span><span class="p">(</span><span class="s2">"This is a banner"</span><span class="p">)</span>
<span class="go">****************************************</span>
<span class="go">            This is a banner</span>
<span class="go">****************************************</span>
</code></pre></div>
<p>This works, but I notice a few problems.</p>
<h3>Problem #1:</h3>
<p>If the supplied text is longer than 40 characters, the border stars '*' will end before the text.</p>
<div class="codehilite"><pre><span></span><code><span class="gp">&gt;&gt;&gt; </span><span class="n">make_banner</span><span class="p">(</span><span class="s2">"This is a really really really long banner!"</span><span class="p">)</span>
<span class="go">****************************************</span>
<span class="go">This is a really really really long banner!</span>
<span class="go">****************************************</span>
</code></pre></div>
<h3>Problem #2:</h3>
<p>If the supplied text length is a negative number, then the padding on the left and right of the text can't be equal.</p>
<div class="codehilite"><pre><span></span><code><span class="gp">&gt;&gt;&gt; </span><span class="n">make_banner</span><span class="p">(</span><span class="s2">"This is a banner!"</span><span class="p">)</span>
<span class="go">****************************************</span>
<span class="go">           This is a banner!</span>
<span class="go">****************************************</span>
</code></pre></div>
<p>Notice there are 11 stars '*' on the left, and 12 on the right. Now, that might not bother you, but as a designer, I just can't 🙈.</p>
<h3>Solution:</h3>
<p>The fix for Problem #1 is simple enough and as you'll see and it solves Problem #2 as well.</p>
<p>I'll just take the length of the provided string into account when calculating how many stars to print.</p>
<div class="codehilite"><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">make_banner</span><span class="p">(</span><span class="n">txt</span><span class="p">):</span>
    <span class="nb">print</span><span class="p">(</span><span class="s2">"*"</span> <span class="o">*</span> <span class="nb">len</span><span class="p">(</span><span class="n">txt</span><span class="p">))</span>
    <span class="nb">print</span><span class="p">(</span><span class="n">txt</span><span class="p">)</span>
    <span class="nb">print</span><span class="p">(</span><span class="s2">"*"</span> <span class="o">*</span> <span class="nb">len</span><span class="p">(</span><span class="n">txt</span><span class="p">))</span>
</code></pre></div>
<p>Running the function give us a better version that works for any length of text and eliminates the centering problem.</p>
<div class="codehilite"><pre><span></span><code><span class="gp">&gt;&gt;&gt; </span><span class="n">make_banner</span><span class="p">(</span><span class="s2">"This is a really really really long banner!"</span><span class="p">)</span>
<span class="go">*******************************************</span>
<span class="go">This is a really really really long banner!</span>
<span class="go">*******************************************</span>
</code></pre></div>
<h2>Type Hints</h2>
<p>Next, I'll make this function super extra fancy with Python's newish type hints (<a href="https://www.python.org/dev/peps/pep-0484/">PEP 484</a>).</p>
<div class="callout">
<p>It is important for the user to be able to define types in a form that can be understood by type checkers. The goal of this PEP is to propose such a systematic way of defining types for type annotations of variables and functions using PEP 3107 syntax. These annotations can be used to avoid many kind of bugs, for documentation purposes, or maybe even to increase speed of program execution.</p>
</div>
<p>Among many things, type hints allow me to specify the type of object my function is expecting to receive. As noted above, this can help with debugging and act as documentation too.</p>
<p>I'll start by specifying that the txt argument should be a string.</p>
<div class="codehilite"><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">make_banner</span><span class="p">(</span><span class="n">txt</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
    <span class="nb">print</span><span class="p">(</span><span class="s2">"*"</span> <span class="o">*</span> <span class="nb">len</span><span class="p">(</span><span class="n">txt</span><span class="p">))</span>
    <span class="nb">print</span><span class="p">(</span><span class="n">txt</span><span class="p">)</span>
    <span class="nb">print</span><span class="p">(</span><span class="s2">"*"</span> <span class="o">*</span> <span class="nb">len</span><span class="p">(</span><span class="n">txt</span><span class="p">))</span>
</code></pre></div>
<p>By adding ': str' to the function argument, I'm letting myself/user/type checker/IDE know that the function expects a string to be supplied as the argument. Not a list, or an integer, a string. Now, you could provide something other than a string, but that's not what is expected and doing so could cause problems with the function operation.</p>
<p>At this point, I haven't changed the functionality of make_banner(), but I could definitely add some bell and whistles to make it better.</p>
<h3>Optional Arguments</h3>
<p>I think the banner would look nicer with padding on each side of the text.</p>
<div class="codehilite"><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">make_banner</span><span class="p">(</span><span class="n">txt</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">padding</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">5</span><span class="p">):</span>
    <span class="nb">print</span><span class="p">(</span><span class="s2">"*"</span> <span class="o">*</span> <span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">txt</span><span class="p">)</span> <span class="o">+</span> <span class="n">padding</span> <span class="o">*</span> <span class="mi">2</span><span class="p">))</span>
    <span class="nb">print</span><span class="p">(</span><span class="s2">" "</span> <span class="o">*</span> <span class="n">padding</span> <span class="o">+</span> <span class="n">txt</span><span class="p">)</span>
    <span class="nb">print</span><span class="p">(</span><span class="s2">"*"</span> <span class="o">*</span> <span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">txt</span><span class="p">)</span> <span class="o">+</span> <span class="n">padding</span> <span class="o">*</span> <span class="mi">2</span><span class="p">))</span>
</code></pre></div>
<p>So, I've added the new argument 'padding' and specified it as an integer with the type hint 'int'. I've also added something else, a default value of 5 '= 5'. This way, if a padding isn't specified in the function call, everything will still look nice.</p>
<div class="codehilite"><pre><span></span><code><span class="gp">&gt;&gt;&gt; </span><span class="n">make_banner</span><span class="p">(</span><span class="s2">"A banner with some padding!"</span><span class="p">)</span>
<span class="go">*************************************</span>
<span class="go">     A banner with some padding!</span>
<span class="go">*************************************</span>
</code></pre></div>
<p>Since I didn't specify any padding, I get the default of five stars '*' on each side of the text.</p>
<div class="codehilite"><pre><span></span><code><span class="gp">&gt;&gt;&gt; </span><span class="n">make_banner</span><span class="p">(</span><span class="s2">"A banner with MORE padding!"</span><span class="p">,</span> <span class="mi">10</span><span class="p">)</span>
<span class="go">***********************************************</span>
<span class="go">          A banner with MORE padding!</span>
<span class="go">***********************************************</span>
</code></pre></div>
<p>Here, I specified a padding value of 10, so I get ten stars '*' on each side of my banner text.</p>
<p>Let's keep going by adding an optional argument for the border character.</p>
<div class="codehilite"><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">make_banner</span><span class="p">(</span><span class="n">txt</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">padding</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">5</span><span class="p">,</span> <span class="n">border</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="s2">"*"</span><span class="p">):</span>
    <span class="nb">print</span><span class="p">(</span><span class="n">border</span> <span class="o">*</span> <span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">txt</span><span class="p">)</span> <span class="o">+</span> <span class="n">padding</span> <span class="o">*</span> <span class="mi">2</span><span class="p">))</span>
    <span class="nb">print</span><span class="p">(</span><span class="s2">" "</span> <span class="o">*</span> <span class="n">padding</span> <span class="o">+</span> <span class="n">txt</span><span class="p">)</span>
    <span class="nb">print</span><span class="p">(</span><span class="n">border</span> <span class="o">*</span> <span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">txt</span><span class="p">)</span> <span class="o">+</span> <span class="n">padding</span> <span class="o">*</span> <span class="mi">2</span><span class="p">))</span>
</code></pre></div>
<p>This new option allows me to change the border character to anything I want. Like before, I set a default of '*', so if nothing is specified, I'll get stars. Let's try it...</p>
<div class="codehilite"><pre><span></span><code><span class="gp">&gt;&gt;&gt; </span><span class="n">make_banner</span><span class="p">(</span><span class="s2">"A banner with some padding!"</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="s2">"-"</span><span class="p">)</span>
<span class="go">---------------------------------</span>
<span class="go">   A banner with some padding!</span>
<span class="go">---------------------------------</span>
</code></pre></div>
<p>Nice! How about a few more?</p>
<div class="codehilite"><pre><span></span><code><span class="gp">&gt;&gt;&gt; </span><span class="n">make_banner</span><span class="p">(</span><span class="s2">"A banner with some padding!"</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="s2">"#"</span><span class="p">)</span>
<span class="go">#################################</span>
<span class="go">   A banner with some padding!</span>
<span class="go">#################################</span>

<span class="gp">&gt;&gt;&gt; </span><span class="n">make_banner</span><span class="p">(</span><span class="s2">"A banner with some padding!"</span><span class="p">,</span> <span class="mi">12</span><span class="p">,</span> <span class="s2">"+"</span><span class="p">)</span>
<span class="go">+++++++++++++++++++++++++++++++++++++++++++++++++++</span>
<span class="go">            A banner with some padding!</span>
<span class="go">+++++++++++++++++++++++++++++++++++++++++++++++++++</span>
</code></pre></div>
<p><strong>A few things to note...</strong> If you specify a border that's more than one character, the alignment will be thrown off.</p>
<p>And, even though the 'padding' and 'border' arguments are optional, they must be provided in the order they are defined in the function. If you want to change the border then you must also provide a padding value. So, txt, padding, border, in that order. If you try something like:</p>
<div class="codehilite"><pre><span></span><code><span class="gp">&gt;&gt;&gt; </span><span class="n">make_banner</span><span class="p">(</span><span class="s2">"A banner with some padding!"</span><span class="p">,</span> <span class="s2">"+"</span><span class="p">)</span>
<span class="go">TypeError: unsupported operand type(s) for +: 'int' and 'str'</span>
</code></pre></div>
<p>You'll get a TypeError as the function is trying to use the '+' for the padding amount. The workaround for this is simple, just specify which argument you are providing to the function.</p>
<div class="codehilite"><pre><span></span><code><span class="gp">&gt;&gt;&gt; </span><span class="n">make_banner</span><span class="p">(</span><span class="n">txt</span><span class="o">=</span><span class="s2">"A banner with some padding!"</span><span class="p">,</span> <span class="n">border</span><span class="o">=</span><span class="s2">"+"</span><span class="p">)</span>
<span class="go">+++++++++++++++++++++++++++++++++++++</span>
<span class="go">     A banner with some padding!</span>
<span class="go">+++++++++++++++++++++++++++++++++++++</span>
</code></pre></div>
<p>By providing the arguments this way, the order actually doesn't matter at all.</p>
<div class="codehilite"><pre><span></span><code><span class="gp">&gt;&gt;&gt; </span><span class="n">make_banner</span><span class="p">(</span><span class="n">border</span><span class="o">=</span><span class="s2">"+"</span><span class="p">,</span> <span class="n">padding</span><span class="o">=</span><span class="mi">10</span><span class="p">,</span> <span class="n">txt</span><span class="o">=</span><span class="s2">"This is craziness!"</span><span class="p">)</span>
<span class="go">++++++++++++++++++++++++++++++++++++++</span>
<span class="go">          This is craziness!</span>
<span class="go">++++++++++++++++++++++++++++++++++++++</span>
</code></pre></div>
<h3>Return Values</h3>
<p>You can also specify the return value of your function using type hints. This helps the user know what to expect when running the function.</p>
<div class="codehilite"><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">make_banner</span><span class="p">(</span><span class="n">txt</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">padding</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">5</span><span class="p">,</span> <span class="n">border</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="s2">"*"</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
    <span class="k">return</span> <span class="p">(</span>
        <span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="n">border</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">txt</span><span class="p">)</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">padding</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mi">2</span><span class="p">)</span><span class="si">}</span><span class="se">\n</span><span class="s2">"</span>
        <span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="s1">' '</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="n">padding</span><span class="si">}{</span><span class="n">txt</span><span class="si">}</span><span class="se">\n</span><span class="s2">"</span>
        <span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="n">border</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">txt</span><span class="p">)</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">padding</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mi">2</span><span class="p">)</span><span class="si">}</span><span class="s2">"</span>
    <span class="p">)</span>
</code></pre></div>
<p>You can see the addition of '-&gt; str:' to the end of the function. This is the letting the user/type checker/IDE know a string will be returned.</p>
<p>To make the function correct, I need to return the banner as a string now instead of just printing it out. This is good practice anyway, as I may want to store a banner as a variable instead of printing it right away. I accomplish this by using Python 3's <a href="https://docs.python.org/3/reference/lexical_analysis.html#f-strings">f-Strings</a>.</p>
<p>Now, to store the banner as a variable I can run:</p>
<div class="codehilite"><pre><span></span><code><span class="gp">&gt;&gt;&gt; </span><span class="n">banner</span> <span class="o">=</span> <span class="n">make_banner</span><span class="p">(</span><span class="s2">"Banner as a variable"</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="s2">"#"</span><span class="p">)</span>
</code></pre></div>
<p>Then, to print out the new banner I can run:</p>
<div class="codehilite"><pre><span></span><code><span class="gp">&gt;&gt;&gt; </span><span class="nb">print</span><span class="p">(</span><span class="n">banner</span><span class="p">)</span>
<span class="go">##############################</span>
<span class="go">    Banner as a variable</span>
<span class="go">##############################</span>
</code></pre></div>
<p>Or, if I want to print the banner right away I can just put the function call inside of a print statement.</p>
<div class="codehilite"><pre><span></span><code><span class="gp">&gt;&gt;&gt; </span><span class="nb">print</span><span class="p">(</span><span class="n">make_banner</span><span class="p">(</span><span class="s2">"Print me now! :)"</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="s2">"#"</span><span class="p">))</span>
<span class="go">########################</span>
<span class="go">    Print me now! :)</span>
<span class="go">########################</span>
</code></pre></div>
<h2>Taking It Further</h2>
<p>This function could definitely be expansed upon... Maybe implementing some coloring, or vertical padding, but I'll save that for another day.</p>
<p><a href="https://gist.github.com/joshbduncan/f2b7a817b1a3b1e45a131a4fc27d8754">View the full script here</a></p>]]></content:encoded></item></channel></rss>