<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.3.4">Jekyll</generator><link href="https://blog.bbs-software.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://blog.bbs-software.com/" rel="alternate" type="text/html" /><updated>2026-06-10T13:01:01+00:00</updated><id>https://blog.bbs-software.com/feed.xml</id><title type="html">Technical Explorations</title><subtitle>Technical Explorations by Keith R. Bennett</subtitle><entry><title type="html">Enforcing Subclass Method Implementation in WiFi Wand Using TracePoint</title><link href="https://blog.bbs-software.com/blog/2025/08/14/enforcing-subcalass-method-implementation-using-tracepoint/" rel="alternate" type="text/html" title="Enforcing Subclass Method Implementation in WiFi Wand Using TracePoint" /><published>2025-08-14T00:00:00+00:00</published><updated>2025-08-14T00:00:00+00:00</updated><id>https://blog.bbs-software.com/blog/2025/08/14/enforcing-subcalass-method-implementation-using-tracepoint</id><content type="html" xml:base="https://blog.bbs-software.com/blog/2025/08/14/enforcing-subcalass-method-implementation-using-tracepoint/"><![CDATA[<h1 id="rubys-inherited-hook-the-timing-problem-and-a-tracepoint-solution-in-wifi-wand">Ruby’s <code class="language-plaintext highlighter-rouge">inherited</code> Hook: The Timing Problem and a TracePoint Solution in WiFi Wand</h1>

<p>When building cross-platform Ruby libraries like WiFi Wand, you’ll likely encounter the <code class="language-plaintext highlighter-rouge">inherited</code> hook. This powerful callback allows parent classes to respond when they’re subclassed. However, there’s a subtle timing issue that can trip up even experienced Ruby developers: <strong>the <code class="language-plaintext highlighter-rouge">inherited</code> hook fires before the subclass is fully defined</strong>.</p>

<h2 id="the-problem-early-execution-of-inherited-in-wifi-wand">The Problem: Early Execution of <code class="language-plaintext highlighter-rouge">inherited</code> in WiFi Wand</h2>

<p>Let’s say you’re building WiFi Wand’s architecture where you want to automatically verify that OS-specific model classes implement all required underscore-prefixed methods. Your first instinct might be to use the <code class="language-plaintext highlighter-rouge">inherited</code> hook:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">BaseModel</span>
  <span class="no">UNDERSCORE_PREFIXED_METHODS</span> <span class="o">=</span> <span class="sx">%i[
    _available_network_names
    _connected_network_name
    _disconnect
    _ip_address
  ]</span><span class="p">.</span><span class="nf">freeze</span>

  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">inherited</span><span class="p">(</span><span class="n">subclass</span><span class="p">)</span>
    <span class="nb">puts</span> <span class="s2">"Verifying underscore methods for </span><span class="si">#{</span><span class="n">subclass</span><span class="si">}</span><span class="s2">..."</span>
    <span class="n">missing_methods</span> <span class="o">=</span> <span class="no">UNDERSCORE_PREFIXED_METHODS</span> <span class="o">-</span> <span class="n">subclass</span><span class="p">.</span><span class="nf">public_instance_methods</span>
    <span class="k">unless</span> <span class="n">missing_methods</span><span class="p">.</span><span class="nf">empty?</span>
      <span class="k">raise</span> <span class="no">NotImplementedError</span><span class="p">,</span> <span class="s2">"Subclass </span><span class="si">#{</span><span class="n">subclass</span><span class="p">.</span><span class="nf">name</span><span class="si">}</span><span class="s2"> must implement </span><span class="si">#{</span><span class="n">missing_methods</span><span class="p">.</span><span class="nf">inspect</span><span class="si">}</span><span class="s2">"</span>
    <span class="k">end</span>
    <span class="nb">puts</span> <span class="s2">"All underscore methods implemented!"</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="k">class</span> <span class="nc">UbuntuModel</span> <span class="o">&lt;</span> <span class="no">BaseModel</span>
  <span class="k">def</span> <span class="nf">_available_network_names</span>
    <span class="c1"># Implementation that scans for available networks</span>
  <span class="k">end</span>
  
  <span class="k">def</span> <span class="nf">_connected_network_name</span>
    <span class="c1"># Implementation that gets current connection</span>
  <span class="k">end</span>
  
  <span class="k">def</span> <span class="nf">_disconnect</span>
    <span class="c1"># Implementation that disconnects from current network</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Running this code produces surprising output:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Verifying underscore methods for UbuntuModel...
NotImplementedError: Subclass UbuntuModel must implement [:_available_network_names, :_connected_network_name, :_disconnect, :_ip_address]
</code></pre></div></div>

<p>The missing methods list includes methods that are actually defined in the subclass! But why?</p>

<h2 id="understanding-the-timing-issue">Understanding the Timing Issue</h2>

<p>The <code class="language-plaintext highlighter-rouge">inherited</code> hook is called immediately when Ruby encounters the class declaration line (<code class="language-plaintext highlighter-rouge">class UbuntuModel &lt; BaseModel</code>), but <strong>before</strong> any of the method definitions in the class body are processed. Here’s the execution order:</p>

<ol>
  <li>Ruby sees <code class="language-plaintext highlighter-rouge">class UbuntuModel &lt; BaseModel</code></li>
  <li><code class="language-plaintext highlighter-rouge">inherited(UbuntuModel)</code> is called immediately</li>
  <li>The class body is processed (method definitions, etc.)</li>
  <li>Class definition completes</li>
</ol>

<p>This timing makes <code class="language-plaintext highlighter-rouge">inherited</code> perfect for setting up class-level configuration or establishing inheritance hierarchies, but useless for inspecting a fully-defined subclass.</p>

<p>Let’s prove this with a more detailed example:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">BaseModel</span>
  <span class="no">UNDERSCORE_PREFIXED_METHODS</span> <span class="o">=</span> <span class="sx">%i[_available_network_names _connected_network_name _disconnect]</span><span class="p">.</span><span class="nf">freeze</span>

  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">inherited</span><span class="p">(</span><span class="n">subclass</span><span class="p">)</span>
    <span class="nb">puts</span> <span class="s2">"inherited called for </span><span class="si">#{</span><span class="n">subclass</span><span class="si">}</span><span class="s2">"</span>
    <span class="nb">puts</span> <span class="s2">"Methods at this point: </span><span class="si">#{</span><span class="n">subclass</span><span class="p">.</span><span class="nf">public_instance_methods</span><span class="p">(</span><span class="kp">false</span><span class="p">)</span><span class="si">}</span><span class="s2">"</span>
    <span class="n">missing</span> <span class="o">=</span> <span class="no">UNDERSCORE_PREFIXED_METHODS</span> <span class="o">-</span> <span class="n">subclass</span><span class="p">.</span><span class="nf">public_instance_methods</span><span class="p">(</span><span class="kp">false</span><span class="p">)</span>
    <span class="nb">puts</span> <span class="s2">"Missing methods: </span><span class="si">#{</span><span class="n">missing</span><span class="si">}</span><span class="s2">"</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="k">class</span> <span class="nc">UbuntuModel</span> <span class="o">&lt;</span> <span class="no">BaseModel</span>
  <span class="nb">puts</span> <span class="s2">"About to define _available_network_names method"</span>
  
  <span class="k">def</span> <span class="nf">_available_network_names</span>
    <span class="sb">`nmcli -t -f SSID dev wifi list`</span><span class="p">.</span><span class="nf">split</span><span class="p">(</span><span class="s2">"</span><span class="se">\n</span><span class="s2">"</span><span class="p">)</span>
  <span class="k">end</span>
  
  <span class="nb">puts</span> <span class="s2">"About to define _connected_network_name method"</span>
  
  <span class="k">def</span> <span class="nf">_connected_network_name</span>
    <span class="sb">`nmcli -t -f NAME,TYPE connection show --active | grep 802-11-wireless | cut -d: -f1`</span><span class="p">.</span><span class="nf">strip</span>
  <span class="k">end</span>
  
  <span class="nb">puts</span> <span class="s2">"About to define _disconnect method"</span>
  
  <span class="k">def</span> <span class="nf">_disconnect</span>
    <span class="sb">`nmcli dev disconnect </span><span class="si">#{</span><span class="n">wifi_interface</span><span class="si">}</span><span class="sb">`</span>
  <span class="k">end</span>
  
  <span class="nb">puts</span> <span class="s2">"Finished defining methods"</span>
<span class="k">end</span>

<span class="nb">puts</span> <span class="s2">"Final methods in UbuntuModel: </span><span class="si">#{</span><span class="no">UbuntuModel</span><span class="p">.</span><span class="nf">public_instance_methods</span><span class="p">(</span><span class="kp">false</span><span class="p">)</span><span class="si">}</span><span class="s2">"</span>
</code></pre></div></div>

<p>Output:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>inherited called for UbuntuModel
Methods at this point: []
Missing methods: [:_available_network_names, :_connected_network_name, :_disconnect]
About to define _available_network_names method
About to define _connected_network_name method
About to define _disconnect method
Finished defining methods
Final methods in UbuntuModel: [:_available_network_names, :_connected_network_name, :_disconnect]
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">inherited</code> hook fires before any method definitions are processed.</p>

<h2 id="alternative-approaches-and-why-they-fall-short">Alternative Approaches (And Why They Fall Short)</h2>

<h3 id="method-1-method_added-hook">Method 1: <code class="language-plaintext highlighter-rouge">method_added</code> Hook</h3>

<p>You might try using <code class="language-plaintext highlighter-rouge">method_added</code> to track each method as it’s defined:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">BaseModel</span>
  <span class="no">UNDERSCORE_PREFIXED_METHODS</span> <span class="o">=</span> <span class="sx">%i[_available_network_names _connected_network_name _disconnect _ip_address]</span><span class="p">.</span><span class="nf">freeze</span>
  <span class="vi">@pending_methods</span> <span class="o">=</span> <span class="p">[]</span>

  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">inherited</span><span class="p">(</span><span class="n">subclass</span><span class="p">)</span>
    <span class="n">subclass</span><span class="p">.</span><span class="nf">instance_variable_set</span><span class="p">(</span><span class="ss">:@missing_methods</span><span class="p">,</span> <span class="no">UNDERSCORE_PREFIXED_METHODS</span><span class="p">.</span><span class="nf">dup</span><span class="p">)</span>
    
    <span class="n">subclass</span><span class="p">.</span><span class="nf">define_singleton_method</span><span class="p">(</span><span class="ss">:method_added</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">method_name</span><span class="o">|</span>
      <span class="vi">@missing_methods</span><span class="p">.</span><span class="nf">delete</span><span class="p">(</span><span class="n">method_name</span><span class="p">)</span>
      <span class="nb">puts</span> <span class="s2">"Added </span><span class="si">#{</span><span class="n">method_name</span><span class="si">}</span><span class="s2">, still missing: </span><span class="si">#{</span><span class="vi">@missing_methods</span><span class="si">}</span><span class="s2">"</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>This works for tracking individual methods, but it needs to run several times and doesn’t give you a clean “class is complete” callback. You’d need additional logic to determine when all methods have been defined.</p>

<h3 id="method-2-manual-registration">Method 2: Manual Registration</h3>

<p>You could require subclasses to manually call a verification method:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">BaseModel</span>
  <span class="no">UNDERSCORE_PREFIXED_METHODS</span> <span class="o">=</span> <span class="sx">%i[_available_network_names _connected_network_name _disconnect _ip_address]</span><span class="p">.</span><span class="nf">freeze</span>

  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">verify_underscore_methods_implemented!</span>
    <span class="n">missing_methods</span> <span class="o">=</span> <span class="no">UNDERSCORE_PREFIXED_METHODS</span> <span class="o">-</span> <span class="nb">public_instance_methods</span><span class="p">(</span><span class="kp">false</span><span class="p">)</span>
    <span class="k">unless</span> <span class="n">missing_methods</span><span class="p">.</span><span class="nf">empty?</span>
      <span class="k">raise</span> <span class="no">NotImplementedError</span><span class="p">,</span> <span class="s2">"</span><span class="si">#{</span><span class="nb">self</span><span class="si">}</span><span class="s2"> must implement </span><span class="si">#{</span><span class="n">missing_methods</span><span class="p">.</span><span class="nf">inspect</span><span class="si">}</span><span class="s2">"</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="k">class</span> <span class="nc">UbuntuModel</span> <span class="o">&lt;</span> <span class="no">BaseModel</span>
  <span class="k">def</span> <span class="nf">_available_network_names</span><span class="p">;</span> <span class="k">end</span>
  <span class="k">def</span> <span class="nf">_connected_network_name</span><span class="p">;</span> <span class="k">end</span>
  <span class="k">def</span> <span class="nf">_disconnect</span><span class="p">;</span> <span class="k">end</span>
  
  <span class="n">verify_underscore_methods_implemented!</span> <span class="c1"># Manual call required</span>
<span class="k">end</span>
</code></pre></div></div>

<p>This works but defeats the purpose of <em>guaranteeing</em> that all subclasses perform verification, since it relies on the developer remembering to call the verification method in every new subclass.</p>

<h2 id="the-solution-tracepoint-to-the-rescue">The Solution: TracePoint to the Rescue</h2>

<p>Ruby’s <code class="language-plaintext highlighter-rouge">TracePoint</code> API allows us to trace various execution events, including when class definitions end. We can combine this with the <code class="language-plaintext highlighter-rouge">inherited</code> hook to get the best of both worlds:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">BaseModel</span>
  <span class="c1"># Methods that subclasses must implement but are called via wrapper methods</span>
  <span class="no">UNDERSCORE_PREFIXED_METHODS</span> <span class="o">=</span> <span class="sx">%i[
    _available_network_names
    _connected_network_name
    _disconnect
    _ip_address
  ]</span><span class="p">.</span><span class="nf">freeze</span>

  <span class="c1"># Verify that a subclass implements all required underscore-prefixed methods</span>
  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">verify_underscore_methods_implemented</span><span class="p">(</span><span class="n">subclass</span><span class="p">)</span>
    <span class="n">missing_methods</span> <span class="o">=</span> <span class="no">UNDERSCORE_PREFIXED_METHODS</span> <span class="o">-</span> <span class="n">subclass</span><span class="p">.</span><span class="nf">public_instance_methods</span>
    <span class="k">unless</span> <span class="n">missing_methods</span><span class="p">.</span><span class="nf">empty?</span>
      <span class="k">raise</span> <span class="no">NotImplementedError</span><span class="p">,</span> <span class="s2">"Subclass </span><span class="si">#{</span><span class="n">subclass</span><span class="p">.</span><span class="nf">name</span><span class="si">}</span><span class="s2"> must implement </span><span class="si">#{</span><span class="n">missing_methods</span><span class="p">.</span><span class="nf">inspect</span><span class="si">}</span><span class="s2">"</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="c1"># Automatically verify underscore methods when a subclass is inherited</span>
  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">inherited</span><span class="p">(</span><span class="n">subclass</span><span class="p">)</span>
    <span class="c1"># Set up a trace to detect when the subclass definition ends</span>
    <span class="n">trace</span> <span class="o">=</span> <span class="no">TracePoint</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">:end</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">tp</span><span class="o">|</span>
      <span class="c1"># Check if we're ending the definition of our subclass</span>
      <span class="k">if</span> <span class="n">tp</span><span class="p">.</span><span class="nf">self</span> <span class="o">==</span> <span class="n">subclass</span>
        <span class="nb">puts</span> <span class="s2">"Class </span><span class="si">#{</span><span class="n">subclass</span><span class="si">}</span><span class="s2"> finished loading!"</span>
        
        <span class="c1"># Now we can safely inspect the fully-defined class</span>
        <span class="n">verify_underscore_methods_implemented</span><span class="p">(</span><span class="n">subclass</span><span class="p">)</span>
        <span class="nb">puts</span> <span class="s2">"✅ All underscore methods implemented correctly!"</span>
        
        <span class="c1"># Clean up - disable the trace since we only need it once</span>
        <span class="n">trace</span><span class="p">.</span><span class="nf">disable</span>
      <span class="k">end</span>
    <span class="k">end</span>
    
    <span class="c1"># Enable the trace</span>
    <span class="n">trace</span><span class="p">.</span><span class="nf">enable</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="k">class</span> <span class="nc">UbuntuModel</span> <span class="o">&lt;</span> <span class="no">BaseModel</span>
  <span class="k">def</span> <span class="nf">_available_network_names</span>
    <span class="c1"># Implementation that scans for available networks</span>
    <span class="sb">`nmcli -t -f SSID,SIGNAL dev wifi list`</span><span class="p">.</span><span class="nf">split</span><span class="p">(</span><span class="s2">"</span><span class="se">\n</span><span class="s2">"</span><span class="p">).</span><span class="nf">map</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:strip</span><span class="p">).</span><span class="nf">reject</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:empty?</span><span class="p">)</span>
  <span class="k">end</span>
  
  <span class="k">def</span> <span class="nf">_connected_network_name</span>
    <span class="c1"># Implementation that gets current connection</span>
    <span class="sb">`nmcli -t -f NAME,TYPE connection show --active | grep 802-11-wireless | cut -d: -f1`</span><span class="p">.</span><span class="nf">strip</span>
  <span class="k">end</span>
  
  <span class="k">def</span> <span class="nf">_disconnect</span>
    <span class="c1"># Implementation that disconnects from current network</span>
    <span class="sb">`nmcli dev disconnect </span><span class="si">#{</span><span class="n">wifi_interface</span><span class="si">}</span><span class="sb">`</span>
  <span class="k">end</span>
  
  <span class="k">def</span> <span class="nf">_ip_address</span>
    <span class="c1"># Implementation that gets IP address</span>
    <span class="sb">`ip -4 addr show </span><span class="si">#{</span><span class="n">wifi_interface</span><span class="si">}</span><span class="sb"> | grep 'inet ' | awk '{print $2}' | cut -d'/' -f1`</span><span class="p">.</span><span class="nf">strip</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="k">class</span> <span class="nc">MacOsModel</span> <span class="o">&lt;</span> <span class="no">BaseModel</span>
  <span class="k">def</span> <span class="nf">_available_network_names</span>
    <span class="c1"># macOS-specific implementation using networksetup</span>
    <span class="c1"># Implementation details...</span>
  <span class="k">end</span>
  
  <span class="k">def</span> <span class="nf">_connected_network_name</span>
    <span class="c1"># macOS-specific implementation</span>
    <span class="c1"># Implementation details...</span>
  <span class="k">end</span>
  
  <span class="k">def</span> <span class="nf">_disconnect</span>
    <span class="c1"># macOS-specific implementation</span>
    <span class="c1"># Implementation details...</span>
  <span class="k">end</span>
  
  <span class="k">def</span> <span class="nf">_ip_address</span>
    <span class="c1"># macOS-specific implementation</span>
    <span class="c1"># Implementation details...</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Output when starting WiFi Wand:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Class UbuntuModel finished loading!
✅ All underscore methods implemented correctly!
Class MacOsModel finished loading!
✅ All underscore methods implemented correctly!
</code></pre></div></div>

<p>Perfect! Now we can inspect the fully-defined subclasses and verify they implement all required platform-specific methods.</p>

<h2 id="how-the-tracepoint-solution-works-in-wifi-wand">How the TracePoint Solution Works in WiFi Wand</h2>

<ol>
  <li><strong>Inherited hook fires</strong>: When <code class="language-plaintext highlighter-rouge">class UbuntuModel &lt; BaseModel</code> is encountered, <code class="language-plaintext highlighter-rouge">inherited(UbuntuModel)</code> is called</li>
  <li><strong>TracePoint setup</strong>: We create a TracePoint that listens for <code class="language-plaintext highlighter-rouge">:end</code> events (when <code class="language-plaintext highlighter-rouge">end</code> keywords are processed)</li>
  <li><strong>Class body processing</strong>: Ruby processes the platform-specific method implementations</li>
  <li><strong>End event fires</strong>: When Ruby hits the final <code class="language-plaintext highlighter-rouge">end</code> of the class definition, our TracePoint callback executes</li>
  <li><strong>Safe inspection</strong>: At this point, the OS-specific class is fully defined and we can safely verify its methods</li>
  <li><strong>Cleanup</strong>: We disable the TracePoint since we only need it once per class</li>
</ol>

<h2 id="wifi-wands-method-design-philosophy">WiFi Wand’s Method Design Philosophy</h2>

<p>In WiFi Wand, underscore-prefixed methods follow a consistent design pattern:</p>

<h3 id="internal-implementation-methods">Internal Implementation Methods</h3>
<p>The underscore-prefixed methods (<code class="language-plaintext highlighter-rouge">_available_network_names</code>, <code class="language-plaintext highlighter-rouge">_connected_network_name</code>, <code class="language-plaintext highlighter-rouge">_disconnect</code>, <code class="language-plaintext highlighter-rouge">_ip_address</code>) represent the core OS-specific functionality that each platform implementation must provide.</p>

<h3 id="public-api-methods">Public API Methods</h3>
<p>The public methods that users call (<code class="language-plaintext highlighter-rouge">available_network_names</code>, <code class="language-plaintext highlighter-rouge">connected_network_name</code>, <code class="language-plaintext highlighter-rouge">disconnect</code>, <code class="language-plaintext highlighter-rouge">ip_address</code>) are provided by the <code class="language-plaintext highlighter-rouge">BaseModel</code> class itself. These public methods add consistent behavior like checking if wifi is on before calling the underscore-prefixed implementations.</p>

<p>This separation allows:</p>
<ul>
  <li><strong>Consistent API</strong> across all platforms regardless of OS differences</li>
  <li><strong>OS-specific implementations</strong> isolated to each subclass</li>
  <li><strong>Cross-cutting concerns</strong> handled in one place (state checking, error handling)</li>
  <li><strong>Clean separation</strong> between what users call and what OS’s implement</li>
</ul>

<h2 id="tracepoint-events-the-actual-implementation-methods-that-each-os-must-provide-theyre-underscore-prefixed-to-indicate-theyre-internal-implementations">TracePoint Events the actual implementation methods that each OS must provide. They’re underscore-prefixed to indicate they’re internal implementations.</h2>

<h3 id="2-public-methods-wifi_on-wifi_off-detect_wifi_interface-etc">2. Public Methods (<code class="language-plaintext highlighter-rouge">wifi_on</code>, <code class="language-plaintext highlighter-rouge">wifi_off</code>, <code class="language-plaintext highlighter-rouge">detect_wifi_interface</code>, etc.)</h3>
<p>These are the public API methods that users call. The <code class="language-plaintext highlighter-rouge">BaseModel</code> provides these implementations by calling the underscore-prefixed versions with appropriate conditions (like checking if wifi is on first).</p>

<p>This separation allows the base class to add consistent behavior (like wifi state checking) while delegating OS-specific operations to subclasses.</p>

<h2 id="tracepoint-events">TracePoint Events</h2>

<p>The <code class="language-plaintext highlighter-rouge">:end</code> event fires whenever Ruby processes an <code class="language-plaintext highlighter-rouge">end</code> keyword, which includes:</p>
<ul>
  <li>End of class definitions</li>
  <li>End of method definitions</li>
  <li>End of module definitions</li>
  <li>End of blocks</li>
</ul>

<p>That’s why we check <code class="language-plaintext highlighter-rouge">tp.self == subclass</code> to ensure we’re responding to the end of the specific OS-specific class we care about.</p>

<h2 id="considerations-and-limitations">Considerations and Limitations</h2>

<h3 id="performance">Performance</h3>
<p>TracePoint has some performance overhead since it hooks into Ruby’s execution. For WiFi Wand, this isn’t a concern since:</p>
<ul>
  <li>We only create one TracePoint per OS-specific model</li>
  <li>The trace is disabled immediately after use</li>
  <li>Model classes are loaded once at startup</li>
</ul>

<h3 id="complexity">Complexity</h3>
<p>This solution is more complex than simple hooks, but the benefit is enormous: <strong>guaranteed API consistency across all OS implementations</strong>. This is crucial for a cross-platform library like WiFi Wand where each OS model must implement the same interface.</p>

<h3 id="maintenance">Maintenance</h3>
<p>The approach requires good documentation. In WiFi Wand, we clearly document the <code class="language-plaintext highlighter-rouge">UNDERSCORE_PREFIXED_METHODS</code> array and explain that each OS model must implement these methods.</p>

<h2 id="real-world-benefits-in-wifi-wand">Real-World Benefits in WiFi Wand</h2>

<p>This TracePoint pattern has been invaluable for WiFi Wand’s development:</p>

<ol>
  <li>
    <p><strong>Early Error Detection</strong>: Missing implementations are caught immediately when the class is defined, not at runtime when a user tries to use the functionality.</p>
  </li>
  <li>
    <p><strong>Consistent API</strong>: All OS models (UbuntuModel, MacOsModel, etc.) are guaranteed to implement the same interface, making WiFi Wand’s public API consistent across platforms.</p>
  </li>
  <li>
    <p><strong>Developer Experience</strong>: When adding a new OS model, developers get immediate feedback if they forget to implement any required methods.</p>
  </li>
  <li>
    <p><strong>Self-Documenting</strong>: The <code class="language-plaintext highlighter-rouge">UNDERSCORE_PREFIXED_METHODS</code> array serves as clear documentation of what each subclass needs to implement.</p>
  </li>
</ol>

<h2 id="conclusion">Conclusion</h2>

<p>Ruby’s <code class="language-plaintext highlighter-rouge">inherited</code> hook is powerful but has a crucial timing limitation: it fires before the subclass is fully defined. When you need to inspect or work with fully-defined subclasses in frameworks like WiFi Wand, combining <code class="language-plaintext highlighter-rouge">inherited</code> with TracePoint’s <code class="language-plaintext highlighter-rouge">:end</code> event provides an elegant solution.</p>

<p>This pattern gives you the automatic behavior of inheritance hooks while ensuring you have access to the complete class definition. For WiFi Wand, this means we can guarantee that every OS-specific model implements exactly the same interface, providing a consistent experience for users regardless of their operating system.</p>

<p>The next time you’re building a Ruby framework and find yourself frustrated that <code class="language-plaintext highlighter-rouge">inherited</code> doesn’t see your subclass methods, reach for TracePoint – it might be exactly what you need.</p>

<hr />

<p>[This article was originally drafted by Claude (Anthropic) AI and subsequently edited and refined.]</p>]]></content><author><name></name></author><category term="blog" /><summary type="html"><![CDATA[Ruby’s inherited Hook: The Timing Problem and a TracePoint Solution in WiFi Wand]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.bbs-software.com/assets/images/bbs-logo-og.png" /><media:content medium="image" url="https://blog.bbs-software.com/assets/images/bbs-logo-og.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Signaling Cursor AI Completion with a File Watcher and Screen Flasher</title><link href="https://blog.bbs-software.com/blog/2025/05/29/desktop-image-flashing-on-cursor-ai-response-completion/" rel="alternate" type="text/html" title="Signaling Cursor AI Completion with a File Watcher and Screen Flasher" /><published>2025-05-29T00:00:00+00:00</published><updated>2025-05-29T00:00:00+00:00</updated><id>https://blog.bbs-software.com/blog/2025/05/29/desktop-image-flashing-on-cursor-ai-response-completion</id><content type="html" xml:base="https://blog.bbs-software.com/blog/2025/05/29/desktop-image-flashing-on-cursor-ai-response-completion/"><![CDATA[<!-- To prevent Webstorm from reporting benign warnings: -->
<!-- noinspection HtmlUnknownTarget -->
<!-- noinspection HtmlRequiredAltAttribute -->
<!-- noinspection CheckImageSize -->
<!-- noinspection HtmlUnknownAttribute -->

<!-- Include CSS and JavaScript for script downloads -->
<link rel="stylesheet" href="/assets/posts/2025-05-29-desktop-image-flashing/css/script-downloads.css" />

<script src="/assets/posts/2025-05-29-desktop-image-flashing/js/script-downloads.js"></script>

<hr />

<blockquote>
  <p><strong>💡 Quick Start: If you want to jump straight to implementation, see the <a href="#complete-setup-instructions">Complete Setup Instructions</a> at the end of this article.</strong></p>
</blockquote>

<hr />

<!-- Quick Access: View and Download Scripts -->
<div class="script-downloads">
  <h3>Quick Access: View and Download Scripts</h3>
  <div style="text-align: right; font-size: 0.9em;">(Remove the '.txt' extension after downloading.)</div>
  <ul>
    <li>
      <details id="file_watcher-expand">
  <summary>
    <span class="view-control">📖 View file_watcher.py with syntax highlighting</span>
    <button onclick="downloadFile('/scripts/file_watcher.py.txt', 'file_watcher.py.txt')" class="download-button">📁 Download</button>
  </summary>
  
<figure class="highlight"><pre><code class="language-python" data-lang="python">  <span class="c1">#!/usr/bin/env python3
</span>
<span class="sh">"""</span><span class="s">
File Watcher - A generic file monitoring and command execution script

This script monitors a specific file and executes a given command when it appears.
It</span><span class="sh">'</span><span class="s">s designed to be a simple, dependency-free tool for automated workflows.

WHAT IT DOES:
- Polls for the existence of a specified file every second.
- When the file is found, it executes a user-defined command and waits for it to complete.
- Checks the exit code of the command to determine success or failure.
- Deletes the file after the command is run to prepare for the next event.

DEPENDENCIES:
- Python 3.6+
- No external libraries required.

USAGE:
1. Run the script with a command to execute:
   ./scripts/file-watcher.py --file FILE [--delay SECONDS] [--verbose] -- [COMMAND] [ARGS...]
   
   The </span><span class="sh">'</span><span class="s">--</span><span class="sh">'</span><span class="s"> is recommended to separate script arguments from the command.

2. The script will start polling.
3. Press Ctrl+C to stop.
</span><span class="sh">"""</span>

<span class="kn">import</span> <span class="n">time</span>
<span class="kn">import</span> <span class="n">subprocess</span>
<span class="kn">import</span> <span class="n">argparse</span>
<span class="kn">import</span> <span class="n">logging</span>
<span class="kn">from</span> <span class="n">pathlib</span> <span class="kn">import</span> <span class="n">Path</span>
<span class="kn">from</span> <span class="n">datetime</span> <span class="kn">import</span> <span class="n">datetime</span>

<span class="c1"># Default configuration
</span><span class="n">DEFAULT_DELAY_SECONDS</span> <span class="o">=</span> <span class="mf">0.5</span>

<span class="k">def</span> <span class="nf">execute_command</span><span class="p">(</span><span class="n">command</span><span class="p">):</span>
    <span class="k">try</span><span class="p">:</span>
        <span class="n">logging</span><span class="p">.</span><span class="nf">debug</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Executing command: </span><span class="si">{</span><span class="sh">'</span><span class="s"> </span><span class="sh">'</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="n">command</span><span class="p">)</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
        <span class="n">result</span> <span class="o">=</span> <span class="n">subprocess</span><span class="p">.</span><span class="nf">run</span><span class="p">(</span>
            <span class="n">command</span><span class="p">,</span>
            <span class="n">capture_output</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span>
            <span class="n">text</span><span class="o">=</span><span class="bp">True</span>
        <span class="p">)</span>

        <span class="k">if</span> <span class="n">result</span><span class="p">.</span><span class="n">returncode</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">:</span>
            <span class="n">logging</span><span class="p">.</span><span class="nf">error</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Command failed with exit code </span><span class="si">{</span><span class="n">result</span><span class="p">.</span><span class="n">returncode</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
            <span class="k">if</span> <span class="n">result</span><span class="p">.</span><span class="n">stderr</span><span class="p">:</span>
                <span class="n">logging</span><span class="p">.</span><span class="nf">error</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Stderr: </span><span class="si">{</span><span class="n">result</span><span class="p">.</span><span class="n">stderr</span><span class="p">.</span><span class="nf">strip</span><span class="p">()</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
        <span class="k">else</span><span class="p">:</span>
            <span class="n">logging</span><span class="p">.</span><span class="nf">info</span><span class="p">(</span><span class="sh">"</span><span class="s">Command executed successfully.</span><span class="sh">"</span><span class="p">)</span>
            <span class="k">if</span> <span class="n">result</span><span class="p">.</span><span class="n">stdout</span><span class="p">:</span>
                <span class="n">logging</span><span class="p">.</span><span class="nf">debug</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Stdout: </span><span class="si">{</span><span class="n">result</span><span class="p">.</span><span class="n">stdout</span><span class="p">.</span><span class="nf">strip</span><span class="p">()</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
            <span class="k">if</span> <span class="n">result</span><span class="p">.</span><span class="n">stderr</span><span class="p">:</span> <span class="c1"># Log non-fatal stderr output too for diagnostics
</span>                <span class="n">logging</span><span class="p">.</span><span class="nf">debug</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Stderr (non-fatal): </span><span class="si">{</span><span class="n">result</span><span class="p">.</span><span class="n">stderr</span><span class="p">.</span><span class="nf">strip</span><span class="p">()</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>

    <span class="k">except</span> <span class="nb">FileNotFoundError</span><span class="p">:</span>
        <span class="n">logging</span><span class="p">.</span><span class="nf">error</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Command not found: </span><span class="si">{</span><span class="n">command</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
    <span class="k">except</span> <span class="nb">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
        <span class="n">logging</span><span class="p">.</span><span class="nf">error</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Error executing command: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>


<span class="k">def</span> <span class="nf">setup_logging</span><span class="p">(</span><span class="n">verbose</span><span class="o">=</span><span class="bp">False</span><span class="p">):</span>
    <span class="sh">"""</span><span class="s">Configure logging based on verbosity level.</span><span class="sh">"""</span>
    <span class="n">level</span> <span class="o">=</span> <span class="n">logging</span><span class="p">.</span><span class="n">DEBUG</span> <span class="k">if</span> <span class="n">verbose</span> <span class="k">else</span> <span class="n">logging</span><span class="p">.</span><span class="n">INFO</span>
    <span class="n">logging</span><span class="p">.</span><span class="nf">basicConfig</span><span class="p">(</span>
        <span class="n">level</span><span class="o">=</span><span class="n">level</span><span class="p">,</span>
        <span class="nb">format</span><span class="o">=</span><span class="sh">'</span><span class="s">%(asctime)s - %(levelname)s - %(message)s</span><span class="sh">'</span><span class="p">,</span>
        <span class="n">datefmt</span><span class="o">=</span><span class="sh">'</span><span class="s">%Y-%m-%d %H:%M:%S</span><span class="sh">'</span>
    <span class="p">)</span>


<span class="k">def</span> <span class="nf">parse_arguments</span><span class="p">():</span>
    <span class="sh">"""</span><span class="s">Parse command line arguments.</span><span class="sh">"""</span>
    <span class="n">parser</span> <span class="o">=</span> <span class="n">argparse</span><span class="p">.</span><span class="nc">ArgumentParser</span><span class="p">(</span>
        <span class="n">description</span><span class="o">=</span><span class="sh">"</span><span class="s">Poll for a file and execute a command when it appears.</span><span class="sh">"</span><span class="p">,</span>
        <span class="n">formatter_class</span><span class="o">=</span><span class="n">argparse</span><span class="p">.</span><span class="n">RawDescriptionHelpFormatter</span><span class="p">,</span>
        <span class="n">epilog</span><span class="o">=</span><span class="sh">"""</span><span class="s">
Examples:
  # Flash screen when file appears, checking every 0.2 seconds
  %(prog)s --file .cursor_response_complete --delay 0.2 -- ./scripts/FlashScreen

  # Watch a different file with default delay
  %(prog)s --file state.log --verbose -- ls -l
        </span><span class="sh">"""</span>
    <span class="p">)</span>
    
    <span class="n">parser</span><span class="p">.</span><span class="nf">add_argument</span><span class="p">(</span>
        <span class="sh">'</span><span class="s">-f</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">--file</span><span class="sh">'</span><span class="p">,</span>
        <span class="n">required</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span>
        <span class="n">help</span><span class="o">=</span><span class="sh">'</span><span class="s">File to watch for.</span><span class="sh">'</span>
    <span class="p">)</span>
    
    <span class="n">parser</span><span class="p">.</span><span class="nf">add_argument</span><span class="p">(</span>
        <span class="sh">'</span><span class="s">-d</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">--delay</span><span class="sh">'</span><span class="p">,</span>
        <span class="nb">type</span><span class="o">=</span><span class="nb">float</span><span class="p">,</span>
        <span class="n">default</span><span class="o">=</span><span class="n">DEFAULT_DELAY_SECONDS</span><span class="p">,</span>
        <span class="n">help</span><span class="o">=</span><span class="sa">f</span><span class="sh">'</span><span class="s">Delay in seconds between polling checks (default: </span><span class="si">{</span><span class="n">DEFAULT_DELAY_SECONDS</span><span class="si">}</span><span class="s">)</span><span class="sh">'</span>
    <span class="p">)</span>
    
    <span class="n">parser</span><span class="p">.</span><span class="nf">add_argument</span><span class="p">(</span>
        <span class="sh">'</span><span class="s">-v</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">--verbose</span><span class="sh">'</span><span class="p">,</span>
        <span class="n">action</span><span class="o">=</span><span class="sh">'</span><span class="s">store_true</span><span class="sh">'</span><span class="p">,</span>
        <span class="n">help</span><span class="o">=</span><span class="sh">'</span><span class="s">Enable verbose logging</span><span class="sh">'</span>
    <span class="p">)</span>
    
    <span class="n">parser</span><span class="p">.</span><span class="nf">add_argument</span><span class="p">(</span>
        <span class="sh">'</span><span class="s">command</span><span class="sh">'</span><span class="p">,</span>
        <span class="n">nargs</span><span class="o">=</span><span class="n">argparse</span><span class="p">.</span><span class="n">REMAINDER</span><span class="p">,</span>
        <span class="n">help</span><span class="o">=</span><span class="sh">'</span><span class="s">Command to execute when file is found. Use -- to separate from script args.</span><span class="sh">'</span>
    <span class="p">)</span>
    
    <span class="n">args</span> <span class="o">=</span> <span class="n">parser</span><span class="p">.</span><span class="nf">parse_args</span><span class="p">()</span>

    <span class="k">if</span> <span class="n">args</span><span class="p">.</span><span class="n">command</span> <span class="ow">and</span> <span class="n">args</span><span class="p">.</span><span class="n">command</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">==</span> <span class="sh">'</span><span class="s">--</span><span class="sh">'</span><span class="p">:</span>
        <span class="n">args</span><span class="p">.</span><span class="n">command</span> <span class="o">=</span> <span class="n">args</span><span class="p">.</span><span class="n">command</span><span class="p">[</span><span class="mi">1</span><span class="p">:]</span>

    <span class="k">if</span> <span class="ow">not</span> <span class="n">args</span><span class="p">.</span><span class="n">command</span><span class="p">:</span>
        <span class="n">parser</span><span class="p">.</span><span class="nf">error</span><span class="p">(</span>
            <span class="sh">"</span><span class="s">No command supplied. You must provide a command to execute after the script options.</span><span class="se">\n</span><span class="sh">"</span>
            <span class="sh">"</span><span class="s">Example: %(prog)s --file ... -- your_command_here</span><span class="sh">"</span>
        <span class="p">)</span>

    <span class="k">return</span> <span class="n">args</span>


<span class="k">def</span> <span class="nf">delete_file</span><span class="p">(</span><span class="n">filespec</span><span class="p">):</span>
    <span class="k">try</span><span class="p">:</span>
        <span class="n">filespec</span><span class="p">.</span><span class="nf">unlink</span><span class="p">()</span>
        <span class="n">logging</span><span class="p">.</span><span class="nf">debug</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Deleted file: </span><span class="si">{</span><span class="n">filespec</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
    <span class="k">except</span> <span class="nb">OSError</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
        <span class="n">logging</span><span class="p">.</span><span class="nf">error</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Error deleting file </span><span class="si">{</span><span class="n">filespec</span><span class="si">}</span><span class="s">: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>


<span class="k">def</span> <span class="nf">process_file_presence</span><span class="p">(</span><span class="n">file_to_watch</span><span class="p">,</span> <span class="n">command</span><span class="p">):</span>
    <span class="n">timestamp</span> <span class="o">=</span> <span class="n">datetime</span><span class="p">.</span><span class="nf">now</span><span class="p">().</span><span class="nf">strftime</span><span class="p">(</span><span class="sh">'</span><span class="s">%Y-%m-%d %H:%M:%S</span><span class="sh">'</span><span class="p">)</span>
    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="si">{</span><span class="n">timestamp</span><span class="si">}</span><span class="s"> - File FOUND: </span><span class="si">{</span><span class="n">file_to_watch</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
    
    <span class="nf">execute_command</span><span class="p">(</span><span class="n">command</span><span class="p">)</span>
    

<span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
    <span class="n">args</span> <span class="o">=</span> <span class="nf">parse_arguments</span><span class="p">()</span>
    <span class="nf">setup_logging</span><span class="p">(</span><span class="n">args</span><span class="p">.</span><span class="n">verbose</span><span class="p">)</span>
    
    <span class="n">file_to_watch</span> <span class="o">=</span> <span class="nc">Path</span><span class="p">(</span><span class="n">args</span><span class="p">.</span><span class="nb">file</span><span class="p">).</span><span class="nf">resolve</span><span class="p">()</span>
    
    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Polling for file: </span><span class="si">{</span><span class="n">file_to_watch</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Command to run: </span><span class="si">{</span><span class="sh">'</span><span class="s"> </span><span class="sh">'</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="n">args</span><span class="p">.</span><span class="n">command</span><span class="p">)</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
    <span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">Press Ctrl+C to stop...</span><span class="sh">"</span><span class="p">)</span>
    
    <span class="k">try</span><span class="p">:</span>
        <span class="k">while</span> <span class="bp">True</span><span class="p">:</span>
            <span class="k">if</span> <span class="n">file_to_watch</span><span class="p">.</span><span class="nf">exists</span><span class="p">():</span>
                <span class="nf">process_file_presence</span><span class="p">(</span><span class="n">file_to_watch</span><span class="p">,</span> <span class="n">args</span><span class="p">.</span><span class="n">command</span><span class="p">)</span>
                <span class="nf">delete_file</span><span class="p">(</span><span class="n">file_to_watch</span><span class="p">)</span>
            <span class="n">time</span><span class="p">.</span><span class="nf">sleep</span><span class="p">(</span><span class="n">args</span><span class="p">.</span><span class="n">delay</span><span class="p">)</span>
    <span class="k">except</span> <span class="nb">KeyboardInterrupt</span><span class="p">:</span>
        <span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="se">\n</span><span class="s">Stopping file watcher.</span><span class="sh">"</span><span class="p">)</span>
    <span class="k">except</span> <span class="nb">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
        <span class="n">logging</span><span class="p">.</span><span class="nf">error</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">An unexpected error occurred: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>

<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="sh">"</span><span class="s">__main__</span><span class="sh">"</span><span class="p">:</span>
    <span class="nf">main</span><span class="p">()</span> 
  </code></pre></figure>

</details> 
    </li>
    <li>
      <details id="swift-expand">
  <summary>
    <span class="view-control">📖 View FlashScreen with syntax highlighting</span>
    <button onclick="downloadFile('/scripts/FlashScreen.txt', 'FlashScreen.txt')" class="download-button">📁 Download</button>
  </summary>
  
<figure class="highlight"><pre><code class="language-swift" data-lang="swift">  <span class="cp">#!/usr/bin/env swift</span>

<span class="kd">import</span> <span class="kt">Cocoa</span>

<span class="c1">// Flash duration constants</span>
<span class="k">let</span> <span class="nv">IMAGE_FLASH_DURATION</span><span class="p">:</span> <span class="kt">TimeInterval</span> <span class="o">=</span> <span class="mf">1.0</span>
<span class="k">let</span> <span class="nv">SOLID_COLOR_FLASH_DURATION</span><span class="p">:</span> <span class="kt">TimeInterval</span> <span class="o">=</span> <span class="mf">0.3</span>

<span class="c1">// Default image path constants</span>
<span class="k">let</span> <span class="nv">DEFAULT_IMAGE_PATH_DISPLAY</span> <span class="o">=</span> <span class="s">"~/system-flash-image.jpg"</span>
<span class="k">let</span> <span class="nv">DEFAULT_IMAGE_PATH</span> <span class="o">=</span> <span class="kt">NSString</span><span class="p">(</span><span class="nv">string</span><span class="p">:</span> <span class="kt">DEFAULT_IMAGE_PATH_DISPLAY</span><span class="p">)</span><span class="o">.</span><span class="n">expandingTildeInPath</span>

<span class="c1">// Color parsing function</span>
<span class="kd">func</span> <span class="nf">parseColor</span><span class="p">(</span><span class="n">_</span> <span class="nv">colorString</span><span class="p">:</span> <span class="kt">String</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">NSColor</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">lowercased</span> <span class="o">=</span> <span class="n">colorString</span><span class="o">.</span><span class="nf">lowercased</span><span class="p">()</span>
    
    <span class="c1">// Handle named colors</span>
    <span class="k">switch</span> <span class="n">lowercased</span> <span class="p">{</span>
    <span class="k">case</span> <span class="s">"white"</span><span class="p">:</span> <span class="k">return</span> <span class="kt">NSColor</span><span class="o">.</span><span class="n">white</span>
    <span class="k">case</span> <span class="s">"black"</span><span class="p">:</span> <span class="k">return</span> <span class="kt">NSColor</span><span class="o">.</span><span class="n">black</span>
    <span class="k">case</span> <span class="s">"red"</span><span class="p">:</span> <span class="k">return</span> <span class="kt">NSColor</span><span class="o">.</span><span class="n">red</span>
    <span class="k">case</span> <span class="s">"green"</span><span class="p">:</span> <span class="k">return</span> <span class="kt">NSColor</span><span class="o">.</span><span class="n">green</span>
    <span class="k">case</span> <span class="s">"blue"</span><span class="p">:</span> <span class="k">return</span> <span class="kt">NSColor</span><span class="o">.</span><span class="n">blue</span>
    <span class="k">case</span> <span class="s">"yellow"</span><span class="p">:</span> <span class="k">return</span> <span class="kt">NSColor</span><span class="o">.</span><span class="n">yellow</span>
    <span class="k">case</span> <span class="s">"orange"</span><span class="p">:</span> <span class="k">return</span> <span class="kt">NSColor</span><span class="o">.</span><span class="n">orange</span>
    <span class="k">case</span> <span class="s">"purple"</span><span class="p">:</span> <span class="k">return</span> <span class="kt">NSColor</span><span class="o">.</span><span class="n">purple</span>
    <span class="k">case</span> <span class="s">"cyan"</span><span class="p">:</span> <span class="k">return</span> <span class="kt">NSColor</span><span class="o">.</span><span class="n">cyan</span>
    <span class="k">case</span> <span class="s">"magenta"</span><span class="p">:</span> <span class="k">return</span> <span class="kt">NSColor</span><span class="o">.</span><span class="n">magenta</span>
    <span class="k">case</span> <span class="s">"gray"</span><span class="p">,</span> <span class="s">"grey"</span><span class="p">:</span> <span class="k">return</span> <span class="kt">NSColor</span><span class="o">.</span><span class="n">gray</span>
    <span class="k">default</span><span class="p">:</span>
        <span class="c1">// Try to parse as hex color</span>
        <span class="k">if</span> <span class="n">colorString</span><span class="o">.</span><span class="nf">hasPrefix</span><span class="p">(</span><span class="s">"#"</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">let</span> <span class="nv">hex</span> <span class="o">=</span> <span class="kt">String</span><span class="p">(</span><span class="n">colorString</span><span class="o">.</span><span class="nf">dropFirst</span><span class="p">())</span>
            <span class="k">if</span> <span class="n">hex</span><span class="o">.</span><span class="n">count</span> <span class="o">==</span> <span class="mi">6</span><span class="p">,</span> <span class="k">let</span> <span class="nv">rgbValue</span> <span class="o">=</span> <span class="kt">UInt32</span><span class="p">(</span><span class="n">hex</span><span class="p">,</span> <span class="nv">radix</span><span class="p">:</span> <span class="mi">16</span><span class="p">)</span> <span class="p">{</span>
                <span class="k">let</span> <span class="nv">red</span> <span class="o">=</span> <span class="kt">CGFloat</span><span class="p">((</span><span class="n">rgbValue</span> <span class="o">&amp;</span> <span class="mh">0xFF0000</span><span class="p">)</span> <span class="o">&gt;&gt;</span> <span class="mi">16</span><span class="p">)</span> <span class="o">/</span> <span class="mf">255.0</span>
                <span class="k">let</span> <span class="nv">green</span> <span class="o">=</span> <span class="kt">CGFloat</span><span class="p">((</span><span class="n">rgbValue</span> <span class="o">&amp;</span> <span class="mh">0x00FF00</span><span class="p">)</span> <span class="o">&gt;&gt;</span> <span class="mi">8</span><span class="p">)</span> <span class="o">/</span> <span class="mf">255.0</span>
                <span class="k">let</span> <span class="nv">blue</span> <span class="o">=</span> <span class="kt">CGFloat</span><span class="p">(</span><span class="n">rgbValue</span> <span class="o">&amp;</span> <span class="mh">0x0000FF</span><span class="p">)</span> <span class="o">/</span> <span class="mf">255.0</span>
                <span class="k">return</span> <span class="kt">NSColor</span><span class="p">(</span><span class="nv">red</span><span class="p">:</span> <span class="n">red</span><span class="p">,</span> <span class="nv">green</span><span class="p">:</span> <span class="n">green</span><span class="p">,</span> <span class="nv">blue</span><span class="p">:</span> <span class="n">blue</span><span class="p">,</span> <span class="nv">alpha</span><span class="p">:</span> <span class="mf">1.0</span><span class="p">)</span>
            <span class="p">}</span>
        <span class="p">}</span>
        <span class="c1">// Default to white if parsing fails</span>
        <span class="k">return</span> <span class="kt">NSColor</span><span class="o">.</span><span class="n">white</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="c1">// Help function</span>
<span class="kd">func</span> <span class="nf">showHelp</span><span class="p">()</span> <span class="p">{</span>
    <span class="nf">print</span><span class="p">(</span><span class="s">"""
Flash Screen - Screen Flash Utility

USAGE:
    ./FlashScreen [OPTIONS] [IMAGE_PATH]
    swift FlashScreen [OPTIONS] [IMAGE_PATH]

ARGUMENTS:
    IMAGE_PATH    Optional path to image file to display during flash
                  If not provided, defaults to </span><span class="se">\(</span><span class="kt">DEFAULT_IMAGE_PATH_DISPLAY</span><span class="se">)</span><span class="s">
                  If default image doesn't exist, displays a solid color flash

DESCRIPTION:
    Creates a fullscreen flash effect for visual notifications.
    - With image: Shows the image for </span><span class="se">\(</span><span class="kt">IMAGE_FLASH_DURATION</span><span class="se">)</span><span class="s"> seconds
    - Without image: Shows solid color flash for </span><span class="se">\(</span><span class="kt">SOLID_COLOR_FLASH_DURATION</span><span class="se">)</span><span class="s"> seconds
    - Default: Checks for </span><span class="se">\(</span><span class="kt">DEFAULT_IMAGE_PATH_DISPLAY</span><span class="se">)</span><span class="s"> if no path specified
    
    The flash window appears at maximum window level and ignores mouse events.

EXAMPLES:
    ./FlashScreen                            # Default image or solid color flash
    ./FlashScreen -n                         # Force solid color flash (ignore default image)
    ./FlashScreen -c red                     # Force red color flash
    ./FlashScreen --color "</span><span class="k">#FF5500</span><span class="s">"          # Force orange color flash using hex
    ./FlashScreen ~/Pictures/flash.jpg       # Image flash
    swift FlashScreen /path/to/image.png     # Image flash with swift command

OPTIONS:
    -n, --no-image    Force solid color flash, ignoring default image
    -c, --color COLOR Specify flash color (named color or hex like #FF0000)
                      Named colors: white, black, red, green, blue, yellow, orange, purple, cyan, magenta, gray
    -h, --help        Show this help message and exit
"""</span><span class="p">)</span>
<span class="p">}</span>

<span class="c1">// Parse command line arguments</span>
<span class="k">let</span> <span class="nv">args</span> <span class="o">=</span> <span class="kt">CommandLine</span><span class="o">.</span><span class="n">arguments</span>
<span class="k">var</span> <span class="nv">forceNoImage</span> <span class="o">=</span> <span class="kc">false</span>
<span class="k">var</span> <span class="nv">imagePath</span><span class="p">:</span> <span class="kt">String</span><span class="p">?</span> <span class="o">=</span> <span class="kc">nil</span>
<span class="k">var</span> <span class="nv">flashColor</span><span class="p">:</span> <span class="kt">NSColor</span> <span class="o">=</span> <span class="kt">NSColor</span><span class="o">.</span><span class="n">white</span>

<span class="c1">// Process arguments</span>
<span class="k">var</span> <span class="nv">i</span> <span class="o">=</span> <span class="mi">1</span>
<span class="k">while</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">args</span><span class="o">.</span><span class="n">count</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">arg</span> <span class="o">=</span> <span class="n">args</span><span class="p">[</span><span class="n">i</span><span class="p">]</span>
    <span class="k">if</span> <span class="n">arg</span> <span class="o">==</span> <span class="s">"-h"</span> <span class="o">||</span> <span class="n">arg</span> <span class="o">==</span> <span class="s">"--help"</span> <span class="p">{</span>
        <span class="nf">showHelp</span><span class="p">()</span>
        <span class="nf">exit</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
    <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="n">arg</span> <span class="o">==</span> <span class="s">"-n"</span> <span class="o">||</span> <span class="n">arg</span> <span class="o">==</span> <span class="s">"--no-image"</span> <span class="p">{</span>
        <span class="n">forceNoImage</span> <span class="o">=</span> <span class="kc">true</span>
        <span class="n">i</span> <span class="o">+=</span> <span class="mi">1</span>
    <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="n">arg</span> <span class="o">==</span> <span class="s">"-c"</span> <span class="o">||</span> <span class="n">arg</span> <span class="o">==</span> <span class="s">"--color"</span> <span class="p">{</span>
        <span class="k">if</span> <span class="n">i</span> <span class="o">+</span> <span class="mi">1</span> <span class="o">&lt;</span> <span class="n">args</span><span class="o">.</span><span class="n">count</span> <span class="p">{</span>
            <span class="n">flashColor</span> <span class="o">=</span> <span class="nf">parseColor</span><span class="p">(</span><span class="n">args</span><span class="p">[</span><span class="n">i</span> <span class="o">+</span> <span class="mi">1</span><span class="p">])</span>
            <span class="n">forceNoImage</span> <span class="o">=</span> <span class="kc">true</span>  <span class="c1">// Color option implies no image</span>
            <span class="n">i</span> <span class="o">+=</span> <span class="mi">2</span>
        <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
            <span class="nf">print</span><span class="p">(</span><span class="s">"Error: -c/--color requires a color value"</span><span class="p">)</span>
            <span class="nf">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
        <span class="p">}</span>
    <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="o">!</span><span class="n">arg</span><span class="o">.</span><span class="nf">hasPrefix</span><span class="p">(</span><span class="s">"-"</span><span class="p">)</span> <span class="p">{</span>
        <span class="c1">// This is an image path</span>
        <span class="n">imagePath</span> <span class="o">=</span> <span class="n">arg</span>
        <span class="k">break</span>
    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
        <span class="nf">print</span><span class="p">(</span><span class="s">"Error: Unknown option </span><span class="se">\(</span><span class="n">arg</span><span class="se">)</span><span class="s">"</span><span class="p">)</span>
        <span class="nf">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="k">let</span> <span class="nv">app</span> <span class="o">=</span> <span class="kt">NSApplication</span><span class="o">.</span><span class="n">shared</span>
<span class="n">app</span><span class="o">.</span><span class="nf">setActivationPolicy</span><span class="p">(</span><span class="o">.</span><span class="n">regular</span><span class="p">)</span>

<span class="k">let</span> <span class="nv">window</span> <span class="o">=</span> <span class="kt">NSWindow</span><span class="p">(</span>
    <span class="nv">contentRect</span><span class="p">:</span> <span class="kt">NSScreen</span><span class="o">.</span><span class="n">main</span><span class="o">!.</span><span class="n">frame</span><span class="p">,</span>
    <span class="nv">styleMask</span><span class="p">:</span> <span class="p">[</span><span class="o">.</span><span class="n">borderless</span><span class="p">],</span>
    <span class="nv">backing</span><span class="p">:</span> <span class="o">.</span><span class="n">buffered</span><span class="p">,</span>
    <span class="nv">defer</span><span class="p">:</span> <span class="kc">false</span>
<span class="p">)</span>

<span class="n">window</span><span class="o">.</span><span class="n">backgroundColor</span> <span class="o">=</span> <span class="n">flashColor</span>
<span class="n">window</span><span class="o">.</span><span class="n">level</span> <span class="o">=</span> <span class="kt">NSWindow</span><span class="o">.</span><span class="kt">Level</span><span class="p">(</span><span class="nv">rawValue</span><span class="p">:</span> <span class="kt">Int</span><span class="p">(</span><span class="kt">CGWindowLevelForKey</span><span class="p">(</span><span class="o">.</span><span class="n">maximumWindow</span><span class="p">)))</span>
<span class="n">window</span><span class="o">.</span><span class="n">isOpaque</span> <span class="o">=</span> <span class="kc">true</span>
<span class="n">window</span><span class="o">.</span><span class="n">ignoresMouseEvents</span> <span class="o">=</span> <span class="kc">true</span>

<span class="c1">// Determine final image path based on arguments and flags</span>
<span class="k">if</span> <span class="n">forceNoImage</span> <span class="p">{</span>
    <span class="c1">// Force solid color flash, ignore any image path or default</span>
    <span class="n">imagePath</span> <span class="o">=</span> <span class="kc">nil</span>
<span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="n">imagePath</span> <span class="o">==</span> <span class="kc">nil</span> <span class="p">{</span>
    <span class="c1">// No explicit image path provided, check for default</span>
    <span class="n">imagePath</span> <span class="o">=</span> <span class="kt">FileManager</span><span class="o">.</span><span class="k">default</span><span class="o">.</span><span class="nf">fileExists</span><span class="p">(</span><span class="nv">atPath</span><span class="p">:</span> <span class="kt">DEFAULT_IMAGE_PATH</span><span class="p">)</span> <span class="p">?</span> <span class="kt">DEFAULT_IMAGE_PATH</span> <span class="p">:</span> <span class="kc">nil</span>
<span class="p">}</span>

<span class="c1">// Try to load and display image</span>
<span class="k">if</span> <span class="k">let</span> <span class="nv">path</span> <span class="o">=</span> <span class="n">imagePath</span><span class="p">,</span>
   <span class="k">let</span> <span class="nv">image</span> <span class="o">=</span> <span class="kt">NSImage</span><span class="p">(</span><span class="nv">contentsOfFile</span><span class="p">:</span> <span class="n">path</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">imageView</span> <span class="o">=</span> <span class="kt">NSImageView</span><span class="p">(</span><span class="nv">frame</span><span class="p">:</span> <span class="n">window</span><span class="o">.</span><span class="n">contentView</span><span class="o">!.</span><span class="n">bounds</span><span class="p">)</span>
    <span class="n">imageView</span><span class="o">.</span><span class="n">image</span> <span class="o">=</span> <span class="n">image</span>
    <span class="n">imageView</span><span class="o">.</span><span class="n">imageScaling</span> <span class="o">=</span> <span class="o">.</span><span class="n">scaleProportionallyUpOrDown</span>
    <span class="n">window</span><span class="o">.</span><span class="n">contentView</span><span class="p">?</span><span class="o">.</span><span class="nf">addSubview</span><span class="p">(</span><span class="n">imageView</span><span class="p">)</span>
    
    <span class="c1">// Show image for longer duration</span>
    <span class="n">window</span><span class="o">.</span><span class="nf">makeKeyAndOrderFront</span><span class="p">(</span><span class="kc">nil</span><span class="p">)</span>
    <span class="n">app</span><span class="o">.</span><span class="nf">activate</span><span class="p">(</span><span class="nv">ignoringOtherApps</span><span class="p">:</span> <span class="kc">true</span><span class="p">)</span>
    <span class="kt">DispatchQueue</span><span class="o">.</span><span class="n">main</span><span class="o">.</span><span class="nf">asyncAfter</span><span class="p">(</span><span class="nv">deadline</span><span class="p">:</span> <span class="o">.</span><span class="nf">now</span><span class="p">()</span> <span class="o">+</span> <span class="kt">IMAGE_FLASH_DURATION</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">window</span><span class="o">.</span><span class="nf">close</span><span class="p">()</span>
        <span class="n">app</span><span class="o">.</span><span class="nf">terminate</span><span class="p">(</span><span class="kc">nil</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
    <span class="c1">// Show solid color flash for shorter duration</span>
    <span class="n">window</span><span class="o">.</span><span class="nf">makeKeyAndOrderFront</span><span class="p">(</span><span class="kc">nil</span><span class="p">)</span>
    <span class="n">app</span><span class="o">.</span><span class="nf">activate</span><span class="p">(</span><span class="nv">ignoringOtherApps</span><span class="p">:</span> <span class="kc">true</span><span class="p">)</span>
    <span class="kt">DispatchQueue</span><span class="o">.</span><span class="n">main</span><span class="o">.</span><span class="nf">asyncAfter</span><span class="p">(</span><span class="nv">deadline</span><span class="p">:</span> <span class="o">.</span><span class="nf">now</span><span class="p">()</span> <span class="o">+</span> <span class="kt">SOLID_COLOR_FLASH_DURATION</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">window</span><span class="o">.</span><span class="nf">close</span><span class="p">()</span>
        <span class="n">app</span><span class="o">.</span><span class="nf">terminate</span><span class="p">(</span><span class="kc">nil</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="n">app</span><span class="o">.</span><span class="nf">run</span><span class="p">()</span> 
  </code></pre></figure>

</details> 
    </li>
    <li>
      <details id="notifier-expand">
  <summary>
    <span class="view-control">📖 View sample_notifier.py with syntax highlighting</span>
    <button onclick="downloadFile('/scripts/sample_notifier.py.txt', 'sample_notifier.py.txt')" class="download-button">📁 Download</button>
  </summary>
  
<figure class="highlight"><pre><code class="language-python" data-lang="python">  <span class="c1">#!/usr/bin/env python3
</span>
<span class="sh">"""</span><span class="s">
This script provides a sample notification by flashing the screen,
speaking a confirmation message, and sending a native macOS notification.
</span><span class="sh">"""</span>

<span class="kn">import</span> <span class="n">subprocess</span>
<span class="kn">from</span> <span class="n">pathlib</span> <span class="kn">import</span> <span class="n">Path</span>

<span class="c1"># --- Configuration ---
</span>
<span class="c1"># Get the directory where this script is located
</span><span class="n">SCRIPT_DIR</span> <span class="o">=</span> <span class="nc">Path</span><span class="p">(</span><span class="n">__file__</span><span class="p">).</span><span class="n">parent</span><span class="p">.</span><span class="nf">resolve</span><span class="p">()</span>

<span class="c1"># Infer the project's absolute path from the script's location (one dir up).
</span><span class="n">PROJECT_DIR_ABSOLUTE</span> <span class="o">=</span> <span class="n">SCRIPT_DIR</span><span class="p">.</span><span class="n">parent</span>

<span class="c1"># Abbreviate the home directory with ~ for a cleaner path if applicable
</span><span class="k">try</span><span class="p">:</span>
    <span class="n">PROJECT_DIR_DISPLAY</span> <span class="o">=</span> <span class="sa">f</span><span class="sh">"</span><span class="s">~/</span><span class="si">{</span><span class="n">PROJECT_DIR_ABSOLUTE</span><span class="p">.</span><span class="nf">relative_to</span><span class="p">(</span><span class="n">Path</span><span class="p">.</span><span class="nf">home</span><span class="p">())</span><span class="si">}</span><span class="sh">"</span>
<span class="k">except</span> <span class="nb">ValueError</span><span class="p">:</span>
    <span class="n">PROJECT_DIR_DISPLAY</span> <span class="o">=</span> <span class="nf">str</span><span class="p">(</span><span class="n">PROJECT_DIR_ABSOLUTE</span><span class="p">)</span>

<span class="c1"># --- Notification Functions ---
</span>
<span class="k">def</span> <span class="nf">flash</span><span class="p">():</span>
    <span class="sh">"""</span><span class="s">
    Flashes the screen. The FlashScreen script handles default image logic internally.
    </span><span class="sh">"""</span>
    <span class="n">flash_script_path</span> <span class="o">=</span> <span class="n">SCRIPT_DIR</span> <span class="o">/</span> <span class="sh">"</span><span class="s">FlashScreen</span><span class="sh">"</span>
    <span class="k">if</span> <span class="ow">not</span> <span class="n">flash_script_path</span><span class="p">.</span><span class="nf">is_file</span><span class="p">():</span>
        <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Error: FlashScreen script not found at </span><span class="si">{</span><span class="n">flash_script_path</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
        <span class="k">return</span>

    <span class="c1"># Use Popen to run in the background, so other notifications can proceed.
</span>    <span class="n">subprocess</span><span class="p">.</span><span class="nc">Popen</span><span class="p">([</span><span class="nf">str</span><span class="p">(</span><span class="n">flash_script_path</span><span class="p">)])</span>

<span class="k">def</span> <span class="nf">speak</span><span class="p">():</span>
    <span class="sh">"""</span><span class="s">Speaks a confirmation message.</span><span class="sh">"""</span>
    <span class="k">try</span><span class="p">:</span>
        <span class="n">subprocess</span><span class="p">.</span><span class="nf">run</span><span class="p">([</span><span class="sh">"</span><span class="s">say</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">AI response complete</span><span class="sh">"</span><span class="p">],</span> <span class="n">check</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
    <span class="k">except</span> <span class="n">subprocess</span><span class="p">.</span><span class="n">CalledProcessError</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
        <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Error with text-to-speech: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
    <span class="k">except</span> <span class="nb">FileNotFoundError</span><span class="p">:</span>
        <span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">Error: </span><span class="sh">'</span><span class="s">say</span><span class="sh">'</span><span class="s"> command not found (macOS only)</span><span class="sh">"</span><span class="p">)</span>

<span class="k">def</span> <span class="nf">terminal_notifier_installed</span><span class="p">():</span>
    <span class="sh">"""</span><span class="s">Check if terminal-notifier is available (only once).</span><span class="sh">"""</span>
    <span class="k">if</span> <span class="ow">not</span> <span class="nf">hasattr</span><span class="p">(</span><span class="n">terminal_notifier_installed</span><span class="p">,</span> <span class="sh">'</span><span class="s">_checked</span><span class="sh">'</span><span class="p">):</span>
        <span class="n">terminal_notifier_installed</span><span class="p">.</span><span class="n">_checked</span> <span class="o">=</span> <span class="bp">True</span>
        <span class="k">try</span><span class="p">:</span>
            <span class="n">subprocess</span><span class="p">.</span><span class="nf">run</span><span class="p">([</span><span class="sh">"</span><span class="s">which</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">terminal-notifier</span><span class="sh">"</span><span class="p">],</span>
                         <span class="n">capture_output</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">text</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">check</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
            <span class="k">return</span> <span class="bp">True</span>
        <span class="k">except</span> <span class="n">subprocess</span><span class="p">.</span><span class="n">CalledProcessError</span><span class="p">:</span>
            <span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">Error: terminal-notifier not found. Install with: brew install terminal-notifier</span><span class="sh">"</span><span class="p">)</span>
            <span class="k">return</span> <span class="bp">False</span>
    <span class="k">return</span> <span class="bp">True</span>

<span class="k">def</span> <span class="nf">os_notify</span><span class="p">():</span>
    <span class="sh">"""</span><span class="s">Sends a native macOS notification using terminal-notifier.</span><span class="sh">"""</span>
    <span class="k">if</span> <span class="ow">not</span> <span class="nf">terminal_notifier_installed</span><span class="p">():</span>
        <span class="k">return</span>
    
    <span class="n">message</span> <span class="o">=</span> <span class="sa">f</span><span class="sh">"</span><span class="s">AI response complete in project: </span><span class="si">{</span><span class="n">PROJECT_DIR_DISPLAY</span><span class="si">}</span><span class="sh">"</span>
    <span class="n">title</span> <span class="o">=</span> <span class="sh">"</span><span class="s">AI Response Complete</span><span class="sh">"</span>
    
    <span class="c1"># Use terminal-notifier for reliable notifications on macOS 15+; Claude Opus reports that 
</span>    <span class="c1"># "macOS 15+ requires explicit notification permissions for command-line tools,
</span>    <span class="c1"># and osascript's basic display notification doesn't always trigger the permission request properly."
</span>    <span class="n">process</span> <span class="o">=</span> <span class="n">subprocess</span><span class="p">.</span><span class="nf">run</span><span class="p">(</span>
        <span class="p">[</span><span class="sh">"</span><span class="s">terminal-notifier</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">-message</span><span class="sh">"</span><span class="p">,</span> <span class="n">message</span><span class="p">,</span> <span class="sh">"</span><span class="s">-title</span><span class="sh">"</span><span class="p">,</span> <span class="n">title</span><span class="p">],</span>
        <span class="n">capture_output</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">text</span><span class="o">=</span><span class="bp">True</span>
    <span class="p">)</span>
    <span class="k">if</span> <span class="n">process</span><span class="p">.</span><span class="n">returncode</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">:</span>
        <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Error sending notification: </span><span class="si">{</span><span class="n">process</span><span class="p">.</span><span class="n">stderr</span><span class="p">.</span><span class="nf">strip</span><span class="p">()</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>

<span class="c1"># --- Main Execution ---
</span><span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="sh">"</span><span class="s">__main__</span><span class="sh">"</span><span class="p">:</span>
    <span class="nf">flash</span><span class="p">()</span>
    <span class="nf">os_notify</span><span class="p">()</span>
    <span class="nf">speak</span><span class="p">()</span>
 
  </code></pre></figure>

</details> 
    </li>
    <li>
          <details id="watch-expand">
  <summary>
    <span class="view-control">📖 View watch with syntax highlighting</span>
    <button onclick="downloadFile('/scripts/watch.txt', 'watch.txt')" class="download-button">📁 Download</button>
  </summary>
  
<figure class="highlight"><pre><code class="language-bash" data-lang="bash">  scripts/file_watcher.py <span class="nt">-f</span> .cursor_response_complete <span class="nt">-v</span> scripts/sample_notifier.py


  </code></pre></figure>

</details> 
    </li>
  </ul>
</div>

<hr />

<h2 id="introduction">Introduction</h2>

<p>I have been exploring AI coding assistance, most recently using Cursor AI with Claude 4.0.</p>

<p>Although it often makes mistakes and produces substandard code, it can also be brilliant, correctly completing in seconds what would otherwise take minutes, hours, or even days.</p>

<p>So I continue to use it…but not without some frustrations. For various reasons, my requests 
sometimes perform quite slowly, even taking several minutes sometimes. While I’m waiting I multitask,
processing my email inbox for example, but then I need to return to Cursor periodically to see
if the response has completed. This frequent context switching kills my productivity.</p>

<h2 id="high-level-solution">High Level Solution</h2>

<p>Since my multitasking will likely bring me to an application other than Cursor, 
a visual notification <em>inside Cursor</em> would not work for me. And since I am often working
in coworking places where silence is required, an audio notification is not always suitable either.
So I needed to find a way to show a visual notification when the response completed, 
regardless of which application had focus. 
I decided to look into flashing the desktop with an image or solid color, and this met my need well.</p>

<p>Later I realized it would be nice to support the other notification types as well, so the 
solution is notification-type-independent.
You can configure whatever notification actions you want, but for your convenience I provided 
a <code class="language-plaintext highlighter-rouge">sample-notifier.py</code> script which demonstrates how to implement the following notification types:</p>

<ul>
  <li>flash the desktop with an image or solid color</li>
  <li>show a notification in the Notification Center (requires <code class="language-plaintext highlighter-rouge">terminal-notifier</code>)</li>
  <li>speak text (using macOS <code class="language-plaintext highlighter-rouge">say</code> command)</li>
</ul>

<p>But how would Cursor AI communicate to my system that the response was ready? 
Cursor operates in a sandbox with very limited ability to interact with the host system. 
There’s one thing that the Cursor AI Agent <em>can</em> do though, and it does it all the time – 
write to the filesystem – specifically, the project directory tree on the filesystem.</p>

<h3 id="solution-architecture">Solution Architecture</h3>

<p>So the solution consists of two parts:</p>

<ul>
  <li>the Cursor AI signaling via the filesystem that the response was completed</li>
  <li>a script that watches the filesystem and performs the notification(s) when signaled</li>
</ul>

<p>Let’s discuss these parts one at a time…</p>

<h2 id="configuring-cursor-to-signal-response-completion-by-creating-a-signal-file">Configuring Cursor to Signal Response Completion By Creating a Signal File</h2>

<p>If you navigate the menus Cursor -&gt; Settings -&gt; Cursor Settings -&gt; Rules, you can add “User Rules”
that will apply to every request of every project you edit in Cursor. I added:</p>

<p><em>Before starting any response, delete <code class="language-plaintext highlighter-rouge">.cursor_response_complete</code> in the project root if it exists. 
After completing that response, recreate (touch) it.</em></p>

<p>This rule requires the use of Agent mode, because in other modes the AI will not write to the filesystem.</p>

<p>While I was originally unwilling to give it this freedom, I’ve come around to allowing it, since I <code class="language-plaintext highlighter-rouge">git commit</code> frequently and can always revert to the most recent commit if I want to undo the AI’s changes.</p>

<h2 id="flashing-the-image-on-signal-file-creation">Flashing the Image On Signal File Creation</h2>

<p>Now that response completion is signalled by the presence of the <code class="language-plaintext highlighter-rouge">.cursor_response_complete</code> file, we need something to watch for that file and flash the image.
The <code class="language-plaintext highlighter-rouge">file_watcher.py</code> script watches for any file and execute any command(s).
(Though it takes a single command string, that string can be a compound command with multiple parts separated by
<code class="language-plaintext highlighter-rouge">;</code>, <code class="language-plaintext highlighter-rouge">&amp;&amp;</code>, or <code class="language-plaintext highlighter-rouge">||</code>, or a script that itself contains multiple commands.)</p>

<p>I decided to put it in the <code class="language-plaintext highlighter-rouge">scripts/</code> directory under my
project root so that it was version controlled and part of my project. 
Another approach would be to put it in a single place on the system where all projects
can access it, and modifications can be made in one place.</p>

<h2 id="technical-implementation-details">Technical Implementation Details</h2>

<h3 id="separate-scripts-for-file-watching-and-image-display">Separate Scripts for File Watching and Image Display</h3>

<p>The implementation uses a clean separation of concerns with two main components:</p>
<ul>
  <li>File monitoring:
    <ul>
      <li>A Python script (<code class="language-plaintext highlighter-rouge">file_watcher.py</code>) for file monitoring</li>
    </ul>
  </li>
  <li>Notification:
    <ul>
      <li>A platform-specific script (<code class="language-plaintext highlighter-rouge">FlashScreen</code>) for displaying the visual flash effect</li>
      <li>A convenience script (<code class="language-plaintext highlighter-rouge">sample_notifier.py</code>) that includes flashing, text-to-speech, and notifications</li>
    </ul>
  </li>
</ul>

<p>This architecture allows different platforms to provide their own flash implementation
without modifying the Python code. The current macOS implementation uses Swift with a
shebang line for direct execution.</p>

<p>A huge benefit of this separation is that you now have scripts you can use for file watching and
desktop flashing in any other use case, not just Cursor’s AI requests.</p>

<h3 id="adapting-for-other-platforms">Adapting for Other Platforms</h3>

<p>The <code class="language-plaintext highlighter-rouge">file_watcher.py</code> script is fully cross-platform since it’s written in Python. The platform-specific components are the notification scripts it is instructed to execute.</p>

<p>This post provides macOS-specific examples:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">FlashScreen</code>: A Swift script using native macOS APIs for the visual flash.</li>
  <li><code class="language-plaintext highlighter-rouge">sample_notifier.py</code>: A Python script that orchestrates <code class="language-plaintext highlighter-rouge">FlashScreen</code>, the macOS <code class="language-plaintext highlighter-rouge">say</code> command, and <code class="language-plaintext highlighter-rouge">terminal-notifier</code>.</li>
</ul>

<p>To implement this solution on <strong>Windows</strong> or <strong>Linux</strong>, you would keep <code class="language-plaintext highlighter-rouge">file_watcher.py</code> and create your own notification script.</p>

<p><strong>Example approaches for other platforms:</strong></p>
<ul>
  <li><strong>Windows:</strong> A PowerShell script (<code class="language-plaintext highlighter-rouge">.ps1</code>) could use .NET libraries to create a fullscreen window and the <code class="language-plaintext highlighter-rouge">BurntToast</code> module to send native notifications.</li>
  <li><strong>Linux:</strong> A shell script could use <code class="language-plaintext highlighter-rouge">notify-send</code> for system notifications and a simple Python script with a GUI library (like Tkinter) to create a fullscreen flash effect.</li>
</ul>

<p>You would then pass your custom script to the file watcher:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Example for a custom Linux script</span>
./scripts/file_watcher.py <span class="nt">--file</span> .cursor_response_complete <span class="nt">--</span> ./scripts/my-linux-notifier.sh
</code></pre></div></div>

<h3 id="help-output">Help Output</h3>

<p>Both scripts include comprehensive help:</p>
<h4 id="python-script-scriptsfile_watcherpy---help-or--h">Python script: <code class="language-plaintext highlighter-rouge">./scripts/file_watcher.py --help</code> (or <code class="language-plaintext highlighter-rouge">-h</code>)</h4>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>usage: file_watcher.py [-h] -f FILE [-d DELAY] [-v] ...

Poll for a file and execute a command when it appears.

positional arguments:
  command            Command to execute when file is found. Use -- to separate
                     from script args.

options:
  -h, --help         show this help message and exit
  -f, --file FILE    File to watch for.
  -d, --delay DELAY  Delay in seconds between polling checks (default: 0.5)
  -v, --verbose      Enable verbose logging

Examples:
  # Flash screen when file appears, checking every 0.2 seconds
  file_watcher.py --file .cursor_response_complete --delay 0.2 -- ./scripts/flash_screen

  # Watch a different file with default delay
  file_watcher.py --file state.log --verbose -- ls -l
</code></pre></div></div>

<h4 id="flash-script-scriptsflashscreen---help-or--h">Flash script: <code class="language-plaintext highlighter-rouge">./scripts/FlashScreen --help</code> (or <code class="language-plaintext highlighter-rouge">-h</code>)</h4>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Flash Screen - Screen Flash Utility

USAGE:
    ./FlashScreen [OPTIONS] [IMAGE_PATH]
    swift FlashScreen [OPTIONS] [IMAGE_PATH]

ARGUMENTS:
    IMAGE_PATH    Optional path to image file to display during flash
                  If not provided, defaults to ~/system-flash-image.jpg
                  If default image doesn't exist, displays a solid color flash

DESCRIPTION:
    Creates a fullscreen flash effect for visual notifications.
    - With image: Shows the image for 1.0 seconds
    - Without image: Shows solid color flash for 0.3 seconds
    - Default: Checks for ~/system-flash-image.jpg if no path specified
    
    The flash window appears at maximum window level and ignores mouse events.

EXAMPLES:
    ./FlashScreen                            # Default image or solid color flash
    ./FlashScreen -n                         # Force solid color flash (ignore default image)
    ./FlashScreen -c red                     # Force red color flash
    ./FlashScreen --color "#FF5500"          # Force orange color flash using hex
    ./FlashScreen ~/Pictures/flash.jpg       # Image flash
    swift FlashScreen /path/to/image.png     # Image flash with swift command

OPTIONS:
    -n, --no-image    Force solid color flash, ignoring default image
    -c, --color COLOR Specify flash color (named color or hex like #FF0000)
                      Named colors: white, black, red, green, blue, yellow, orange, purple, cyan, magenta, gray
    -h, --help        Show this help message and exit
</code></pre></div></div>
<h2 id="other-notes">Other Notes</h2>

<h3 id="using-a-fixed-image-name-for-easy-changes">Using a Fixed Image Name for Easy Changes</h3>

<p>I tend to get tired of the same image after a while, so it’s helpful to have a scheme
that lets me change the image without changing the script configuration.
The simplest approach is to always copy your desired image to the same filename:</p>

<p><strong>Simple approach:</strong></p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Copy your current image to a fixed name</span>
<span class="nb">cp</span> ~/Pictures/my-favorite-image.jpg ~/system-flash-image.jpg

<span class="c"># Later, to change the image, just copy a different one:</span>
<span class="nb">cp</span> ~/Pictures/different-image.png ~/system-flash-image.jpg
</code></pre></div></div>

<p><strong>Note:</strong> The flash script accepts any image format that macOS can display (JPEG, PNG, etc.), 
so format conversion is usually unnecessary. Even copying a .png file to the .jpg filename seems to work;
the system ignores the extension and examines the file to determine its type.</p>

<p><strong>Advanced: Using symbolic links (optional):</strong>
If you’re comfortable with symbolic links, they’re even more convenient since you don’t need to copy large files.</p>

<h2 id="implementation-code">Implementation Code</h2>

<p>The complete implementation consists of two scripts:</p>

<h3 id="python-file-watcher-script">Python File Watcher Script</h3>

<p>This Python script handles file monitoring using a simple polling approach. Key features:</p>
<ul>
  <li>Monitors any specified file creation with <code class="language-plaintext highlighter-rouge">--file</code> argument</li>
  <li>Executes any command when the file appears</li>
  <li>Configurable polling delay with <code class="language-plaintext highlighter-rouge">--delay</code> argument (default: 0.5 seconds)</li>
  <li>No external dependencies - uses only the Python standard library</li>
  <li>Cross-platform compatible</li>
</ul>

<p><strong>Dependencies:</strong> None - uses only the Python 3.6+ standard library</p>

<details id="file_watcher-expand">
  <summary>
    <span class="view-control">📖 View file_watcher.py with syntax highlighting</span>
    <button onclick="downloadFile('/scripts/file_watcher.py.txt', 'file_watcher.py.txt')" class="download-button">📁 Download</button>
  </summary>
  
<figure class="highlight"><pre><code class="language-python" data-lang="python">  <span class="c1">#!/usr/bin/env python3
</span>
<span class="sh">"""</span><span class="s">
File Watcher - A generic file monitoring and command execution script

This script monitors a specific file and executes a given command when it appears.
It</span><span class="sh">'</span><span class="s">s designed to be a simple, dependency-free tool for automated workflows.

WHAT IT DOES:
- Polls for the existence of a specified file every second.
- When the file is found, it executes a user-defined command and waits for it to complete.
- Checks the exit code of the command to determine success or failure.
- Deletes the file after the command is run to prepare for the next event.

DEPENDENCIES:
- Python 3.6+
- No external libraries required.

USAGE:
1. Run the script with a command to execute:
   ./scripts/file-watcher.py --file FILE [--delay SECONDS] [--verbose] -- [COMMAND] [ARGS...]
   
   The </span><span class="sh">'</span><span class="s">--</span><span class="sh">'</span><span class="s"> is recommended to separate script arguments from the command.

2. The script will start polling.
3. Press Ctrl+C to stop.
</span><span class="sh">"""</span>

<span class="kn">import</span> <span class="n">time</span>
<span class="kn">import</span> <span class="n">subprocess</span>
<span class="kn">import</span> <span class="n">argparse</span>
<span class="kn">import</span> <span class="n">logging</span>
<span class="kn">from</span> <span class="n">pathlib</span> <span class="kn">import</span> <span class="n">Path</span>
<span class="kn">from</span> <span class="n">datetime</span> <span class="kn">import</span> <span class="n">datetime</span>

<span class="c1"># Default configuration
</span><span class="n">DEFAULT_DELAY_SECONDS</span> <span class="o">=</span> <span class="mf">0.5</span>

<span class="k">def</span> <span class="nf">execute_command</span><span class="p">(</span><span class="n">command</span><span class="p">):</span>
    <span class="k">try</span><span class="p">:</span>
        <span class="n">logging</span><span class="p">.</span><span class="nf">debug</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Executing command: </span><span class="si">{</span><span class="sh">'</span><span class="s"> </span><span class="sh">'</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="n">command</span><span class="p">)</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
        <span class="n">result</span> <span class="o">=</span> <span class="n">subprocess</span><span class="p">.</span><span class="nf">run</span><span class="p">(</span>
            <span class="n">command</span><span class="p">,</span>
            <span class="n">capture_output</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span>
            <span class="n">text</span><span class="o">=</span><span class="bp">True</span>
        <span class="p">)</span>

        <span class="k">if</span> <span class="n">result</span><span class="p">.</span><span class="n">returncode</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">:</span>
            <span class="n">logging</span><span class="p">.</span><span class="nf">error</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Command failed with exit code </span><span class="si">{</span><span class="n">result</span><span class="p">.</span><span class="n">returncode</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
            <span class="k">if</span> <span class="n">result</span><span class="p">.</span><span class="n">stderr</span><span class="p">:</span>
                <span class="n">logging</span><span class="p">.</span><span class="nf">error</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Stderr: </span><span class="si">{</span><span class="n">result</span><span class="p">.</span><span class="n">stderr</span><span class="p">.</span><span class="nf">strip</span><span class="p">()</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
        <span class="k">else</span><span class="p">:</span>
            <span class="n">logging</span><span class="p">.</span><span class="nf">info</span><span class="p">(</span><span class="sh">"</span><span class="s">Command executed successfully.</span><span class="sh">"</span><span class="p">)</span>
            <span class="k">if</span> <span class="n">result</span><span class="p">.</span><span class="n">stdout</span><span class="p">:</span>
                <span class="n">logging</span><span class="p">.</span><span class="nf">debug</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Stdout: </span><span class="si">{</span><span class="n">result</span><span class="p">.</span><span class="n">stdout</span><span class="p">.</span><span class="nf">strip</span><span class="p">()</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
            <span class="k">if</span> <span class="n">result</span><span class="p">.</span><span class="n">stderr</span><span class="p">:</span> <span class="c1"># Log non-fatal stderr output too for diagnostics
</span>                <span class="n">logging</span><span class="p">.</span><span class="nf">debug</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Stderr (non-fatal): </span><span class="si">{</span><span class="n">result</span><span class="p">.</span><span class="n">stderr</span><span class="p">.</span><span class="nf">strip</span><span class="p">()</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>

    <span class="k">except</span> <span class="nb">FileNotFoundError</span><span class="p">:</span>
        <span class="n">logging</span><span class="p">.</span><span class="nf">error</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Command not found: </span><span class="si">{</span><span class="n">command</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
    <span class="k">except</span> <span class="nb">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
        <span class="n">logging</span><span class="p">.</span><span class="nf">error</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Error executing command: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>


<span class="k">def</span> <span class="nf">setup_logging</span><span class="p">(</span><span class="n">verbose</span><span class="o">=</span><span class="bp">False</span><span class="p">):</span>
    <span class="sh">"""</span><span class="s">Configure logging based on verbosity level.</span><span class="sh">"""</span>
    <span class="n">level</span> <span class="o">=</span> <span class="n">logging</span><span class="p">.</span><span class="n">DEBUG</span> <span class="k">if</span> <span class="n">verbose</span> <span class="k">else</span> <span class="n">logging</span><span class="p">.</span><span class="n">INFO</span>
    <span class="n">logging</span><span class="p">.</span><span class="nf">basicConfig</span><span class="p">(</span>
        <span class="n">level</span><span class="o">=</span><span class="n">level</span><span class="p">,</span>
        <span class="nb">format</span><span class="o">=</span><span class="sh">'</span><span class="s">%(asctime)s - %(levelname)s - %(message)s</span><span class="sh">'</span><span class="p">,</span>
        <span class="n">datefmt</span><span class="o">=</span><span class="sh">'</span><span class="s">%Y-%m-%d %H:%M:%S</span><span class="sh">'</span>
    <span class="p">)</span>


<span class="k">def</span> <span class="nf">parse_arguments</span><span class="p">():</span>
    <span class="sh">"""</span><span class="s">Parse command line arguments.</span><span class="sh">"""</span>
    <span class="n">parser</span> <span class="o">=</span> <span class="n">argparse</span><span class="p">.</span><span class="nc">ArgumentParser</span><span class="p">(</span>
        <span class="n">description</span><span class="o">=</span><span class="sh">"</span><span class="s">Poll for a file and execute a command when it appears.</span><span class="sh">"</span><span class="p">,</span>
        <span class="n">formatter_class</span><span class="o">=</span><span class="n">argparse</span><span class="p">.</span><span class="n">RawDescriptionHelpFormatter</span><span class="p">,</span>
        <span class="n">epilog</span><span class="o">=</span><span class="sh">"""</span><span class="s">
Examples:
  # Flash screen when file appears, checking every 0.2 seconds
  %(prog)s --file .cursor_response_complete --delay 0.2 -- ./scripts/FlashScreen

  # Watch a different file with default delay
  %(prog)s --file state.log --verbose -- ls -l
        </span><span class="sh">"""</span>
    <span class="p">)</span>
    
    <span class="n">parser</span><span class="p">.</span><span class="nf">add_argument</span><span class="p">(</span>
        <span class="sh">'</span><span class="s">-f</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">--file</span><span class="sh">'</span><span class="p">,</span>
        <span class="n">required</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span>
        <span class="n">help</span><span class="o">=</span><span class="sh">'</span><span class="s">File to watch for.</span><span class="sh">'</span>
    <span class="p">)</span>
    
    <span class="n">parser</span><span class="p">.</span><span class="nf">add_argument</span><span class="p">(</span>
        <span class="sh">'</span><span class="s">-d</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">--delay</span><span class="sh">'</span><span class="p">,</span>
        <span class="nb">type</span><span class="o">=</span><span class="nb">float</span><span class="p">,</span>
        <span class="n">default</span><span class="o">=</span><span class="n">DEFAULT_DELAY_SECONDS</span><span class="p">,</span>
        <span class="n">help</span><span class="o">=</span><span class="sa">f</span><span class="sh">'</span><span class="s">Delay in seconds between polling checks (default: </span><span class="si">{</span><span class="n">DEFAULT_DELAY_SECONDS</span><span class="si">}</span><span class="s">)</span><span class="sh">'</span>
    <span class="p">)</span>
    
    <span class="n">parser</span><span class="p">.</span><span class="nf">add_argument</span><span class="p">(</span>
        <span class="sh">'</span><span class="s">-v</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">--verbose</span><span class="sh">'</span><span class="p">,</span>
        <span class="n">action</span><span class="o">=</span><span class="sh">'</span><span class="s">store_true</span><span class="sh">'</span><span class="p">,</span>
        <span class="n">help</span><span class="o">=</span><span class="sh">'</span><span class="s">Enable verbose logging</span><span class="sh">'</span>
    <span class="p">)</span>
    
    <span class="n">parser</span><span class="p">.</span><span class="nf">add_argument</span><span class="p">(</span>
        <span class="sh">'</span><span class="s">command</span><span class="sh">'</span><span class="p">,</span>
        <span class="n">nargs</span><span class="o">=</span><span class="n">argparse</span><span class="p">.</span><span class="n">REMAINDER</span><span class="p">,</span>
        <span class="n">help</span><span class="o">=</span><span class="sh">'</span><span class="s">Command to execute when file is found. Use -- to separate from script args.</span><span class="sh">'</span>
    <span class="p">)</span>
    
    <span class="n">args</span> <span class="o">=</span> <span class="n">parser</span><span class="p">.</span><span class="nf">parse_args</span><span class="p">()</span>

    <span class="k">if</span> <span class="n">args</span><span class="p">.</span><span class="n">command</span> <span class="ow">and</span> <span class="n">args</span><span class="p">.</span><span class="n">command</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">==</span> <span class="sh">'</span><span class="s">--</span><span class="sh">'</span><span class="p">:</span>
        <span class="n">args</span><span class="p">.</span><span class="n">command</span> <span class="o">=</span> <span class="n">args</span><span class="p">.</span><span class="n">command</span><span class="p">[</span><span class="mi">1</span><span class="p">:]</span>

    <span class="k">if</span> <span class="ow">not</span> <span class="n">args</span><span class="p">.</span><span class="n">command</span><span class="p">:</span>
        <span class="n">parser</span><span class="p">.</span><span class="nf">error</span><span class="p">(</span>
            <span class="sh">"</span><span class="s">No command supplied. You must provide a command to execute after the script options.</span><span class="se">\n</span><span class="sh">"</span>
            <span class="sh">"</span><span class="s">Example: %(prog)s --file ... -- your_command_here</span><span class="sh">"</span>
        <span class="p">)</span>

    <span class="k">return</span> <span class="n">args</span>


<span class="k">def</span> <span class="nf">delete_file</span><span class="p">(</span><span class="n">filespec</span><span class="p">):</span>
    <span class="k">try</span><span class="p">:</span>
        <span class="n">filespec</span><span class="p">.</span><span class="nf">unlink</span><span class="p">()</span>
        <span class="n">logging</span><span class="p">.</span><span class="nf">debug</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Deleted file: </span><span class="si">{</span><span class="n">filespec</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
    <span class="k">except</span> <span class="nb">OSError</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
        <span class="n">logging</span><span class="p">.</span><span class="nf">error</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Error deleting file </span><span class="si">{</span><span class="n">filespec</span><span class="si">}</span><span class="s">: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>


<span class="k">def</span> <span class="nf">process_file_presence</span><span class="p">(</span><span class="n">file_to_watch</span><span class="p">,</span> <span class="n">command</span><span class="p">):</span>
    <span class="n">timestamp</span> <span class="o">=</span> <span class="n">datetime</span><span class="p">.</span><span class="nf">now</span><span class="p">().</span><span class="nf">strftime</span><span class="p">(</span><span class="sh">'</span><span class="s">%Y-%m-%d %H:%M:%S</span><span class="sh">'</span><span class="p">)</span>
    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="si">{</span><span class="n">timestamp</span><span class="si">}</span><span class="s"> - File FOUND: </span><span class="si">{</span><span class="n">file_to_watch</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
    
    <span class="nf">execute_command</span><span class="p">(</span><span class="n">command</span><span class="p">)</span>
    

<span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
    <span class="n">args</span> <span class="o">=</span> <span class="nf">parse_arguments</span><span class="p">()</span>
    <span class="nf">setup_logging</span><span class="p">(</span><span class="n">args</span><span class="p">.</span><span class="n">verbose</span><span class="p">)</span>
    
    <span class="n">file_to_watch</span> <span class="o">=</span> <span class="nc">Path</span><span class="p">(</span><span class="n">args</span><span class="p">.</span><span class="nb">file</span><span class="p">).</span><span class="nf">resolve</span><span class="p">()</span>
    
    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Polling for file: </span><span class="si">{</span><span class="n">file_to_watch</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Command to run: </span><span class="si">{</span><span class="sh">'</span><span class="s"> </span><span class="sh">'</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="n">args</span><span class="p">.</span><span class="n">command</span><span class="p">)</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
    <span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">Press Ctrl+C to stop...</span><span class="sh">"</span><span class="p">)</span>
    
    <span class="k">try</span><span class="p">:</span>
        <span class="k">while</span> <span class="bp">True</span><span class="p">:</span>
            <span class="k">if</span> <span class="n">file_to_watch</span><span class="p">.</span><span class="nf">exists</span><span class="p">():</span>
                <span class="nf">process_file_presence</span><span class="p">(</span><span class="n">file_to_watch</span><span class="p">,</span> <span class="n">args</span><span class="p">.</span><span class="n">command</span><span class="p">)</span>
                <span class="nf">delete_file</span><span class="p">(</span><span class="n">file_to_watch</span><span class="p">)</span>
            <span class="n">time</span><span class="p">.</span><span class="nf">sleep</span><span class="p">(</span><span class="n">args</span><span class="p">.</span><span class="n">delay</span><span class="p">)</span>
    <span class="k">except</span> <span class="nb">KeyboardInterrupt</span><span class="p">:</span>
        <span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="se">\n</span><span class="s">Stopping file watcher.</span><span class="sh">"</span><span class="p">)</span>
    <span class="k">except</span> <span class="nb">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
        <span class="n">logging</span><span class="p">.</span><span class="nf">error</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">An unexpected error occurred: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>

<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="sh">"</span><span class="s">__main__</span><span class="sh">"</span><span class="p">:</span>
    <span class="nf">main</span><span class="p">()</span> 
  </code></pre></figure>

</details>

<p><strong>📝 Note:</strong> After downloading, rename to <code class="language-plaintext highlighter-rouge">file_watcher.py</code> and ensure it’s executable with <code class="language-plaintext highlighter-rouge">chmod +x file_watcher.py</code>.</p>

<h3 id="swift-flash-script">Swift Flash Script</h3>

<p>This Swift script creates the visual flash effect using macOS NSWindow APIs. Key features:</p>
<ul>
  <li>Displays fullscreen image for 1.0 second, or solid color flash for 0.3 seconds</li>
  <li>Supports any image format macOS can display (JPEG, PNG, etc.)</li>
  <li>Supports custom colors with <code class="language-plaintext highlighter-rouge">-c/--color</code> option (named colors or hex values)</li>
  <li>Force solid color mode with <code class="language-plaintext highlighter-rouge">-n/--no-image</code> option</li>
  <li>Default image path: <code class="language-plaintext highlighter-rouge">~/system-flash-image.jpg</code></li>
  <li>Self-contained executable with help documentation</li>
  <li>Uses shebang line for direct execution (<code class="language-plaintext highlighter-rouge">#!/usr/bin/env swift</code>)</li>
</ul>

<p><strong>Requirements:</strong> macOS with Swift support (Xcode installed)</p>

<details id="swift-expand">
  <summary>
    <span class="view-control">📖 View FlashScreen with syntax highlighting</span>
    <button onclick="downloadFile('/scripts/FlashScreen.txt', 'FlashScreen.txt')" class="download-button">📁 Download</button>
  </summary>
  
<figure class="highlight"><pre><code class="language-swift" data-lang="swift">  <span class="cp">#!/usr/bin/env swift</span>

<span class="kd">import</span> <span class="kt">Cocoa</span>

<span class="c1">// Flash duration constants</span>
<span class="k">let</span> <span class="nv">IMAGE_FLASH_DURATION</span><span class="p">:</span> <span class="kt">TimeInterval</span> <span class="o">=</span> <span class="mf">1.0</span>
<span class="k">let</span> <span class="nv">SOLID_COLOR_FLASH_DURATION</span><span class="p">:</span> <span class="kt">TimeInterval</span> <span class="o">=</span> <span class="mf">0.3</span>

<span class="c1">// Default image path constants</span>
<span class="k">let</span> <span class="nv">DEFAULT_IMAGE_PATH_DISPLAY</span> <span class="o">=</span> <span class="s">"~/system-flash-image.jpg"</span>
<span class="k">let</span> <span class="nv">DEFAULT_IMAGE_PATH</span> <span class="o">=</span> <span class="kt">NSString</span><span class="p">(</span><span class="nv">string</span><span class="p">:</span> <span class="kt">DEFAULT_IMAGE_PATH_DISPLAY</span><span class="p">)</span><span class="o">.</span><span class="n">expandingTildeInPath</span>

<span class="c1">// Color parsing function</span>
<span class="kd">func</span> <span class="nf">parseColor</span><span class="p">(</span><span class="n">_</span> <span class="nv">colorString</span><span class="p">:</span> <span class="kt">String</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">NSColor</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">lowercased</span> <span class="o">=</span> <span class="n">colorString</span><span class="o">.</span><span class="nf">lowercased</span><span class="p">()</span>
    
    <span class="c1">// Handle named colors</span>
    <span class="k">switch</span> <span class="n">lowercased</span> <span class="p">{</span>
    <span class="k">case</span> <span class="s">"white"</span><span class="p">:</span> <span class="k">return</span> <span class="kt">NSColor</span><span class="o">.</span><span class="n">white</span>
    <span class="k">case</span> <span class="s">"black"</span><span class="p">:</span> <span class="k">return</span> <span class="kt">NSColor</span><span class="o">.</span><span class="n">black</span>
    <span class="k">case</span> <span class="s">"red"</span><span class="p">:</span> <span class="k">return</span> <span class="kt">NSColor</span><span class="o">.</span><span class="n">red</span>
    <span class="k">case</span> <span class="s">"green"</span><span class="p">:</span> <span class="k">return</span> <span class="kt">NSColor</span><span class="o">.</span><span class="n">green</span>
    <span class="k">case</span> <span class="s">"blue"</span><span class="p">:</span> <span class="k">return</span> <span class="kt">NSColor</span><span class="o">.</span><span class="n">blue</span>
    <span class="k">case</span> <span class="s">"yellow"</span><span class="p">:</span> <span class="k">return</span> <span class="kt">NSColor</span><span class="o">.</span><span class="n">yellow</span>
    <span class="k">case</span> <span class="s">"orange"</span><span class="p">:</span> <span class="k">return</span> <span class="kt">NSColor</span><span class="o">.</span><span class="n">orange</span>
    <span class="k">case</span> <span class="s">"purple"</span><span class="p">:</span> <span class="k">return</span> <span class="kt">NSColor</span><span class="o">.</span><span class="n">purple</span>
    <span class="k">case</span> <span class="s">"cyan"</span><span class="p">:</span> <span class="k">return</span> <span class="kt">NSColor</span><span class="o">.</span><span class="n">cyan</span>
    <span class="k">case</span> <span class="s">"magenta"</span><span class="p">:</span> <span class="k">return</span> <span class="kt">NSColor</span><span class="o">.</span><span class="n">magenta</span>
    <span class="k">case</span> <span class="s">"gray"</span><span class="p">,</span> <span class="s">"grey"</span><span class="p">:</span> <span class="k">return</span> <span class="kt">NSColor</span><span class="o">.</span><span class="n">gray</span>
    <span class="k">default</span><span class="p">:</span>
        <span class="c1">// Try to parse as hex color</span>
        <span class="k">if</span> <span class="n">colorString</span><span class="o">.</span><span class="nf">hasPrefix</span><span class="p">(</span><span class="s">"#"</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">let</span> <span class="nv">hex</span> <span class="o">=</span> <span class="kt">String</span><span class="p">(</span><span class="n">colorString</span><span class="o">.</span><span class="nf">dropFirst</span><span class="p">())</span>
            <span class="k">if</span> <span class="n">hex</span><span class="o">.</span><span class="n">count</span> <span class="o">==</span> <span class="mi">6</span><span class="p">,</span> <span class="k">let</span> <span class="nv">rgbValue</span> <span class="o">=</span> <span class="kt">UInt32</span><span class="p">(</span><span class="n">hex</span><span class="p">,</span> <span class="nv">radix</span><span class="p">:</span> <span class="mi">16</span><span class="p">)</span> <span class="p">{</span>
                <span class="k">let</span> <span class="nv">red</span> <span class="o">=</span> <span class="kt">CGFloat</span><span class="p">((</span><span class="n">rgbValue</span> <span class="o">&amp;</span> <span class="mh">0xFF0000</span><span class="p">)</span> <span class="o">&gt;&gt;</span> <span class="mi">16</span><span class="p">)</span> <span class="o">/</span> <span class="mf">255.0</span>
                <span class="k">let</span> <span class="nv">green</span> <span class="o">=</span> <span class="kt">CGFloat</span><span class="p">((</span><span class="n">rgbValue</span> <span class="o">&amp;</span> <span class="mh">0x00FF00</span><span class="p">)</span> <span class="o">&gt;&gt;</span> <span class="mi">8</span><span class="p">)</span> <span class="o">/</span> <span class="mf">255.0</span>
                <span class="k">let</span> <span class="nv">blue</span> <span class="o">=</span> <span class="kt">CGFloat</span><span class="p">(</span><span class="n">rgbValue</span> <span class="o">&amp;</span> <span class="mh">0x0000FF</span><span class="p">)</span> <span class="o">/</span> <span class="mf">255.0</span>
                <span class="k">return</span> <span class="kt">NSColor</span><span class="p">(</span><span class="nv">red</span><span class="p">:</span> <span class="n">red</span><span class="p">,</span> <span class="nv">green</span><span class="p">:</span> <span class="n">green</span><span class="p">,</span> <span class="nv">blue</span><span class="p">:</span> <span class="n">blue</span><span class="p">,</span> <span class="nv">alpha</span><span class="p">:</span> <span class="mf">1.0</span><span class="p">)</span>
            <span class="p">}</span>
        <span class="p">}</span>
        <span class="c1">// Default to white if parsing fails</span>
        <span class="k">return</span> <span class="kt">NSColor</span><span class="o">.</span><span class="n">white</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="c1">// Help function</span>
<span class="kd">func</span> <span class="nf">showHelp</span><span class="p">()</span> <span class="p">{</span>
    <span class="nf">print</span><span class="p">(</span><span class="s">"""
Flash Screen - Screen Flash Utility

USAGE:
    ./FlashScreen [OPTIONS] [IMAGE_PATH]
    swift FlashScreen [OPTIONS] [IMAGE_PATH]

ARGUMENTS:
    IMAGE_PATH    Optional path to image file to display during flash
                  If not provided, defaults to </span><span class="se">\(</span><span class="kt">DEFAULT_IMAGE_PATH_DISPLAY</span><span class="se">)</span><span class="s">
                  If default image doesn't exist, displays a solid color flash

DESCRIPTION:
    Creates a fullscreen flash effect for visual notifications.
    - With image: Shows the image for </span><span class="se">\(</span><span class="kt">IMAGE_FLASH_DURATION</span><span class="se">)</span><span class="s"> seconds
    - Without image: Shows solid color flash for </span><span class="se">\(</span><span class="kt">SOLID_COLOR_FLASH_DURATION</span><span class="se">)</span><span class="s"> seconds
    - Default: Checks for </span><span class="se">\(</span><span class="kt">DEFAULT_IMAGE_PATH_DISPLAY</span><span class="se">)</span><span class="s"> if no path specified
    
    The flash window appears at maximum window level and ignores mouse events.

EXAMPLES:
    ./FlashScreen                            # Default image or solid color flash
    ./FlashScreen -n                         # Force solid color flash (ignore default image)
    ./FlashScreen -c red                     # Force red color flash
    ./FlashScreen --color "</span><span class="k">#FF5500</span><span class="s">"          # Force orange color flash using hex
    ./FlashScreen ~/Pictures/flash.jpg       # Image flash
    swift FlashScreen /path/to/image.png     # Image flash with swift command

OPTIONS:
    -n, --no-image    Force solid color flash, ignoring default image
    -c, --color COLOR Specify flash color (named color or hex like #FF0000)
                      Named colors: white, black, red, green, blue, yellow, orange, purple, cyan, magenta, gray
    -h, --help        Show this help message and exit
"""</span><span class="p">)</span>
<span class="p">}</span>

<span class="c1">// Parse command line arguments</span>
<span class="k">let</span> <span class="nv">args</span> <span class="o">=</span> <span class="kt">CommandLine</span><span class="o">.</span><span class="n">arguments</span>
<span class="k">var</span> <span class="nv">forceNoImage</span> <span class="o">=</span> <span class="kc">false</span>
<span class="k">var</span> <span class="nv">imagePath</span><span class="p">:</span> <span class="kt">String</span><span class="p">?</span> <span class="o">=</span> <span class="kc">nil</span>
<span class="k">var</span> <span class="nv">flashColor</span><span class="p">:</span> <span class="kt">NSColor</span> <span class="o">=</span> <span class="kt">NSColor</span><span class="o">.</span><span class="n">white</span>

<span class="c1">// Process arguments</span>
<span class="k">var</span> <span class="nv">i</span> <span class="o">=</span> <span class="mi">1</span>
<span class="k">while</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">args</span><span class="o">.</span><span class="n">count</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">arg</span> <span class="o">=</span> <span class="n">args</span><span class="p">[</span><span class="n">i</span><span class="p">]</span>
    <span class="k">if</span> <span class="n">arg</span> <span class="o">==</span> <span class="s">"-h"</span> <span class="o">||</span> <span class="n">arg</span> <span class="o">==</span> <span class="s">"--help"</span> <span class="p">{</span>
        <span class="nf">showHelp</span><span class="p">()</span>
        <span class="nf">exit</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
    <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="n">arg</span> <span class="o">==</span> <span class="s">"-n"</span> <span class="o">||</span> <span class="n">arg</span> <span class="o">==</span> <span class="s">"--no-image"</span> <span class="p">{</span>
        <span class="n">forceNoImage</span> <span class="o">=</span> <span class="kc">true</span>
        <span class="n">i</span> <span class="o">+=</span> <span class="mi">1</span>
    <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="n">arg</span> <span class="o">==</span> <span class="s">"-c"</span> <span class="o">||</span> <span class="n">arg</span> <span class="o">==</span> <span class="s">"--color"</span> <span class="p">{</span>
        <span class="k">if</span> <span class="n">i</span> <span class="o">+</span> <span class="mi">1</span> <span class="o">&lt;</span> <span class="n">args</span><span class="o">.</span><span class="n">count</span> <span class="p">{</span>
            <span class="n">flashColor</span> <span class="o">=</span> <span class="nf">parseColor</span><span class="p">(</span><span class="n">args</span><span class="p">[</span><span class="n">i</span> <span class="o">+</span> <span class="mi">1</span><span class="p">])</span>
            <span class="n">forceNoImage</span> <span class="o">=</span> <span class="kc">true</span>  <span class="c1">// Color option implies no image</span>
            <span class="n">i</span> <span class="o">+=</span> <span class="mi">2</span>
        <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
            <span class="nf">print</span><span class="p">(</span><span class="s">"Error: -c/--color requires a color value"</span><span class="p">)</span>
            <span class="nf">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
        <span class="p">}</span>
    <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="o">!</span><span class="n">arg</span><span class="o">.</span><span class="nf">hasPrefix</span><span class="p">(</span><span class="s">"-"</span><span class="p">)</span> <span class="p">{</span>
        <span class="c1">// This is an image path</span>
        <span class="n">imagePath</span> <span class="o">=</span> <span class="n">arg</span>
        <span class="k">break</span>
    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
        <span class="nf">print</span><span class="p">(</span><span class="s">"Error: Unknown option </span><span class="se">\(</span><span class="n">arg</span><span class="se">)</span><span class="s">"</span><span class="p">)</span>
        <span class="nf">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="k">let</span> <span class="nv">app</span> <span class="o">=</span> <span class="kt">NSApplication</span><span class="o">.</span><span class="n">shared</span>
<span class="n">app</span><span class="o">.</span><span class="nf">setActivationPolicy</span><span class="p">(</span><span class="o">.</span><span class="n">regular</span><span class="p">)</span>

<span class="k">let</span> <span class="nv">window</span> <span class="o">=</span> <span class="kt">NSWindow</span><span class="p">(</span>
    <span class="nv">contentRect</span><span class="p">:</span> <span class="kt">NSScreen</span><span class="o">.</span><span class="n">main</span><span class="o">!.</span><span class="n">frame</span><span class="p">,</span>
    <span class="nv">styleMask</span><span class="p">:</span> <span class="p">[</span><span class="o">.</span><span class="n">borderless</span><span class="p">],</span>
    <span class="nv">backing</span><span class="p">:</span> <span class="o">.</span><span class="n">buffered</span><span class="p">,</span>
    <span class="nv">defer</span><span class="p">:</span> <span class="kc">false</span>
<span class="p">)</span>

<span class="n">window</span><span class="o">.</span><span class="n">backgroundColor</span> <span class="o">=</span> <span class="n">flashColor</span>
<span class="n">window</span><span class="o">.</span><span class="n">level</span> <span class="o">=</span> <span class="kt">NSWindow</span><span class="o">.</span><span class="kt">Level</span><span class="p">(</span><span class="nv">rawValue</span><span class="p">:</span> <span class="kt">Int</span><span class="p">(</span><span class="kt">CGWindowLevelForKey</span><span class="p">(</span><span class="o">.</span><span class="n">maximumWindow</span><span class="p">)))</span>
<span class="n">window</span><span class="o">.</span><span class="n">isOpaque</span> <span class="o">=</span> <span class="kc">true</span>
<span class="n">window</span><span class="o">.</span><span class="n">ignoresMouseEvents</span> <span class="o">=</span> <span class="kc">true</span>

<span class="c1">// Determine final image path based on arguments and flags</span>
<span class="k">if</span> <span class="n">forceNoImage</span> <span class="p">{</span>
    <span class="c1">// Force solid color flash, ignore any image path or default</span>
    <span class="n">imagePath</span> <span class="o">=</span> <span class="kc">nil</span>
<span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="n">imagePath</span> <span class="o">==</span> <span class="kc">nil</span> <span class="p">{</span>
    <span class="c1">// No explicit image path provided, check for default</span>
    <span class="n">imagePath</span> <span class="o">=</span> <span class="kt">FileManager</span><span class="o">.</span><span class="k">default</span><span class="o">.</span><span class="nf">fileExists</span><span class="p">(</span><span class="nv">atPath</span><span class="p">:</span> <span class="kt">DEFAULT_IMAGE_PATH</span><span class="p">)</span> <span class="p">?</span> <span class="kt">DEFAULT_IMAGE_PATH</span> <span class="p">:</span> <span class="kc">nil</span>
<span class="p">}</span>

<span class="c1">// Try to load and display image</span>
<span class="k">if</span> <span class="k">let</span> <span class="nv">path</span> <span class="o">=</span> <span class="n">imagePath</span><span class="p">,</span>
   <span class="k">let</span> <span class="nv">image</span> <span class="o">=</span> <span class="kt">NSImage</span><span class="p">(</span><span class="nv">contentsOfFile</span><span class="p">:</span> <span class="n">path</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">imageView</span> <span class="o">=</span> <span class="kt">NSImageView</span><span class="p">(</span><span class="nv">frame</span><span class="p">:</span> <span class="n">window</span><span class="o">.</span><span class="n">contentView</span><span class="o">!.</span><span class="n">bounds</span><span class="p">)</span>
    <span class="n">imageView</span><span class="o">.</span><span class="n">image</span> <span class="o">=</span> <span class="n">image</span>
    <span class="n">imageView</span><span class="o">.</span><span class="n">imageScaling</span> <span class="o">=</span> <span class="o">.</span><span class="n">scaleProportionallyUpOrDown</span>
    <span class="n">window</span><span class="o">.</span><span class="n">contentView</span><span class="p">?</span><span class="o">.</span><span class="nf">addSubview</span><span class="p">(</span><span class="n">imageView</span><span class="p">)</span>
    
    <span class="c1">// Show image for longer duration</span>
    <span class="n">window</span><span class="o">.</span><span class="nf">makeKeyAndOrderFront</span><span class="p">(</span><span class="kc">nil</span><span class="p">)</span>
    <span class="n">app</span><span class="o">.</span><span class="nf">activate</span><span class="p">(</span><span class="nv">ignoringOtherApps</span><span class="p">:</span> <span class="kc">true</span><span class="p">)</span>
    <span class="kt">DispatchQueue</span><span class="o">.</span><span class="n">main</span><span class="o">.</span><span class="nf">asyncAfter</span><span class="p">(</span><span class="nv">deadline</span><span class="p">:</span> <span class="o">.</span><span class="nf">now</span><span class="p">()</span> <span class="o">+</span> <span class="kt">IMAGE_FLASH_DURATION</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">window</span><span class="o">.</span><span class="nf">close</span><span class="p">()</span>
        <span class="n">app</span><span class="o">.</span><span class="nf">terminate</span><span class="p">(</span><span class="kc">nil</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
    <span class="c1">// Show solid color flash for shorter duration</span>
    <span class="n">window</span><span class="o">.</span><span class="nf">makeKeyAndOrderFront</span><span class="p">(</span><span class="kc">nil</span><span class="p">)</span>
    <span class="n">app</span><span class="o">.</span><span class="nf">activate</span><span class="p">(</span><span class="nv">ignoringOtherApps</span><span class="p">:</span> <span class="kc">true</span><span class="p">)</span>
    <span class="kt">DispatchQueue</span><span class="o">.</span><span class="n">main</span><span class="o">.</span><span class="nf">asyncAfter</span><span class="p">(</span><span class="nv">deadline</span><span class="p">:</span> <span class="o">.</span><span class="nf">now</span><span class="p">()</span> <span class="o">+</span> <span class="kt">SOLID_COLOR_FLASH_DURATION</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">window</span><span class="o">.</span><span class="nf">close</span><span class="p">()</span>
        <span class="n">app</span><span class="o">.</span><span class="nf">terminate</span><span class="p">(</span><span class="kc">nil</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="n">app</span><span class="o">.</span><span class="nf">run</span><span class="p">()</span> 
  </code></pre></figure>

</details>

<p><strong>📝 Note:</strong> After downloading, rename to <code class="language-plaintext highlighter-rouge">FlashScreen</code> and ensure it’s executable with <code class="language-plaintext highlighter-rouge">chmod +x FlashScreen</code>.</p>

<h3 id="sample-notifier-script">Sample Notifier Script</h3>

<p>For users who want more comprehensive notifications including screen flash, text-to-speech, 
and native macOS notifications, we provide a sample notifier script. This Python script demonstrates
how to combine multiple notification types:</p>

<p>Key features:</p>
<ul>
  <li>Flashes the screen using the <code class="language-plaintext highlighter-rouge">FlashScreen</code> script</li>
  <li>Speaks “AI response complete” using macOS <code class="language-plaintext highlighter-rouge">say</code> command</li>
  <li>Sends a native macOS notification using <code class="language-plaintext highlighter-rouge">terminal-notifier</code></li>
  <li>Shows the project directory in notifications for context</li>
  <li>Kicks off all three notifications in quick succession for a near-parallel effect</li>
</ul>

<p><strong>Requirements:</strong></p>
<ul>
  <li>macOS (for <code class="language-plaintext highlighter-rouge">say</code> command)</li>
  <li><code class="language-plaintext highlighter-rouge">terminal-notifier</code> (install with: <code class="language-plaintext highlighter-rouge">brew install terminal-notifier</code>)</li>
</ul>

<details id="notifier-expand">
  <summary>
    <span class="view-control">📖 View sample_notifier.py with syntax highlighting</span>
    <button onclick="downloadFile('/scripts/sample_notifier.py.txt', 'sample_notifier.py.txt')" class="download-button">📁 Download</button>
  </summary>
  
<figure class="highlight"><pre><code class="language-python" data-lang="python">  <span class="c1">#!/usr/bin/env python3
</span>
<span class="sh">"""</span><span class="s">
This script provides a sample notification by flashing the screen,
speaking a confirmation message, and sending a native macOS notification.
</span><span class="sh">"""</span>

<span class="kn">import</span> <span class="n">subprocess</span>
<span class="kn">from</span> <span class="n">pathlib</span> <span class="kn">import</span> <span class="n">Path</span>

<span class="c1"># --- Configuration ---
</span>
<span class="c1"># Get the directory where this script is located
</span><span class="n">SCRIPT_DIR</span> <span class="o">=</span> <span class="nc">Path</span><span class="p">(</span><span class="n">__file__</span><span class="p">).</span><span class="n">parent</span><span class="p">.</span><span class="nf">resolve</span><span class="p">()</span>

<span class="c1"># Infer the project's absolute path from the script's location (one dir up).
</span><span class="n">PROJECT_DIR_ABSOLUTE</span> <span class="o">=</span> <span class="n">SCRIPT_DIR</span><span class="p">.</span><span class="n">parent</span>

<span class="c1"># Abbreviate the home directory with ~ for a cleaner path if applicable
</span><span class="k">try</span><span class="p">:</span>
    <span class="n">PROJECT_DIR_DISPLAY</span> <span class="o">=</span> <span class="sa">f</span><span class="sh">"</span><span class="s">~/</span><span class="si">{</span><span class="n">PROJECT_DIR_ABSOLUTE</span><span class="p">.</span><span class="nf">relative_to</span><span class="p">(</span><span class="n">Path</span><span class="p">.</span><span class="nf">home</span><span class="p">())</span><span class="si">}</span><span class="sh">"</span>
<span class="k">except</span> <span class="nb">ValueError</span><span class="p">:</span>
    <span class="n">PROJECT_DIR_DISPLAY</span> <span class="o">=</span> <span class="nf">str</span><span class="p">(</span><span class="n">PROJECT_DIR_ABSOLUTE</span><span class="p">)</span>

<span class="c1"># --- Notification Functions ---
</span>
<span class="k">def</span> <span class="nf">flash</span><span class="p">():</span>
    <span class="sh">"""</span><span class="s">
    Flashes the screen. The FlashScreen script handles default image logic internally.
    </span><span class="sh">"""</span>
    <span class="n">flash_script_path</span> <span class="o">=</span> <span class="n">SCRIPT_DIR</span> <span class="o">/</span> <span class="sh">"</span><span class="s">FlashScreen</span><span class="sh">"</span>
    <span class="k">if</span> <span class="ow">not</span> <span class="n">flash_script_path</span><span class="p">.</span><span class="nf">is_file</span><span class="p">():</span>
        <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Error: FlashScreen script not found at </span><span class="si">{</span><span class="n">flash_script_path</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
        <span class="k">return</span>

    <span class="c1"># Use Popen to run in the background, so other notifications can proceed.
</span>    <span class="n">subprocess</span><span class="p">.</span><span class="nc">Popen</span><span class="p">([</span><span class="nf">str</span><span class="p">(</span><span class="n">flash_script_path</span><span class="p">)])</span>

<span class="k">def</span> <span class="nf">speak</span><span class="p">():</span>
    <span class="sh">"""</span><span class="s">Speaks a confirmation message.</span><span class="sh">"""</span>
    <span class="k">try</span><span class="p">:</span>
        <span class="n">subprocess</span><span class="p">.</span><span class="nf">run</span><span class="p">([</span><span class="sh">"</span><span class="s">say</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">AI response complete</span><span class="sh">"</span><span class="p">],</span> <span class="n">check</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
    <span class="k">except</span> <span class="n">subprocess</span><span class="p">.</span><span class="n">CalledProcessError</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
        <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Error with text-to-speech: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
    <span class="k">except</span> <span class="nb">FileNotFoundError</span><span class="p">:</span>
        <span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">Error: </span><span class="sh">'</span><span class="s">say</span><span class="sh">'</span><span class="s"> command not found (macOS only)</span><span class="sh">"</span><span class="p">)</span>

<span class="k">def</span> <span class="nf">terminal_notifier_installed</span><span class="p">():</span>
    <span class="sh">"""</span><span class="s">Check if terminal-notifier is available (only once).</span><span class="sh">"""</span>
    <span class="k">if</span> <span class="ow">not</span> <span class="nf">hasattr</span><span class="p">(</span><span class="n">terminal_notifier_installed</span><span class="p">,</span> <span class="sh">'</span><span class="s">_checked</span><span class="sh">'</span><span class="p">):</span>
        <span class="n">terminal_notifier_installed</span><span class="p">.</span><span class="n">_checked</span> <span class="o">=</span> <span class="bp">True</span>
        <span class="k">try</span><span class="p">:</span>
            <span class="n">subprocess</span><span class="p">.</span><span class="nf">run</span><span class="p">([</span><span class="sh">"</span><span class="s">which</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">terminal-notifier</span><span class="sh">"</span><span class="p">],</span>
                         <span class="n">capture_output</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">text</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">check</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
            <span class="k">return</span> <span class="bp">True</span>
        <span class="k">except</span> <span class="n">subprocess</span><span class="p">.</span><span class="n">CalledProcessError</span><span class="p">:</span>
            <span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">Error: terminal-notifier not found. Install with: brew install terminal-notifier</span><span class="sh">"</span><span class="p">)</span>
            <span class="k">return</span> <span class="bp">False</span>
    <span class="k">return</span> <span class="bp">True</span>

<span class="k">def</span> <span class="nf">os_notify</span><span class="p">():</span>
    <span class="sh">"""</span><span class="s">Sends a native macOS notification using terminal-notifier.</span><span class="sh">"""</span>
    <span class="k">if</span> <span class="ow">not</span> <span class="nf">terminal_notifier_installed</span><span class="p">():</span>
        <span class="k">return</span>
    
    <span class="n">message</span> <span class="o">=</span> <span class="sa">f</span><span class="sh">"</span><span class="s">AI response complete in project: </span><span class="si">{</span><span class="n">PROJECT_DIR_DISPLAY</span><span class="si">}</span><span class="sh">"</span>
    <span class="n">title</span> <span class="o">=</span> <span class="sh">"</span><span class="s">AI Response Complete</span><span class="sh">"</span>
    
    <span class="c1"># Use terminal-notifier for reliable notifications on macOS 15+; Claude Opus reports that 
</span>    <span class="c1"># "macOS 15+ requires explicit notification permissions for command-line tools,
</span>    <span class="c1"># and osascript's basic display notification doesn't always trigger the permission request properly."
</span>    <span class="n">process</span> <span class="o">=</span> <span class="n">subprocess</span><span class="p">.</span><span class="nf">run</span><span class="p">(</span>
        <span class="p">[</span><span class="sh">"</span><span class="s">terminal-notifier</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">-message</span><span class="sh">"</span><span class="p">,</span> <span class="n">message</span><span class="p">,</span> <span class="sh">"</span><span class="s">-title</span><span class="sh">"</span><span class="p">,</span> <span class="n">title</span><span class="p">],</span>
        <span class="n">capture_output</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">text</span><span class="o">=</span><span class="bp">True</span>
    <span class="p">)</span>
    <span class="k">if</span> <span class="n">process</span><span class="p">.</span><span class="n">returncode</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">:</span>
        <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Error sending notification: </span><span class="si">{</span><span class="n">process</span><span class="p">.</span><span class="n">stderr</span><span class="p">.</span><span class="nf">strip</span><span class="p">()</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>

<span class="c1"># --- Main Execution ---
</span><span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="sh">"</span><span class="s">__main__</span><span class="sh">"</span><span class="p">:</span>
    <span class="nf">flash</span><span class="p">()</span>
    <span class="nf">os_notify</span><span class="p">()</span>
    <span class="nf">speak</span><span class="p">()</span>
 
  </code></pre></figure>

</details>

<p><strong>📝 Note:</strong> After downloading, rename to <code class="language-plaintext highlighter-rouge">sample_notifier.py</code> and ensure it’s executable with <code class="language-plaintext highlighter-rouge">chmod +x sample_notifier.py</code>.</p>

<p>To use this script instead of just the FlashScreen:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./scripts/file_watcher.py <span class="nt">--file</span> .cursor_response_complete <span class="nt">--</span> ./scripts/sample_notifier.py
</code></pre></div></div>

<h3 id="watch-convenience-script">Watch Convenience Script</h3>

<p><code class="language-plaintext highlighter-rouge">./scripts/watch</code> is a simple shell script to run the file watcher with the notifier, for convenience:</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="c">#!/bin/bash</span>
scripts/file_watcher.py <span class="nt">-f</span> .cursor_response_complete <span class="nt">-v</span> scripts/sample_notifier.py</code></pre></figure>

<hr />

<p><a name="complete-setup-instructions"></a></p>
<h2 id="complete-setup-instructions">Complete Setup Instructions</h2>

<p>Here’s everything you need to set up the desktop image flashing system:</p>

<h3 id="prerequisites">Prerequisites</h3>

<ul>
  <li><strong>macOS</strong> (for the current implementation)</li>
  <li><strong>Python 3</strong> (usually pre-installed on macOS)</li>
  <li><strong>Cursor AI</strong> with Agent mode enabled</li>
</ul>

<h3 id="step-1-configure-cursor-ai-user-rules">Step 1: Configure Cursor AI User Rules</h3>

<ol>
  <li>Open Cursor AI</li>
  <li>Navigate to: <strong>Cursor → Settings → Cursor Settings → Rules</strong></li>
  <li>Add this User Rule (applies to all projects):</li>
</ol>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Before starting any response, delete `.cursor_response_complete` in the project root if it exists.
After completing that response, recreate (`touch`) it.
</code></pre></div></div>

<ol>
  <li><strong>Important:</strong> You must use <strong>Agent mode</strong> for this to work (other modes don’t write to the filesystem)</li>
  <li><strong>Important:</strong> Enable the AI to complete responses without asking for confirmation</li>
</ol>

<h3 id="step-2-download-and-setup-scripts">Step 2: Download and Setup Scripts</h3>

<ol>
  <li><strong>Download the scripts</strong> using the download buttons in the <a href="#implementation-code">Implementation Code</a> section above</li>
  <li><strong>Create a scripts directory</strong> in your project root:
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir </span>scripts
</code></pre></div>    </div>
  </li>
  <li><strong>Place the downloaded files</strong> in the scripts directory:
    <ul>
      <li>Rename <code class="language-plaintext highlighter-rouge">file_watcher.py.txt</code> to <code class="language-plaintext highlighter-rouge">file_watcher.py</code></li>
      <li>Rename <code class="language-plaintext highlighter-rouge">FlashScreen.txt</code> to <code class="language-plaintext highlighter-rouge">FlashScreen</code></li>
    </ul>
  </li>
  <li><strong>Make them executable:</strong>
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">chmod</span> +x scripts/file_watcher.py
<span class="nb">chmod</span> +x scripts/FlashScreen
<span class="c"># or</span>
<span class="nb">chmod</span> +x scripts/<span class="k">*</span>  <span class="c"># Make all files in the 'scripts' directory executable</span>
</code></pre></div>    </div>
  </li>
</ol>

<h3 id="step-3-setup-your-flash-image">Step 3: Setup Your Flash Image</h3>

<p>Choose one of these options:</p>

<p><strong>Option A: Use the default image path (recommended)</strong></p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Copy your image to the default location where he FlashScreen script will automatically use it</span>
<span class="nb">cp</span> ~/Pictures/my-flash-image.jpg ~/system-flash-image.jpg
</code></pre></div></div>

<p><strong>Option B: Pass a custom image path</strong></p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Pass any image file to FlashScreen directly  </span>
./scripts/file_watcher.py <span class="nt">--file</span> .cursor_response_complete <span class="nt">--</span> ./scripts/FlashScreen ~/Pictures/my-flash-image.jpg
</code></pre></div></div>

<h3 id="step-4-start-the-monitoring-script">Step 4: Start the Monitoring Script</h3>

<p>Once your scripts are in place, you can start monitoring for the signal file. Open a terminal, navigate to your project’s root directory, and choose one of the following methods.</p>

<p><strong>Option A: Basic Usage (Flash Only)</strong></p>

<p>This command watches for the signal file and triggers only the screen flash.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd</span> /path/to/your/project
./scripts/file_watcher.py <span class="nt">--file</span> .cursor_response_complete <span class="nt">--</span> ./scripts/FlashScreen
</code></pre></div></div>

<p><strong>Option B: Full Notifications (Recommended)</strong></p>

<p>This command uses the <code class="language-plaintext highlighter-rouge">sample_notifier.py</code> script to trigger all three notifications 
(flash, sound, and system notification). 
The <code class="language-plaintext highlighter-rouge">scripts/watch</code> convenience script does the same thing with verbose logging enabled.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Using the notifier script directly</span>
<span class="nb">cd</span> /path/to/your/project
./scripts/file_watcher.py <span class="nt">--file</span> .cursor_response_complete <span class="nt">--</span> ./scripts/sample_notifier.py

<span class="c"># Or using the convenience script</span>
<span class="nb">cd</span> /path/to/your/project
./scripts/watch
</code></pre></div></div>

<p>You can add the <code class="language-plaintext highlighter-rouge">--verbose</code> flag to any <code class="language-plaintext highlighter-rouge">file_watcher.py</code> command to see more detailed logging.</p>

<h3 id="step-5-test-the-setup">Step 5: Test the Setup</h3>

<ol>
  <li><strong>Test the flash script directly:</strong>
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Test with default image or white flash</span>
./scripts/FlashScreen
   
<span class="c"># Test with a specific image</span>
./scripts/FlashScreen ~/Pictures/test-image.jpg
   
<span class="c"># Test with a colored flash</span>
./scripts/FlashScreen <span class="nt">-c</span> red
</code></pre></div>    </div>
  </li>
  <li><strong>Test the complete workflow:</strong>
    <ul>
      <li>Start the monitoring script</li>
      <li>Ask Cursor AI a question in Agent mode</li>
      <li>Wait for the response to complete</li>
      <li>You should see a flash when the response finishes</li>
    </ul>
  </li>
</ol>

<h3 id="troubleshooting">Troubleshooting</h3>

<ul>
  <li><strong>Symptom: Nothing happens when a response completes.</strong>
    <ul>
      <li><strong>Cause:</strong> The signal file (<code class="language-plaintext highlighter-rouge">.cursor_response_complete</code>) is likely not being created.</li>
      <li><strong>Solution:</strong> Ensure you are using <strong>Agent Mode</strong> in Cursor AI and that your file-creation rule is correctly configured and enabled.</li>
    </ul>
  </li>
  <li><strong>Symptom: Terminal shows a “Permission denied” error.</strong>
    <ul>
      <li><strong>Cause:</strong> A script you’re trying to run lacks execute permissions.</li>
      <li><strong>Solution:</strong> Make the script executable. For example: <code class="language-plaintext highlighter-rouge">chmod +x scripts/file_watcher.py</code>.</li>
    </ul>
  </li>
  <li><strong>Symptom: Terminal shows a “command not found” error.</strong>
    <ul>
      <li><strong>Cause:</strong> A program needed by the scripts is either not installed or not in your system’s <code class="language-plaintext highlighter-rouge">PATH</code>.</li>
      <li><strong>Solution:</strong> Identify which command is missing (e.g., <code class="language-plaintext highlighter-rouge">python3</code>, <code class="language-plaintext highlighter-rouge">say</code>, <code class="language-plaintext highlighter-rouge">terminal-notifier</code>) and ensure it is installed and accessible from your terminal.</li>
    </ul>
  </li>
  <li><strong>Symptom: The screen flash is a solid color instead of your image.</strong>
    <ul>
      <li><strong>Cause:</strong> The <code class="language-plaintext highlighter-rouge">FlashScreen</code> script cannot find the image file.</li>
      <li><strong>Solution:</strong> If using the default, verify that <code class="language-plaintext highlighter-rouge">~/system-flash-image.jpg</code> exists. If passing a path directly, ensure the path is correct.</li>
    </ul>
  </li>
  <li><strong>Symptom: The <code class="language-plaintext highlighter-rouge">say</code> command and system notification appear, but the screen does not flash.</strong>
    <ul>
      <li><strong>Cause:</strong> This points to an issue specifically with the <code class="language-plaintext highlighter-rouge">FlashScreen</code> script.</li>
      <li><strong>Solution:</strong>
        <ol>
          <li>Confirm that <code class="language-plaintext highlighter-rouge">scripts/FlashScreen</code> exists and is executable.</li>
          <li>Run <code class="language-plaintext highlighter-rouge">./scripts/FlashScreen</code> directly from your terminal to see if it produces any errors on its own.</li>
        </ol>
      </li>
    </ul>
  </li>
</ul>

<h3 id="advanced-options">Advanced Options</h3>

<ul>
  <li><strong>Run from any directory:</strong> Use absolute paths in the script configuration</li>
  <li><strong>Different flash duration:</strong> Modify the Swift script’s timer values</li>
  <li><strong>Cross-platform:</strong> Create platform-specific flash scripts for Windows/Linux to replace <code class="language-plaintext highlighter-rouge">scripts/FlashScreen</code>, <code class="language-plaintext highlighter-rouge">say</code>, and <code class="language-plaintext highlighter-rouge">terminal-notifier</code></li>
  <li><strong>Multiple projects:</strong> Place scripts in a system-wide location and use absolute paths</li>
  <li><strong>Customize scripts:</strong> View and download the source code in the <a href="#implementation-code">Implementation Code</a> section above</li>
</ul>

<hr />

<h2 id="about-the-author">About the Author</h2>

<p>This solution was developed by <strong>Keith Bennett</strong> of <a href="https://www.bbs-software.com">Bennett Business Solutions, Inc.</a> 
Keith is an experienced software engineer and consultant specializing in automation, development tooling, 
and creative technical solutions. He is currently open to work and available for employment and consulting engagements.
He can be reached at <a href="mailto:kbennett@bbs-software.com">kbennett@bbs-software.com</a> or other methods
listed in the contact section at <a href="https://www.bbs-software.com">the website mentioned above</a>.</p>]]></content><author><name></name></author><category term="blog" /><summary type="html"><![CDATA[]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.bbs-software.com/assets/images/bbs-logo-og.png" /><media:content medium="image" url="https://blog.bbs-software.com/assets/images/bbs-logo-og.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Synchronous, Thread, and Fiber HTTP Requests in Ruby</title><link href="https://blog.bbs-software.com/blog/2024/09/20/synchronous-threaded-fiber-http-requests-in-ruby/" rel="alternate" type="text/html" title="Synchronous, Thread, and Fiber HTTP Requests in Ruby" /><published>2024-09-20T00:00:00+00:00</published><updated>2024-09-20T00:00:00+00:00</updated><id>https://blog.bbs-software.com/blog/2024/09/20/synchronous-threaded-fiber-http-requests-in-ruby</id><content type="html" xml:base="https://blog.bbs-software.com/blog/2024/09/20/synchronous-threaded-fiber-http-requests-in-ruby/"><![CDATA[<h2 id="introduction">Introduction</h2>

<p>In this article, we will explore different ways to make HTTP requests in Ruby and compare their performance. We will focus on three approaches: synchronous, threaded, and fiber-based. We will use a simple example of checking the availability of the same URL multiple times. Our tests will measure sending 1 to 256 requests using the different approaches.</p>

<p>Why do I care? I have a Ruby on Rails web site containing many links to YouTube song videos, and they are subject to being pulled by their owner or taken down due to copyright issues. I wanted to automate the checking of these links for availability as a rake task, so that I can run the check with minimal effort from time to time. The links are stored in the project in a YAML file, so it’s easy to read them into memory. However, how would I check them for accessibility?</p>

<p>To simplify the examples below, instead of fetching the real URL’s, I will fetch the same URL multiple times. I’ll use a URL built with <code class="language-plaintext highlighter-rouge">"https://httpbin.org/delay/#{sleep_seconds}"</code> to access the Internet and simulate the response delay with a sleep on the server. The examples will return an array containing the responses.</p>

<h3 id="the-synchronous-approach">The Synchronous Approach</h3>

<p>I started out the simple and conventional way, using <code class="language-plaintext highlighter-rouge">Net::HTTP</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">get_responses_synchrously</span><span class="p">(</span><span class="n">count</span><span class="p">)</span>
  <span class="n">logger</span><span class="p">.</span><span class="nf">debug</span><span class="p">(</span><span class="s2">"Getting </span><span class="si">#{</span><span class="n">count</span><span class="si">}</span><span class="s2"> responses synchronously"</span><span class="p">)</span>
  <span class="n">count</span><span class="p">.</span><span class="nf">times</span><span class="p">.</span><span class="nf">with_object</span><span class="p">([])</span> <span class="k">do</span> <span class="o">|</span><span class="n">_n</span><span class="p">,</span> <span class="n">responses</span><span class="o">|</span>
    <span class="n">responses</span> <span class="o">&lt;&lt;</span> <span class="no">Net</span><span class="o">::</span><span class="no">HTTP</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="no">URI</span><span class="p">(</span><span class="n">url</span><span class="p">))</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>However, the time it took was frustratingly long. How could I make this faster?</p>

<h3 id="the-thread-approach">The Thread Approach</h3>

<p>Having been a fan of threads for a long time, this was the next thing I tried. Since the number of links was just a few dozen, this number was low enough that creating a thread for each link was feasible. Note that we still use <code class="language-plaintext highlighter-rouge">Net::HTTP.get</code>, but each call runs in its own thread. Fortunately, <code class="language-plaintext highlighter-rouge">Net::HTTP.get</code> supports threaded use by yielding its thread’s control after sending the request, thereby avoiding CPU time waste while waiting for the response:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">get_responses_using_threads</span><span class="p">(</span><span class="n">count</span><span class="p">)</span>
  <span class="n">logger</span><span class="p">.</span><span class="nf">debug</span><span class="p">(</span><span class="s2">"Getting </span><span class="si">#{</span><span class="n">count</span><span class="si">}</span><span class="s2"> responses using threads"</span><span class="p">)</span>
  <span class="n">threads</span> <span class="o">=</span> <span class="no">Array</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">count</span><span class="p">)</span> <span class="k">do</span>
    <span class="no">Thread</span><span class="p">.</span><span class="nf">new</span> <span class="p">{</span> <span class="no">Net</span><span class="o">::</span><span class="no">HTTP</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="no">URI</span><span class="p">(</span><span class="n">url</span><span class="p">))</span> <span class="p">}</span>
  <span class="k">end</span>
  <span class="n">threads</span><span class="p">.</span><span class="nf">map</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:value</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<p>As you can guess, this was <em>way</em> faster.</p>

<h3 id="the-fiber-approach">The Fiber Approach</h3>

<p>I wasn’t done though – recently I <em>finally</em> got around to learning about Ruby fibers, and wanted to try them here. Rather than write the low level Fiber code myself, it was simpler to use @ioquatix’s (Samuel Williams’) excellent <code class="language-plaintext highlighter-rouge">async</code> Ruby gems (I needed <code class="language-plaintext highlighter-rouge">async</code> and <code class="language-plaintext highlighter-rouge">async-http</code>) to handle the low level plumbing. The resulting code was more complex than the previous two approaches, but not too bad (run <code class="language-plaintext highlighter-rouge">gem install async-http</code> if necessary):</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">get_responses_using_fibers</span><span class="p">(</span><span class="n">count</span><span class="p">)</span>
  <span class="n">logger</span><span class="p">.</span><span class="nf">debug</span><span class="p">(</span><span class="s2">"Getting </span><span class="si">#{</span><span class="n">count</span><span class="si">}</span><span class="s2"> responses using fibers"</span><span class="p">)</span>
  <span class="n">responses</span> <span class="o">=</span> <span class="p">[]</span>
  <span class="no">Async</span> <span class="k">do</span>
    <span class="k">begin</span>
      <span class="n">internet</span> <span class="o">=</span> <span class="no">Async</span><span class="o">::</span><span class="n">https</span><span class="o">::</span><span class="no">Internet</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">connection_limit: </span><span class="n">count</span><span class="p">)</span>
      <span class="n">count</span><span class="p">.</span><span class="nf">times</span> <span class="k">do</span>
        <span class="no">Async</span> <span class="k">do</span>
          <span class="k">begin</span>
            <span class="n">response</span> <span class="o">=</span> <span class="n">internet</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
            <span class="n">responses</span> <span class="o">&lt;&lt;</span> <span class="n">response</span>
          <span class="k">ensure</span>
            <span class="n">response</span><span class="o">&amp;</span><span class="p">.</span><span class="nf">finish</span>
          <span class="k">end</span>
        <span class="k">end</span>
      <span class="k">end</span>
    <span class="k">ensure</span>
      <span class="n">internet</span><span class="o">&amp;</span><span class="p">.</span><span class="nf">close</span>
    <span class="k">end</span>
  <span class="k">end</span><span class="p">.</span><span class="nf">wait</span>
  <span class="n">responses</span>
<span class="k">end</span>
</code></pre></div></div>

<p>The magic is in the <code class="language-plaintext highlighter-rouge">Async</code> framework and <code class="language-plaintext highlighter-rouge">Async::https::Internet</code>’s <code class="language-plaintext highlighter-rouge">get</code> method, which is fiber-aware and yields control of the CPU while waiting for a response.</p>

<h3 id="comparing-the-performance-results">Comparing the Performance Results</h3>

<p>I did a benchmark comparing the results of fetching request counts for powers of 2 ranging from 1 to 256 (1, 2, 4, 8, 16, 32, 64, 128, and 256).</p>

<p>Here are the results comparing all three approaches, using the averages of several test runs:</p>

<p><img src="/assets/requests-article-fibers-threads-synchronous-graph.png" alt="Synchronous, Threaded, and Fiber-based" /></p>

<p>As expected, the synchronous approach was by far the slowest, since only one request could be active at any given time. Both the thread and fiber approach were dramatically faster, and not that different from each other. To zoom in on the difference between the thread and fiber approach, this graph omits the synchronous approach:</p>

<p><img src="/assets/requests-article-fibers-threads-graph.png" alt="Threaded and Fiber-based" /></p>

<p>What do we make of this though? Which should we choose to use?</p>

<h3 id="fibers-vs-threads">Fibers vs. Threads</h3>

<p>If one knows that the numbers will always fall within the bounds of 1 to 256, then it probably doesn’t much matter which to use. However, if there is a possibility of higher request counts, then fibers make more sense. Here are some ways in which threads and fibers differ:</p>

<p><strong>Threads:</strong></p>

<ul>
  <li><strong>OS-Mapped:</strong> In Ruby versions 1.9 and later, Ruby threads are mapped to operating system threads. Each thread has its own dedicated stack and other resources allocated by the OS.</li>
  <li><strong>Context Switching Overhead:</strong> Switching between threads involves saving and restoring the entire execution context (registers, stack pointers, etc.), which is a relatively expensive operation.</li>
  <li><strong>System Limits:</strong> The operating system typically imposes limits on the number of threads a process can create due to resource constraints.</li>
</ul>

<p><strong>Fibers:</strong></p>

<ul>
  <li><strong>User-Level:</strong> Fibers are a user-level construct managed entirely within the Ruby interpreter. They share the same stack and other resources with the thread they’re running in.</li>
  <li><strong>Cooperative Scheduling:</strong> Fibers explicitly yield control to each other, making context switching much faster and less resource-intensive.</li>
  <li><strong>Lightweight:</strong> Due to their cooperative nature and shared resources, fibers have a much smaller memory footprint than threads.</li>
</ul>

<h3 id="operating-system-open-file-handle-limits">Operating System Open File Handle Limits</h3>

<p>A file handle (aka “file descriptor”) is an object (typically a small non-negative integer) stored in a variable on the OS level used to refer to a resource, which can be a file, pipe, socket, or terminal. The OS restricts the number of file handles that can be used by a given process. In our testing, we need to stay within or increase that limit.</p>

<p>In general, the synchronous approach results in only one file handle being used at a time for all the requests. In contrast, the thread and fiber approaches may theoretically use file handles for all the requests at the same time, since they do not wait for one to finish to start another.</p>

<p>Even with as few as 256 simultaneous requests, the operating system session’s file handle limit may be exceeded. If you get an error saying that all the process’ file handles have been used, in Linux and Mac OS you can use <code class="language-plaintext highlighter-rouge">ulimit</code> to increase the maximum file handle count (used for both files and network sockets) for the terminal session, and then rerun the program. For example: <code class="language-plaintext highlighter-rouge">ulimit -n 2048 &amp;&amp; my-program</code>. However, <code class="language-plaintext highlighter-rouge">ulimit</code> will only do this successfully if the systemwide maximum file count is large enough to accommodate it.</p>

<p>Threads are far more heavyweight than fibers, so for large request counts, one would need to implement some kind of thread pooling, and this would probably result in far fewer requests per second than fibers. Sam Williams posted a YouTube video (<a href="https://www.youtube.com/watch?v=Dtn9Uudw4Mo">RubyConf Taiwan 2019 - The Journey to One Million by Samuel Williams - YouTube</a>) in which he showed one million fibers running network requests!</p>

<h3 id="jruby">JRuby</h3>

<p>No discussion of Ruby concurrency is complete without a reminder that even with multiple threads, C Ruby’s Global Interpreter Lock (aka “the GIL”) guarantees that only one CPU can be used at a time. In contrast, JRuby (Ruby running on the Java Virtual Machine), threads <em>do</em> run truly concurrently, on multiple CPU’s. This can make threading in JRuby much more performant.</p>

<p>That said, these requests are not making heavy use of the CPU, and JRuby threads (really, Java threads) are still far more heavyweight than fibers, so even with JRuby fibers may be the better choice.</p>

<h3 id="large-request-counts">Large Request Counts</h3>

<p>This article covered small numbers of requests, but what if you need to make thousands or millions of requests? In that case,</p>

<ul>
  <li>
    <p><strong>Thread Pooling:</strong> If you are using threads, you may need to implement a thread pool to limit the number of threads created and manage the requests.</p>
  </li>
  <li>
    <p><strong>Fiber Pooling:</strong> If you are using fibers, you may need to implement a fiber pool to limit the number of fibers created and manage the requests.</p>
  </li>
  <li>
    <p><strong>Async Gems:</strong> If you are using fibers, you may want to consider using the <code class="language-plaintext highlighter-rouge">async</code> gems to handle the low-level plumbing for you. This will make your code simpler and more maintainable, but will add a dependency to your project.</p>
  </li>
  <li>
    <p><strong>Operating System Limits:</strong> Be aware of the operating system’s file handle limits and ensure that you stay within them.</p>
  </li>
  <li>
    <p><strong>JRuby:</strong> If you are using JRuby, you may be able to take advantage of true concurrency with threads.</p>
  </li>
  <li>
    <p><strong>Performance Testing:</strong> Thoroughly test your code with the expected number of requests to ensure that it performs as expected.</p>
  </li>
  <li>
    <p><strong>Error Handling:</strong> Ensure that your code handles errors gracefully and does not crash when an error occurs.</p>
  </li>
  <li>
    <p><strong>Logging:</strong> Add logging to your code to help you diagnose issues and monitor performance.</p>
  </li>
  <li>
    <p><strong>Code Complexity:</strong> Consider the complexity of your code and choose the approach that is simplest and easiest to maintain.</p>
  </li>
  <li>
    <p><strong>Code Review:</strong> Have your code reviewed by a colleague to ensure that it is correct and follows best practices.</p>
  </li>
  <li>
    <p><strong>Documentation:</strong> Document your code to make it easier for others to understand and maintain.</p>
  </li>
  <li>
    <p><strong>Testing:</strong> Write tests for your code to ensure that it works as expected and to catch any regressions.</p>
  </li>
  <li>
    <p><strong>Performance Monitoring:</strong> Monitor the performance of your code in production to identify any bottlenecks and optimize as needed.</p>
  </li>
</ul>

<h3 id="conclusion">Conclusion</h3>

<p>Which approach to use depends on a number of factors:</p>

<ul>
  <li>What will be the <em>average</em> request count?</li>
  <li>What will be the <em>maximum</em> request count?</li>
  <li>How often will this be used?</li>
  <li>How important is faster completion?</li>
  <li>Do I want or need to avoid the additional dependency of the async gems?</li>
  <li>How important is code simplicity?</li>
</ul>

<p>Thorough research may be necessary to determine the very best approach for any given situation, but here is one policy that balances performance and simplicity:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: center">Request Count</th>
      <th style="text-align: center">Approach</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: center">n &lt;= 3</td>
      <td style="text-align: center">Synchronous</td>
    </tr>
    <tr>
      <td style="text-align: center">4 &lt;= n &lt;= 16</td>
      <td style="text-align: center">Threaded</td>
    </tr>
    <tr>
      <td style="text-align: center">n &gt; 16</td>
      <td style="text-align: center">Fiber</td>
    </tr>
  </tbody>
</table>

<h3 id="addendum">Addendum</h3>

<p>The complete Ruby program used to measure request performance can be found at <a href="https://gist.github.com/keithrbennett/719af73894458a4378aa3e3a5cc9b70a">https://gist.github.com/keithrbennett/719af73894458a4378aa3e3a5cc9b70a</a> and is also pasted here:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">#!/usr/bin/env ruby</span>
<span class="c1"># frozen_string_literal: true</span>

<span class="c1"># IMPORTANT: You may need to increase the number of available file handles available to this process.</span>
<span class="c1"># The number should be greater than the maximum number of requests you want to make, because each process</span>
<span class="c1"># opens at least 3 file handles (stdin, stdout, stderr), and other files may be opened during the program's</span>
<span class="c1"># run (e.g. for the logger).</span>
<span class="c1"># ulimit -n 300 &amp;&amp; scripts/compare_request_methods.rb</span>

<span class="nb">require</span> <span class="s1">'async/http/internet'</span> <span class="c1"># gem install async-http if necessary</span>
<span class="nb">require</span> <span class="s1">'awesome_print'</span>
<span class="nb">require</span> <span class="s1">'benchmark'</span>
<span class="nb">require</span> <span class="s1">'json'</span>
<span class="nb">require</span> <span class="s1">'logger'</span>
<span class="nb">require</span> <span class="s1">'net/http'</span>
<span class="nb">require</span> <span class="s1">'pry'</span>
<span class="nb">require</span> <span class="s1">'yaml'</span>

<span class="c1"># These are the external gems that must be installed for the program to run.</span>
<span class="no">REQUIRED_EXTERNAL_GEMS</span> <span class="o">=</span> <span class="sx">%w[async-http awesome_print pry]</span><span class="p">.</span><span class="nf">freeze</span>

<span class="no">Thread</span><span class="p">.</span><span class="nf">abort_on_exception</span> <span class="o">=</span> <span class="kp">true</span>
<span class="no">Thread</span><span class="p">.</span><span class="nf">report_on_exception</span> <span class="o">=</span> <span class="kp">true</span>

<span class="k">class</span> <span class="nc">Benchmarker</span>
  <span class="nb">attr_reader</span> <span class="ss">:logger</span><span class="p">,</span> <span class="ss">:request_count_per_run</span><span class="p">,</span> <span class="ss">:sleep_seconds</span><span class="p">,</span> <span class="ss">:url</span>

  <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">request_count_per_run</span><span class="p">,</span> <span class="n">sleep_seconds</span><span class="p">,</span> <span class="n">logger</span><span class="p">)</span>
    <span class="vi">@request_count_per_run</span> <span class="o">=</span> <span class="n">request_count_per_run</span>
    <span class="vi">@sleep_seconds</span> <span class="o">=</span> <span class="n">sleep_seconds</span>
    <span class="vi">@logger</span> <span class="o">=</span> <span class="n">logger</span>
    <span class="vi">@url</span> <span class="o">=</span> <span class="s2">"https://httpbin.org/delay/</span><span class="si">#{</span><span class="n">sleep_seconds</span><span class="si">}</span><span class="s2">"</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">get_responses_synchrously</span><span class="p">(</span><span class="n">count</span><span class="p">)</span>
    <span class="n">logger</span><span class="p">.</span><span class="nf">debug</span><span class="p">(</span><span class="s2">"Getting </span><span class="si">#{</span><span class="n">count</span><span class="si">}</span><span class="s2"> responses synchronously"</span><span class="p">)</span>
    <span class="n">count</span><span class="p">.</span><span class="nf">times</span><span class="p">.</span><span class="nf">with_object</span><span class="p">([])</span> <span class="k">do</span> <span class="o">|</span><span class="n">_n</span><span class="p">,</span> <span class="n">responses</span><span class="o">|</span>
      <span class="n">responses</span> <span class="o">&lt;&lt;</span> <span class="no">Net</span><span class="o">::</span><span class="no">HTTP</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="no">URI</span><span class="p">(</span><span class="n">url</span><span class="p">))</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">get_responses_using_threads</span><span class="p">(</span><span class="n">count</span><span class="p">)</span>
    <span class="n">logger</span><span class="p">.</span><span class="nf">debug</span><span class="p">(</span><span class="s2">"Getting </span><span class="si">#{</span><span class="n">count</span><span class="si">}</span><span class="s2"> responses using threads"</span><span class="p">)</span>
    <span class="n">threads</span> <span class="o">=</span> <span class="no">Array</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">count</span><span class="p">)</span> <span class="k">do</span>
      <span class="no">Thread</span><span class="p">.</span><span class="nf">new</span> <span class="p">{</span> <span class="no">Net</span><span class="o">::</span><span class="no">HTTP</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="no">URI</span><span class="p">(</span><span class="n">url</span><span class="p">))</span> <span class="p">}</span>
    <span class="k">end</span>
    <span class="n">threads</span><span class="p">.</span><span class="nf">map</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:value</span><span class="p">)</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">get_responses_using_fibers</span><span class="p">(</span><span class="n">count</span><span class="p">)</span>
    <span class="n">logger</span><span class="p">.</span><span class="nf">debug</span><span class="p">(</span><span class="s2">"Getting </span><span class="si">#{</span><span class="n">count</span><span class="si">}</span><span class="s2"> responses using fibers"</span><span class="p">)</span>
    <span class="n">responses</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="no">Async</span> <span class="k">do</span>
      <span class="k">begin</span>
        <span class="n">internet</span> <span class="o">=</span> <span class="no">Async</span><span class="o">::</span><span class="n">https</span><span class="o">::</span><span class="no">Internet</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">connection_limit: </span><span class="n">count</span><span class="p">)</span>
        <span class="n">count</span><span class="p">.</span><span class="nf">times</span> <span class="k">do</span>
          <span class="no">Async</span> <span class="k">do</span>
            <span class="k">begin</span>
              <span class="n">response</span> <span class="o">=</span> <span class="n">internet</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
              <span class="n">responses</span> <span class="o">&lt;&lt;</span> <span class="n">response</span>
            <span class="k">ensure</span>
              <span class="n">response</span><span class="o">&amp;</span><span class="p">.</span><span class="nf">finish</span>
            <span class="k">end</span>
          <span class="k">end</span>
        <span class="k">end</span>
      <span class="k">ensure</span>
        <span class="n">internet</span><span class="o">&amp;</span><span class="p">.</span><span class="nf">close</span>
      <span class="k">end</span>
    <span class="k">end</span><span class="p">.</span><span class="nf">wait</span>
    <span class="n">responses</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">call</span><span class="p">(</span><span class="n">request_count_per_run</span><span class="p">,</span> <span class="n">sleep_seconds</span><span class="p">,</span> <span class="n">logger</span><span class="p">)</span>
    <span class="nb">self</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">request_count_per_run</span><span class="p">,</span> <span class="n">sleep_seconds</span><span class="p">,</span> <span class="n">logger</span><span class="p">).</span><span class="nf">call</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">output_results</span><span class="p">(</span><span class="n">results</span><span class="p">)</span>
    <span class="n">logger</span><span class="p">.</span><span class="nf">info</span><span class="p">(</span><span class="s1">'-'</span> <span class="o">*</span> <span class="mi">60</span><span class="p">)</span>
    <span class="n">logger</span><span class="p">.</span><span class="nf">info</span><span class="p">(</span><span class="n">results</span><span class="p">.</span><span class="nf">to_json</span><span class="p">)</span>
    <span class="n">ap</span><span class="p">(</span><span class="n">results</span><span class="p">)</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">call</span>
    <span class="n">logger</span><span class="p">.</span><span class="nf">info</span><span class="p">(</span><span class="s2">"Starting run with </span><span class="si">#{</span><span class="n">request_count_per_run</span><span class="si">}</span><span class="s2"> requests each sleeping </span><span class="si">#{</span><span class="n">sleep_seconds</span><span class="si">}</span><span class="s2"> seconds"</span><span class="p">)</span>
    <span class="n">results</span> <span class="o">=</span> <span class="p">{</span>
      <span class="ss">time:        </span><span class="no">Time</span><span class="p">.</span><span class="nf">new</span><span class="p">.</span><span class="nf">utc</span><span class="p">,</span>
      <span class="ss">sleep:       </span><span class="n">sleep_seconds</span><span class="p">,</span>
      <span class="ss">count:       </span><span class="n">request_count_per_run</span><span class="p">,</span>
      <span class="ss">fibers:      </span><span class="no">Benchmark</span><span class="p">.</span><span class="nf">measure</span> <span class="p">{</span> <span class="n">get_responses_using_fibers</span><span class="p">(</span><span class="n">request_count_per_run</span><span class="p">)</span> <span class="p">}.</span><span class="nf">real</span><span class="p">,</span>
      <span class="ss">threads:     </span><span class="no">Benchmark</span><span class="p">.</span><span class="nf">measure</span> <span class="p">{</span> <span class="n">get_responses_using_threads</span><span class="p">(</span><span class="n">request_count_per_run</span><span class="p">)</span> <span class="p">}.</span><span class="nf">real</span><span class="p">,</span>
      <span class="ss">synchronous: </span><span class="no">Benchmark</span><span class="p">.</span><span class="nf">measure</span> <span class="p">{</span> <span class="n">get_responses_synchrously</span><span class="p">(</span><span class="n">request_count_per_run</span><span class="p">)</span> <span class="p">}.</span><span class="nf">real</span><span class="p">,</span>
    <span class="p">}</span>
    <span class="n">output_results</span><span class="p">(</span><span class="n">results</span><span class="p">)</span>
    <span class="n">results</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="k">class</span> <span class="nc">Runner</span>
  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">call</span><span class="p">()</span> <span class="o">=</span> <span class="n">new</span><span class="p">.</span><span class="nf">call</span>

  <span class="k">def</span> <span class="nf">setup_logger</span>
    <span class="n">logger</span> <span class="o">=</span> <span class="no">Logger</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="s1">'compare_request_methods.log'</span><span class="p">)</span>
    <span class="n">logger</span><span class="p">.</span><span class="nf">level</span> <span class="o">=</span> <span class="no">Logger</span><span class="o">::</span><span class="no">INFO</span>

    <span class="n">logger</span><span class="p">.</span><span class="nf">info</span><span class="p">(</span><span class="s1">'='</span> <span class="o">*</span> <span class="mi">60</span><span class="p">)</span>
    <span class="n">logger</span>
  <span class="k">end</span>

  <span class="c1"># Measure the time it takes to run a block of code</span>
  <span class="c1"># @return [Array] The return value of the block and the duration in seconds</span>
  <span class="k">def</span> <span class="nf">time_it</span>
    <span class="n">start_time</span> <span class="o">=</span> <span class="no">Process</span><span class="p">.</span><span class="nf">clock_gettime</span><span class="p">(</span><span class="no">Process</span><span class="o">::</span><span class="no">CLOCK_MONOTONIC</span><span class="p">)</span>
    <span class="n">return_value</span> <span class="o">=</span> <span class="k">yield</span>
    <span class="n">end_time</span> <span class="o">=</span> <span class="no">Process</span><span class="p">.</span><span class="nf">clock_gettime</span><span class="p">(</span><span class="no">Process</span><span class="o">::</span><span class="no">CLOCK_MONOTONIC</span><span class="p">)</span>
    <span class="n">duration_in_seconds</span> <span class="o">=</span> <span class="n">end_time</span> <span class="o">-</span> <span class="n">start_time</span>
    <span class="p">[</span><span class="n">return_value</span><span class="p">,</span> <span class="n">duration_in_seconds</span><span class="p">]</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">write_results</span><span class="p">(</span><span class="n">logger</span><span class="p">,</span> <span class="n">results</span><span class="p">,</span> <span class="n">duration_secs</span><span class="p">)</span>
    <span class="n">timestamp</span> <span class="o">=</span> <span class="no">Time</span><span class="p">.</span><span class="nf">now</span><span class="p">.</span><span class="nf">utc</span><span class="p">.</span><span class="nf">strftime</span><span class="p">(</span><span class="s1">'%Y-%m-%d-%H-%M-%S'</span><span class="p">)</span>
    <span class="no">File</span><span class="p">.</span><span class="nf">write</span><span class="p">(</span><span class="s2">"</span><span class="si">#{</span><span class="n">timestamp</span><span class="si">}</span><span class="s2">-results.yaml"</span><span class="p">,</span> <span class="n">results</span><span class="p">.</span><span class="nf">to_yaml</span><span class="p">)</span>
    <span class="n">logger</span><span class="p">.</span><span class="nf">info</span><span class="p">(</span><span class="n">results</span><span class="p">.</span><span class="nf">to_json</span><span class="p">)</span>
    <span class="nb">puts</span><span class="p">(</span><span class="s2">"Done. Entire suite took </span><span class="si">#{</span><span class="n">duration_secs</span><span class="p">.</span><span class="nf">round</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span><span class="si">}</span><span class="s2"> seconds."</span><span class="p">)</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">call</span>
    <span class="n">counts</span> <span class="o">=</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">8</span><span class="p">,</span> <span class="mi">16</span><span class="p">,</span> <span class="mi">32</span><span class="p">,</span> <span class="mi">64</span><span class="p">,</span> <span class="mi">128</span><span class="p">,</span> <span class="mi">256</span><span class="p">]</span>
    <span class="n">logger</span> <span class="o">=</span> <span class="n">setup_logger</span>
    <span class="nb">puts</span> <span class="s2">"Starting run with counts: </span><span class="si">#{</span><span class="n">counts</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="s1">', '</span><span class="p">)</span><span class="si">}</span><span class="s2">"</span>

    <span class="n">results</span><span class="p">,</span> <span class="n">duration_secs</span> <span class="o">=</span> <span class="n">time_it</span> <span class="k">do</span>
      <span class="n">counts</span><span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="o">|</span><span class="n">count</span><span class="o">|</span> <span class="no">Benchmarker</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="n">count</span><span class="p">,</span> <span class="mf">0.0001</span><span class="p">,</span> <span class="n">logger</span><span class="p">)</span> <span class="p">}</span>
    <span class="k">end</span>

    <span class="n">write_results</span><span class="p">(</span><span class="n">logger</span><span class="p">,</span> <span class="n">results</span><span class="p">,</span> <span class="n">duration_secs</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="k">class</span> <span class="nc">GemChecker</span>
  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">call</span><span class="p">(</span><span class="n">required_external_gems</span><span class="p">)</span> <span class="o">=</span> <span class="n">new</span><span class="p">.</span><span class="nf">ensure_gems_available</span><span class="p">(</span><span class="n">required_external_gems</span><span class="p">)</span>

  <span class="k">def</span> <span class="nf">gem_exists?</span><span class="p">(</span><span class="n">gem_name</span><span class="p">)</span>
    <span class="k">begin</span>
      <span class="n">gem</span><span class="p">(</span><span class="n">gem_name</span><span class="p">)</span>
      <span class="kp">true</span>
    <span class="k">rescue</span> <span class="no">Gem</span><span class="o">::</span><span class="no">MissingSpecError</span>
      <span class="kp">false</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">find_missing_gems</span><span class="p">(</span><span class="n">required_gems</span><span class="p">)</span>
    <span class="n">required_gems</span><span class="p">.</span><span class="nf">reject</span> <span class="p">{</span> <span class="o">|</span><span class="nb">name</span><span class="o">|</span> <span class="n">gem_exists?</span><span class="p">(</span><span class="nb">name</span><span class="p">)</span> <span class="p">}</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">ensure_gems_available</span><span class="p">(</span><span class="n">required_external_gems</span><span class="p">)</span>
    <span class="n">missing_gems</span> <span class="o">=</span> <span class="n">find_missing_gems</span><span class="p">(</span><span class="n">required_external_gems</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">missing_gems</span><span class="p">.</span><span class="nf">any?</span>
      <span class="nb">puts</span> <span class="s2">"Need to install missing gems: </span><span class="si">#{</span><span class="n">missing_gems</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="s1">', '</span><span class="p">)</span><span class="si">}</span><span class="s2">"</span>
      <span class="nb">exit</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">)</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="no">GemChecker</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="no">REQUIRED_EXTERNAL_GEMS</span><span class="p">)</span>
<span class="no">Runner</span><span class="p">.</span><span class="nf">call</span>
</code></pre></div></div>]]></content><author><name></name></author><category term="blog" /><summary type="html"><![CDATA[Introduction]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.bbs-software.com/assets/images/bbs-logo-og.png" /><media:content medium="image" url="https://blog.bbs-software.com/assets/images/bbs-logo-og.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">AutoPilot, CoPilot, or AI Assistant?</title><link href="https://blog.bbs-software.com/blog/2024/01/30/autopilot-copilot-or-ai-assistant/" rel="alternate" type="text/html" title="AutoPilot, CoPilot, or AI Assistant?" /><published>2024-01-30T00:00:00+00:00</published><updated>2024-01-30T00:00:00+00:00</updated><id>https://blog.bbs-software.com/blog/2024/01/30/autopilot-copilot-or-ai-assistant</id><content type="html" xml:base="https://blog.bbs-software.com/blog/2024/01/30/autopilot-copilot-or-ai-assistant/"><![CDATA[<h3 id="ai-downsides">AI Downsides</h3>

<p>In <a href="https://visualstudiomagazine.com/articles/2024/01/25/copilot-research.aspx">New GitHub Copilot Research Finds ‘Downward Pressure on Code Quality’</a>, Visual Studio magazine cites a <a href="https://www.gitclear.com/coding_on_copilot_data_shows_ais_downward_pressure_on_code_quality">GitClear study</a> that points out that AI for coding results in less maintainable (and therefore costlier) code.</p>

<p>They point out, for example, that copying and pasting code in multiple locations where it is needed, rather than moving it to a single location and referring to it there, is prone to producing repetitive code, that is, code that is not <a href="https://en.wikipedia.org/wiki/Don't_repeat_yourself">DRY</a>.</p>

<p>In addition, at the time of this writing (January 2024), AI still makes <em>plenty</em> of mistakes; that is, a) misunderstandings of intent, b) hallucinations, and c) downright errors.</p>

<h3 id="resisting-oversimplification">Resisting Oversimplification</h3>

<p>While the study describes in great detail the pitfalls it found in AI-assisted coding, we must not succumb to the oversimplification so prevalent in these times and generalize that “AI is 100% bad”. We need to realize that the positive or negative value of AI depends on this crucial nuance: the extent to which the developer <em>relies on</em> and <em>defers to</em> AI, rather than <em>supervising</em> it with a critical eye.</p>

<p>The name “Copilot” is apt. It must not be “Autopilot”. However, even “Copilot” may not sufficiently express the subservient position that AI should take; “AI Assistant” would probably be more accurate, and is exactly <a href="https://www.jetbrains.com/help/idea/ai-assistant.html">the name used by JetBrains</a>, the company that produces outstanding IDE’s.</p>

<h3 id="the-optimum-balance">The Optimum Balance</h3>

<p>An organization that mandates excessive reliance on AI to quickly reduce labor costs will have a rude awakening when, over time, the poor code quality reveals itself in the form of reduced quality and increased cost.</p>

<p>On the other hand, an organization that completely ignores the immense productivity boost that coding AI offers will “leave money on the table” and fall behind the competition.</p>

<h3 id="organizational-responsibility">Organizational Responsibility</h3>

<p>To quote my favorite paragraph in the study:</p>

<p>“If there isn’t a CTO or VP of Engineering who actively schedules time to reduce tech
debt, you can add “executive-driven time pressures” to the list of reasons that newly
added copy/paste code will never be consolidated into the component libraries that
underpin long-term development velocity.”</p>

<p>In other words, resisting the temptation to build working code quickly in the short term at the expense of the medium and long term requires will power and effort, plus initiative or at minimum support from the highest levels of the organization. Those organizations that are not able or willing to delay gratification will suffer the consequences.</p>

<p>Investing in code quality pays off well, but does have an initial cost. In the case of AI, this will be especially important.</p>

<h3 id="conclusion">Conclusion</h3>

<p>The bottom line is that we still need expert software developers to know how to use the tool, appreciating its knowledge and assistance, but taking the lead to verify and integrate it correctly. The AI landscape is changing at astonishing speed. Organizations and their developers need to reevaluate tools and policies frequently to ensure they are making optimal use of this game changing technology.</p>]]></content><author><name></name></author><category term="blog" /><category term="ai," /><category term="copilot," /><category term="github," /><category term="quality" /><summary type="html"><![CDATA[AI Downsides]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.bbs-software.com/assets/images/bbs-logo-og.png" /><media:content medium="image" url="https://blog.bbs-software.com/assets/images/bbs-logo-og.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Installing International Language Keyboard Input Methods on Linux Mint 20 Cinnamon &amp;amp; Mate</title><link href="https://blog.bbs-software.com/blog/2020/08/23/language-input-on-linux-mint-20/" rel="alternate" type="text/html" title="Installing International Language Keyboard Input Methods on Linux Mint 20 Cinnamon &amp;amp; Mate" /><published>2020-08-23T00:00:00+00:00</published><updated>2020-08-23T00:00:00+00:00</updated><id>https://blog.bbs-software.com/blog/2020/08/23/language-input-on-linux-mint-20</id><content type="html" xml:base="https://blog.bbs-software.com/blog/2020/08/23/language-input-on-linux-mint-20/"><![CDATA[<p>I just started using a desktop system using Linux Mint 20 Cinnamon, and wanted to make sure that keyboard input methods worked for Thai and Korean. This took me down a long road, consulting several blog articles, questions, and answers, so I decided to document it to save others (and future me!) this effort.</p>

<p>I’ve also briefly tested that this procedure works with Arabic, French, Hebrew, Russian, and Swedish, so I’m fairly confident that it will work for any supported language and input method.</p>

<p>Below are the instructions for Linux Mint Cinnamon; I believe they will be identical for Mate.</p>

<h3 id="language-support">Language Support</h3>

<ul>
  <li>Press the {Super} key, start typing <code class="language-plaintext highlighter-rouge">Languages</code>, and select the “Languages” application.</li>
  <li>Click “Install/Remove Languages” at the bottom right.</li>
  <li>If requested, enter your login password to grant superuser rights to “Languages”.</li>
</ul>

<p>For each language you want to add:</p>

<ul>
  <li>Click “Add”</li>
  <li>Start typing the name of the language, select the language you want, and then click the “Install” button. 
There may not be any confirmation that it has completed.</li>
  <li>Find the newly installed language in the list (again, you can start to type it to find it), select it,
and click the “Install Language Packs” button.</li>
  <li>Confirm any confirmation dialogs as necessary.</li>
</ul>

<h3 id="if">If…</h3>

<p>The remainder of work to be done involves the IBus input method framework.</p>

<p>If at any point during this procedure, you see a dialog saying “The IBus daemon is not running. Do you wish to start it?”, click “Yes”.</p>

<p>Also, if you encounter a dialog saying “IBus has been started! If you cannot use IBus, add the following lines…”, just click OK. I have seen this happen but have never needed the information displayed in the dialog.</p>

<h3 id="install-ibus">Install IBus</h3>

<p><strong>Install the IBus keyboard input management framework and the Hangul (native Korean alphabet) support:</strong></p>

<p><code class="language-plaintext highlighter-rouge">sudo apt install -y ibus-m17n ibus-hangul</code></p>

<h3 id="add-ibus-to-the-startup-applications-list">Add IBus to the Startup Applications List</h3>

<p>Press the {Super} (Windows/Mac) key, type <code class="language-plaintext highlighter-rouge">startup</code> and select “Startup Applications”.</p>

<ul>
  <li>Click the “+” button.</li>
  <li>Select “Custom command”.</li>
  <li>For Name: <code class="language-plaintext highlighter-rouge">IBus</code></li>
  <li>For Command: <code class="language-plaintext highlighter-rouge">ibus-daemon</code></li>
  <li>Click “Add”.</li>
</ul>

<h3 id="configure-ibus">Configure IBus</h3>

<p>Press the {Super} key and type <code class="language-plaintext highlighter-rouge">ibus</code>. Select “IBus Preferences”.</p>

<h4 id="add-a-keyboard-shortcut">Add a keyboard shortcut</h4>

<p>Cinnamon uses {Super}{Space} to navigate panel entries, and I have not found a way to disable that, so I assign another shortcut for language switching, {Ctrl}{Super}{Alt}-k (“k” for keyboard). Here’s how:</p>

<p>On the “General” tab:</p>

<ul>
  <li>click the button labelled “…” to the right of the “Next Input Method” text field</li>
  <li>with “{Super}<space>" selected, click the "Delete" button</space></li>
  <li>Replace any text in the “Key code” input field with “k”</li>
  <li>check “Control”, “Alt”, and “Super”</li>
  <li>click the “Add” button</li>
  <li>click the “OK” button</li>
</ul>

<p>You may not need to do this if you are using Mate and the {Super}{Space} key combination is available for IBus to use.</p>

<p>Also, I found that {Ctrl}{Super}{Alt}-k <em>did not work</em> when pressed while certain input methods were active. I briefly researched how to disable Cinnamon’s use of {Super}{Space} but could not find an answer. Anyone? Assigning multiple key combinations to this action is supported, so that is another approach.</p>

<h4 id="add-input-methods">Add input methods</h4>

<p>On the “Input Method” tab, click “Add”.</p>

<p>For each desired input method, click your desired language, or if it is not listed (as with Korean and Thai), click the three vertical dots entry, click the text field to give it focus, and type the language into the text field, then select the entry below the language name that you want. In the case of Korean, there is only one (“Hangul”). Click “Add”.</p>

<h3 id="reboot-and-test">Reboot and Test</h3>

<p>Reboot the system. (If you want to save time, you could log out and then in again instead of rebooting, but that will not verify that IBus was started on system startup.)</p>

<p>Test switching input methods with {Ctrl}{Super}{Alt}-k, and typing text into an application that can accept it.</p>

<h3 id="the-language-panel-applet">The Language Panel Applet</h3>

<p>There are two methods for invoking input method selection:</p>

<ul>
  <li>the keyboard shortcut ({Ctrl}{Super}{Alt}-k, {Super}{Space}, etc.)</li>
  <li>the panel applet</li>
</ul>

<p>The panel applet is on the system panel, and will display the currently selected language. Clicking it will display a list of languages from which you can select a different one. This applet can be used as a fallback mechanism for changing language if the keyboard shortcut does not work.</p>

<p>Unfortunately, the text on this indicator is displayed in a dark blue text that is difficult to see against the black background, so it may take some effort to find. The Korean language setting deals with this by showing a colorful icon instead of text.</p>

<h3 id="cautions">Cautions</h3>

<p><strong>Invoking Input Method Selection</strong></p>

<p>As mentioned, if the keyboard shortcut for selecting an input method does not work, remember the panel applet and use that instead.</p>

<p><strong>Know Your Input Methods</strong></p>

<ul>
  <li>The Korean input method uses a {Shift}{Space} toggle key combination to toggle between Hangul and English character input. When you switch into Korean input mode from another language, it will initially be in English mode, so it may not be obvious that you have successfully switched to Korean. Press {Shift}{Space} to toggle to Hangul input mode.</li>
  <li>If you’re new to a language, there may be things to learn about that language’s computer input methods that are not taught in school or otherwise obvious. If the input does not appear to work correctly, make sure you’re not overlooking a feature of that input method.</li>
  <li>Some languages have many input methods from which to choose, and some may be much better than others for your purposes. For example, some are Dvorak layouts of interest only to the hardiest of souls.</li>
</ul>

<p><strong>Language-Specific Configuration</strong></p>

<p>There may be language specific requirements with other languages like the need to install the ibus-hangul package for Korean.</p>

<h3 id="youre-done">You’re done!</h3>

<p>Thanks for reading, and let me know in a comment on the <a href="https://dev.to/keithrbennett/installing-international-language-keyboard-input-methods-on-linux-mint-20-cinnamon-mate-3d6d">dev.to article page</a> if you have any corrections or improvements to offer.</p>]]></content><author><name></name></author><category term="blog" /><category term="linux," /><category term="mint," /><category term="I18N," /><category term="internationalization" /><summary type="html"><![CDATA[I just started using a desktop system using Linux Mint 20 Cinnamon, and wanted to make sure that keyboard input methods worked for Thai and Korean. This took me down a long road, consulting several blog articles, questions, and answers, so I decided to document it to save others (and future me!) this effort.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.bbs-software.com/assets/images/bbs-logo-og.png" /><media:content medium="image" url="https://blog.bbs-software.com/assets/images/bbs-logo-og.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Add Bootstrap to Rails 6 with Two Shell Commands</title><link href="https://blog.bbs-software.com/blog/2020/05/07/add-bootstrap-to-rails-6-with-two-shell-commands/" rel="alternate" type="text/html" title="Add Bootstrap to Rails 6 with Two Shell Commands" /><published>2020-05-07T00:00:00+00:00</published><updated>2020-05-07T00:00:00+00:00</updated><id>https://blog.bbs-software.com/blog/2020/05/07/add-bootstrap-to-rails-6-with-two-shell-commands</id><content type="html" xml:base="https://blog.bbs-software.com/blog/2020/05/07/add-bootstrap-to-rails-6-with-two-shell-commands/"><![CDATA[<h3 id="introduction">Introduction</h3>

<p>In this article I will discuss a very simple approach to configuring your Rails 6 application to work with
<a href="https://getbootstrap.com/">Bootstrap</a>. An <a href="https://github.com/keithrbennett/rails-bootstrap-example">entire Rails repo</a>
is provided, including commit history, but this might be all you need:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl https://raw.githubusercontent.com/keithrbennett/rails-bootstrap-example/master/0001-Add-Bootstrap-configuration.patch | git apply -
yarn add bootstrap jquery popper.js
</code></pre></div></div>

<p>I recently wanted to add Bootstrap to a new Rails 6 application,
but even after reading the <a href="https://getbootstrap.com/docs/4.4/getting-started/introduction/">documentation</a>
and several blog articles, success eluded me.</p>

<p>What finally worked was to follow along with 
<a href="https://gorails.com/episodes/how-to-use-bootstrap-with-webpack-and-rails">this article</a>
by <a href="https://twitter.com/excid3">Chris Oliver</a> – well, more precicely,
the <a href="https://www.youtube.com/watch?v=bn9arlhfaXc"><em>video</em></a> linked to in the article.
In it, he shows exactly what to do – and doing what he said to do worked for me.</p>

<h3 id="even-simpler--a-patch">Even Simpler – A Patch</h3>

<p>In order to even further simplify the process for future developers, I generated a 
<a href="https://github.com/keithrbennett/rails-bootstrap-example/blob/master/0001-Add-Bootstrap-configuration.patch">patch file</a>
to make the changes needed to provide the correct configuration. (These changes do assume a new Rails application,
so it’s possible that some modification to the changes would be required for existing projects.)</p>

<p>You can make the changes to your existing project or a fresh one generated with <code class="language-plaintext highlighter-rouge">rails new</code>. Here’s how to do it:</p>

<p>Change directory to your project root.</p>

<p>I suggest you have a “clean working tree” (no git-relevant changes since the most recent commit)
before you apply the patch; this will make it easier to revert the change or limit your next commit
to only the Bootstrap configuration change.</p>

<p>You can download the patch in its 
<a href="https://raw.githubusercontent.com/keithrbennett/rails-bootstrap-example/master/0001-Add-Bootstrap-configuration.patch">raw format</a> (i.e. only the unadorned text and not the HTML web page displaying it)
to your local filesystem with the following command:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl -o bootstrap.patch https://raw.githubusercontent.com/keithrbennett/rails-bootstrap-example/master/0001-Add-Bootstrap-configuration.patch
</code></pre></div></div>

<p>Then apply the patch:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git apply bootstrap.patch
</code></pre></div></div>

<p>Alternatively, you can combine the above two steps into one:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl https://raw.githubusercontent.com/keithrbennett/rails-bootstrap-example/master/0001-Add-Bootstrap-configuration.patch | git apply -
</code></pre></div></div>

<p>You can see the changes with a <code class="language-plaintext highlighter-rouge">git diff</code>.</p>

<h3 id="adding-the-javascript-libraries">Adding the JavaScript Libraries</h3>

<p>At some point (either before or after the patch) you will need to add the necessary JavaScript libraries:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>yarn add bootstrap jquery popper.js
</code></pre></div></div>

<p>This will make changes to <code class="language-plaintext highlighter-rouge">package.json</code> and <code class="language-plaintext highlighter-rouge">yarn.lock</code> that will need to be committed.</p>

<h3 id="testing-that-bootstrap-works">Testing that Bootstrap Works</h3>

<p>We’ll want to exercise Bootstrap to verify that it is working correctly.</p>

<p>The <code class="language-plaintext highlighter-rouge">index.html.erb</code> 
<a href="https://github.com/keithrbennett/rails-bootstrap-example/blob/master/app/views/home/index.html.erb">file in the repo</a>
uses Bootstrap colored border spinners requiring both CSS and JavaScript provided by Bootstrap, and is a good test.
Of course, you can find many other components to use in the Bootstrap
<a href="https://getbootstrap.com/docs/4.4/getting-started/introduction/">docs</a>.</p>

<p>The rest of this section discusses setting up a sample app using the patches provided.
Here is a list of all the patches:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># already used above, to configure Bootstrap
0001-Add-Bootstrap-configuration.patch                       

# These next 3 can be used to put something in the
# home page to test that Bootstrap is working:
0002-rails-g-controller-home-index.patch
0003-Change-root-route-to-go-to-new-page.patch
0004-Add-Bootstrap-color-border-spinners-to-page.patch
</code></pre></div></div>

<p>These patches are in the <a href="https://github.com/keithrbennett/rails-bootstrap-example">project root of the sample repo</a>.</p>

<p>Applying patches #2 through #4 will set up the sample code to show that Bootstrap is working. 
Here are commands that will apply the patches directly from Github (without creating patch files on your local filesystem):</p>

<pre><code class="language-˚">curl https://raw.githubusercontent.com/keithrbennett/rails-bootstrap-example/master/0002-rails-g-controller-home-index.patch | git apply -
curl https://raw.githubusercontent.com/keithrbennett/rails-bootstrap-example/master/0003-Change-root-route-to-go-to-new-page.patch | git apply -
curl https://raw.githubusercontent.com/keithrbennett/rails-bootstrap-example/master/0004-Add-Bootstrap-color-border-spinners-to-page.patch | git apply -
</code></pre>

<p>This is all you should need to do! At this point you can run <code class="language-plaintext highlighter-rouge">rails s</code> and connect to it in your browser. 
If all goes well you will see something like this:</p>

<p><img src="/assets/success-page.png" alt="successful Bootstrap page" /></p>

<h3 id="gits-patch-support">Git’s Patch Support</h3>

<p>You may have noticed that the patch file names were numbered and contained the first part of the commit messages
in the names. This was not something I did myself, this was done automatically by git. Git has great patch support,
and all I needed to do to generate the patches was issue this command:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ git format-patch HEAD~4
0001-rails-g-controller-home-index.patch
0002-Change-root-route-to-go-to-new-page.patch
0003-Add-Bootstrap-color-border-spinners-to-page.patch
0004-Add-patch-files-git-format-patch-HEAD-4.patch
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">HEAD~4</code> told git how far back I wanted to start (4 commits).</p>

<p>As you saw above, applying a patch to a code base is as simple as:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git apply something.patch
</code></pre></div></div>

<hr />

<h3 id="conclusion">Conclusion</h3>

<p>I hope that the time I invested in creating and documenting this simplifying procedure will pay off
in the form of time saved for you. If you think I could be useful on your project, or would just like
to say hello, please give me a holler at <a href="mailto:kbennett@bbs-software.com">kbennett@bbs-software.com</a>.</p>

<hr />

<p>Note: The patch (#1) originally published with this article did not include some configuration code needed to
expose JQuery and Popper, which could have been a problem implementing customizations. 
It was fixed on 2020-05-12. You can see the changes <a href="https://gist.github.com/keithrbennett/1ee95d21ab9597602184ab689ca0a6f1/revisions">here</a>.</p>]]></content><author><name></name></author><category term="blog" /><category term="rails," /><category term="bootstrap," /><category term="web" /><summary type="html"><![CDATA[Introduction]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.bbs-software.com/assets/images/bbs-logo-og.png" /><media:content medium="image" url="https://blog.bbs-software.com/assets/images/bbs-logo-og.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">The Case for Stabby Lambda Notation</title><link href="https://blog.bbs-software.com/blog/2019/11/30/the-case-for-stabby-lambda-notation/" rel="alternate" type="text/html" title="The Case for Stabby Lambda Notation" /><published>2019-11-30T00:00:00+00:00</published><updated>2019-11-30T00:00:00+00:00</updated><id>https://blog.bbs-software.com/blog/2019/11/30/the-case-for-stabby-lambda-notation</id><content type="html" xml:base="https://blog.bbs-software.com/blog/2019/11/30/the-case-for-stabby-lambda-notation/"><![CDATA[<h3 id="the-stabby-lambda--">The Stabby Lambda (<code class="language-plaintext highlighter-rouge">-&gt;</code>)</h3>

<p>Although the <code class="language-plaintext highlighter-rouge">-&gt;</code> “stabby lambda” notation has been available for creating lambdas since Ruby version 1.9, old habits die hard and acceptance and adoption has been slow. In this article I will explain why I recommend using it instead of the <code class="language-plaintext highlighter-rouge">lambda</code> notation.</p>

<h3 id="stabby-notation-as-an-indicator-of-preferred-and-default-proc-type">Stabby Notation as an Indicator of Preferred and Default Proc Type</h3>

<p>In a previous article, “<a href="https://dev.to/keithrbennett/lambdas-are-better-than-procs-52a1">lambdas Are Better Than procs</a>”, I proposed that lambdas should be used rather than procs in almost all cases, given that they are safer in terms of argument count checking and return behavior.</p>

<p>So it makes sense that <code class="language-plaintext highlighter-rouge">-&gt;</code> should create a lambda and not a proc. (As an aside, it always puzzles me when people use the term stabby <em>proc</em>, when it creates a lambda.)</p>

<p>One way to look at it is, by using the stabby lambda notation, we are
saying “make me Ruby’s implementation of an objectless function”. This is at a level higher than “make me a lambda” or “make me a proc”, and is probably a better interface to the programmer, especially the newer Rubyist.</p>

<h3 id="-s-picture-like-notation"><code class="language-plaintext highlighter-rouge">-&gt;</code>’s Picture-Like Notation</h3>

<p>The picture-like notation <code class="language-plaintext highlighter-rouge">-&gt;</code> is quite different from the <code class="language-plaintext highlighter-rouge">lambda</code> and <code class="language-plaintext highlighter-rouge">proc</code> forms, because although all result in method calls that create <code class="language-plaintext highlighter-rouge">Proc</code> instances, <code class="language-plaintext highlighter-rouge">lambda</code> and <code class="language-plaintext highlighter-rouge">proc</code> <em>look like</em> method calls, while <code class="language-plaintext highlighter-rouge">-&gt;</code> does not, instead appearing more like a <em>language construct</em>. On the higher level, it really <em>is</em> a language construct, and the fact that a method needs to be called to create a lambda is an implementation detail that should not matter to the programmer.</p>

<p>The striking appearance of <code class="language-plaintext highlighter-rouge">-&gt;</code> says to the reader “take note, something different is happening here, this marks the beginning of a definition of executable code that will probably be called somewhere <em>else</em>”. If a picture is worth a thousand words, then a text picture like <code class="language-plaintext highlighter-rouge">-&gt;</code> is worth, well, at least ten.</p>

<h3 id="the-need-for-visual-differentiation">The Need for Visual Differentiation</h3>

<p>Unlike other code in a method, a lambda’s code is not called in sequence (unless it is immediately called as a self invoking anonymous function, but this is rare). Also, sometimes a lambda can be used as if it were a nested method, containing lower level code that may be called multiple times in the method in which it was defined. For these reasons, a pictorial indication setting it apart from other code in the method is especially helpful.</p>

<h3 id="rubocop">Rubocop</h3>

<p>Rubocop is a very useful tool for normalizing code style. For better or worse though, Rubocop’s defaults constitute implicit recommendations, and deviating from the defaults can require lengthy and contentious team discussions. Because of this potentially high cost of overriding the defaults, it is important that the basis in reasoning for the selection of the default be sound.</p>

<p>Rubocop’s <a href="https://www.rubydoc.info/gems/rubocop/RuboCop/Cop/Style/Lambda">default setting for lambdas</a> is to use <code class="language-plaintext highlighter-rouge">-&gt;</code> with lambda one-liners but <code class="language-plaintext highlighter-rouge">lambda</code> for multiline lambdas. While this is not a matter of monumental importance, I believe it’s misguided and should be changed.</p>

<p>My guess is that it is intended to mirror the Ruby code block notation convention of <code class="language-plaintext highlighter-rouge">{..}</code> for single line blocks and <code class="language-plaintext highlighter-rouge">do...end</code> for multi-line blocks. However, the code block case is different because the <code class="language-plaintext highlighter-rouge">do</code> and <code class="language-plaintext highlighter-rouge">end</code> are at the end and beginning of the line, respectively (though it is true that if there are arguments they will appear after the <code class="language-plaintext highlighter-rouge">do</code>). Although the indentation of the code block within the <code class="language-plaintext highlighter-rouge">lambda do...end</code> makes it easy to see that <em>something</em> is going on, it is easy to miss the <code class="language-plaintext highlighter-rouge">lambda</code> and assume it is a normal code block. The pictorial nature of <code class="language-plaintext highlighter-rouge">-&gt;</code> reduces this risk.</p>

<p>I believe that the Rubocop default should be changed to prefer (or at minimum permit) <code class="language-plaintext highlighter-rouge">-&gt;</code> in all cases.</p>

<p>Note: Since writing this article I posted an issue on the Rubocop project site 
<a href="https://github.com/rubocop-hq/rubocop/issues/7566">here</a>.</p>

<h3 id="conclusion">Conclusion</h3>

<p>Lambdas are, thankfully, first class objects in Ruby. That is, they can be passed to and returned from methods, and can be assigned to variables. This is a pretty major construct, and I believe a special notation (<code class="language-plaintext highlighter-rouge">-&gt;</code>), rather than a method name (<code class="language-plaintext highlighter-rouge">lambda</code>) is justified and helpful. While it is true that <code class="language-plaintext highlighter-rouge">class</code>, <code class="language-plaintext highlighter-rouge">module</code>, and <code class="language-plaintext highlighter-rouge">def</code> also mark the beginning of major language constructs, they are likely to be the first token on a line, whereas lambdas are usually assigned to variables or passed to methods or other lambdas, and are not.</p>

<p>The conciseness and pictorial nature of <code class="language-plaintext highlighter-rouge">-&gt;</code> encourage the use of lambdas, and in my opinion, that is a Good Thing. Lambdas are underused in the Ruby community, and many opportunities for cleaner and clearer code are missed.</p>]]></content><author><name></name></author><category term="blog" /><category term="ruby," /><category term="functional," /><category term="lambda" /><summary type="html"><![CDATA[The Stabby Lambda (-&gt;)]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.bbs-software.com/assets/images/bbs-logo-og.png" /><media:content medium="image" url="https://blog.bbs-software.com/assets/images/bbs-logo-og.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">lambdas Are Better Than procs</title><link href="https://blog.bbs-software.com/blog/2019/11/19/lambdas-are-better-than-procs/" rel="alternate" type="text/html" title="lambdas Are Better Than procs" /><published>2019-11-19T00:00:00+00:00</published><updated>2019-11-19T00:00:00+00:00</updated><id>https://blog.bbs-software.com/blog/2019/11/19/lambdas-are-better-than-procs</id><content type="html" xml:base="https://blog.bbs-software.com/blog/2019/11/19/lambdas-are-better-than-procs/"><![CDATA[<p>Many Rubyists believe that lambda and nonlambda Procs are pretty much the same and that choosing which one to use is a subjective preference. This is an unfortunate fallacy.</p>

<p>This article will attempt to achieve two purposes:</p>

<p>1) to explain the difference between lambdas and procs</p>

<p>2) to persuade you to use lambdas unless there is a compelling reason not to</p>

<p>There are many resources available that explain lambdas and procs <sup id="a1">[<a href="#f1">1</a>]</sup>, and I will assume you know at least a little about them.</p>

<p>Before we look at some examples, here are some characteristics of lambdas and procs:</p>

<ul>
  <li>a <code class="language-plaintext highlighter-rouge">Proc</code> instance can be either lambda or a proc <sup id="a2">[<a href="#f2">2</a>]</sup></li>
  <li>all <code class="language-plaintext highlighter-rouge">lambda</code>s are <code class="language-plaintext highlighter-rouge">Proc</code>s</li>
  <li>all <code class="language-plaintext highlighter-rouge">proc</code>s are <code class="language-plaintext highlighter-rouge">Proc</code>s</li>
  <li>code blocks behave like <code class="language-plaintext highlighter-rouge">proc</code>s</li>
  <li>you can determine the kind of Proc by calling <code class="language-plaintext highlighter-rouge">lambda?</code> on it</li>
</ul>

<hr />

<h3 id="arity-argument-count-checking-behavior-differences">Arity (Argument Count) Checking Behavior Differences</h3>

<p>A <code class="language-plaintext highlighter-rouge">lambda</code>, like a method, strictly enforces its argument count, but a <code class="language-plaintext highlighter-rouge">proc</code> does not. When we call a <code class="language-plaintext highlighter-rouge">proc</code> with the wrong number of arguments, there are no complaints by the Ruby runtime <sup id="a3">[<a href="#f3">3</a>]</sup>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="mf">2.6</span><span class="o">.</span><span class="mi">5</span> <span class="p">:</span><span class="mo">006</span> <span class="o">&gt;</span> <span class="n">pfn</span> <span class="o">=</span> <span class="nb">proc</span> <span class="p">{</span> <span class="o">|</span><span class="n">arg</span><span class="o">|</span> <span class="p">}</span>
 <span class="o">=&gt;</span> <span class="c1">#&lt;Proc:0x00007f93828bd298@(irb):6&gt;</span>
<span class="mf">2.6</span><span class="o">.</span><span class="mi">5</span> <span class="p">:</span><span class="mo">007</span> <span class="o">&gt;</span> <span class="n">pfn</span><span class="p">.</span><span class="nf">call</span>
 <span class="o">=&gt;</span> <span class="kp">nil</span>
</code></pre></div></div>

<p>In contrast, when we do the same with a lambda, we get an error <sup id="a4">[<a href="#f4">4</a>]</sup>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="mf">2.6</span><span class="o">.</span><span class="mi">5</span> <span class="p">:</span><span class="mo">002</span> <span class="o">&gt;</span> <span class="n">lfn</span> <span class="o">=</span> <span class="o">-&gt;</span><span class="p">(</span><span class="n">arg</span><span class="p">)</span> <span class="p">{}</span>
    <span class="o">=&gt;</span> <span class="c1">#&lt;Proc:0x00007f9383118ed8@(irb):2 (lambda)&gt;</span>
   <span class="mf">2.6</span><span class="o">.</span><span class="mi">5</span> <span class="p">:</span><span class="mo">003</span> <span class="o">&gt;</span> <span class="n">lfn</span><span class="p">.</span><span class="nf">call</span>
   <span class="o">...</span>
   <span class="no">ArgumentError</span> <span class="p">(</span><span class="n">wrong</span> <span class="n">number</span> <span class="n">of</span> <span class="n">arguments</span> <span class="p">(</span><span class="n">given</span> <span class="mi">0</span><span class="p">,</span> <span class="n">expected</span> <span class="mi">1</span><span class="p">))</span>
</code></pre></div></div>

<p>Which behavior would <em>you</em> prefer?</p>

<p>Clearly, arity checking is helpful, and we abandon it at our peril.</p>

<hr />

<h3 id="return-behavior-differences">Return Behavior Differences</h3>

<p>What happens when you pass a code block somewhere, and it executes a <code class="language-plaintext highlighter-rouge">return</code>? Does it return from the block? Well, yes, but it does much more than that; it returns from the method that <em>yielded</em> to the block. <code class="language-plaintext highlighter-rouge">proc</code>s behave the same way; in addition to returning from themselves, they will return from the method in which they were called:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">using_proc</span>
  <span class="n">pfn</span> <span class="o">=</span> <span class="nb">proc</span> <span class="p">{</span> <span class="k">return</span> <span class="p">}</span>
  <span class="nb">puts</span> <span class="s2">"Before calling"</span>
  <span class="n">pfn</span><span class="p">.</span><span class="nf">call</span>
  <span class="nb">puts</span> <span class="s2">"After calling"</span>
<span class="k">end</span>

<span class="c1"># ...</span>
<span class="mf">2.6</span><span class="o">.</span><span class="mi">5</span> <span class="p">:</span><span class="mo">015</span> <span class="o">&gt;</span> <span class="n">using_proc</span>
<span class="no">Before</span> <span class="n">calling</span>

</code></pre></div></div>

<p>Before proceeding to the lambda behavior, I’d like to point out that this <code class="language-plaintext highlighter-rouge">proc</code> behavior is such that implicit and explicit returns do very different things. An implicit return will return from the proc, but an explicit return will return from the context that called it! Weird, eh? Here is the same code, but without the explicit return; the proc will end and exit naturally:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">using_proc_without_return</span>
  <span class="n">pfn</span> <span class="o">=</span> <span class="nb">proc</span> <span class="p">{</span> <span class="p">}</span>
  <span class="nb">puts</span> <span class="s2">"Before calling"</span>
  <span class="n">pfn</span><span class="p">.</span><span class="nf">call</span>
  <span class="nb">puts</span> <span class="s2">"After calling"</span>
<span class="k">end</span>
<span class="c1"># ...</span>
<span class="mf">2.6</span><span class="o">.</span><span class="mi">5</span> <span class="p">:</span><span class="mo">007</span> <span class="o">&gt;</span> <span class="n">using_proc_without_return</span>
<span class="no">Before</span> <span class="n">calling</span>
<span class="no">After</span> <span class="n">calling</span>
</code></pre></div></div>

<p>When we first learn Ruby, we learn that a <code class="language-plaintext highlighter-rouge">return</code> at the end of a method is redundant (it <em>is</em>, of course), but in the case of the <code class="language-plaintext highlighter-rouge">proc</code> (and code block) it is not!</p>

<p>In contrast, a <code class="language-plaintext highlighter-rouge">lambda</code>’s <code class="language-plaintext highlighter-rouge">return</code> returns from itself to the context that called it:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">using_lambda</span>
  <span class="n">lfn</span> <span class="o">=</span> <span class="o">-&gt;</span> <span class="p">{</span> <span class="k">return</span> <span class="p">}</span>
  <span class="nb">puts</span> <span class="s2">"Before calling"</span>
  <span class="n">lfn</span><span class="p">.</span><span class="nf">call</span>
  <span class="nb">puts</span> <span class="s2">"After calling"</span>
<span class="k">end</span>
<span class="c1"># ...</span>
<span class="mf">2.6</span><span class="o">.</span><span class="mi">5</span> <span class="p">:</span><span class="mo">00</span><span class="mi">8</span> <span class="o">&gt;</span> <span class="n">using_lambda</span>
<span class="no">Before</span> <span class="n">calling</span>
<span class="no">After</span> <span class="n">calling</span>
</code></pre></div></div>

<hr />

<h3 id="a-lambda-is-more-method-like-than-a-proc">A <code class="language-plaintext highlighter-rouge">lambda</code> is More Method-Like Than a <code class="language-plaintext highlighter-rouge">proc</code></h3>

<p>In both of the above cases, the lambda behaves more like a method than a proc does. The newer <code class="language-plaintext highlighter-rouge">-&gt;(args)</code> notation for creating a lambda reveals that intent by defining the arguments as a method does, in a parenthesized list, and is therefore preferable to the older <code class="language-plaintext highlighter-rouge">lambda</code> notation:</p>

<p>New:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">fn</span> <span class="o">=</span> <span class="o">-&gt;</span><span class="p">(</span><span class="n">arg1</span><span class="p">,</span> <span class="n">arg2</span><span class="p">)</span> <span class="p">{</span> <span class="o">...</span> <span class="p">}</span>
</code></pre></div></div>

<p>Old:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">fn</span> <span class="o">=</span> <span class="nb">lambda</span> <span class="p">{</span> <span class="o">|</span><span class="n">arg1</span><span class="p">,</span> <span class="n">arg2</span><span class="o">|</span> <span class="o">...</span> <span class="p">}</span>
</code></pre></div></div>

<hr />

<h3 id="conclusion">Conclusion</h3>

<p>Here are some principles I’ve learned to code by:</p>

<ul>
  <li>prefer simplicity to complexity</li>
  <li>limit things to the narrowest possible scope</li>
  <li>specify things with minimal ambiguity</li>
  <li>use language features that minimize the risk of errors</li>
</ul>

<p>Regarding everything said so far, the lambda wins over the proc. There is no reason to use a proc unless you specifically need the odd and potentially hazardous behaviors described above.</p>

<p>You may think it will never matter in your case. Maybe you’re never calling the lambda yourself, but passing it to a framework such as Rails that is doing all the calling. Nevertheless, if given the added protection for free, why would you <em>not</em> want it? Especially since the <code class="language-plaintext highlighter-rouge">-&gt;</code> notation is somewhat pictorial and more concise?</p>

<hr />

<h3 id="footnotes">Footnotes</h3>

<p><b id="f1">[1]</b> There are many good resources; here are some that I have produced (articles and a conference talk):</p>

<ul>
  <li><a href="https://dev.to/keithrbennett/using-lambdas-to-simplify-varying-behaviors-in-your-code-1d5ff">Using Lambdas to Simplify Varying Behaviors in Your Code</a></li>
  <li><a href="https://dev.to/keithrbennett/ruby-enumerables-make-your-code-short-and-sweet-2nl0">Ruby Enumerables Make Your Code Short and Sweet</a></li>
  <li><a href="https://www.youtube.com/watch?v=nGEy-vFJCSE">Functional Programming in Ruby</a>, video of a talk given at Functional Conf in Bangalore, India in 2014
<a href="#a1">↩</a></li>
</ul>

<p><b id="f2">[2]</b> This terminology is unfortunate, as <code class="language-plaintext highlighter-rouge">Proc</code> and <code class="language-plaintext highlighter-rouge">proc</code>, when spoken, sound identical.
<a href="#a2">↩</a></p>

<p><b id="f3">[3]</b> In this article I’ve used the <code class="language-plaintext highlighter-rouge">.call</code> variant of calling a Proc because it is the most obvious for the reader, but in practice I prefer the shorthand notation <code class="language-plaintext highlighter-rouge">.()</code>.
<a href="#a3">↩</a></p>

<p><b id="f4">[4]</b> The shorthand <code class="language-plaintext highlighter-rouge">-&gt;</code> can be used in place of the <code class="language-plaintext highlighter-rouge">lambda</code> keyword to more succinctly define a lambda.
<a href="#a4">↩</a></p>]]></content><author><name></name></author><category term="blog" /><category term="ruby," /><category term="functional," /><category term="lambda" /><summary type="html"><![CDATA[Many Rubyists believe that lambda and nonlambda Procs are pretty much the same and that choosing which one to use is a subjective preference. This is an unfortunate fallacy.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.bbs-software.com/assets/images/bbs-logo-og.png" /><media:content medium="image" url="https://blog.bbs-software.com/assets/images/bbs-logo-og.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Ruby Enumerables Make Your Code Short and Sweet</title><link href="https://blog.bbs-software.com/blog/2019/11/13/enumerables-short-and-sweet/" rel="alternate" type="text/html" title="Ruby Enumerables Make Your Code Short and Sweet" /><published>2019-11-13T00:00:00+00:00</published><updated>2019-11-13T00:00:00+00:00</updated><id>https://blog.bbs-software.com/blog/2019/11/13/enumerables-short-and-sweet</id><content type="html" xml:base="https://blog.bbs-software.com/blog/2019/11/13/enumerables-short-and-sweet/"><![CDATA[<p>One of the most amazing things about Ruby is the richness of its <code class="language-plaintext highlighter-rouge">Enumerable</code> library; there are <em>so many</em> things it can do. Another is Ruby’s ability to express intent with the utmost conciseness and clarity. However, out in the wild I very often see code that fails to take full advantage of these qualities.</p>

<p>As a contrived example, let’s say we’re keeping track of letter frequencies in a document. We define a class to contain them as:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">LetterFrequency</span> <span class="o">=</span> <span class="no">Struct</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">:letter</span><span class="p">,</span> <span class="ss">:frequency</span><span class="p">,</span> <span class="ss">:vowel?</span><span class="p">)</span>
</code></pre></div></div>

<p>I’ve seen a lot code that looks like this:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">filtered_and_transformed_records_1</span><span class="p">(</span><span class="n">records</span><span class="p">)</span>
  <span class="n">results</span> <span class="o">=</span> <span class="p">[]</span>

  <span class="n">records</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">record</span><span class="o">|</span>
    <span class="k">next</span> <span class="k">unless</span> <span class="n">record</span><span class="p">.</span><span class="nf">vowel?</span>
    <span class="n">results</span> <span class="o">&lt;&lt;</span> <span class="p">[</span><span class="n">record</span><span class="p">.</span><span class="nf">letter</span><span class="p">,</span> <span class="n">record</span><span class="p">.</span><span class="nf">frequency</span><span class="p">]</span>
  <span class="k">end</span>

  <span class="n">results</span>
<span class="k">end</span>
</code></pre></div></div>

<p>In more primitive languages one must use these approaches, but in Ruby we have some major refactorings that can make this code much, much simpler.</p>

<p>First, we can use <code class="language-plaintext highlighter-rouge">each_with_object</code> to eliminate the need for the explicit initialization of the function-local variable containing the array and its explicit return, on the first and last lines of the method:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">filtered_and_transformed_records_2</span><span class="p">(</span><span class="n">records</span><span class="p">)</span>
  <span class="n">records</span><span class="p">.</span><span class="nf">each_with_object</span><span class="p">([])</span> <span class="k">do</span> <span class="o">|</span><span class="n">record</span><span class="p">,</span> <span class="n">results</span><span class="o">|</span>
    <span class="k">next</span> <span class="k">unless</span> <span class="n">record</span><span class="p">.</span><span class="nf">vowel?</span>
    <span class="n">results</span> <span class="o">&lt;&lt;</span> <span class="p">[</span><span class="n">record</span><span class="p">.</span><span class="nf">letter</span><span class="p">,</span> <span class="n">record</span><span class="p">.</span><span class="nf">frequency</span><span class="p">]</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>I say <em>function-local</em> because we do need the <em>block-local</em> variable <code class="language-plaintext highlighter-rouge">results</code> inside the <code class="language-plaintext highlighter-rouge">each_with_object</code> block. However, we’ve narrowed the scope of the <code class="language-plaintext highlighter-rouge">results</code> variable, and that’s always a good thing.</p>

<p><code class="language-plaintext highlighter-rouge">each_with_object</code> is like <code class="language-plaintext highlighter-rouge">each</code> except that it will pass <em>two</em> variables to the block instead of one. In addition to the object from the Enumerable that <code class="language-plaintext highlighter-rouge">each</code> passes, it passes the object you are using to accumulate results. You initialize the accumulator by passing its initial value to the <code class="language-plaintext highlighter-rouge">each_with_object</code> method. In this case we are passing a newly created empty array.</p>

<p><code class="language-plaintext highlighter-rouge">each_with_object</code>’s return value is the accumulator object, so you don’t need to specify the accumulator explicitly for it to be the value returned by the method.</p>

<p>The <code class="language-plaintext highlighter-rouge">each_with_object</code> usage may not feel natural at first, but once you’ve seen it a few times your mind will parse it with almost zero effort. (By the way, I always had trouble remembering the order of its arguments until I realized that they were in the same order as in the method name itself; <code class="language-plaintext highlighter-rouge">each</code> for the enumerated object and <code class="language-plaintext highlighter-rouge">object</code> for the accumulator object.)</p>

<p>The second refactoring is instead of using control flow constructs like <code class="language-plaintext highlighter-rouge">next</code>, we can use the <code class="language-plaintext highlighter-rouge">Enumerable</code> methods <code class="language-plaintext highlighter-rouge">select</code> or <code class="language-plaintext highlighter-rouge">reject</code>. We could refactor the code further into:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">filtered_and_transformed_records_3</span><span class="p">(</span><span class="n">records</span><span class="p">)</span>
  <span class="n">records</span><span class="p">.</span><span class="nf">select</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:vowel?</span><span class="p">).</span><span class="nf">each_with_object</span><span class="p">([])</span> <span class="k">do</span> <span class="o">|</span><span class="n">record</span><span class="p">,</span> <span class="n">results</span><span class="o">|</span>
    <span class="n">results</span> <span class="o">&lt;&lt;</span> <span class="p">[</span><span class="n">record</span><span class="p">.</span><span class="nf">letter</span><span class="p">,</span> <span class="n">record</span><span class="p">.</span><span class="nf">frequency</span><span class="p">]</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>After this refactoring, we see the filter where it is more appropriate and helpful. Instead of it being on a line inside the block, it’s just a few characters immediately after the input array (<code class="language-plaintext highlighter-rouge">records.select...</code>).</p>

<p>We’ve already simplified this method quite a bit, but there’s even more we can do. Because <code class="language-plaintext highlighter-rouge">select</code> returns the filtered array, we can simplify even further by using <code class="language-plaintext highlighter-rouge">map</code> instead of <code class="language-plaintext highlighter-rouge">each_with_object</code>!:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">filtered_and_transformed_records_4</span><span class="p">(</span><span class="n">records</span><span class="p">)</span>
  <span class="n">records</span><span class="p">.</span><span class="nf">select</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:vowel?</span><span class="p">).</span><span class="nf">map</span> <span class="p">{</span> <span class="o">|</span><span class="n">record</span><span class="o">|</span> <span class="p">[</span><span class="n">record</span><span class="p">.</span><span class="nf">letter</span><span class="p">,</span> <span class="n">record</span><span class="p">.</span><span class="nf">frequency</span><span class="p">]</span> <span class="p">}</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Although as software developers our mission is to deliver functionality, the other side of that coin is to do so as simply as possible. Put otherwise, we need to remove <em>accidental complexity</em> (a.k.a. <em>incidental complexity</em>) so that only the <em>essential complexity</em> remains. The functional approaches described here are extremely effective at doing this. We’ve ended up with a simple one-liner.</p>

<hr />

<p>Whenever you start feeling that your code is getting verbose or awkward, ask yourself “could I improve this code with <code class="language-plaintext highlighter-rouge">Enumerable</code>?” The answer may well be <em>yes</em>.</p>

<hr />

<p>For your reference, <a href="https://github.com/keithrbennett/bbs-blog/blob/master/source-code/short_sweet.rb">here</a> is a file that contains the methods in the article, and verifies that they all produce the same result.</p>]]></content><author><name></name></author><category term="blog" /><category term="ruby," /><category term="enumerable," /><category term="functional" /><summary type="html"><![CDATA[One of the most amazing things about Ruby is the richness of its Enumerable library; there are so many things it can do. Another is Ruby’s ability to express intent with the utmost conciseness and clarity. However, out in the wild I very often see code that fails to take full advantage of these qualities. As a contrived example, let’s say we’re keeping track of letter frequencies in a document. We define a class to contain them as:]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.bbs-software.com/assets/images/bbs-logo-og.png" /><media:content medium="image" url="https://blog.bbs-software.com/assets/images/bbs-logo-og.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Automate the Little Things Too</title><link href="https://blog.bbs-software.com/blog/2019/09/24/automate-the-little-things-too/" rel="alternate" type="text/html" title="Automate the Little Things Too" /><published>2019-09-24T00:00:00+00:00</published><updated>2019-09-24T00:00:00+00:00</updated><id>https://blog.bbs-software.com/blog/2019/09/24/automate-the-little-things-too</id><content type="html" xml:base="https://blog.bbs-software.com/blog/2019/09/24/automate-the-little-things-too/"><![CDATA[<p>We developers automate highly complex tasks, but when it comes to the smaller repetitive tasks, we tend to do things manually, or fail to do them at all. By combining Ruby with robust and richly functional command line tools such as MPlayer, we can save ourselves lots of time and have fun in the process.</p>

<p>I recently decided that it would be nice to trim my collection of many video files downloaded from my phones over the years. Realizing this would be quite tedious, I asked myself “would this be easier with Ruby?” The answer, of course, was <em>Yes!</em></p>

<h3 id="integrating-mplayer-and-ruby">Integrating MPlayer and Ruby</h3>

<p><a href="https://www.mplayerhq.hu/">MPlayer</a> is a Unix <em>command line</em> multimedia player that can be installed with your favorite package manager (e.g. <code class="language-plaintext highlighter-rouge">brew</code>, <code class="language-plaintext highlighter-rouge">apt</code>, or <code class="language-plaintext highlighter-rouge">yum</code>). By driving MPlayer from Ruby, we can create a workflow that will enable you to view and decide about video files with a minimum of keystrokes, <em>without needing to use the mouse</em>.</p>

<p>Files to process are specified on the command line. Multiple arguments can be specified, either absolute or relative, and either with or without wildcards. All filespecs are normalized to their absolute form so that duplicates can be eliminated.</p>

<p>MPlayer plays each file for the user, responding to cursor keys to move forward and backward in time, change the speed, etc. I recommend viewing the man page (<code class="language-plaintext highlighter-rouge">man mplayer</code>), but here are the most relevant options:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>keyboard control
      LEFT and RIGHT
           Seek backward/forward 10 seconds.
      UP and DOWN
           Seek forward/backward 1 minute.
      PGUP and PGDWN
           Seek forward/backward 10 minutes.
      [ and ]
           Decrease/increase current playback speed by 10%.
      { and }
           Halve/double current playback speed.
      BACKSPACE
           Reset playback speed to normal.

</code></pre></div></div>

<p>When the user has seen enough to make a decision, <code class="language-plaintext highlighter-rouge">q</code> or <code class="language-plaintext highlighter-rouge">[ESC]</code> can be pressed, and MPlayer returns control to the Ruby script, which accepts a one character response to mark it to be saved (<code class="language-plaintext highlighter-rouge">s</code>), deleted (<code class="language-plaintext highlighter-rouge">d</code>), or marked as undecided (<code class="language-plaintext highlighter-rouge">u</code>) for future reprocessing; or <code class="language-plaintext highlighter-rouge">q</code> to quit the application.</p>

<h3 id="the-high-level-view">The High Level View</h3>

<p>Here is the highest level method in the script:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">main</span>
  <span class="n">check_presence_of_mplayer</span>
  <span class="n">create_dirs</span>
  <span class="nb">puts</span> <span class="n">greeting</span>
  <span class="n">files_to_process</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">filespec</span><span class="o">|</span>
    <span class="n">play_file</span><span class="p">(</span><span class="n">filespec</span><span class="p">)</span>
    <span class="nb">print</span> <span class="n">disposition_prompt</span><span class="p">(</span><span class="n">filespec</span><span class="p">)</span>
    <span class="n">destination_subdir</span> <span class="o">=</span> <span class="n">get_disposition_from_user</span>
    <span class="sb">`mv </span><span class="si">#{</span><span class="n">filespec</span><span class="si">}</span><span class="sb"> </span><span class="si">#{</span><span class="n">destination_subdir</span><span class="si">}</span><span class="sb">`</span>
    <span class="n">log</span><span class="p">(</span><span class="n">filespec</span><span class="p">,</span> <span class="n">destination_subdir</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<h3 id="using-subdirectories">Using Subdirectories</h3>

<p>For simplicity of implementation and added safety, the application “marks” each multimedia file by moving it to one of the three subdirectories it has created, based on the user’s choice. The user selects <code class="language-plaintext highlighter-rouge">d</code> for deletes, <code class="language-plaintext highlighter-rouge">s</code> for saves, or  <code class="language-plaintext highlighter-rouge">u</code> for undecideds. <code class="language-plaintext highlighter-rouge">create_dirs</code> creates the three subdirectories:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">create_dirs</span>
  <span class="sx">%w{deletes  saves  undecideds}</span><span class="p">.</span><span class="nf">each</span> <span class="p">{</span> <span class="o">|</span><span class="n">dir</span><span class="o">|</span> <span class="no">FileUtils</span><span class="p">.</span><span class="nf">mkdir_p</span><span class="p">(</span><span class="n">dir</span><span class="p">)</span> <span class="p">}</span>
<span class="k">end</span>
</code></pre></div></div>

<p>When the user is finished processing all files, they will probably want to move any files that have been moved to <code class="language-plaintext highlighter-rouge">./undecided</code> back to <code class="language-plaintext highlighter-rouge">.</code> and run the program again.</p>

<p>Finally, when there are no files left in <code class="language-plaintext highlighter-rouge">undecided</code>, one will probably want to do something like this:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rmdir undecideds
rm -rf deletes
mv saves/* .
rmdir saves
</code></pre></div></div>

<h3 id="an-example">An Example</h3>

<p>For example, let’s say you run the following command:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>organize-av-files 'video/*mp4' 'audio/*mp3'
</code></pre></div></div>

<p>MPlayer will begin playing the first file. When you are ready to finish viewing it, you will press <code class="language-plaintext highlighter-rouge">q</code> or <code class="language-plaintext highlighter-rouge">ESC</code>, and be presented with a prompt like this:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/Users/kbennett/android/video/20160102_234426.mp4:
s = save, d = delete, u = undecided, q = quit:
</code></pre></div></div>

<p>Type your response choice and then <code class="language-plaintext highlighter-rouge">[Enter]</code>. The program will move the file as appropriate, and immediately start playing the next file.</p>

<h3 id="shell-vs-ruby-wildcard-expansion">Shell vs. Ruby Wildcard Expansion</h3>

<p>Be careful when using wildcards. If you enter <code class="language-plaintext highlighter-rouge">*mp4</code> in a directory with 10,000 MP4 files, the shell will try to expand it into 10,000 arguments, which might exceed the maximum command line size and result in an error. You can instead quote the filemask (as <code class="language-plaintext highlighter-rouge">'*mp4'</code>), and it will then be passed to Ruby as a single argument, and Ruby will perform the expansion. You can usually use double quotes, but be aware that the single and double quotes behavior differs (see <a href="https://stackoverflow.com/questions/6697753/difference-between-single-and-double-quotes-in-bash">this helpful StackOverflow article</a>).</p>

<p>One case where the shell’s expansion would be preferable is with the use of environment variables in the filespec (<code class="language-plaintext highlighter-rouge">$FOO</code> is more concise than <code class="language-plaintext highlighter-rouge">ENV['FOO']</code>), and in the case of using <code class="language-plaintext highlighter-rouge">~</code> for users other than the current user (e.g. <code class="language-plaintext highlighter-rouge">~someoneelse</code>).</p>

<h3 id="also">Also…</h3>

<ul>
  <li>This workflow can be used with any multimedia files recognized by MPlayer, and that includes audio files.</li>
  <li>There are many, many nice-to-have features that have not been implemented, since speed of implementation was a high priority. Feel free to add your own!</li>
  <li>Although using Ruby probably enables writing the most concise and intention-revealing code, other languages such as Python would do fine as well.</li>
  <li>The code for this script (“organize-av-files”) is currently at <a href="https://gist.github.com/keithrbennett/4d9953e66ea35e2c52abae52650ebb1b">https://gist.github.com/keithrbennett/4d9953e66ea35e2c52abae52650ebb1b</a>.</li>
</ul>

<h3 id="conclusion">Conclusion</h3>

<p>I hope you can see that with a modest amount of code you can build a highly useful (albeit not fancy) automation tool. The amount of expected use and the benefit per use determines the optimum amount of effort, and you have the freedom to choose any point in that continuum. The notion that all applications need to be feature-rich is not a useful one, and often results in inaction altogether.</p>

<p>Ruby is a great tool for this sort of thing. Why not use it?</p>

<p>— The End —</p>

<hr />

<p>For your convenience, the script is displayed below:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">#!/usr/bin/env ruby</span>

<span class="c1"># organize-av-files - Organizes files playable by mplayer</span>
<span class="c1"># into 'saves', 'deletes', and 'undecideds' subdirectories</span>
<span class="c1"># of the current working directory.</span>
<span class="c1">#</span>
<span class="c1"># Be careful, if you specify files to process in multiple directories,</span>
<span class="c1"># they will all be moved to the same subdirectories, so they will no</span>
<span class="c1"># longer be organized by directory, and if there are multiple files</span>
<span class="c1"># of the same name, some may be lost if overwritten.</span>
<span class="c1">#</span>
<span class="c1"># stored at:</span>
<span class="c1"># https://gist.github.com/keithrbennett/4d9953e66ea35e2c52abae52650ebb1b</span>


<span class="nb">require</span> <span class="s1">'date'</span>
<span class="nb">require</span> <span class="s1">'fileutils'</span>
<span class="nb">require</span> <span class="s1">'set'</span>

<span class="no">LOG_FILESPEC</span> <span class="o">=</span> <span class="s1">'organize-av-files.log'</span>

<span class="k">def</span> <span class="nf">create_dirs</span>
  <span class="sx">%w{deletes  saves  undecideds}</span><span class="p">.</span><span class="nf">each</span> <span class="p">{</span> <span class="o">|</span><span class="n">dir</span><span class="o">|</span> <span class="no">FileUtils</span><span class="p">.</span><span class="nf">mkdir_p</span><span class="p">(</span><span class="n">dir</span><span class="p">)</span> <span class="p">}</span>
<span class="k">end</span>


<span class="k">def</span> <span class="nf">check_presence_of_mplayer</span>
  <span class="k">if</span> <span class="sb">`which mplayer`</span><span class="p">.</span><span class="nf">chomp</span><span class="p">.</span><span class="nf">size</span> <span class="o">==</span> <span class="mi">0</span>
    <span class="k">raise</span> <span class="s2">"mplayer not detected. "</span>
        <span class="s2">"Please install it (with apt, brew, yum, etc.)"</span>
  <span class="k">end</span>
<span class="k">end</span>


<span class="c1"># Takes all ARGV elements, expands any wildcards,</span>
<span class="c1"># converts to normalized (absolute) form,</span>
<span class="c1"># and eliminates duplicates.</span>
<span class="k">def</span> <span class="nf">files_to_process</span>

  <span class="c1"># Dir[] does not understand ~, need to process it ourselves.</span>
  <span class="c1"># This does *not* handle the `~username` form.</span>
  <span class="n">replace_tilde_if_needed</span> <span class="o">=</span> <span class="o">-&gt;</span><span class="p">(</span><span class="n">filespec</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">filespec</span><span class="p">.</span><span class="nf">start_with?</span><span class="p">(</span><span class="s1">'~/'</span><span class="p">)</span>                    <span class="p">\</span>
        <span class="p">?</span> <span class="no">File</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="no">ENV</span><span class="p">[</span><span class="s1">'HOME'</span><span class="p">],</span> <span class="n">filespec</span><span class="p">[</span><span class="mi">2</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">])</span> <span class="p">\</span>
        <span class="p">:</span> <span class="n">filespec</span>
  <span class="k">end</span>

  <span class="c1"># When Dir[] gets a directory it returns no files.</span>
  <span class="c1"># Need to add '/*' to it.</span>
  <span class="n">add_star_to_dirspec_if_needed</span> <span class="o">=</span> <span class="o">-&gt;</span><span class="p">(</span><span class="n">filespec</span><span class="p">)</span> <span class="k">do</span>
    <span class="no">File</span><span class="p">.</span><span class="nf">directory?</span><span class="p">(</span><span class="n">filespec</span><span class="p">)</span>      <span class="p">\</span>
        <span class="p">?</span> <span class="no">File</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="n">filespec</span><span class="p">,</span> <span class="s1">'*'</span><span class="p">)</span> <span class="p">\</span>
        <span class="p">:</span> <span class="n">filespec</span>
  <span class="k">end</span>

  <span class="c1"># Default to all nonhidden files in current directory</span>
  <span class="c1"># but not its subdirectories.</span>
  <span class="no">ARGV</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">||=</span> <span class="s1">'*'</span>

  <span class="n">all_filespecs</span> <span class="o">=</span> <span class="no">ARGV</span><span class="p">.</span><span class="nf">each_with_object</span><span class="p">(</span><span class="no">Set</span><span class="p">.</span><span class="nf">new</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">filemask</span><span class="p">,</span> <span class="n">all_filespecs</span><span class="o">|</span>
    <span class="n">filemask</span> <span class="o">=</span> <span class="n">replace_tilde_if_needed</span><span class="o">.</span><span class="p">(</span><span class="n">filemask</span><span class="p">)</span>
    <span class="n">filemask</span> <span class="o">=</span> <span class="n">add_star_to_dirspec_if_needed</span><span class="o">.</span><span class="p">(</span><span class="n">filemask</span><span class="p">)</span>

    <span class="no">Dir</span><span class="p">[</span><span class="n">filemask</span><span class="p">]</span>                          <span class="p">\</span>
        <span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="o">|</span><span class="n">f</span><span class="o">|</span> <span class="no">File</span><span class="p">.</span><span class="nf">absolute_path</span><span class="p">(</span><span class="n">f</span><span class="p">)</span> <span class="p">}</span> <span class="p">\</span>
        <span class="p">.</span><span class="nf">select</span> <span class="p">{</span> <span class="o">|</span><span class="n">f</span><span class="o">|</span> <span class="no">File</span><span class="p">.</span><span class="nf">file?</span><span class="p">(</span><span class="n">f</span><span class="p">)</span> <span class="p">}</span>      <span class="p">\</span>
        <span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">filespec</span><span class="o">|</span>
      <span class="n">all_filespecs</span> <span class="o">&lt;&lt;</span> <span class="n">filespec</span>
    <span class="k">end</span>
  <span class="k">end</span>
  <span class="n">all_filespecs</span><span class="p">.</span><span class="nf">sort</span>
<span class="k">end</span>


<span class="k">def</span> <span class="nf">greeting</span>
  <span class="nb">puts</span> <span class="o">&lt;&lt;~</span><span class="no">GREETING</span><span class="sh">
      organize-av-files

      Enables the vetting of audio and video files. 

      For each file, plays it with mplayer, and prompts for what you would like to do 
      with that file, moving the file to one of the following subdirectories:

      * deletes
      * saves
      * undecideds

      This software uses mplayer to play audio files. Use cursor keys to move forwards/backwards in time.
      Press 'q' or 'ESC' to abort playback and specify disposition of that file.

      Run `man mplayer` for more on mplayer.

      Assumes all files specified are playable by mplayer.
      Creates subdirectories in the current directory: deletes, saves, undecideds.
      Logs to file '</span><span class="si">#{</span><span class="no">LOG_FILESPEC</span><span class="si">}</span><span class="sh">'

</span><span class="no">  GREETING</span>
<span class="k">end</span>


<span class="k">def</span> <span class="nf">play_file</span><span class="p">(</span><span class="n">filespec</span><span class="p">)</span>
  <span class="c1"># If you have mplayer problems, remove the redirection ("2&gt; /dev/null")</span>
  <span class="c1"># to see any errors.</span>
  <span class="sb">`mplayer </span><span class="si">#{</span><span class="n">filespec</span><span class="si">}</span><span class="sb"> 2&gt; /dev/null`</span>
<span class="k">end</span>


<span class="k">def</span> <span class="nf">disposition_prompt</span><span class="p">(</span><span class="n">filespec</span><span class="p">)</span>
  <span class="s2">"</span><span class="se">\n\n</span><span class="si">#{</span><span class="n">filespec</span><span class="si">}</span><span class="s2">:</span><span class="se">\n</span><span class="s2">s = save, d = delete, u = undecided, q = quit: "</span>
<span class="k">end</span>


<span class="k">def</span> <span class="nf">get_disposition_from_user</span>
  <span class="kp">loop</span> <span class="k">do</span>
    <span class="n">response</span> <span class="o">=</span> <span class="vg">$stdin</span><span class="p">.</span><span class="nf">gets</span><span class="p">.</span><span class="nf">chomp</span><span class="p">.</span><span class="nf">downcase</span>

    <span class="k">if</span> <span class="n">response</span> <span class="o">==</span> <span class="s1">'q'</span>
      <span class="nb">exit</span>
    <span class="k">elsif</span> <span class="sx">%w(s d u)</span><span class="p">.</span><span class="nf">include?</span><span class="p">(</span><span class="n">response</span><span class="p">)</span>
      <span class="k">return</span> <span class="p">{</span>
          <span class="s1">'s'</span> <span class="o">=&gt;</span> <span class="s1">'saves'</span><span class="p">,</span>
          <span class="s1">'d'</span> <span class="o">=&gt;</span> <span class="s1">'deletes'</span><span class="p">,</span>
          <span class="s1">'u'</span> <span class="o">=&gt;</span> <span class="s1">'undecideds'</span>
      <span class="p">}[</span><span class="n">response</span><span class="p">]</span>
    <span class="k">else</span>
      <span class="nb">print</span> <span class="s2">"s = save, d = delete, u = undecided, q = quit: "</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>


<span class="k">def</span> <span class="nf">log</span><span class="p">(</span><span class="n">filespec</span><span class="p">,</span> <span class="n">destination_subdir</span><span class="p">)</span>
  <span class="n">dest_abbrev</span> <span class="o">=</span> <span class="n">destination_subdir</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nf">upcase</span> <span class="c1"># 'S' for saves, etc.</span>
  <span class="n">log_message</span> <span class="o">=</span> <span class="s2">"</span><span class="si">#{</span><span class="n">dest_abbrev</span><span class="si">}</span><span class="s2">  </span><span class="si">#{</span><span class="no">Time</span><span class="p">.</span><span class="nf">now</span><span class="si">}</span><span class="s2">  </span><span class="si">#{</span><span class="n">filespec</span><span class="si">}</span><span class="s2">"</span>
  <span class="sb">`echo </span><span class="si">#{</span><span class="n">log_message</span><span class="si">}</span><span class="sb"> &gt;&gt; </span><span class="si">#{</span><span class="no">LOG_FILESPEC</span><span class="si">}</span><span class="sb">`</span>
<span class="k">end</span>


<span class="k">def</span> <span class="nf">main</span>
  <span class="n">check_presence_of_mplayer</span>
  <span class="n">create_dirs</span>
  <span class="nb">puts</span> <span class="n">greeting</span>
  <span class="n">files_to_process</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">filespec</span><span class="o">|</span>
    <span class="n">play_file</span><span class="p">(</span><span class="n">filespec</span><span class="p">)</span>
    <span class="nb">print</span> <span class="n">disposition_prompt</span><span class="p">(</span><span class="n">filespec</span><span class="p">)</span>
    <span class="n">destination_subdir</span> <span class="o">=</span> <span class="n">get_disposition_from_user</span>
    <span class="sb">`mv </span><span class="si">#{</span><span class="n">filespec</span><span class="si">}</span><span class="sb"> </span><span class="si">#{</span><span class="n">destination_subdir</span><span class="si">}</span><span class="sb">`</span>
    <span class="n">log</span><span class="p">(</span><span class="n">filespec</span><span class="p">,</span> <span class="n">destination_subdir</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>


<span class="n">main</span>
</code></pre></div></div>]]></content><author><name></name></author><category term="blog" /><summary type="html"><![CDATA[Using Ruby for Primitive but Productive Workflows]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.bbs-software.com/assets/images/bbs-logo-og.png" /><media:content medium="image" url="https://blog.bbs-software.com/assets/images/bbs-logo-og.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>