<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Clixel Blog</title>
    <link>https://blog.clixel.com/</link>
    <description>Recent content on Clixel Blog</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <copyright>Nate Beaty</copyright>
    <lastBuildDate>Mon, 03 Feb 2025 00:00:00 +0000</lastBuildDate><atom:link href="https://blog.clixel.com/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Implementing PHP Background Tasks With Beanstalk</title>
      <link>https://blog.clixel.com/posts/2025-02-03-implementing-php-background-tasks/</link>
      <pubDate>Mon, 03 Feb 2025 00:00:00 +0000</pubDate>
      
      <guid>https://blog.clixel.com/posts/2025-02-03-implementing-php-background-tasks/</guid>
      <description>&lt;p&gt;&lt;img src=&#34;https://blog.clixel.com/images/beanstalkin.jpg&#34; alt=&#34;a bunch of terminal log output with silly graphics on top conveying computer consternation&#34;&gt;&lt;/p&gt;
&lt;p&gt;I have a sprawling custom PHP codebase for a client that I started nearly a quarter century ago (!!), which has rapidly been increasing in complexity with database-intensive caching of various aggregate sums for customers and orders, a complex stock report / on order pipeline for products + distributors in various warehouse locations, and more long-running email + API operations that I&amp;rsquo;d like to keep out of the normal web request cycle.&lt;/p&gt;
&lt;p&gt;Over the years, I slowly expanded on a rudimentary delayed task queue, with several periodically run cron jobs which query a &lt;code&gt;cronjobs&lt;/code&gt; table for matching tasks based on &lt;code&gt;type&lt;/code&gt;, &lt;code&gt;item_id&lt;/code&gt; and &lt;code&gt;date_added&lt;/code&gt; fields. This has worked well for all sorts of needs, especially when accumulating a list of long-running tasks to do in batches late at night or every few hours, but I&amp;rsquo;ve found myself having more urgent background tasks that need to run more quickly than &amp;ldquo;sometime in the next 1-60 seconds.&amp;rdquo; The user needs to see results from their actions within a few seconds, or they&amp;rsquo;ll think it isn&amp;rsquo;t working. I considered querying &lt;code&gt;cronjobs&lt;/code&gt; and showing users a notice like &lt;em&gt;&amp;ldquo;Hey! There&amp;rsquo;s a pending update for this item. Refresh in a minute or two to see changes.&amp;rdquo;&lt;/em&gt; — but that just feels dumb. Also, managing the lifecycle and schedules of application &amp;lt;-&amp;gt; mysql &amp;lt;-&amp;gt; stand-alone script &amp;lt;-&amp;gt; cronjob is a pain and annoyingly distributed (e.g., the crontab config isn&amp;rsquo;t stored in git).&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;After much searching around, I first tried &lt;a href=&#34;https://gearman.org/&#34;&gt;Gearman&lt;/a&gt; since it had been around so long, assuming it was feature complete and solid. Overall it was pretty simple to set up, but it was a bit of a mess trying to get it running locally on my M1 MacBook, where I use homebrew to switch between a few versions of PHP&lt;sup id=&#34;fnref:2&#34;&gt;&lt;a href=&#34;#fn:2&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;2&lt;/a&gt;&lt;/sup&gt;. I kept having a &amp;ldquo;Please install libgearman&amp;rdquo; error barfed up when running &lt;code&gt;pecl install gearman&lt;/code&gt;. I tried all sorts of fixes until &lt;a href=&#34;https://stackoverflow.com/questions/9705925/how-to-install-gearman-extension-on-mamp/16295084#comment139990831_16295084&#34;&gt;I realized&lt;/a&gt; I could easily install it from source (duh! c&amp;rsquo;mon Nate), allowing me to specify necessary homebrew paths with &lt;code&gt;./configure --with-php-config=/opt/homebrew/opt/php@8.3/bin/php-config --with-gearman=$(brew --prefix gearman)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Once I had it running, I was able to set up a few worker classes and fired up &lt;a href=&#34;https://github.com/brianlmoon/GearmanManager&#34;&gt;Gearman Manager&lt;/a&gt; to handle daemonizing. Everything was working great until I realized I have some workers I would like to queue up in the future, and a few searches later I realized that would introduce even &lt;em&gt;more&lt;/em&gt; dependencies for me to manage, possibly even reverting to using my &lt;code&gt;cronjob&lt;/code&gt; setup with some tweaks for async tasks. I really wanted to keep this as simple and self-contained as possible.&lt;sup id=&#34;fnref:3&#34;&gt;&lt;a href=&#34;#fn:3&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;3&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;While searching around for delayed queue support in Gearman, I saw mention of &lt;a href=&#34;https://beanstalkd.github.io/&#34;&gt;Beanstalk&lt;/a&gt; as an alternative which supported future-dated tasks. It also seemed like Beanstalk was a bit more straightforward. Ok! Tear it all down! Let&amp;rsquo;s try this again!&lt;/p&gt;
&lt;p&gt;Switching to Beanstalk was pretty painless, but ironically there was an aspect of Gearman that it &lt;em&gt;didn&amp;rsquo;t&lt;/em&gt; support: unique keys for each task, to avoid queueing up duplicates. I saw a suggested solution that reminded me of what I was just trying to avoid: setting up your own middleware to track keys and avoid duplication. Argh! I decided to worry about it later.&lt;sup id=&#34;fnref:4&#34;&gt;&lt;a href=&#34;#fn:4&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;4&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;I started with two workers, one for order-related hooha, and one for stock report giggles. The workers basically look like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-php&#34; data-lang=&#34;php&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;?&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;php&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;require&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;__DIR__&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;/bootstrap.php&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;use&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Pheanstalk\Pheanstalk&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;use&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Pheanstalk\Values\TubeName&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$pheanstalk &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Pheanstalk&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;::&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;create&lt;/span&gt;($_ENV[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;BEANSTALKD_SERVER&amp;#39;&lt;/span&gt;]);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$tube &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;TubeName&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;orders&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$pheanstalk&lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;watch&lt;/span&gt;($tube);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// This hangs until a Job is produced
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$job &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; $pheanstalk&lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;reserve&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  $data &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;json_decode&lt;/span&gt;($job&lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;getData&lt;/span&gt;(), &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;// Merge in &amp;#34;force-run&amp;#34; flag to opts to trigger running task immediately
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  $opts &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;array_merge&lt;/span&gt;(($data[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;opts&amp;#39;&lt;/span&gt;] &lt;span style=&#34;color:#f92672&#34;&gt;??&lt;/span&gt; []), [
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;force-run&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ]);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;// Run method with &amp;#39;force-run&amp;#39; flag + original params
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;APP&lt;/span&gt;()&lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;orders&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;doFooBar&lt;/span&gt;($opts);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;// Delete job from queue
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  $pheanstalk&lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;delete&lt;/span&gt;($job);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;} &lt;span style=&#34;color:#66d9ef&#34;&gt;catch&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;\Exception&lt;/span&gt; $e) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  $error_msg &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; $e&lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;getMessage&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;// If mysql timed out, no need to log error, just release for next worker
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#f92672&#34;&gt;!&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;preg_match&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;/MySQL server has gone away/i&amp;#39;&lt;/span&gt;, $error_msg)) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;echo&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;sprintf&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;%s: Error running Job %s (#%s): %s&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;\n&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;date&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;Y-m-d H:i:s&amp;#39;&lt;/span&gt;), $data[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;task&amp;#39;&lt;/span&gt;], $job&lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;getId&lt;/span&gt;(), $error_msg);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;// Let another worker retry this job
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  $pheanstalk&lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;release&lt;/span&gt;($job);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I manage these workers with Supervisor, configured in &lt;code&gt;/etc/supervisor/conf.d/workers.conf&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-ini&#34; data-lang=&#34;ini&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;[program:orders]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;command&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;/usr/bin/php /var/www/app/workers/%(program_name)s.php&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;process_name&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;%(program_name)s_%(process_num)02d&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;stdout_logfile&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;/var/logs/supervisor/%(program_name)s.log&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;stderr_logfile&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;/var/logs/supervisor/%(program_name)s-error.log&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;user&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;www-data&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;autostart&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;autorestart&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;numprocs&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;startsecs&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;[program:stock-report]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;command&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;/usr/bin/php /var/www/app/workers/%(program_name)s.php&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;process_name&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;%(program_name)s_%(process_num)02d&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;stdout_logfile&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;/var/logs/supervisor/%(program_name)s.log&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;stderr_logfile&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;/var/logs/supervisor/%(program_name)s-error.log&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;user&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;www-data&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;autostart&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;autorestart&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;numprocs&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;startsecs&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I was having issues keeping workers alive, as many of my &amp;ldquo;long-running&amp;rdquo; tasks turned out to be &lt;em&gt;much&lt;/em&gt; quicker than supervisor liked, and it would stop respawning after a few requests (by default, &lt;code&gt;startretries=3&lt;/code&gt;). These tasks hang while waiting for a job, but as soon as a job enters the queue, they process it and quit in a fraction of a second. Supervisor was upset until I added &lt;code&gt;startsecs=0&lt;/code&gt; to the config, which specifies &amp;ldquo;the number of seconds a program must stay running after a startup to be considered successfully started.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;Originally I thought these would be daemon processes, and I might switch to that method, using a &lt;code&gt;while&lt;/code&gt; statement with &lt;code&gt;sleep&lt;/code&gt; after each job to keep it from hogging CPU. But for now, it seems to be working fine to just fire up a new process to handle each job.&lt;/p&gt;
&lt;p&gt;I set up several of my existing methods that I want to support async with a &lt;code&gt;force-run&lt;/code&gt; flag. If it&amp;rsquo;s not present, they will add the task to a Beanstalk queue&lt;sup id=&#34;fnref:5&#34;&gt;&lt;a href=&#34;#fn:5&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;5&lt;/a&gt;&lt;/sup&gt; with a JSON string of the params and return. The worker then appends &lt;code&gt;force-run&lt;/code&gt; to the params, and runs the same method in the background process.&lt;/p&gt;
&lt;p&gt;Everything has been working well for a first dabble in background/async tasks. I had a hard time finding comprehensive explanations of the entire setup + configs, so hopefully this helps any other poor sucker hacking away at a custom PHP stack.&lt;/p&gt;
&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id=&#34;fn:1&#34;&gt;
&lt;p&gt;All said, my homegrown crontab system is still in use for several scripts. I could improve the system by having a central cron pipeline, and only one &lt;code&gt;check-pending-cronjobs&lt;/code&gt; script that runs every minute. It just hasn&amp;rsquo;t hit that threshold of pain yet to inspire refactoring.&amp;#160;&lt;a href=&#34;#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:2&#34;&gt;
&lt;p&gt;I have tried several times to get on the Docker train, but it always seems to introduce more problems than solutions for my local development setup. I just have so many legacy projects + a handful of more modern Craft/Statamic sites, and for the most part they all work fine sharing the same instances of Apache/MariaDB/PHP 8.x.&amp;#160;&lt;a href=&#34;#fnref:2&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:3&#34;&gt;
&lt;p&gt;I tend to dive into all sorts of obscure tech solutions, learn enough to get it working for my needs, then not touch for a year or more. When I come back to it I have to reverse engineer my own dang work. I do leave copious comments everywhere for future Nate, but that doesn&amp;rsquo;t help much when I invariably have to run updates and suddenly there are incompatibilities and changes that make my old notes useless.&amp;#160;&lt;a href=&#34;#fnref:3&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:4&#34;&gt;
&lt;p&gt;Looking back now, I realize this isn&amp;rsquo;t worth worrying about, as most of my tasks run immediately, and &lt;em&gt;should&lt;/em&gt; run again if a duplicate is queued up soon afterward.&amp;#160;&lt;a href=&#34;#fnref:4&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:5&#34;&gt;
&lt;p&gt;Only if Beanstalk is running, otherwise it just runs the method immediately. This allows the staging site to not require its own set of supervisor&amp;rsquo;d PHP processes constantly running.&amp;#160;&lt;a href=&#34;#fnref:5&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
</description>
    </item>
    
    <item>
      <title>Getting US Timezones From Zipcodes in PHP Without an API</title>
      <link>https://blog.clixel.com/posts/getting-us-timezones-from-zipcodes-in-php-without-an-api/</link>
      <pubDate>Thu, 28 Mar 2024 00:00:00 +0000</pubDate>
      
      <guid>https://blog.clixel.com/posts/getting-us-timezones-from-zipcodes-in-php-without-an-api/</guid>
      <description>&lt;p&gt;A client wanted to batch update a customer database with timezones so their sales team could have a better idea when best to harass folks by phone. Looking for solutions to determine timezones from an address really sent me down a rabbit hole. Stack Overflow has answers that go all over the place over the last decade plus, most of them coming down to using Google Maps API, or Nominatim/OpenStreetMap via geopy in python. The first step is to determine latitude and longitude, and then either make another API call to get timezone, or some manner of querying via boundary maps. Another solution I found was to have a giant SQL table of zipcode to timezone lookups, but it was out of date, and required quite a bit of storage for such a simple task.&lt;/p&gt;
&lt;p&gt;I really didn&amp;rsquo;t need extreme accuracy, so I figured using zipcodes would be ideal. Searching around I found &lt;a href=&#34;https://github.com/farski/TZip&#34;&gt;TZip&lt;/a&gt;, a long-abandoned-but-oft-forked Ruby gem which did just this, and without any external API requirements. I seem to keep finding abandoned Ruby libraries that are &lt;em&gt;almost&lt;/em&gt; perfect solutions to obscure requirements I have, like the &lt;a href=&#34;https://github.com/natebeaty/onix&#34;&gt;ONIX gem that I forked and hacked away at myself&lt;/a&gt;, which I used in a &lt;a href=&#34;https://github.com/natebeaty/onix-sinatra&#34;&gt;simple Sinatra app&lt;/a&gt; to convert book data in JSON to ugly ONIX XML.&lt;/p&gt;
&lt;p&gt;When I peeked under the hood, I realized this Ruby library would be easy to adapt to PHP, so I could keep from having another decoupled service running that needed updating separate from my behemoth custom PHP codebase. I refactored output to match the terminology my client requested (&amp;ldquo;Eastern&amp;rdquo; instead of &amp;ldquo;America/Eastern&amp;rdquo;), and iirc made a few changes to zip lookups for changes in the last 15 years.&lt;/p&gt;
&lt;p&gt;Usage:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;$timezone = new Timezone();
echo $timezone-&amp;gt;findTimezone(27705);
// &amp;#39;Eastern&amp;#39;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I haven&amp;rsquo;t heard any complaints after using this for over a year, so I think it&amp;rsquo;s as accurate as needed! Hopefully it&amp;rsquo;s of use to someone else.&lt;/p&gt;
&lt;p&gt;Revisiting this post (which I originally started writing in early 2023, egads), I had to track down the Ruby library I mentioned, and in doing so found &lt;a href=&#34;https://github.com/infused/ziptz&#34;&gt;Ziptz&lt;/a&gt;, a newer Ruby gem that would do the trick if you need this in Ruby.&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;/**
 * Timezone lookups based on ZIP code
 */
class Timezone {

  // Zonings and mappings for determining timezone as accurately as possible
  public $zonings = [];
  public $mappings = [];

  /**
   * Init
   */
  public function __construct() {
    $this-&amp;gt;zonings[&amp;#39;Alaska&amp;#39;] = explode(&amp;#39; &amp;#39;, &amp;#39;995 996 997 998 999&amp;#39;);
    $this-&amp;gt;zonings[&amp;#39;Central&amp;#39;] =  explode(&amp;#39; &amp;#39;, &amp;#39;35 36 370 3720 3723 381 383 39&amp;#39;);
    $this-&amp;gt;zonings[&amp;#39;Central&amp;#39;] = array_merge($this-&amp;gt;zonings[&amp;#39;Central&amp;#39;], explode(&amp;#39; &amp;#39;, &amp;#39;4641&amp;#39;));
    $this-&amp;gt;zonings[&amp;#39;Central&amp;#39;] = array_merge($this-&amp;gt;zonings[&amp;#39;Central&amp;#39;], explode(&amp;#39; &amp;#39;, &amp;#39;50 51 52 53 54 55 56 57 581 582 585&amp;#39;));
    $this-&amp;gt;zonings[&amp;#39;Central&amp;#39;] = array_merge($this-&amp;gt;zonings[&amp;#39;Central&amp;#39;], explode(&amp;#39; &amp;#39;, &amp;#39;60 61 62 63 64 65 660 661 6660 672 680 681 685&amp;#39;));
    $this-&amp;gt;zonings[&amp;#39;Central&amp;#39;] = array_merge($this-&amp;gt;zonings[&amp;#39;Central&amp;#39;], explode(&amp;#39; &amp;#39;, &amp;#39;7&amp;#39;));
    $this-&amp;gt;zonings[&amp;#39;Central&amp;#39;] = array_merge($this-&amp;gt;zonings[&amp;#39;Central&amp;#39;], explode(&amp;#39; &amp;#39;, &amp;#39;371 372 373 375 377 380 382 384 385 386 387 388 389 401 420 421 422 423 424 426 427 498 498 499 580 583 584 587 588 662 664 665 666 667 668 669 670 671 673 674 675 676 677 678 679 683 684 686 687 688 689 690 691 692&amp;#39;));
    # Indiana zip prefixes span Eastern/Central timezones so the full 5 digits is needed
    $this-&amp;gt;zonings[&amp;#39;Central&amp;#39;] = array_merge($this-&amp;gt;zonings[&amp;#39;Central&amp;#39;], explode(&amp;#39; &amp;#39;, &amp;#39;47514 47515 47520 47525 47551 47574 47576 47586 47588 46301 46302 46303 46304 46307 46308 46310 46311 46312 46319 46320 46321 46322 46323 46324 46325 46327 46340 46342 46345 46346 46347 46348 46349 46350 46352 46355 46356 46360 46361 46365 46368 46371 46372 46373 46375 46376 46377 46379 46380 46381 46382 46383 46384 46385 46390 46391 46392 46393 46394 46366 46401 46402 46403 46404 46405 46407 46409 46531 46534 46968 46341 46374 46532 46552 47523 47537 47577 47601 47639 47640 47660 47943 47948&amp;#39;));

    $this-&amp;gt;zonings[&amp;#39;Eastern&amp;#39;] = explode(&amp;#39; &amp;#39;, &amp;#39;0 1 2&amp;#39;);
    $this-&amp;gt;zonings[&amp;#39;Eastern&amp;#39;] = array_merge($this-&amp;gt;zonings[&amp;#39;Eastern&amp;#39;], explode(&amp;#39; &amp;#39;, &amp;#39;30 31 32 33 34 376 379 398 399&amp;#39;));
    $this-&amp;gt;zonings[&amp;#39;Eastern&amp;#39;] = array_merge($this-&amp;gt;zonings[&amp;#39;Eastern&amp;#39;], explode(&amp;#39; &amp;#39;, &amp;#39;402 405 410 43 44 45 460 462 462 480 481 482 483 485 488 489 490 495 496 &amp;#39;));
    $this-&amp;gt;zonings[&amp;#39;Eastern&amp;#39;] = array_merge($this-&amp;gt;zonings[&amp;#39;Eastern&amp;#39;], explode(&amp;#39; &amp;#39;, &amp;#39;569&amp;#39;));
    $this-&amp;gt;zonings[&amp;#39;Eastern&amp;#39;] = array_merge($this-&amp;gt;zonings[&amp;#39;Eastern&amp;#39;], explode(&amp;#39; &amp;#39;, &amp;#39;021 034 037 038 040 041 042 046 047 048 049 053 056 058 063 064 066 067 068 069 070 074 077 085 086 088 373 374 377 378 400 401 403 404 407 408 409 411 412 413 414 415 417 418 484 486 487 491 492 493 494 497 498 499&amp;#39;));
    $this-&amp;gt;zonings[&amp;#39;Eastern&amp;#39;] = array_merge($this-&amp;gt;zonings[&amp;#39;Eastern&amp;#39;], explode(&amp;#39; &amp;#39;, &amp;#39;400 401 403 404 406 407 408 409 411 412 413 414 415 416 417 418 420 421 422 423 424 425 426 427&amp;#39;));
    # Indiana zip prefixes span Eastern/Central timezones so the full 5 digits is needed
    $this-&amp;gt;zonings[&amp;#39;Eastern&amp;#39;] = array_merge($this-&amp;gt;zonings[&amp;#39;Eastern&amp;#39;], explode(&amp;#39; &amp;#39;, &amp;#39;46112 46113 46121 46124 46130 46133 46148 46158 46160 46164 46180 46181 46186 46406 46408 46506 46511 46550 46562 46574 46705 46723 46725 46732 46747 46761 46767 46770 46776 46783 46788 46792 46910 46919 46929 46940 46950 46960 46970 46982 46991 47001 47006 47018 47040 47041 47060 47106 47118 47122 47125 47138 47140 47147 47170 47177 47224 47230 47232 47235 47244 47250 47264 47272 47281 47325 47336 47352 47354 47359 47368 47373 47384 47432 47433 47436 47438 47448 47456 47468 47513 47532 47553 47833 47834 47837 47842 47858 47868 47874 47917 47918 47926 47932 47952 47960 47970 46102 46103 46104 46105 46106 46107 46110 46111 46115 46117 46118 46120 46122 46123 46125 46126 46127 46128 46129 46131 46135 46140 46142 46143 46144 46146 46147 46149 46150 46151 46154 46155 46156 46157 46161 46162 46163 46165 46166 46167 46168 46170 46171 46172 46173 46175 46176 46182 46183 46184 46501 46502 46504 46507 46508 46510 46513 46514 46515 46516 46517 46524 46526 46527 46528 46530 46536 46537 46538 46539 46540 46542 46543 46544 46545 46546 46553 46554 46555 46556 46561 46563 46565 46567 46570 46571 46572 46573 46580 46581 46582 46590 46595 46601 46613 46614 46615 46616 46617 46619 46624 46626 46628 46634 46635 46637 46660 46680 46699 46701 46702 46703 46704 46706 46710 46711 46713 46714 46721 46730 46731 46733 46737 46738 46740 46741 46742 46743 46745 46746 46748 46750 46755 46759 46760 46763 46764 46765 46766 46769 46771 46772 46773 46774 46777 46778 46779 46780 46781 46782 46784 46785 46786 46787 46789 46791 46793 46794 46795 46796 46797 46798 46799 46801 46802 46803 46804 46805 46806 46807 46808 46809 46814 46815 46816 46818 46819 46825 46835 46845 46850 46851 46852 46853 46854 46855 46856 46857 46858 46859 46860 46861 46862 46863 46864 46865 46866 46867 46868 46869 46885 46895 46896 46897 46898 46899 46901 46902 46903 46904 46911 46912 46913 46914 46915 46916 46917 46920 46921 46922 46923 46926 46928 46930 46931 46932 46933 46935 46936 46937 46938 46939 46941 46942 46943 46945 46946 46947 46951 46952 46953 46957 46958 46959 46961 46962 46965 46967 46971 46974 46975 46977 46978 46979 46980 46984 46986 46987 46988 46989 46990 46992 46994 46995 46998 47003 47010 47012 47016 47017 47021 47022 47023 47024 47025 47030 47031 47032 47033 47034 47035 47036 47037 47039 47042 47102 47104 47107 47108 47110 47111 47112 47114 47115 47117 47119 47120 47124 47126 47129 47130 47131 47132 47133 47134 47135 47136 47141 47142 47143 47144 47146 47150 47151 47160 47161 47162 47163 47164 47165 47166 47167 47172 47190 47199 47201 47202 47203 47220 47223 47225 47226 47227 47228 47229 47231 47234 47236 47240 47243 47245 47246 47247 47249 47260 47261 47263 47265 47270 47273 47274 47280 47282 47283 47302 47303 47304 47305 47306 47307 47308 47320 47322 47324 47326 47327 47330 47331 47334 47335 47337 47338 47339 47340 47341 47342 47344 47345 47346 47348 47351 47353 47355 47356 47357 47358 47360 47361 47362 47366 47367 47369 47370 47371 47374 47375 47380 47381 47382 47383 47385 47386 47387 47388 47390 47392 47393 47394 47396 47401 47402 47403 47404 47405 47406 47407 47408 47420 47421 47424 47426 47427 47429 47431 47434 47435 47437 47439 47441 47443 47445 47446 47449 47451 47452 47453 47454 47455 47458 47459 47460 47462 47463 47464 47465 47467 47469 47470 47471 47531 47536 47550 47552 47556 47579 47610 47611 47612 47613 47615 47616 47617 47618 47619 47620 47629 47630 47631 47633 47634 47635 47637 47638 47647 47648 47649 47654 47665 47666 47670 47683 47701 47702 47703 47704 47705 47706 47708 47710 47711 47712 47713 47714 47715 47716 47719 47720 47721 47722 47724 47725 47728 47730 47731 47732 47733 47734 47735 47736 47737 47740 47747 47750 47801 47802 47803 47804 47805 47807 47808 47809 47830 47831 47832 47836 47838 47840 47841 47845 47846 47847 47848 47849 47850 47851 47852 47853 47854 47855 47857 47859 47860 47861 47862 47863 47865 47866 47869 47870 47871 47872 47875 47876 47878 47879 47880 47881 47882 47884 47885 47901 47902 47903 47904 47905 47906 47907 47909 47916 47920 47921 47922 47923 47924 47925 47928 47929 47930 47933 47940 47941 47942 47944 47949 47950 47951 47954 47955 47958 47959 47962 47963 47964 47965 47966 47967 47968 47969 47971 47974 47975 47977 47978 47980 47981 47982 47983 47984 47986 47987 47988 47989 47990 47991 47992 47993 47994 47995 47996 47997 47116 47123 47137 47145 47174 47175 47457 47501 47512 47516 47519 47521 47522 47524 47527 47528 47529 47535 47541 47542 47545 47546 47547 47549 47557 47558 47561 47562 47568 47573 47575 47578 47580 47581 47591 47596 47597 47564 47567 47584 47585 47590 47598 46985 46996 47946 47957 47011 47019 47020 47038 47043&amp;#39;));

    $this-&amp;gt;zonings[&amp;#39;Hawaii&amp;#39;] = explode(&amp;#39; &amp;#39;, &amp;#39;967 968&amp;#39;);

    $this-&amp;gt;zonings[&amp;#39;Mountain&amp;#39;] = explode(&amp;#39; &amp;#39;, &amp;#39;577&amp;#39;);
    $this-&amp;gt;zonings[&amp;#39;Mountain&amp;#39;] = array_merge($this-&amp;gt;zonings[&amp;#39;Mountain&amp;#39;], explode(&amp;#39; &amp;#39;, &amp;#39;59&amp;#39;));
    $this-&amp;gt;zonings[&amp;#39;Mountain&amp;#39;] = array_merge($this-&amp;gt;zonings[&amp;#39;Mountain&amp;#39;], explode(&amp;#39; &amp;#39;, &amp;#39;798 799&amp;#39;));
    $this-&amp;gt;zonings[&amp;#39;Mountain&amp;#39;] = array_merge($this-&amp;gt;zonings[&amp;#39;Mountain&amp;#39;], explode(&amp;#39; &amp;#39;, &amp;#39;80 81 82 83 831 84 8501 87 88&amp;#39;));
    $this-&amp;gt;zonings[&amp;#39;Mountain&amp;#39;] = array_merge($this-&amp;gt;zonings[&amp;#39;Mountain&amp;#39;], explode(&amp;#39; &amp;#39;, &amp;#39;979&amp;#39;));
    $this-&amp;gt;zonings[&amp;#39;Mountain&amp;#39;] = array_merge($this-&amp;gt;zonings[&amp;#39;Mountain&amp;#39;], explode(&amp;#39; &amp;#39;, &amp;#39;586 677 678 690 691 692 693&amp;#39;));

    $this-&amp;gt;zonings[&amp;#39;Pacific&amp;#39;] = explode(&amp;#39; &amp;#39;, &amp;#39;835 838 889 89&amp;#39;);
    $this-&amp;gt;zonings[&amp;#39;Pacific&amp;#39;] = array_merge($this-&amp;gt;zonings[&amp;#39;Pacific&amp;#39;], explode(&amp;#39; &amp;#39;, &amp;#39;90 91 92 93 94 95 960 961 962 963 964 965 969 972 973 975 977 98 990 991 992 993 994&amp;#39;));
    $this-&amp;gt;zonings[&amp;#39;Pacific&amp;#39;] = array_merge($this-&amp;gt;zonings[&amp;#39;Pacific&amp;#39;], explode(&amp;#39; &amp;#39;, &amp;#39;970 971 974 976 978&amp;#39;));
    $this-&amp;gt;zonings[&amp;#39;Pacific&amp;#39;] = array_merge($this-&amp;gt;zonings[&amp;#39;Pacific&amp;#39;], explode(&amp;#39; &amp;#39;, &amp;#39;85 86&amp;#39;)); // Arizona

    $this-&amp;gt;zonings[&amp;#39;Unused&amp;#39;] = explode(&amp;#39; &amp;#39;, &amp;#39;000 002 003 004 099&amp;#39;);
    $this-&amp;gt;zonings[&amp;#39;Unused&amp;#39;] = array_merge($this-&amp;gt;zonings[&amp;#39;Unused&amp;#39;], explode(&amp;#39; &amp;#39;, &amp;#39;213 269&amp;#39;));
    $this-&amp;gt;zonings[&amp;#39;Unused&amp;#39;] = array_merge($this-&amp;gt;zonings[&amp;#39;Unused&amp;#39;], explode(&amp;#39; &amp;#39;, &amp;#39;343 348 353&amp;#39;));
    $this-&amp;gt;zonings[&amp;#39;Unused&amp;#39;] = array_merge($this-&amp;gt;zonings[&amp;#39;Unused&amp;#39;], explode(&amp;#39; &amp;#39;, &amp;#39;419 429&amp;#39;));
    $this-&amp;gt;zonings[&amp;#39;Unused&amp;#39;] = array_merge($this-&amp;gt;zonings[&amp;#39;Unused&amp;#39;], explode(&amp;#39; &amp;#39;, &amp;#39;517 518 519 529 533 536 552 568 578 579 589&amp;#39;));
    $this-&amp;gt;zonings[&amp;#39;Unused&amp;#39;] = array_merge($this-&amp;gt;zonings[&amp;#39;Unused&amp;#39;], explode(&amp;#39; &amp;#39;, &amp;#39;621 632 642 643 659 663 682 694 695 696 697 698 699&amp;#39;));
    $this-&amp;gt;zonings[&amp;#39;Unused&amp;#39;] = array_merge($this-&amp;gt;zonings[&amp;#39;Unused&amp;#39;], explode(&amp;#39; &amp;#39;, &amp;#39;702 709 715 732 742 771&amp;#39;));
    $this-&amp;gt;zonings[&amp;#39;Unused&amp;#39;] = array_merge($this-&amp;gt;zonings[&amp;#39;Unused&amp;#39;], explode(&amp;#39; &amp;#39;, &amp;#39;817 818 819 839 848 849 854 858 861 862 866 867 868 869 876 886 887 888 892 896 899&amp;#39;));
    $this-&amp;gt;zonings[&amp;#39;Unused&amp;#39;] = array_merge($this-&amp;gt;zonings[&amp;#39;Unused&amp;#39;], explode(&amp;#39; &amp;#39;, &amp;#39;909 929 987&amp;#39;));

    // Create lookup array for determining timezone
    foreach ($this-&amp;gt;zonings as $zone =&amp;gt; $prefixes) {
      foreach ($prefixes as $prefix) {
        $this-&amp;gt;mappings[$prefix] = $zone;
      }
    }
  }

  /**
   * Find timezone for a ZIP code
   */
  public function findTimezone($zipcode) {
    $zipcode = substr(trim($zipcode), 0, 5);
    // Loop through levels of specificity to try finding a timezone
    foreach([5,4,3,2,1] as $i) {
      $chk = substr($zipcode, 0, $i);
      if (array_key_exists($chk, $this-&amp;gt;mappings)) {
        if ($this-&amp;gt;mappings[$chk] == &amp;#39;Unused&amp;#39;)
          return &amp;#39;&amp;#39;;
        else
          return $this-&amp;gt;mappings[$chk];
      }
    }
    return &amp;#39;&amp;#39;;
  }

}
&lt;/code&gt;&lt;/pre&gt;</description>
    </item>
    
    <item>
      <title>Where I Live, 2023 Edition</title>
      <link>https://blog.clixel.com/posts/where-i-live-2023-edition/</link>
      <pubDate>Sun, 08 Jan 2023 00:00:00 +0000</pubDate>
      
      <guid>https://blog.clixel.com/posts/where-i-live-2023-edition/</guid>
      <description>&lt;p&gt;I used to do these &lt;a href=&#34;https://blog.clixel.com/posts/where-i-live-2010-edition/&#34;&gt;every&lt;/a&gt; &lt;a href=&#34;https://blog.clixel.com/posts/where-i-live-2011-edition/&#34;&gt;few years&lt;/a&gt;, listing out all the software I use on a daily basis. But looking back at the last one I posted &lt;a href=&#34;https://blog.clixel.com/posts/where-i-live-2013-edition/&#34;&gt;ten years ago&lt;/a&gt; (gulp!), I realize not &lt;em&gt;that&lt;/em&gt; much has changed. Sadly? Gladly? I&amp;rsquo;m not sure tbh. Good software is timeless software, apparently. Though as a developer, I know that keeping any sort of software afloat over time is a lot of work, no matter the platform, so I appreciate that some of these are still around, let alone still best of breed.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://blog.clixel.com/images/where-i-live-2023.png&#34; alt=&#34;Icons of several apps described in the article&#34;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&#34;https://www.sublimetext.com&#34;&gt;Sublime Text&lt;/a&gt; (now at v4) is where I’ve now spent a good portion of my life for 12+ years. I know everybody&amp;rsquo;s salivating over VS Code these days, but I recently gave it a shot, as well as another earnest go at &lt;a href=&#34;https://nova.app&#34;&gt;Nova&lt;/a&gt;, and I just keep coming back to Sublime Text. It&amp;rsquo;s so damn fast, and it has come a long way with context-aware autocompletes, especially for highly-custom codebases, which I spend most of my time in. I have a few sites that now stretch back 20 years, and their quirkiness is welcome in ST-land, where VS Code and Nova don&amp;rsquo;t recognize my weird brain patterns 90% of the time.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;I&amp;rsquo;ve switched to mostly using Safari these days, because of how fast &amp;amp; lean it is, but it&amp;rsquo;s probably 50/50 with &lt;a href=&#34;https://brave.com&#34;&gt;Brave&lt;/a&gt; usage. I prefer how Brave handles View Source, and Safari drives me crazy how it so often treats a manually-entered &lt;code&gt;*.localhost&lt;/code&gt; URL as a search. If the path isn&amp;rsquo;t in your history, you have to prepend &lt;code&gt;http://&lt;/code&gt; like an animal, or you end up on a useless search page for &lt;code&gt;clixel.localhost/whatever&lt;/code&gt;. For the life of me, I can&amp;rsquo;t figure out how to tell Safari to &lt;em&gt;always&lt;/em&gt; resolve &lt;code&gt;*.localhost&lt;/code&gt; to 127.0.0.1 for dev domains.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;I used The Hit List for ages, but it started crashing consistently at some point, and seems to be abandoned by the folks that bought it (Karelia) about 5 years ago. I decided to finally give &lt;a href=&#34;https://culturedcode.com/things/&#34;&gt;Things&lt;/a&gt; a try, and haven&amp;rsquo;t looked back. It&amp;rsquo;s a very streamlined, opinionated to-do app that I love using on my Mac and iPhone (and Watch).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&#34;https://git-fork.com&#34;&gt;Fork&lt;/a&gt; has taken over as my git client from GitX. They&amp;rsquo;re very similar, but Fork has excelled at making everything a bit more elegant and streamlined, and is a paid app, so hopefully it keeps on keepin&amp;rsquo; on. It&amp;rsquo;s an amazing git GUI client, well worth the $50.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;I&amp;rsquo;m elated to say that &lt;a href=&#34;https://www.pixelmator.com/pro/&#34;&gt;Pixelmator Pro&lt;/a&gt; has been able to totally replace Photoshop for me. It&amp;rsquo;s insane what you get $50, and they often have sales where you can get it for an even more insane $20. (After leaving Firebelly in 2022, I lost my access to a CC subscription, and I refuse to give Adobe a monthly fee for their garbage apps.) I also keep up my registration and fire up &lt;a href=&#34;https://flyingmeat.com/acorn/&#34;&gt;Acorn&lt;/a&gt; often for quick image needs. Both Acorn and Pixelmator Pro are super powerful options. My dream of Adobe having strong indie competition on the Mac has finally been fulfilled.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;I still use &lt;a href=&#34;https://www.binarynights.com&#34;&gt;ForkLift&lt;/a&gt; 24/7, now at version 3. It&amp;rsquo;s an amazing dual-pane local/remote file manager, which I&amp;rsquo;ve used daily since I started these silly posts 13 years ago. I still pay for &lt;a href=&#34;https://panic.com/transmit/&#34;&gt;Transmit&lt;/a&gt; (and Nova, which has Transmit built-in), but honestly never use it unless ForkLift barfs at some obscure FTP requirement (I&amp;rsquo;m looking at you, &lt;a href=&#34;https://www.nearlyfreespeech.net&#34;&gt;nearlyfreespeech&lt;/a&gt;).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&#34;https://sequel-ace.com&#34;&gt;Sequel Ace&lt;/a&gt; has taken over for Sequel Pro, as it&amp;rsquo;s regularly updated. It&amp;rsquo;s pretty much the same layout and functionality, and still a great app for local mysql management, though I do miss the old syrup-drenched Sequel Pro icon.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&#34;https://iterm2.com&#34;&gt;iTerm2&lt;/a&gt; is still open 24/7 on my machine. I&amp;rsquo;ve tried various other terminals but always come back to iTerm.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;For notes, I switched from Notational Velocity to using &lt;a href=&#34;https://zettelkasten.de/the-archive/&#34;&gt;The Archive&lt;/a&gt; for several years, but in the last year have been using &lt;a href=&#34;https://getdrafts.com&#34;&gt;Drafts&lt;/a&gt; full time as a daily to-do and archival note repository. It&amp;rsquo;s infinitely configurable and oft-updated, plus I appreciate that it has a healthy pay-to-play setup so it will stick around for the long run.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;I&amp;rsquo;ve honestly been pretty disgusted with how Apple software has been falling apart (really&amp;hellip;it&amp;rsquo;s been &lt;em&gt;so bad&lt;/em&gt;), and Music is one of the worst. &lt;a href=&#34;https://brushedtype.co/doppler/&#34;&gt;Doppler&lt;/a&gt; is a lean &amp;amp; focused local music player for Mac that I&amp;rsquo;ve been enjoying as a replacement. The dev is super responsive and has added several features from feedback I&amp;rsquo;ve sent.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Another app still in regular use is &lt;a href=&#34;https://tapbots.com/tweetbot/mac/&#34;&gt;Tweetbot&lt;/a&gt;, but since Elon has taken over Twitter, I&amp;rsquo;ve been spending more and more time at &lt;a href=&#34;https://mastodon.art/@natebeaty&#34;&gt;Mastodon&lt;/a&gt;, and look forward to Tapbots&amp;rsquo; Ivory client. In the meantime I&amp;rsquo;ve been using the very well crafted &lt;a href=&#34;https://mastonaut.app&#34;&gt;Mastonaut&lt;/a&gt; for Mac, and the default Mastodon client for iOS.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;I still pay for &lt;a href=&#34;https://codekitapp.com&#34;&gt;CodeKit&lt;/a&gt; but only use it on a few personal projects. Usually I have a gulp or webpack setup. Hilarious version update notes are still a bonus.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Apple Mail is on the list because I&amp;rsquo;ve been doing my best to return to owning my data. I deleted my Facebook, then switched from using 100% Gmail to hosted email via &lt;a href=&#34;mailto:nate@clixel.com&#34;&gt;nate@clixel.com&lt;/a&gt; on &lt;a href=&#34;https://brushedtype.co/doppler/&#34;&gt;Opalstack&lt;/a&gt; (which came about when WebFaction shit the bed), and recently even deleted my Instagram, which I&amp;rsquo;m still ambivalent about. Everybody&amp;rsquo;s still on Instagram! But I kept finding myself hating my time spent there, and the direction they&amp;rsquo;ve been taking the app, chasing TikTok as if all services must compete for the exact same experience. Instagram was such a great site when it was solely focused on photography (eventually with a dash of short-form video) from your friends. I miss ye olde Instagram as much as I miss Flickr.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;For invoicing, I used &lt;a href=&#34;https://stuntsoftware.com/onthejob/&#34;&gt;On The Job&lt;/a&gt; for much of the last 10 years, but when Catalina came out it stopped working. After struggling with several betas and frequent issues + slow updates, I ended up writing my own web-based invoicing software (!!), and have been using that since, even utilizing it as a menubar app via &lt;a href=&#34;https://www.bzgapps.com/unite&#34;&gt;Unite&lt;/a&gt; which creates a Mac app from a webpage (previously I used &lt;a href=&#34;https://fluidapp.com&#34;&gt;Fluid&lt;/a&gt; until that also stopped being updated).&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>Moving from Fabric 1.x to Fabric 2.x</title>
      <link>https://blog.clixel.com/posts/moving-from-fabric-1-to-fabric-2/</link>
      <pubDate>Thu, 07 Jan 2021 17:48:39 -0500</pubDate>
      
      <guid>https://blog.clixel.com/posts/moving-from-fabric-1-to-fabric-2/</guid>
      <description>&lt;p&gt;I&amp;rsquo;ve long used Fabric for simple deploy scripts to essentially ssh in, &lt;code&gt;git pull&lt;/code&gt;, &lt;code&gt;composer install&lt;/code&gt; or &lt;code&gt;bundle&lt;/code&gt;, restart apps, clear caches, etc. It&amp;rsquo;s also handy for local dev shortcuts like &lt;code&gt;fab assets&lt;/code&gt; as a stand-in for the more wordy &lt;code&gt;npx gulp --production&lt;/code&gt; or &lt;code&gt;fab dev&lt;/code&gt; to simultaneously fire up sphinx, a tornado websocket app, gulp, etc.&lt;/p&gt;
&lt;p&gt;With a new M1 MacBook Air, I ran into issues getting &lt;code&gt;fabric@1.4&lt;/code&gt; to play nicely with homebrew, and decided to once again try my hand at converting my simple deploy scripts to the new v2 syntax. Information is scarce on sample Fabric 2 scripts, but I did find an aptly titled post &amp;ldquo;&lt;a href=&#34;https://vsupalov.com/fabric-2-example-fabfile/&#34;&gt;Why Is Fabric 2 so Hard?&lt;/a&gt;&amp;rdquo; which helped me get started.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s an &lt;a href=&#34;http://www.fabfile.org/upgrading.html&#34;&gt;upgrade guide&lt;/a&gt;, but my god it&amp;rsquo;s complicated, until it&amp;rsquo;s not complicated enough with the sample v1-&amp;gt;v2 migration at the end of the page.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s my simple v1 fabfile.py:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from fabric.api import *

env.hosts = [&#39;natebeaty.opalstacked.com&#39;]
env.user = &#39;deploy&#39;
env.remotepath = &#39;/home/natebeaty/apps/nb-craft-staging&#39;
env.git_branch = &#39;master&#39;
env.forward_agent = True
env.php_binary = &#39;php74&#39;

def production():
  env.remotepath = &#39;/home/natebeaty/apps/nb-craft-staging&#39;
  env.hosts = [&#39;natebeaty.com&#39;]

def assets():
  local(&#39;npx gulp --production&#39;)

def deploy(composer=&#39;y&#39;):
  update()
  if composer == &#39;y&#39;:
    composer_install()
  clear_cache()

def update():
  with cd(env.remotepath):
    run(&#39;git pull origin {0}&#39;.format(env.git_branch))

def composer_install():
  with cd(env.remotepath):
    run(&#39;%s ~/bin/composer.phar install&#39; % env.php_binary)

def clear_cache():
  with cd(env.remotepath):
    run(&#39;./craft clear-caches/compiled-templates&#39;)
    run(&#39;./craft clear-caches/data&#39;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And this is where I&amp;rsquo;m at so far with a functional v2 version:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from fabric import task
from invoke import run as local

remote_path = &amp;quot;/home/natebeaty/apps/nb-craft-staging&amp;quot;
remote_hosts = [&amp;quot;deploy@natebeaty.opalstacked.com&amp;quot;]
php_command = &amp;quot;php74&amp;quot;

# set to production
@task
def production(c):
    global remote_hosts, remote_path
    remote_hosts = [&amp;quot;deploy@natebeaty.com&amp;quot;]
    remote_path = &amp;quot;/home/natebeaty/apps/nb-craft-production&amp;quot;

# deploy
@task(hosts=remote_hosts)
def deploy(c):
    update(c)
    composer_update(c)
    clear_cache(c)

def update(c):
    c.run(&amp;quot;cd {} &amp;amp;&amp;amp; git pull&amp;quot;.format(remote_path))

def composer_update(c):
    c.run(&amp;quot;cd {} &amp;amp;&amp;amp; {} ~/bin/composer.phar install&amp;quot;.format(remote_path, php_command))

def clear_cache(c):
    c.run(&amp;quot;cd {} &amp;amp;&amp;amp; ./craft clear-caches/compiled-templates&amp;quot;.format(remote_path))
    c.run(&amp;quot;cd {} &amp;amp;&amp;amp; ./craft clear-caches/data&amp;quot;.format(remote_path))

# local commands
@task
def assets(c):
    local(&amp;quot;npx gulp --production&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Still a few things to port over, but it&amp;rsquo;s working well enough. The part that feels ugly is the use of &lt;code&gt;global&lt;/code&gt; in &lt;code&gt;production()&lt;/code&gt;, but I couldn&amp;rsquo;t find any examples of how folks are overriding connection configs in a Fabric v2 file.&lt;/p&gt;
&lt;p&gt;Using &lt;code&gt;c.local()&lt;/code&gt; I was getting errors finding &lt;code&gt;npx&lt;/code&gt; because my $PATH was different, missing the M1 homebrew &lt;code&gt;/opt/homebrew/bin&lt;/code&gt;. This &lt;a href=&#34;https://stackoverflow.com/questions/51793744/how-do-i-run-a-local-command-with-fabric-2#comment109146032_55704170&#34;&gt;StackOverflow comment&lt;/a&gt; pointed me to using &lt;code&gt;from invoke import run as local&lt;/code&gt; then &lt;code&gt;local()&lt;/code&gt; instead of &lt;code&gt;c.local()&lt;/code&gt;. This fixed my $PATH issues and &lt;code&gt;npx gulp&lt;/code&gt; worked fine.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Gitlog Automagical Timesheet Filler Outer Redux</title>
      <link>https://blog.clixel.com/posts/gitlog-automagical-timesheet-filler-outer-redux/</link>
      <pubDate>Tue, 21 May 2019 00:00:00 +0000</pubDate>
      
      <guid>https://blog.clixel.com/posts/gitlog-automagical-timesheet-filler-outer-redux/</guid>
      <description>&lt;p&gt;I&amp;rsquo;ve been using variations on this script for several years to spit out my git commit log in a timesheet-friendly format, &lt;a href=&#34;https://blog.clixel.com/post/111391696763/daily-git-log-for-your-timesheet&#34;&gt;initially just as a single command&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve now refined it to a &lt;a href=&#34;https://gist.github.com/natebeaty/b3edf108434a2d16cc600bd0686e92a8&#34;&gt;li&amp;rsquo;l bash script&lt;/a&gt; that has a few extra tricks up its sleeve (updated yet again in 2025):&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;#!/bin/bash
# Pulls git log entries for a day formatted for pasting in a timesheet

# Usage:
# gitlog = all commits today
# gitlog -2 = all commits -2 days ago
# gitlog 2024-10-03 or 10/3/2024 or 10-3

args=$1
# Replace / with - in args for alternate date formats e.g. 10/3/2024
args=${args//\//\-}

# Default day to today if no args are sent
day=`date +%F`

# Convert various argument formats to YYYY-MM-DD
if [[ $args =~ ^[0-9]{1,4}\-[0-9]{1,2}\-[0-9]{1,2}$ ]]; then
	# already in format needed
	day=$args
elif [[ $args =~ ^[0-9]{1,2}\-[0-9]{1,2}$ ]]; then
	# 10-10 -&amp;gt; 2024-10-10
	day=`date -jf %m-%d +%Y-%m-%d $args`
elif [[ $args =~ ^[0-9]{1,2}\-[0-9]{1,2}\-[0-9]{1,4}$ ]]; then
	# 10-10-2024 -&amp;gt; 2024-10-10
	day=`date -jf %m-%d-%Y +%Y-%m-%d $args`
elif [[ $args =~ ^\-[0-9]{1,2}$ ]]; then
	# -2 (days ago)
	day=`date -v&amp;#34;$args&amp;#34;d +%F`
fi

# Store output to var so we can spit it out to console then shove it in the clipboard
logs=$(git log --all --author=&amp;#34;`git config user.name`&amp;#34; --no-merges --format=&amp;#34;%s %ai&amp;#34; | grep $day | perl -pe &amp;#39;s/ (\d{4}\-\d{2}\-\d{2} \d{2}:\d{2}:\d{2} \-\d{4})//g&amp;#39; | perl -pe &amp;#39;s/^(compiled assets|pedant|cleanup|typo)\n$//g&amp;#39; | tail -r | perl -pe &amp;#39;s/\n/; /&amp;#39; | sed -e &amp;#34;s/; $//g&amp;#34;)

if [ &amp;#34;$logs&amp;#34; = &amp;#34;&amp;#34; ]; then
	echo &amp;#34;No git logs found for $day&amp;#34;
else
	echo -e &amp;#34;Log for $day (also in clipboard):\n----\n$logs&amp;#34;
	echo -e $logs | pbcopy
fi
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;You set your name in the script, drop it in your &lt;code&gt;~/bin&lt;/code&gt; dir and &lt;code&gt;chmod +x&lt;/code&gt; it, then run &lt;code&gt;gitlog&lt;/code&gt; from a repo dir with various options, the default being the current day. Use &lt;code&gt;gitlog -1&lt;/code&gt; to show commits from yesterday, or &lt;code&gt;gitlog 2025-01-01&lt;/code&gt; to show a specific date. Your git commit messages are concatenated in a format suitable for timesheets, ready to be pasted into whatever time-tracking program you prefer. e.g.:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;$ gitlog
Log for 2025-02-20 (also in clipboard):
----
slight copy edits; update highlightjs and change theme to agate; host highlightjs locally; simple dark theme
&lt;/code&gt;&lt;/pre&gt;</description>
    </item>
    
    <item>
      <title>Chrome 63 breaks local *.dev domains</title>
      <link>https://blog.clixel.com/posts/chrome-63-breaks-local-dev-domains/</link>
      <pubDate>Tue, 12 Dec 2017 00:00:00 +0000</pubDate>
      
      <guid>https://blog.clixel.com/posts/chrome-63-breaks-local-dev-domains/</guid>
      <description>&lt;p&gt;I did a complete wipe and reinstall of macOS High Sierra on my aging 2012 mbp, thinking it would solve all my random woes (false). As I was setting up my dev environment, I was baffled when both Safari and Chrome were redirecting all &lt;code&gt;*.dev&lt;/code&gt; domain requests to https. Console showed no 301 redirects. Wth? I thought it was something I messed up in Apache config and spent a few hours beating my head against the keyboard to no avail.&lt;/p&gt;

&lt;p&gt;Turns out &lt;code&gt;.dev&lt;/code&gt; is a TLD owned by Google, and they recently &lt;a href=&#34;https://ma.ttias.be/chrome-force-dev-domains-https-via-preloaded-hsts/&#34;&gt;preloaded HSTS rules for the TLD&lt;/a&gt; which means you cannot use http with &lt;code&gt;*.dev&lt;/code&gt; any more. Fun!&lt;/p&gt;

&lt;p&gt;I decided to switch all my dev domains to &lt;code&gt;*.localhost&lt;/code&gt;. (I also toyed around with adding self-signed SSL certs for each &lt;code&gt;*.dev&lt;/code&gt; domain, with some luck on my home computer and less luck on my work computer. In the end I decided to just go with switching to &lt;code&gt;*.localhost&lt;/code&gt;.)&lt;/p&gt;

&lt;p&gt;Here&amp;rsquo;s a quick rundown of things I had to do. Hopefully it helps someone.&lt;/p&gt;

&lt;p&gt;I use dnsmasq to avoid having to edit &lt;code&gt;/etc/hosts&lt;/code&gt; for every dev domain I work on. &lt;a href=&#34;https://gist.github.com/szhajdu/6911696c6f8a2a50814fe6a5724f05bf&#34;&gt;This gist&lt;/a&gt; has the latest way to do this.&lt;/p&gt;

&lt;p&gt;First, add &lt;code&gt;*.localhost&lt;/code&gt; to dnsmasq setup:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;echo &#39;address=/.localhost/127.0.0.1&#39; &amp;gt;&amp;gt; $(brew --prefix)/etc/dnsmasq.conf
sudo bash -c &#39;echo &#34;nameserver 127.0.0.1&#34; &amp;gt; /etc/resolver/localhost&#39;
sudo brew services stop dnsmasq &amp;amp;&amp;amp; sudo brew services start dnsmasq
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;ping foo.localhost&lt;/code&gt; for sanity to see if it works. May have to reboot if it doesn&amp;rsquo;t. I did, but it may have been from other finagling.&lt;/p&gt;

&lt;p&gt;I use Homebrew Apache and &lt;code&gt;mod_vhost_alias&lt;/code&gt; (&lt;a href=&#34;https://mallinson.ca/osx-web-development/&#34;&gt;good tutorial here&lt;/a&gt;) so open up the vhost config with &lt;code&gt;subl /usr/local/etc/httpd/extra/httpd-vhosts.conf&lt;/code&gt; and add this block:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;virtualhost&amp;gt;
  ServerAlias localhost *.localhost
  VirtualDocumentRoot /Users/nate/Sites/%1/web/
  UseCanonicalName Off
&amp;lt;/virtualhost&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;sudo apachectl restart&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;We use Bedrock to make Wordpress development 17% less painful (note document root above with &amp;hellip;/web/). The only thing you need to do is update your local &lt;code&gt;.env&lt;/code&gt; file with &lt;code&gt;WP_HOME=http://cfs.localhost&lt;/code&gt;. If you use the super handy &lt;a href=&#34;http://wp-cli.org/&#34;&gt;wp-cli&lt;/a&gt; you can run &lt;code&gt;wp search-replace &#39;cfs.dev&#39; &#39;cfs.localhost&#39;&lt;/code&gt; to update db references.&lt;/p&gt;

&lt;p&gt;We have long been using this &lt;a href=&#34;https://getgrav.org/blog/macos-sierra-apache-multiple-php-versions&#34;&gt;excellent guide from Grav&lt;/a&gt; as a jumping off point to set up our dev environments, which already has an update noting this change from Chrome 63.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>WebFaction Https Redirects</title>
      <link>https://blog.clixel.com/posts/webfaction-https-redirects/</link>
      <pubDate>Tue, 04 Oct 2016 00:00:00 +0000</pubDate>
      
      <guid>https://blog.clixel.com/posts/webfaction-https-redirects/</guid>
      <description>&lt;p&gt;Yet another thing to file under &amp;ldquo;beating my head against a wall for hours before figuring out something simple&amp;quot;—argh!&lt;/p&gt;

&lt;p&gt;I wanted to redirect all requests to https for a WebFaction-hosted site, and wasn&amp;rsquo;t having any luck with the normal:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;RewriteEngine On
RewriteCond %{HTTPS} !on
RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I was getting &amp;quot;too many redirects&amp;rdquo; errors. After digging around I found mention of https redirects in (now-defunct) WebFaction docs with this code:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;RewriteEngine On
RewriteCond %{HTTP:X-Forwarded-SSL} !on
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Note the &lt;code&gt;%{HTTP:X-Forwarded-SSL}&lt;/code&gt; instead of &lt;code&gt;%{HTTPS}&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Well howdy-doody it works now, hallelujah, back to more important coding matters like my &lt;a href=&#34;http://codepen.io/natebeaty/full/yJdgpj/&#34;&gt;giant SVG head navigation&lt;/a&gt;.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>libsass, node-sass, gulp-sass, my-ass</title>
      <link>https://blog.clixel.com/posts/libsass-node-sass-gulp-sass-my-ass/</link>
      <pubDate>Tue, 17 Nov 2015 00:00:00 +0000</pubDate>
      
      <guid>https://blog.clixel.com/posts/libsass-node-sass-gulp-sass-my-ass/</guid>
      <description>&lt;p&gt;This error:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Error: `libsass` bindings not found. Try reinstalling `node-sass`
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;was showing up in older Bedrock projects, causing two devs to pull some hair out—well one of them still has hair—and it turned out to be a simple update of &lt;code&gt;gulp-sass&lt;/code&gt; in the packages.json file:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;-    &#34;gulp-sass&#34;: &#34;^1.3.3&#34;,
+    &#34;gulp-sass&#34;: &#34;^2.0.0&#34;,
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;npm update&lt;/code&gt; and you&amp;rsquo;re good to go.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Sequel Pro bombing after MySQL update</title>
      <link>https://blog.clixel.com/posts/sequel-pro-bombing-after-mysql-update/</link>
      <pubDate>Tue, 17 Nov 2015 00:00:00 +0000</pubDate>
      
      <guid>https://blog.clixel.com/posts/sequel-pro-bombing-after-mysql-update/</guid>
      <description>&lt;p&gt;After a &lt;code&gt;brew update&lt;/code&gt; and &lt;code&gt;brew upgrade&lt;/code&gt;, Sequel Pro suddenly stopped connecting to localhost, bombing without much explanation. Several google searches later, I found &lt;a href=&#34;https://github.com/sequelpro/sequelpro/issues/2302&#34;&gt;this ticket&lt;/a&gt; which mentioned running mysql_upgrade.&lt;/p&gt;

&lt;p&gt;It&amp;rsquo;s possible I missed a homebrew notice about this, as it updated a ton of packages, but this ended up solving the issue:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;mysql_upgrade -u root -p&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Followed by restarting mysql:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;launchctl unload ~/Library/LaunchAgents/homebrew.mxcl.mysql.plist&lt;/code&gt;
&lt;code&gt;launchctl load ~/Library/LaunchAgents/homebrew.mxcl.mysql.plist&lt;/code&gt;&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Duotone Images With ImageMagick In Wordpress</title>
      <link>https://blog.clixel.com/posts/duotone-images-with-imagemagick-in-wordpress/</link>
      <pubDate>Tue, 12 May 2015 00:00:00 +0000</pubDate>
      
      <guid>https://blog.clixel.com/posts/duotone-images-with-imagemagick-in-wordpress/</guid>
      <description>&lt;p&gt;My dayjob studio &lt;a href=&#34;http://www.firebellydesign.com/&#34;&gt;Firebelly&lt;/a&gt; loves duotone images in their site designs. When we&amp;rsquo;re using Rails, it&amp;rsquo;s pretty easy to use Paperclip or Dragonfly to send along commands to ImageMagick on the fly, e.g.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;# Paperclip
has_attached_file :image_file, 
  styles: { wallpaper: &#34;1600x1000&amp;gt;&#34; },
  convert_options: {
    wallpaper: &#34;-quality 60 -strip -fill Black -colorize 27% -background Black \\( +clone -fill White -colorize 100 -background \&#34;Gray(70%)\&#34; -vignette 0x65+25%+5% \\) -compose multiply -composite&#34;
  }

# Dragonfly
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;We&amp;rsquo;re using this technique on a Bedrock/Sage Wordpress site, and I wanted to avoid applying this to every uploaded image via a WP hook. I ended up with this beast of a function (fugly PHP alert) that creates a duotone image using two hex values:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;/**
 * Get header bg for post, duotone treated with the random IHC_BACKGROUND + Dark Blue 
 */
function get_header_bg($post) {
  $header_bg = false;
  if (has_post_thumbnail($post-&amp;gt;ID)) {
    $thumb_id = get_post_thumbnail_id($post-&amp;gt;ID);
    $background_image = get_attached_file($thumb_id, &#39;full&#39;, true);
    $upload_dir = wp_upload_dir();
    $base_dir = $upload_dir[&#39;basedir&#39;] . &#39;/backgrounds/&#39;;

    // Build treated filename with thumb_id in case there are filename conflicts
    $treated_filename = preg_replace(&#34;/.+\/(.+)\.(\w{2,5})$/&#34;, $thumb_id.&#34;-$1-&#34;.IHC_BACKGROUND.&#34;.$2&#34;, $background_image);
    $treated_image = $base_dir . $treated_filename;

    // If treated file doesn&#39;t exist, create it
    if (!file_exists($treated_image)) {

      // Create background directory if necessary
      if(!file_exists($base_dir)) {
        mkdir($base_dir);
      }

      exec(&#39;/usr/local/bin/convert &#39;.$background_image.&#39; -colorspace gray -level +10% +level-colors &#34;#44607f&#34;,&#34;#&#39;.IHC_BACKGROUND.&#39;&#34; &#39;.$treated_image);
    }    
    $header_bg = &#39; style=&#34;background-image:url(&#39; . $upload_dir[&#39;baseurl&#39;] . &#39;/backgrounds/&#39; . $treated_filename . &#39;);&#34;&#39;;
  }
  return $header_bg;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;It&amp;rsquo;s always so difficult finding info on this sort of thing in ImageMagick forums, thought I&amp;rsquo;d give it a home in case someone else needs it. The basic command is:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;convert source.jpg -colorspace gray -level +10% +level-colors &#34;#44607f&#34;,&#34;#c1d6d8&#34; treated.jpg
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Which will give you this sort of treatment:&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;https://blog.clixel.com/images/posts/tumblr_inline_no9fbvQrRR1qzvtrf_540.jpg&#34; alt=&#34;&#34;/&gt;&lt;/p&gt;

&lt;p&gt;Update 4/2017: If newer versions of ImageMagick are giving you trouble, you can use this variation to get a similar duotone effect:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;convert in.jpg +profile &#34;*&#34; -resize 1400x -quality 65 -modulate 100,0 -size 256x1! gradient:#44607f-#C2D6D9 -clut out.jpg
&lt;/code&gt;&lt;/pre&gt;
</description>
    </item>
    
    <item>
      <title>String Interpolation Using Rails ActiveRecord Objects</title>
      <link>https://blog.clixel.com/posts/string-interpolation-using-rails-activerecord-objects/</link>
      <pubDate>Mon, 23 Feb 2015 00:00:00 +0000</pubDate>
      
      <guid>https://blog.clixel.com/posts/string-interpolation-using-rails-activerecord-objects/</guid>
      <description>&lt;p&gt;I often find myself wanting to use several attributes of a ActiveRecord object in a string, but have until now been unable to track down exactly how to do that.&lt;/p&gt;

&lt;p&gt;Say I have this simple model with a custom method:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;class Applicant &amp;lt; ActiveRecord::Base
    def full_name
      [first_name,last_name].reject{ |e| e.empty? }.join &#39; &#39;
    end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If I want to build an email header using that custom method and a few standard attributes, trying this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;subject = &#34;New Application from %{full_name} at %{city}, %{state}&#34; % @applicant
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;will cough up the error:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ArgumentError: one hash required&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Ok, so we need a hash out of our AR object. How about:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;subject = &#34;New Application from %{full_name} at %{city}, %{state}&#34; % @applicant.as_json
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;gives us this:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;KeyError: key{full_name} not found&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Right! We have to add our custom method manually:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;subject = &#34;New Application from %{full_name} at %{city}, %{state}&#34; % @applicant.as_json(methods: :full_name)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Crap! Still failing:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;KeyError: key{full_name} not found&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Turns out the hash needs to have keys that are symbols. No fear, &lt;a href=&#34;http://apidock.com/rails/Hash/symbolize_keys&#34;&gt;symbolize_keys&lt;/a&gt; to the rescue:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;subject = &#34;New Application from %{full_name} at %{city}, %{state}&#34; % @applicant.as_json(methods: :full_name).symbolize_keys
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And voila:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&#34;New Application from Nate Beaty at Chicago, IL&#34;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;For what seems like such a simple task, this has tripped me up for quite some time, and it&amp;rsquo;s been difficult to Google exactly what was needed to make this happen.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Getting Image Dimensions For Sublime</title>
      <link>https://blog.clixel.com/posts/getting-image-dimensions-for-sublime/</link>
      <pubDate>Sat, 21 Feb 2015 00:00:00 +0000</pubDate>
      
      <guid>https://blog.clixel.com/posts/getting-image-dimensions-for-sublime/</guid>
      <description>&lt;p&gt;Sublime Text doesn&amp;rsquo;t have any way to get dimensions for an image in the sidebar yet, though a sidebar API may be coming soon. &lt;sup id=&#34;fnref:fn-simplerintm&#34;&gt;&lt;a href=&#34;#fn:fn-simplerintm&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;I&amp;rsquo;ve found myself writing this simple script a few times on various machines, so I thought I&amp;rsquo;d jot it down:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;#!/bin/bash

FILENAME=`basename $1`
DIMS=`/usr/local/bin/identify -format &#34;width: %wpx;\nheight: %hpx;&#34; $1`
printf &#34;background: url($FILENAME) no-repeat;\n$DIMS\n&#34; | pbcopy
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This uses a &lt;a href=&#34;http://brew.sh/&#34;&gt;Homebrew&lt;/a&gt; installed Imagemagick&amp;rsquo;s &lt;code&gt;identify&lt;/code&gt; command to toss CSS-ready width/height into your clipboard. I then set up a custom Tool entry inside the-dusty-albeit-still-my-fav-dual-pane-file-manager &lt;a href=&#34;http://www.binarynights.com/forklift/&#34;&gt;ForkLift&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;http://i.imgur.com/rJOBfbT.png&#34; alt=&#34;ForkLift Tool prefs&#34;/&gt;&lt;/p&gt;

&lt;p&gt;And if you want, you can set a keyboard shortcut also. (Note that you have to close prefs after adding your tool, and then &lt;code&gt;cmd-,&lt;/code&gt; again to have it show up in the Keyboard Shortcuts pane—one of several minor longstanding bugs I try to ignore.)&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;http://i.imgur.com/rWMGdCm.png&#34; alt=&#34;ForkLift Keyboard prefs&#34;/&gt;&lt;/p&gt;

&lt;p&gt;Now you can select an image in ForkLift, hit the key command you specified, and then &lt;code&gt;cmd-shift-v&lt;/code&gt; will paste it into Sublime Text respecting indentation.&lt;/p&gt;

&lt;p&gt;Of course you may need to fix the relative path of the image filename. Still somewhat useful until ST3 manages to support this type of functionality from the sidebar.&lt;/p&gt;

&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;
&lt;hr&gt;&lt;ol&gt;&lt;li id=&#34;fn:fn-simplerintm&#34; role=&#34;doc-endnote&#34;&gt;
&lt;p&gt;This is something I miss from TextMate, where you can just drag an image into your code to do this, even in the ancient 1.x releases: &lt;img src=&#34;http://i.imgur.com/HLYJ9T4.gif&#34; alt=&#34;TextMate FTW&#34;/&gt; (Granted, it&amp;rsquo;s getting the relative image path wrong here anyway! You just can&amp;rsquo;t win.) &lt;a href=&#34;#fnref:fn-simplerintm&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;&lt;/div&gt;
</description>
    </item>
    
    <item>
      <title>Daily Git Log For Your Timesheet</title>
      <link>https://blog.clixel.com/posts/daily-git-log-for-your-timesheet/</link>
      <pubDate>Wed, 18 Feb 2015 00:00:00 +0000</pubDate>
      
      <guid>https://blog.clixel.com/posts/daily-git-log-for-your-timesheet/</guid>
      <description>
&lt;p&gt;&lt;strong&gt;Update:&lt;/strong&gt; I made this into &lt;a href=&#34;https://gist.github.com/natebeaty/b3edf108434a2d16cc600bd0686e92a8&#34;&gt;a bash script&lt;/a&gt; that has sensible defaults (&lt;code&gt;gitlog&lt;/code&gt; for today&#39;s log) and a few tricks (e.g., &lt;code&gt;gitlog -1&lt;/code&gt; to yank yesterday&#39;s log into the clipboard).&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;This is probably the most satisfying command-line lifehack of the year for me:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;git log --author=Nate --since=1.day.ago --all-match --format=&#39;%s&#39; | tail -r | paste -s -d : - | sed -e &#39;s/:/; /g&#39; | pbcopy
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This takes all your commit messages for the last day, concatenates them with a semicolon, and shoves them into your clipboard to paste into Harvest (or &lt;a href=&#34;http://stuntsoftware.com/onthejob/&#34;&gt;On The Job&lt;/a&gt; if I&amp;rsquo;m working freelance).&lt;/p&gt;

&lt;p&gt;Breakdown:&lt;/p&gt;

&lt;ul&gt;&lt;li&gt;&lt;code&gt;git log&lt;/code&gt; options seem pretty obvious except &lt;code&gt;--format=&#39;%s&#39;&lt;/code&gt; which outputs just the commit message on each line&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tail -r&lt;/code&gt; reverses order of lines&lt;/li&gt;
&lt;li&gt;&lt;code&gt;paste -s -d : -&lt;/code&gt; concatenates &amp;amp; separates each line with a colon&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sed -e &#39;s/:/; /g&#39;&lt;/code&gt; replaces the colon with a semicolon and a space (couldn&amp;rsquo;t figure out a way to do this in one command)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pbcopy&lt;/code&gt; shoves the result into your clipboard on OS X&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;I&amp;rsquo;m guessing there&amp;rsquo;s a slightly prettier way to do this, but there&amp;rsquo;s a reason I keep trying to get everyone to start calling me Hacksaw because I&amp;rsquo;ve always really wanted a nickname and then I could get a tattoo.&lt;/p&gt;

&lt;p&gt;You can also do a a custom span of time e.g. &lt;code&gt;--since=3.days.ago --until=1.day.ago&lt;/code&gt; if you want more than just the last day of commits.&lt;/p&gt;

&lt;p&gt;If you are in the thick of push battles with your coworker on a project and want to avoid the &amp;ldquo;Merge branch&amp;rdquo; lines from git, you can just add &lt;code&gt;sed &#39;/Merge branch/d&#39;&lt;/code&gt; to the mix:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;git log --author=Nate --since=1.day.ago --all-match --format=&#39;%s&#39; | sed &#39;/Merge branch/d&#39; | tail -r | paste -s -d : - | sed -e &#39;s/:/; /g&#39; | pbcopy
&lt;/code&gt;&lt;/pre&gt;
</description>
    </item>
    
    <item>
      <title>Yosemite Pip Install Fix</title>
      <link>https://blog.clixel.com/posts/yosemite-pip-install-fix/</link>
      <pubDate>Sat, 14 Feb 2015 00:00:00 +0000</pubDate>
      
      <guid>https://blog.clixel.com/posts/yosemite-pip-install-fix/</guid>
      <description>&lt;p&gt;I was getting compile errors when pip installing anything requiring compilation after upgrading to Yosemite, barfing out all sorts of cryptic nonsense ending with:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;error: command &#39;/usr/local/bin/gcc-4.2&#39; failed with exit status 1
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Googling that error didn&amp;rsquo;t help, and I tried all sorts of fixes until I realized the real problem was this line in my &lt;code&gt;~/.bash_profile&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;export CC=/usr/local/bin/gcc-4.2
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;When I removed that, opened a new terminal, reactivated my virtualenv and &lt;code&gt;pip install -r requirements.txt&lt;/code&gt; everything worked perfectly. Hopefully this helps someone else who&amp;rsquo;s been having issues after upgrading to Yosemite and have some lingering bash remnants from earlier dev setups.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>It&#39;s a Prepending Party</title>
      <link>https://blog.clixel.com/posts/it-s-a-prepending-party/</link>
      <pubDate>Thu, 10 Jul 2014 00:00:00 +0000</pubDate>
      
      <guid>https://blog.clixel.com/posts/it-s-a-prepending-party/</guid>
      <description>&lt;p&gt;I just realized a very powerful and simple feature of &lt;a href=&#34;https://codekitapp.com/&#34;&gt;CodeKit&lt;/a&gt; that has made my life immensely easier, as well as giving a speed boost to my sites.&lt;/p&gt;

&lt;p&gt;You can prepend all the libraries you&amp;rsquo;re using on a project at the top of your main javascript file like so:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;// @codekit-prepend &#34;mootools-core-1.5.0.js&#34;
// @codekit-prepend &#34;mootools-more-1.5.0.js&#34;
// @codekit-prepend &#34;shadowbox.js&#34;
// @codekit-prepend &#34;chosen.js&#34;
// @codekit-prepend &#34;slider.js&#34;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And then set the output to be Minified + source map (for easier debugging):&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;https://blog.clixel.com/images/posts/tumblr_inline_n8ibg3eukY1qzvtrf.png&#34; alt=&#34;Codekit options for javascript&#34;/&gt;&lt;/p&gt;

&lt;p&gt;And then in your footer you can get all your behavior with one HTTP request:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;script src=&#34;https://blog.clixel.com/js/min/application-ck.js&#34; type=&#34;text/javascript&#34;&amp;gt;&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Stupid simple, but for some reason it just hit me that this was possible, and now I use it all the time. I love finding new ways to improve my workflow with CodeKit.&lt;/p&gt;

&lt;p&gt;(Underwear drawer notes: yes, I still use &lt;a href=&#34;http://mootools.net/&#34;&gt;MooTools&lt;/a&gt; on several of my older sites, as well as scrappy ol&amp;rsquo; PHP. I have been wooed to jQuery and sexier Ruby + Python methods of coding over the years, but it&amp;rsquo;s good to keep a wide variety of skills in your tool belt.)&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Flat File Future</title>
      <link>https://blog.clixel.com/posts/flat-file-future/</link>
      <pubDate>Sun, 12 Jan 2014 00:00:00 +0000</pubDate>
      
      <guid>https://blog.clixel.com/posts/flat-file-future/</guid>
      <description>&lt;p&gt;I&amp;rsquo;m not sure exactly where I entered the rabbit hole, but I saw a mention of &lt;a href=&#34;http://statamic.com/&#34;&gt;Statamic&lt;/a&gt; somewhere, and one link led to another until I realized there is a &lt;em&gt;myriad&lt;/em&gt; of very slick, flat-file-based frameworks to choose from, in just about any language you prefer.&lt;/p&gt;

&lt;p&gt;Perhaps the idea of a barebones, well-orchestrated platform was such a relief because I&amp;rsquo;d been spending my entire weekend hand-coding &lt;a href=&#34;http://d.pr/i/HzTy&#34;&gt;PHP to send multi-part MIME&lt;/a&gt;  order confirmation emails with &lt;a href=&#34;http://d.pr/i/TiHd&#34;&gt;crusty table markup&lt;/a&gt;, in the thick of aging procedural code. From &lt;em&gt;&amp;ldquo;GOOD GOD WHAT AM I DOING WITH MY LIFE&amp;rdquo;&lt;/em&gt; to &lt;em&gt;&amp;ldquo;Oh cool, Statamic looks nice.&amp;rdquo;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Statamic just needs PHP 5.3. Pretty slim requirements&lt;sup id=&#34;fnref:fn-payupsucker&#34;&gt;&lt;a href=&#34;#fn:fn-payupsucker&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;. It uses YAML for all config files, and creating plugins looks &lt;a href=&#34;https://github.com/fasterhorses/Dots-Statamic/blob/master/pi.dots.php&#34;&gt;damn simple&lt;/a&gt;, enough to make PHP appear not-too-shabby as a language.&lt;/p&gt;

&lt;p&gt;One of the earliest to start this trend is &lt;a href=&#34;http://jekyllrb.com/&#34;&gt;Jekyll&lt;/a&gt;, which has been around for some time, looking just gorgeous in its Ruby minimalism suit of adult soothing dark techy hues. Installing it is ridiculously simple:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;gem install jekyll
jekyll create hotsauce
cd hotsauce &amp;amp;&amp;amp; jekyll serve
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Oo-lala! Like Python? I do. How about &lt;a href=&#34;https://github.com/koenbok/Cactus&#34;&gt;Cactus&lt;/a&gt;?&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sudo easy_install cactus
cactus create holysmokes
cd holysmokes &amp;amp;&amp;amp; cactus serve
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Looks familiar! Jekyll comes a little more pre-styled, but they&amp;rsquo;re both very similar concepts. And along with Statamic, they all use a variant of the Django templating language (Cactus uses the real deal), which is fantastic.&lt;/p&gt;

&lt;p&gt;But every time I get excited about using one of these handsomely simple frameworks, like &lt;a href=&#34;https://ghost.org/&#34;&gt;Ghost&lt;/a&gt;, I remember that most sites I&amp;rsquo;ve worked on just aren&amp;rsquo;t minimal enough to be served entirely by these solutions. As soon as you need products or some custom data type the client can manage, or a page that mixes &amp;amp; matches data types (just about &lt;em&gt;every site&lt;/em&gt; we do at &lt;a href=&#34;http://firebellydesign.com&#34;&gt;Firebelly&lt;/a&gt; to my chagrin), I&amp;rsquo;d be fighting against their opinionated limitations.&lt;/p&gt;

&lt;p&gt;All said, the next time I decide to redo my portfolio site, or have a simple site to build, I&amp;rsquo;m definitely going to give one of these a try. Being able to just create a new folder to extend heirarchy, work with markup entirely in inheriting templates, and version control the entire stack sounds damn refreshing.&lt;/p&gt;

&lt;p&gt;For now, back to my PHP spaghetti.&lt;/p&gt;

&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;
&lt;hr&gt;&lt;ol&gt;&lt;li id=&#34;fn:fn-payupsucker&#34; role=&#34;doc-endnote&#34;&gt;
&lt;p&gt;Though one thing about Statamic: it&amp;rsquo;s not free. I&amp;rsquo;ve noticed a trend emerging where folks are actually -gasp- charging for their work, and as a fellow developer I think this is a fine direction to go in. &lt;a href=&#34;#fnref:fn-payupsucker&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;&lt;/div&gt;
</description>
    </item>
    
    <item>
      <title>Hiding Settings Tab in Refinery 2</title>
      <link>https://blog.clixel.com/posts/hiding-settings-tab-in-refinery-2/</link>
      <pubDate>Tue, 07 Jan 2014 00:00:00 +0000</pubDate>
      
      <guid>https://blog.clixel.com/posts/hiding-settings-tab-in-refinery-2/</guid>
      <description>&lt;p&gt;Quick note of something that took me a while to find: if you install the &lt;a href=&#34;https://github.com/refinery/refinerycms-blog&#34;&gt;Refinery Blog engine&lt;/a&gt; but don&amp;rsquo;t need the Settings tab showing in the admin, you can easily hide it by adding config/initializers/refinery/settings.rb with:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Refinery::Settings.configure do |config|
  config.enable_interface = false
end
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;(This is for refinerycms / refinerycms-blog v2.1.0)&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Where I Live, 2013 Edition</title>
      <link>https://blog.clixel.com/posts/where-i-live-2013-edition/</link>
      <pubDate>Sat, 05 Oct 2013 00:00:00 +0000</pubDate>
      
      <guid>https://blog.clixel.com/posts/where-i-live-2013-edition/</guid>
      <description>&lt;p&gt;&lt;figure&gt;&lt;img alt=&#34;image of several icons from the software described below&#34; src=&#34;https://blog.clixel.com/images/where-i-live-2013.png&#34;&gt;&lt;/figure&gt;&lt;/p&gt;

&lt;ul&gt;
	&lt;li&gt;&lt;a href=&#34;http://www.sublimetext.com/&#34;&gt;Sublime Text&lt;/a&gt; (now at v3 beta) is where I’ve spent a good portion of my life over the last 2-&amp;frac12; years. It’s a great cave as far as code caves go.&lt;/li&gt;
	&lt;li&gt;Sadly Photoshop CC still sits in my toolbelt, &lt;a href=&#34;http://www.flyingmeat.com/acorn/&#34;&gt;Acorn&lt;/a&gt; is getting so damn good (same with &lt;a href=&#34;http://www.pixelmator.com/&#34;&gt;Pixelmator&lt;/a&gt;) but it’s still not quite enough to supplant this required, ever-worsening POS. I pine for the day indie apps kick the crap out of the CC suite.&lt;/li&gt;
	&lt;li&gt;&lt;a href=&#34;http://tapbots.com/software/tweetbot/mac/&#34;&gt;Tweetbot&lt;/a&gt; because I spend entirely too much time &lt;a href=&#34;https://twitter.com/natebeaty&#34;&gt;blathering in 140-char chunks&lt;/a&gt;.&lt;/li&gt;
	&lt;li&gt;&lt;a href=&#34;http://gitx.laullon.com/&#34;&gt;GitX (L)&lt;/a&gt; because it’s a perfect git GUI. And free. I’ve tried so many and keep coming back to GitX (L).&lt;br/&gt;&lt;/li&gt;
	&lt;li&gt;&lt;a href=&#34;http://stuntsoftware.com/onthejob/&#34;&gt;On The Job&lt;/a&gt; for time tracking &amp;amp; invoicing. It’s not perfect but it’s the closest I could find to my simple needs. For many years I used Billable and only switched because I didn’t like the successor, Profit Train.&lt;/li&gt;
	&lt;li&gt;Chromium because I trust Google less every day. It’s always a pain finding a download for this. I can’t even find one now.&lt;/li&gt;
	&lt;li&gt;&lt;a href=&#34;http://www.binarynights.com/&#34;&gt;ForkLift 2&lt;/a&gt; is still in my belt, because there’s nothing quite like it. I still use &lt;a href=&#34;http://panic.com/transmit/&#34;&gt;Transmit&lt;/a&gt; for DockSend. ForkLift has sadly been left to pasture by BinaryNights while they focus on Locko, a 1Password clone.&lt;/li&gt;
	&lt;li&gt;&lt;a href=&#34;http://www.potionfactory.com/thehitlist/&#34;&gt;The Hit List&lt;/a&gt; is still the best damn to-do app. I don’t opt to pay for the syncing &amp;amp; iOS app. Reminders is my on-the-go queue for tasks that I dump into Hit List when back at the desk.&lt;/li&gt;
	&lt;li&gt;&lt;a href=&#34;http://www.sequelpro.com/&#34;&gt;Sequel Pro&lt;/a&gt; for when you want to navigate MySQL locally. It’s crazy this is free. I just realized you can donate and dropped $10 in the beer fund.&lt;/li&gt;
	&lt;li&gt;&lt;a href=&#34;http://incident57.com/codekit/&#34;&gt;CodeKit&lt;/a&gt; was a random purchase to compile Less files, but has become an essential tool for Scss compiling, Javascript combining/minimizing, image optimizing, etc. Hilarious version update notes are a bonus.&lt;/li&gt;
	&lt;li&gt;&lt;a href=&#34;http://www.iterm2.com/#/section/home&#34;&gt;iTerm2&lt;/a&gt; became stable enough to replace Terminal.&lt;/li&gt;
	&lt;li&gt;&lt;a href=&#34;http://notational.net/&#34;&gt;Notational Velocity&lt;/a&gt; is the sharpest text-editing knife. Plays nice with Dropbox folder of plain text files.&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>My Instapaper Reader</title>
      <link>https://blog.clixel.com/posts/my-instapaper-reader/</link>
      <pubDate>Wed, 05 Oct 2011 00:00:00 +0000</pubDate>
      
      <guid>https://blog.clixel.com/posts/my-instapaper-reader/</guid>
      <description>&lt;figure class=&#34;tmblr-full&#34; data-orig-height=&#34;333&#34; data-orig-width=&#34;500&#34; data-orig-src=&#34;https://blog.clixel.com/images/posts/tumblr_inline_mu7mbu9Vrh1qzvtrf.jpg&#34;&gt;&lt;img src=&#34;https://blog.clixel.com/images/posts/tumblr_inline_pbq0gel8hX1qzvtrf_540.jpg&#34; alt=&#34;image&#34; data-orig-height=&#34;333&#34; data-orig-width=&#34;500&#34; data-orig-src=&#34;https://blog.clixel.com/images/posts/tumblr_inline_mu7mbu9Vrh1qzvtrf.jpg&#34;/&gt;&lt;/figure&gt;

&lt;p&gt;A more appropriate branding for my Kindle.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Where I Live, 2011 Edition</title>
      <link>https://blog.clixel.com/posts/where-i-live-2011-edition/</link>
      <pubDate>Wed, 05 Oct 2011 00:00:00 +0000</pubDate>
      
      <guid>https://blog.clixel.com/posts/where-i-live-2011-edition/</guid>
      <description>&lt;figure class=&#34;tmblr-full&#34; data-orig-height=&#34;107&#34; data-orig-width=&#34;500&#34; data-orig-src=&#34;https://blog.clixel.com/images/posts/tumblr_inline_mu7mdq6eGD1qzvtrf.png&#34;&gt;&lt;img src=&#34;https://blog.clixel.com/images/posts/tumblr_inline_pbq0gdEqCi1qzvtrf_540.png&#34; alt=&#34;image&#34; data-orig-height=&#34;107&#34; data-orig-width=&#34;500&#34; data-orig-src=&#34;https://blog.clixel.com/images/posts/tumblr_inline_mu7mdq6eGD1qzvtrf.png&#34;/&gt;&lt;/figure&gt;

&lt;p&gt;TextMate has been supplanted by a new fling, &lt;a href=&#34;http://www.sublimetext.com/2&#34;&gt;Sublime Text 2&lt;/a&gt;, shown here with my own icon:&lt;/p&gt;
&lt;figure class=&#34;tmblr-full&#34; data-orig-height=&#34;504&#34; data-orig-width=&#34;500&#34; data-orig-src=&#34;https://blog.clixel.com/images/posts/tumblr_inline_mu85h33P0P1qzvtrf.png&#34;&gt;&lt;img src=&#34;https://blog.clixel.com/images/posts/tumblr_inline_pbq0gezOT11qzvtrf_540.png&#34; alt=&#34;image&#34; data-orig-height=&#34;504&#34; data-orig-width=&#34;500&#34; data-orig-src=&#34;https://blog.clixel.com/images/posts/tumblr_inline_mu85h33P0P1qzvtrf.png&#34;/&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;

&lt;p&gt;to replace the gray turd that comes supplied. &lt;a href=&#34;http://flyingmeat.com/acorn/&#34;&gt;Acorn 3&lt;/a&gt; has finally come along far enough to replace Photoshop for most mockups and simple graphic tasks. Chrome&amp;rsquo;s slightly-enhanced version of WebKit&amp;rsquo;s Inspector is now good enough to replace the only reason to use Geezer Firefox: Firebug. &lt;a href=&#34;http://www.hogbaysoftware.com/products/taskpaper&#34;&gt;TaskPaper&lt;/a&gt; usurped the forever-beta-expiration-updating Hit List, though there&amp;rsquo;s been murmurs of activity in the long-dormant lands of Potion Factory. I&amp;rsquo;ve been leaving &lt;a href=&#34;http://panic.com/transmit/&#34;&gt;Transmit&lt;/a&gt; open for the single feature ForkLift lacks: DockSend. &lt;/p&gt;
&lt;p&gt;Dark gray version:&lt;/p&gt;
&lt;figure class=&#34;tmblr-full&#34; data-orig-height=&#34;504&#34; data-orig-width=&#34;500&#34; data-orig-src=&#34;https://blog.clixel.com/images/posts/tumblr_inline_mu85xxRqnV1qzvtrf.png&#34;&gt;&lt;img src=&#34;https://blog.clixel.com/images/posts/tumblr_inline_pbq0geLcP41qzvtrf_540.png&#34; data-orig-height=&#34;504&#34; data-orig-width=&#34;500&#34; data-orig-src=&#34;https://blog.clixel.com/images/posts/tumblr_inline_mu85xxRqnV1qzvtrf.png&#34;/&gt;&lt;/figure&gt;
</description>
    </item>
    
  </channel>
</rss>
