<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
    <channel>
      <title>Pete&#x27;s Points</title>
      <link>https://peterlyons.com</link>
      <description></description>
      <generator>Zola</generator>
      <language>en</language>
      <atom:link href="https://peterlyons.com/rss.xml" rel="self" type="application/rss+xml"/>
      <lastBuildDate>Sat, 07 Feb 2026 05:00:24 +0000</lastBuildDate>
      <item>
          <title>Year of Wayland on the Desktop</title>
          <pubDate>Sat, 07 Feb 2026 05:00:24 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2025/06/wayland/</link>
          <guid>https://peterlyons.com/problog/2025/06/wayland/</guid>
          <description xml:base="https://peterlyons.com/problog/2025/06/wayland/">&lt;h2 id=&quot;disclaimer-lengthy-draft-period&quot;&gt;Disclaimer: Lengthy Draft Period&lt;&#x2F;h2&gt;
&lt;p&gt;Most of this post was authored in June 2025 then sat around as a draft for 8 months (things got hard at work). But there&#x27;s too much material for me to fully adjust it, so this is mostly a stale post with some small updates at the bottom&lt;&#x2F;p&gt;
&lt;h2 id=&quot;x11-thank-you-for-your-service&quot;&gt;X11, thank you for your service&lt;&#x2F;h2&gt;
&lt;p&gt;After struggling with getting awesomewm+X11 to respond fast enough to my leader keybind, for like a million years, plus seeing some interesting wayland compositor projects float across hacker news, I decided to see if claude code could help me set up a wayland session configuration alongside my awesomewm+X11 setup that has been my daily driver for the last 5 years give or take.&lt;&#x2F;p&gt;
&lt;p&gt;This post will document some of the packages and configurations I have so far. This is mostly for future me to reference. It&#x27;s early days and since my work machine is a mac I don&#x27;t clock that many hours using linux on the desktop anymore, so my needs are relatively basic these days.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;experiments&quot;&gt;Experiments&lt;&#x2F;h2&gt;
&lt;p&gt;I ended up over several months putting significant work toward using niri, then KDE plasma, then hyprland, then back to niri. Each environment had a few compelling things and a few deal-breaker issues. Overall it has not been a great experience. At the moment I have no plans to go back to X11+awesome since the slowness issues I have there are a real nuisance, but overall wayland has been a pain to work with and I&#x27;ve had to cobble together every - damn - thing one at a time. Briefly, the experiment results were:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;niri try 1
&lt;ul&gt;
&lt;li&gt;overall promising&lt;&#x2F;li&gt;
&lt;li&gt;rows &amp;amp; columns layout is interesting&lt;&#x2F;li&gt;
&lt;li&gt;overview is both useful and fancy&lt;&#x2F;li&gt;
&lt;li&gt;I got tired of ONLY having keyboard shortcuts for everything&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;KDE plasma
&lt;ul&gt;
&lt;li&gt;works great for a basic traditional desktop&lt;&#x2F;li&gt;
&lt;li&gt;no performance issues&lt;&#x2F;li&gt;
&lt;li&gt;scripting KWin was very limited and quite bizarre&lt;&#x2F;li&gt;
&lt;li&gt;I would need to rely on keybinds and scripting a lot less&lt;&#x2F;li&gt;
&lt;li&gt;The script debugging workflow is awful&lt;&#x2F;li&gt;
&lt;li&gt;during this experiment, I switched display manager from lightdm to SDDM and ended up liking it and keeping it&lt;&#x2F;li&gt;
&lt;li&gt;the dotfiles situation is absolute madness and I bailed within a week. Don&#x27;t put application cache data in my &lt;code&gt;~&#x2F;.config&lt;&#x2F;code&gt; ya dingleheads!&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;hyprland
&lt;ul&gt;
&lt;li&gt;more community add-ons available relative to niri&lt;&#x2F;li&gt;
&lt;li&gt;window manager tracks focus history which is useful&lt;&#x2F;li&gt;
&lt;li&gt;window rules debugging is deeply unfun&lt;&#x2F;li&gt;
&lt;li&gt;deal breaker issues with dialog boxes in FreeCAD&lt;&#x2F;li&gt;
&lt;li&gt;1Password dialog box busted&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;niri try 2
&lt;ul&gt;
&lt;li&gt;had to go back to niri so I could use 1Password and FreeCAD&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;I find myself thinking about buying a used M3 macbook and just having a laptop that works. But for now it&#x27;s OK so on with the details.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;compositor-niri&quot;&gt;Compositor: niri&lt;&#x2F;h2&gt;
&lt;p&gt;I&#x27;m trying &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;YaLTeR&#x2F;niri&quot;&gt;niri&lt;&#x2F;a&gt; as my compositor. It seemed like this or &lt;a href=&quot;https:&#x2F;&#x2F;hypr.land&#x2F;&quot;&gt;hyprland&lt;&#x2F;a&gt; were the only 2 interesting choices. The fact that niri is written in rust, uses &lt;a href=&quot;https:&#x2F;&#x2F;kdl.dev&#x2F;&quot;&gt;kdl&lt;&#x2F;a&gt; as its config file format, and has an interesting workspace+window organization approach was enough to convince me to take it for a spin.&lt;&#x2F;p&gt;
&lt;p&gt;I was able to get my &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;canonical&#x2F;lightdm&quot;&gt;lightdm&lt;&#x2F;a&gt; display manager (login screen) configured with an additional session type and log in to niri pretty easily. This left my awesomewm+X11 setup intact so I was in safe sandbox mode for experimenting, which I had been worried might be tricky but so far all is well.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;essentials-the-same&quot;&gt;Essentials: the same&lt;&#x2F;h2&gt;
&lt;p&gt;Most of my essentials worked in wayland without any changes or with very common changes:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;ghostty terminal&lt;&#x2F;li&gt;
&lt;li&gt;kitty terminal&lt;&#x2F;li&gt;
&lt;li&gt;firefox web browser&lt;&#x2F;li&gt;
&lt;li&gt;1password password manager&lt;&#x2F;li&gt;
&lt;li&gt;obsidian note taking&lt;&#x2F;li&gt;
&lt;li&gt;music with youtube music, mpd, and ario&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;application-switching-with-gofi-leader-keys&quot;&gt;Application Switching with gofi leader keys&lt;&#x2F;h2&gt;
&lt;p&gt;My desktop interaction is based upon a leader key modal model. Tapping my leader key brings up a small app I wrote, gofi, which presents me with a menu heirarchy navigable with single-letter mappings. So for example leader then &quot;b&quot; focuses my web browser. leader then &quot;t&quot; focuses my terminal. This lets me jump to my primary apps super fast. For less common things I can nest into a menu, so for example leader then &quot;a&quot; (mnemonic &quot;applications&quot;) then &quot;c&quot; brings up a calculator. But the mechanism gofi uses to focus windows is very tightly coupled to the window manager, which for X11 was awesomewm. So gofi itself didn&#x27;t need any niri changes, but I did need a separate gofi config file just for niri, and I had to port the script I have that focuses windows by application to niri, so I wrote &lt;code&gt;~&#x2F;bin&#x2F;niri-focus&lt;&#x2F;code&gt; which uses &lt;code&gt;niri msg --json windows&lt;&#x2F;code&gt; and &lt;code&gt;jq&lt;&#x2F;code&gt; to identify windows and tell niri to switch focus to them by id.&lt;&#x2F;p&gt;
&lt;p&gt;I bound my leader key to locate the window which has gofi running inside ghostty, and if not present to start it. That all came together fairly easily although porting &lt;code&gt;~&#x2F;bin&#x2F;niri-focus&lt;&#x2F;code&gt; had a learning curve. But I&#x27;ve scripted both awesomewm and hammerspoon this way already so I had a very clear idea of how things should work.&lt;&#x2F;p&gt;
&lt;p&gt;During niri experiment 2, I found &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;MaxVerevkin&#x2F;wlr-which-key&quot;&gt;wlr-which-key&lt;&#x2F;a&gt; which is essentially an exact functional equivalent of gofi, but written in rust as a true GUI app with a floating window. I had claude code port my gofi config to wlr-which-key and it worked perfectly the first time, so now I finally have a proper leader app.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;menu-bar&quot;&gt;Menu Bar&lt;&#x2F;h2&gt;
&lt;p&gt;By default, niri has essentially no UI whatsoever. There&#x27;s no task bar or dock or anything. It seems the vibes in wayland favor this. Like ideal state after login is some fancy desktop wallpaper image and literally no other pixels. There&#x27;s even screen lockers that are just like &quot;it seems like your keyboard and mouse are broken until you type the correct password and hit enter then things just magically start working&quot;.&lt;&#x2F;p&gt;
&lt;p&gt;Anyway, wayland is weird about task bars and they call them something like layer shells or something. I have a nearly-default &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;Alexays&#x2F;Waybar&quot;&gt;waybar&lt;&#x2F;a&gt; config and it&#x27;s fine I guess. My awesomewm wibar was perfectly dialed. I had it along the left edge with a vertical window list so I could see enough of the title of every window, and the bottom left corner was a dense cluster of exactly the widgets I want and nothing I don&#x27;t.&lt;&#x2F;p&gt;
&lt;p&gt;I eventually discovered &lt;a href=&quot;https:&#x2F;&#x2F;elkowar.github.io&#x2F;eww&#x2F;eww.html&quot;&gt;eww&lt;&#x2F;a&gt; and eventually got that dialed for a vertical bar on the left edge with tray widgets at the bottom and a window list at the top. It&#x27;s very nice. The main annoying bit of glue is the &lt;code&gt;watch.sh&lt;&#x2F;code&gt; script I need to integrate eww with niri. It&#x27;s kind of silly but it mostly works and things like highlighting the focused window update in real time correctly.&lt;&#x2F;p&gt;
&lt;p&gt;I think the author of niri is on to something with this idea of your stable reference stuff like your browser going on a workspace directly adjacent to a workspace with your activity-specific tools (like an editor and terminal) so you can navigate in space horizontally for your main flow and then up&#x2F;down to reference docs etc.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;niri-keybindings-and-app-nav&quot;&gt;niri keybindings and app-nav&lt;&#x2F;h2&gt;
&lt;p&gt;I got my keybindings which I call &quot;row nav&quot; set up pretty nicely. I hold a combo on my right hand then my left hand gets 3 rows of hjkl style vim directions:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;home row is &quot;app nav&quot; which means left&#x2F;right is tabs and up&#x2F;down is windows in the same app (or something that roughly matches this paradigm)&lt;&#x2F;li&gt;
&lt;li&gt;bottom row is window manager nav so niri columns left&#x2F;right and workspaces up&#x2F;down&lt;&#x2F;li&gt;
&lt;li&gt;top row is regular arrow keys which I mostly use for &quot;up&quot; in terminal to get the previous command and this layer has enter too so I can do things like switch between several terminal tabs and re-run the previous command in them all without lifting my right hand layer mod.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The app-nav part relies on querying niri to figure out which app is focused, then dispatching the correct simulated hotkey to do things like &quot;previous tab&quot; and &quot;next tab&quot; etc. This needed to be ported to wayland as described below.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;sending-hotkeys-from-scripts-wtype&quot;&gt;sending hotkeys from scripts: wtype&lt;&#x2F;h2&gt;
&lt;p&gt;As an alternative to X11&#x27;s &lt;code&gt;xdotool&lt;&#x2F;code&gt; I ported my stuff to &lt;code&gt;wtype&lt;&#x2F;code&gt;. So far so good.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;clipboard-history-and-scripting&quot;&gt;Clipboard history and scripting&lt;&#x2F;h2&gt;
&lt;p&gt;I use the clipboard from scripts super heavily and it&#x27;s critical to many of my automations. I did get &lt;code&gt;copyq&lt;&#x2F;code&gt; working sort of which is what I use on X11, but there were some issues with its window, so I bounced over to &lt;code&gt;cliphist&lt;&#x2F;code&gt; and &lt;code&gt;wofi&lt;&#x2F;code&gt; for the chooser. I was able to get my snippet system working fairly easily with these tools.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fuzzy-chooser-wofi&quot;&gt;fuzzy chooser: wofi&lt;&#x2F;h2&gt;
&lt;p&gt;I&#x27;ve completely adopted a &lt;code&gt;dmenu&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;rofi&lt;&#x2F;code&gt; style fuzzy matching workflow to the level of &quot;it&#x27;s a complete lifestyle&quot; so I installed and started integrating &lt;code&gt;wofi&lt;&#x2F;code&gt; right away. I can&#x27;t believe fuzzy matching is not the default, but once you set the config file to fuzzy matching, it works great for my usage.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wifi-manager-iwgtk-and-iwd&quot;&gt;wifi manager: iwgtk and iwd&lt;&#x2F;h2&gt;
&lt;p&gt;On X11 I run &lt;code&gt;NetworkManager&lt;&#x2F;code&gt; and &lt;code&gt;nm-applet&lt;&#x2F;code&gt;. They are kind of clunky and I sort of hate them, but they work. There&#x27;s a bunch of silly nonsense here I don&#x27;t really want to bother with, but I ended up using &lt;code&gt;iwd&lt;&#x2F;code&gt; and &lt;code&gt;iwgtk&lt;&#x2F;code&gt;. I&#x27;m not sure why the ecosystem for this is so weird, but it is. It&#x27;s like, in the default config it will let you pick a wifi network and join it, and then like just &lt;strong&gt;not&lt;&#x2F;strong&gt; run DHCP client to actually make it work. There&#x27;s some linux mind virus that is like &quot;whoa whoa whoa we can&#x27;t assume folks want to use an Internet Protocol Address. What if they are on a commune with a local IPv6+lora configuration?&quot;. &quot;What about if they are in a deep sea submarine???&quot;. It&#x27;s ridiculous and completely stupid. So I went in to the config file like every single user must do and said &quot;yes I connect to the wifi and internet the same way billions of people do across the world&quot; and then it started working.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;notifications-dunst&quot;&gt;notifications: dunst&lt;&#x2F;h2&gt;
&lt;p&gt;My &lt;code&gt;dunst&lt;&#x2F;code&gt; setup just worked. It&#x27;s compatible with both X11 and wayland I think. Literally nothing to do. I tried my &lt;code&gt;~&#x2F;bin&#x2F;notify&lt;&#x2F;code&gt; script and it worked first time. On to the next.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;screenshots-grim-slurp-satty&quot;&gt;screenshots: grim + slurp + satty&lt;&#x2F;h2&gt;
&lt;p&gt;This one went overboard in the &quot;do one thing, even if that thing is obviously an incomplete thing&quot; so it seems the waylanders think 3 programs to take a screenshot is the correct design. Anyway I 100% just followed claude&#x27;s advice on this one and ported my scripts from flameshot to this stack of &lt;code&gt;grim&lt;&#x2F;code&gt; for the actual screenshotting plus &lt;code&gt;slurp&lt;&#x2F;code&gt; I guess for selecting a region on the screen and &lt;code&gt;satty&lt;&#x2F;code&gt; for annotating the resulting image with text and arrows etc.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;bambu-studio-3d-printing-slicer&quot;&gt;bambu studio 3D printing slicer&lt;&#x2F;h2&gt;
&lt;p&gt;So far one important app that is giving me trouble is the Bambu Studio program I use for 3D printing. It seems it does not support wayland directly and I have yet to figure out if there&#x27;s some X11 compatibility wrapping I can do to trick it in to working.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;screen-locking-swaylock&quot;&gt;screen locking: swaylock&lt;&#x2F;h2&gt;
&lt;p&gt;This is another linux mind virus of you need 7 programs to do something every operating system just does with no configuration. But anyway, detecting when user input goes idle, and actually locking the screen in response to that are apparently separate programs in wayland land. I didn&#x27;t yet want to futz with idle detection so I got a &lt;code&gt;swaylock&lt;&#x2F;code&gt; 1-liner command integrated into my &lt;code&gt;~&#x2F;bin&#x2F;lock-screen&lt;&#x2F;code&gt; script and that&#x27;s adequate.&lt;&#x2F;p&gt;
&lt;p&gt;It&#x27;s kind of funny and kind of nice that out of the box, once you&#x27;re logged in, it&#x27;ll just leave you logged in and your computer open forever. Post pandemic it&#x27;s like, no one is going to come into my house and try to figure out my extremely bizarre keyboard &amp;amp; computer setup and somehow like hack my gmail or something. Maybe I don&#x27;t need to be typing my password dozens of times a day to mitigate that attack vector like the old days of working in crowded insecure offices.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;gui-prompts-for-input-yad&quot;&gt;GUI prompts for input: yad&lt;&#x2F;h2&gt;
&lt;p&gt;It seems &lt;code&gt;yad&lt;&#x2F;code&gt; mostly supports both X11 and wayland, so the scripts where I use it to pop up a text input seem to work fine.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;leader-key-focus&quot;&gt;leader key focus&lt;&#x2F;h2&gt;
&lt;p&gt;One thing that is a huge win for me is switching keyboard focus into wlr-which-key or gofi when I hit the leader key seems to be fast enough to not bother me, and not send accidental keystrokes to the current window. I never managed to solve this on awesomewm so this the killer feature for me.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;x11-nonsense-with-xwayland&quot;&gt;X11 nonsense with xwayland&lt;&#x2F;h2&gt;
&lt;p&gt;Both Bambu Studio and Prusa Slicer seem to not support wayland so I have to do X11 compatibility sadness.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sh&quot; class=&quot;language-sh &quot;&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt; Xwayland -geometry $(wlr-randr | grep preferred| awk &amp;#x27;{print $1}&amp;#x27;) :1 &amp;amp;
 DISPLAY=:1 prusa-slicer
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;update-8-months-later&quot;&gt;Update 8 months later&lt;&#x2F;h2&gt;
&lt;p&gt;I&#x27;m still on niri and I even switched my media server laptop to niri too. There are still shortcomings and mismatches with my ideal workflow, but overall it&#x27;s excellent. My eww sidebar is great. I&#x27;ve been able to send slides to an external monitor for teaching classes without doing 20 minutes of research. There&#x27;s nothing even slightly making me miss X11. All of my absurd keyboard customization nonsense is handled by &lt;code&gt;keyd&lt;&#x2F;code&gt; which is amazing and works at the correct system level.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Guitar Pedal PCB</title>
          <pubDate>Fri, 26 Dec 2025 01:00:24 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2025/12/guitar-pedal-pcb/</link>
          <guid>https://peterlyons.com/problog/2025/12/guitar-pedal-pcb/</guid>
          <description xml:base="https://peterlyons.com/problog/2025/12/guitar-pedal-pcb/">&lt;p&gt;This fall I collaborated with my friend Brock on a guitar pedal project. It&#x27;s based on a pedal schematic called &quot;Yet Another Fuzz Face&quot;. Brock had done electronics projects but never designed and ordered a custom PCB. So I encouraged him to try inputting his schematic in KiCAD and designing a small circuit board he could mount into his guitar pedal enclosure. There was a pretty steep learning curve for KiCAD but he struggled through it and got a PCB design that looked good, was small enough to fit nicely inside the pedal, and passed the design rule checker.&lt;&#x2F;p&gt;
&lt;p&gt;So we ordered 10 prototype PCBs from &lt;a href=&quot;https:&#x2F;&#x2F;www.pcbway.com&#x2F;&quot;&gt;PCBWay&lt;&#x2F;a&gt;, which arrived in just a few days.&lt;&#x2F;p&gt;
&lt;p&gt;Here&#x27;s a video of us unboxing the circuit boards. Thanks for PCBWay for sponsoring this project and getting another hobbyist maker their first custom circuit board!&lt;&#x2F;p&gt;
&lt;div class=&quot;youtube-video-container&quot;&gt;
&lt;iframe src=&quot;https:&#x2F;&#x2F;www.youtube-nocookie.com&#x2F;embed&#x2F;e6_CZNVCopc&quot; allowfullscreen&gt;&lt;&#x2F;iframe&gt;
&lt;&#x2F;div&gt;
</description>
      </item>
      <item>
          <title>Custom CNC Desktop Mark II</title>
          <pubDate>Sun, 30 Nov 2025 21:24:39 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2025/11/custom-cnc-desktop-mark-ii/</link>
          <guid>https://peterlyons.com/problog/2025/11/custom-cnc-desktop-mark-ii/</guid>
          <description xml:base="https://peterlyons.com/problog/2025/11/custom-cnc-desktop-mark-ii/">&lt;p&gt;I made another custom desktop on the CNC with recesses for my split keyboard. I&#x27;m really happy with how it came out. I got the project finished up and installed over the Thanksgiving holiday break, and my wife painted two of the walls in my office - one has a logogram from the movie &quot;Arrival&quot;, and the other is a geometric pattern. All this adds up to make my office looking and feeling so much nicer now!&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2025&amp;#x2F;2025-11-30-01KBADM7T0REAR0DFT1BX1GR1P.2048.jpg&quot;&gt;
  &lt;figcaption&gt;custom desktop mark II&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2025&amp;#x2F;2025-11-30-01KBADQ1N0QXMREJR4KG4YMWVE.2048.jpg&quot;&gt;
  &lt;figcaption&gt;low angle shot showing the recess depth&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2025&amp;#x2F;2025-11-30-01KBADNCXG95ZAPEFMAG51WP0X.2048.jpg&quot;&gt;
  &lt;figcaption&gt;center view&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;arrival-logogram-artwork&quot;&gt;Arrival Logogram Artwork&lt;&#x2F;h2&gt;
&lt;p&gt;The movie &quot;Arrival&quot; contains my all-time favorite visual effect. The alien writing system, referred to as logograms in the movie, are these strikingly beautiful circular smoke patterns. I thought it would be a great thing to put in my office and make an awesome background for video calls. My wife has done a few paintings directly on the walls using a projector to establish a baseline pencil sketch. So that&#x27;s what she did and it came out great. On another wall I was inspired by a nearby commercial building with a geometric mountain pattern, and she did that on the adjacent wall.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2025&amp;#x2F;2025-11-28-01KB5QRD0G8M9KWQ1K6NDJKJ29.2048.jpg&quot;&gt;
  &lt;figcaption&gt;Arrival logogram artwork: Louise has question&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2025&amp;#x2F;2025-11-30-01KBADSMN8R7AMA8NR7KYFHYNH.2048.jpg&quot;&gt;
  &lt;figcaption&gt;view of whole office with geometric wall pattern, Arrival art, and desktop setups for my work and personal workstations&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;cnc-desktop-refinements&quot;&gt;CNC Desktop Refinements&lt;&#x2F;h2&gt;
&lt;p&gt;Mark I was made of 3 layers of MDF. For this version, with more CNC &amp;amp; CAM experienced, I realized I could get by with 2 layers by treating them like a sandwich and milling out the interior of both slices. So I milled the cable channels into the top piece by placing it into the CNC upside down. This also means the final assembly is 1&quot; thick and lighter weight.&lt;&#x2F;p&gt;
&lt;p&gt;I have been bumping my swivel chair into the armrests a bit, so I shrunk those down a few inches to hopefully reduce that.&lt;&#x2F;p&gt;
&lt;p&gt;I moved the coffee recess back a bit to try to leave full thickness in the center where my flexispot desk has 4 screws for attaching the desktop to the base. In the end there&#x27;s only clearance for 2 of the 4 screws, which is better. I just omit the other 2 screws because installing them they would poke up into the coffee mug recess because there&#x27;s only about 1&#x2F;4&quot; of material at that position.&lt;&#x2F;p&gt;
&lt;p&gt;I did a roundover on the edge to hopefully make it a bit more durable against dings.&lt;&#x2F;p&gt;
&lt;p&gt;Even though the desktop overall is thinner, I had a bigger depth to work with for the keyboard recess, so they are recessed quite a bit deeper such that most of the keycaps are below the desktop surface. Only the tilted angle keycaps stick out of the recess by a tiny bit at their tips.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ergonomics-and-typing-video&quot;&gt;Ergonomics and typing video&lt;&#x2F;h2&gt;
&lt;p&gt;Here&#x27;s videos of me typing at the desk both seated and standing.&lt;&#x2F;p&gt;
&lt;div class=&quot;youtube-video-container&quot;&gt;
&lt;iframe src=&quot;https:&#x2F;&#x2F;www.youtube-nocookie.com&#x2F;embed&#x2F;ZYG5xPfSfRE&quot; allowfullscreen&gt;&lt;&#x2F;iframe&gt;
&lt;&#x2F;div&gt;
&lt;h2 id=&quot;failures&quot;&gt;Failures&lt;&#x2F;h2&gt;
&lt;p&gt;I did a CNC attempt with 1&#x2F;2&quot; plywood. It did not cut well due to layers wanting to chip and splinter. That model had the cable channels too deep and the bottom of them too thin, so there was huge chunks that just exploded out entirely during CNC and it was an irrecoverable failure. After that I decided to go back to MDF which mills very consistently and nicely.&lt;&#x2F;p&gt;
&lt;p&gt;I had several CAD and CAM issues this time around. Even more than the first one which was unexpected.&lt;&#x2F;p&gt;
&lt;p&gt;I forgot to add tabs and had the dreaded projectile cutout situation at one point. Nothing went beyond the surface of the CNC though so no injuries.&lt;&#x2F;p&gt;
&lt;p&gt;In the revised CAD for this, I made a square at 7&quot; per side. Then I filleted the corners in the sketch. And for some reason when that happens, FreeCAD drops most of the dimension constraints. I re-added the 7&quot; dimension to the y axis but forgot to re-add it to the x-axis. So I ended up with rectangles 7&quot; by 6.69&quot; very unexpectedly. Definitely a head scratcher when the keyboard insert didn&#x27;t fit by a large margin. As a workaround, I had to adjust the 3D print to match those dimensions, and luckily there&#x27;s enough extra space to still fit the keyboard.&lt;&#x2F;p&gt;
&lt;p&gt;The deep recess meant my thumb would hit the edge of the keyboard holder insert, so I had to adjust the model to remove the wall there to make clearance.&lt;&#x2F;p&gt;
&lt;p&gt;Heading back to work tomorrow after the holiday break should be a lot more pleasant and inspiring surrounded by cool art and furniture I made to be highly tailored to me.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Catalog of Custom 3D Prints</title>
          <pubDate>Fri, 10 Oct 2025 18:27:36 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2025/10/catalog-of-custom-3d-prints/</link>
          <guid>https://peterlyons.com/problog/2025/10/catalog-of-custom-3d-prints/</guid>
          <description xml:base="https://peterlyons.com/problog/2025/10/catalog-of-custom-3d-prints/">&lt;p&gt;I felt a need to review all the custom CAD projects I&#x27;ve done and write up something about them. Many of them are trivial and don&#x27;t warrant any online posts or even sharing the models. But I knew I had done many, many of these and I wanted to survey them and look at patterns, skill building, actual utility, etc.&lt;&#x2F;p&gt;
&lt;p&gt;The easiest data I have at hand is my git repo of .FCStd FreeCAD project files. This represents at least 95% of my projects, but will omit a minority of things I&#x27;ve done in Fusion, TinkerCAD, or a few other CAD programs.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;stats-and-categories&quot;&gt;stats and categories&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;225 total designs&lt;&#x2F;li&gt;
&lt;li&gt;holders&#x2F;mounts&#x2F;racks: 50&lt;&#x2F;li&gt;
&lt;li&gt;bushings&#x2F;washers&#x2F;clips&#x2F;etc: 5&lt;&#x2F;li&gt;
&lt;li&gt;door related: 16&lt;&#x2F;li&gt;
&lt;li&gt;bespoke designs for friends
&lt;ul&gt;
&lt;li&gt;antique wash basin plug&lt;&#x2F;li&gt;
&lt;li&gt;mount for speaker on bike frame&lt;&#x2F;li&gt;
&lt;li&gt;replacement chair leg foot&lt;&#x2F;li&gt;
&lt;li&gt;rain barrel adapter&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;repairs for major items
&lt;ul&gt;
&lt;li&gt;wheel barrow bushing&lt;&#x2F;li&gt;
&lt;li&gt;pergola cotter pin&lt;&#x2F;li&gt;
&lt;li&gt;sun umbrella housing&lt;&#x2F;li&gt;
&lt;li&gt;bed frame bolt spacer replacement&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2024&amp;#x2F;2024-06-09-01HZYWN80GDVKR4FA9QYZEGCW7.2048.jpg&quot;&gt;
  &lt;figcaption&gt;antique wash basin plug I duplicated&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;earliest-projects&quot;&gt;earliest projects&lt;&#x2F;h2&gt;
&lt;p&gt;I&#x27;m pretty sure the first things I tried to CAD, even before I knew how to use a 3D printer, were belt clips for my everyday carry which I would then bring to the library and have them print them. At the time I had no idea how much skill and effort the slicing and printing process was, and that it was a far cry from &quot;transfer the .stl file to the printer and click print&quot;. I think I also had the library print me some tent posts for my ErgoDox split keyboard.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;easy-project-that-was-not-actually-easy&quot;&gt;easy project that was not actually easy&lt;&#x2F;h2&gt;
&lt;p&gt;This one for sure goes to a phone holder for my car dashboard. Should be easy, right? Well I iterated on this over the course of 3+ years before both giving up on supporting many phones and just designing it to exactly fit my iPhone 13 Pro, as well as finding online designs that were able to fit a lot more phones with very clever and complex mechanisms.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;most-ambitious-project&quot;&gt;most ambitious project&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a href=&quot;&#x2F;problog&#x2F;2022&#x2F;01&#x2F;squeezebox-keyboard-v2112&#x2F;&quot;&gt;SqueezeBox Keyboard&lt;&#x2F;a&gt; for sure.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;simplest-cad-that-s-actually-useful&quot;&gt;simplest CAD that&#x27;s actually useful&lt;&#x2F;h2&gt;
&lt;p&gt;&quot;TP Tube Tube&quot; literally just a cyclinder a bit narrower than a toilet paper tube that acts as an insert and rolls smoothly even if the cardboard tube is crushed and folded into an oval.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;most-generally-useful&quot;&gt;most generally useful&lt;&#x2F;h2&gt;
&lt;p&gt;Probably the &lt;a href=&quot;https:&#x2F;&#x2F;www.printables.com&#x2F;model&#x2F;1133532-index-card-case-v2&quot;&gt;index card holder&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;published-models&quot;&gt;published models&lt;&#x2F;h2&gt;
&lt;p&gt;My collection of models is &lt;a href=&quot;https:&#x2F;&#x2F;www.printables.com&#x2F;@focusaurus_121068&#x2F;models?ordering=makes&quot;&gt;here on printables.com&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;favorite-community-models&quot;&gt;favorite community models&lt;&#x2F;h2&gt;
&lt;p&gt;Things I did not design but found on printables and really like:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.printables.com&#x2F;model&#x2F;25128-light-switch-lock&quot;&gt;light switch lock&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.printables.com&#x2F;model&#x2F;104758-car-vent-gravity-phone-holder-v4&quot;&gt;Car Vent Gravity Phone Holder v4&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.printables.com&#x2F;model&#x2F;110502-better-bucket-handle-grip-no-screws&quot;&gt;bucket handle&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;some-community-notes&quot;&gt;some community notes&lt;&#x2F;h2&gt;
&lt;p&gt;The &quot;functional print&quot; subset seems way smaller than the figurines and art pieces subset of the community. A lot of hobbyist designs are just not good. Tolerances are all wrong. Fragile. Way too much material, etc. Many people (heyoo gridfinity) are willing to spend WAY more time&#x2F;effort&#x2F;material printing multipart or large systems vs just like tossing stuff into shoe boxes and amazon boxes.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Custom CNC Desktop</title>
          <pubDate>Mon, 04 Aug 2025 14:17:15 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2025/08/custom-cnc-desktop/</link>
          <guid>https://peterlyons.com/problog/2025/08/custom-cnc-desktop/</guid>
          <description xml:base="https://peterlyons.com/problog/2025/08/custom-cnc-desktop/">&lt;p&gt;I built a custom desktop on a CNC router. It&#x27;s a 3-layer stack of 1&#x2F;2&quot; MDF. It has recessed areas for my split keyboard and coffee mug. Cables can tunnel through the middle layer of the desk. No this is not a desktop PC, it&#x27;s literally the top of a desk.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2025&amp;#x2F;2025-08-03-01K1RSEKDGS47KZPZD1KXNQ8H3.2048.jpg&quot;&gt;
  &lt;figcaption&gt;desktop view&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;project-goals&quot;&gt;Project Goals&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;Replace my add-on forearm rests with a single desktop&lt;&#x2F;li&gt;
&lt;li&gt;Recess my split keyboard into the desk surface for straighter wrist ergonomics&lt;&#x2F;li&gt;
&lt;li&gt;Give my coffee a bit more of a spill-resistant setup&lt;&#x2F;li&gt;
&lt;li&gt;route cabling within channels inside the desktop interior
&lt;ul&gt;
&lt;li&gt;Especially for the cable connecting the halves of the split keyboard&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;learn enough CNC for this project&lt;&#x2F;li&gt;
&lt;li&gt;Get experience building with and painting MDF&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;a-slow-learning-curve&quot;&gt;A slow learning curve&lt;&#x2F;h2&gt;
&lt;p&gt;This was my first CNC project. It took months of calendar time to come to fruition - mostly due to scheduling and tooling overhead accessing the CNCs I had access to.&lt;&#x2F;p&gt;
&lt;p&gt;Getting this project done took several months, learning a total of 4 CAD&#x2F;CAM software processes across 3 operating systems, loading a full sheet of MDF into my van about 4 extra times, and juggling CNC schedules. The details are so boring that I can&#x27;t even bring myself to type them out. Let&#x27;s just say &quot;hey, that looks like a great CNC project&quot; does not articulate the requisite overhead.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;3d-printed-inserts&quot;&gt;3D-printed inserts&lt;&#x2F;h2&gt;
&lt;p&gt;I used basic rounded rectangles in the actual MDF with the idea of potentially wanting different inserts over time. So I modeled up inserts for my keyboard&#x27;s PCB shape using files exported from the &lt;a href=&quot;&#x2F;problog&#x2F;2024&#x2F;05&#x2F;kipra-keyboard&#x2F;&quot;&gt;ergogen process I used to make my keyboard&lt;&#x2F;a&gt;. In addition to a pocket for the PCB to fit into, I needed to clear space for the cables to be connected and a hole so cables could be routed via the desktop layer 2 tunnels.&lt;&#x2F;p&gt;
&lt;p&gt;I also made a coffee holder for my favorite coffee mug. And before you point out how silly and limiting this is - don&#x27;t you worry - I made an adapter so my second-favorite coffee mug fits too!&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2025&amp;#x2F;2025-08-04-01K1T8KFK8ZD3J78HTXFESH6WB.2048.jpg&quot;&gt;
  &lt;figcaption&gt;Insert for my Mini coffee mug&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;key-photos&quot;&gt;Key Photos&lt;&#x2F;h2&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2025&amp;#x2F;2025-08-03-01K1RSE3SGDCDAXC1DB6YKR6Z3.2048.jpg&quot;&gt;
  &lt;figcaption&gt;front view&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2025&amp;#x2F;2025-08-03-01K1RSF228YCG1HG38XX11TMF2.2048.jpg&quot;&gt;
  &lt;figcaption&gt;looking down toward the desktop&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2025&amp;#x2F;2025-08-03-01K1RNH3SGEAF6T0X0BC4GMDH1.2048.jpg&quot;&gt;
  &lt;figcaption&gt;my old setup with stock parts&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;social-media-posts&quot;&gt;Social Media Posts&lt;&#x2F;h2&gt;
&lt;p&gt;Feel free to comment, share, upvote, engage your little hearts out!&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.reddit.com&#x2F;r&#x2F;ErgoMechKeyboards&#x2F;comments&#x2F;1micni6&#x2F;split_keyboard_recessed_into_custom_cnc_desktop&#x2F;&quot;&gt;Reddit &#x2F;r&#x2F;ergomechkeyboards&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;bsky.app&#x2F;profile&#x2F;peterlyons.com&#x2F;post&#x2F;3lvo6s454rk27&quot;&gt;Bluesky&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;recurse.social&#x2F;@focusaurus&#x2F;114977320565550400&quot;&gt;Mastodon&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;full-photo-gallery-of-build-process&quot;&gt;Full Photo Gallery of Build Process&lt;&#x2F;h2&gt;



&lt;div class=&quot;plop-album&quot;&gt;
  &lt;figure class=&quot;album-viewer&quot;&gt;
    &lt;img class=&quot;album-photo&quot; src=&quot;&quot; alt=&quot;&quot;&gt;
  &lt;&#x2F;figure&gt;
  &lt;div class=&quot;album-controls&quot;&gt;
    &lt;button class=&quot;album-btn album-prev&quot; aria-label=&quot;Previous photo&quot;&gt;&amp;larr; Prev&lt;&#x2F;button&gt;
    &lt;span class=&quot;album-counter&quot;&gt;&lt;&#x2F;span&gt;
    &lt;button class=&quot;album-btn album-next&quot; aria-label=&quot;Next photo&quot;&gt;Next &amp;rarr;&lt;&#x2F;button&gt;
  &lt;&#x2F;div&gt;
  &lt;figcaption class=&quot;album-caption&quot;&gt;&lt;&#x2F;figcaption&gt;

  &lt;script&gt;
    (function() {
      &#x2F;&#x2F; Load photos from JSON data
      const photos = [{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2025&#x2F;2025-06-03-01JWVFWV28VNG0B8W12XGTZZ1T.2048.jpg&quot;,&quot;caption&quot;:&quot;CAM with vCarve&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2025&#x2F;2025-06-03-01JWVKQFW0YXJ49NPKW2NT94T7.2048.jpg&quot;,&quot;caption&quot;:&quot;Color scheme inspiration&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2025&#x2F;2025-07-09-01JZRDMT6RBXXYVPB8Q7TCCEQT.2048.jpg&quot;,&quot;caption&quot;:&quot;CNC certification test&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2025&#x2F;2025-07-09-01JZRE2GP0F44873XZ3CTJ3JY0.2048.jpg&quot;,&quot;caption&quot;:&quot;Small test job in LinuxCNC&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2025&#x2F;2025-07-09-01JZRE6JJ8F0R7GDJVZA9V5NVJ.2048.jpg&quot;,&quot;caption&quot;:&quot;Small test job success&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2025&#x2F;2025-07-27-01K15YPGNGQHTJZ9BW1PXYE297.2048.mov&quot;,&quot;caption&quot;:&quot;CNC in progress&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2025&#x2F;2025-07-27-01K15YQJV8A8EWE76ZDWYKW0S6.2048.jpg&quot;,&quot;caption&quot;:&quot;CNC in progress&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2025&#x2F;2025-07-27-01K15YTVB0Z5KB2RYK1HR15WE8.2048.jpg&quot;,&quot;caption&quot;:&quot;Cutting perimiter profile of the top&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2025&#x2F;2025-07-27-01K15YTX9G5T9W5GE2Q4RHP6NN.2048.mov&quot;,&quot;caption&quot;:&quot;CNC on the top layer&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2025&#x2F;2025-07-27-01K15ZXRH0DG0D9350XV2NFNP7.2048.jpg&quot;,&quot;caption&quot;:&quot;top layer CNC done&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2025&#x2F;2025-07-27-01K163TSG892RARA367QVF5G3E.2048.jpg&quot;,&quot;caption&quot;:&quot;top and middle layers done&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2025&#x2F;2025-07-28-01K197N1AGYSDB5F2GDCNH5K6W.2048.jpg&quot;,&quot;caption&quot;:&quot;tape layout for cable channels&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2025&#x2F;2025-07-29-01K1BV980058EKRX66P3EXKC2J.2048.jpg&quot;,&quot;caption&quot;:&quot;Cutting middle layer cable channels&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2025&#x2F;2025-07-29-01K1BW60WRJRWH4KXEGXN0M8VE.2048.jpg&quot;,&quot;caption&quot;:&quot;Plunge cuts plus hand saw&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2025&#x2F;2025-07-29-01K1BWDTWRDVNQ3SBKAYWJXPV6.2048.jpg&quot;,&quot;caption&quot;:&quot;Middle layer showing cable routes&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2025&#x2F;2025-07-30-01K1EF6JN8AG5YZBK37V7XY1D2.2048.jpg&quot;,&quot;caption&quot;:&quot;pieces staged for test rig&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2025&#x2F;2025-07-30-01K1EFDX18VHRZF6CR2PQH5GJE.2048.jpg&quot;,&quot;caption&quot;:&quot;dry fit and test rig setup&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2025&#x2F;2025-07-30-01K1EFVGJR49VE9BDK9Z5KT4PC.2048.jpg&quot;,&quot;caption&quot;:&quot;keyboard recessed into the test rig&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2025&#x2F;2025-07-30-01K1EFW06RSW7A6REEXC91X4VF.2048.jpg&quot;,&quot;caption&quot;:&quot;test rig no 3D printed inserts&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2025&#x2F;2025-07-31-01K1GJAF2RMDKWF87K5VTR17Z0.2048.jpg&quot;,&quot;caption&quot;:&quot;test rig with 3D printed inserts&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2025&#x2F;2025-08-01-01K1KB7898RFJAXAK2X30J04PZ.2048.jpg&quot;,&quot;caption&quot;:&quot;glue up middle to bottom&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2025&#x2F;2025-08-01-01K1KKQE80Q5TN0SAWH6EQ6CN2.2048.jpg&quot;,&quot;caption&quot;:&quot;more glue up with cauls&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2025&#x2F;2025-08-02-01K1N7PMX054YJFRM41EW37PXP.2048.jpg&quot;,&quot;caption&quot;:&quot;priming the cable channels&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2025&#x2F;2025-08-02-01K1NDDEGGKT1MJSKB1KSBXRJS.2048.jpg&quot;,&quot;caption&quot;:&quot;painting the interior of the cable channels&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2025&#x2F;2025-08-02-01K1NK59AG7V72X62PW8PP6RR5.2048.jpg&quot;,&quot;caption&quot;:&quot;2nd coat of paint in the cable channels&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2025&#x2F;2025-08-02-01K1NKZK40MQW74D80BW1GX27T.2048.jpg&quot;,&quot;caption&quot;:&quot;top layer glue up&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2025&#x2F;2025-08-03-01K1QNK6M0SYHKDDBEFPRSA73B.2048.jpg&quot;,&quot;caption&quot;:&quot;first coat of primer&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2025&#x2F;2025-08-03-01K1QTJZX0Y85NQFAXHSG5MS2K.2048.jpg&quot;,&quot;caption&quot;:&quot;second coat of primer&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2025&#x2F;2025-08-03-01K1QZV3S8HTDS4PNA00QGD5NJ.2048.jpg&quot;,&quot;caption&quot;:&quot;first coat of paint&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2025&#x2F;2025-08-03-01K1QZVGFGN6CDD6MS0F9X2GR6.2048.jpg&quot;,&quot;caption&quot;:&quot;first coat of paint&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2025&#x2F;2025-08-03-01K1R6QND0XTZYBHT0SZJ11QDN.2048.jpg&quot;,&quot;caption&quot;:&quot;second coat of paint&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2025&#x2F;2025-08-03-01K1RNH3SGEAF6T0X0BC4GMDH1.2048.jpg&quot;,&quot;caption&quot;:&quot;The old desktop&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2025&#x2F;2025-08-03-01K1RSE3SGDCDAXC1DB6YKR6Z3.2048.jpg&quot;,&quot;caption&quot;:&quot;new top installed&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2025&#x2F;2025-08-03-01K1RSEKDGS47KZPZD1KXNQ8H3.2048.jpg&quot;,&quot;caption&quot;:&quot;recessed split keyboard&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2025&#x2F;2025-08-03-01K1RSF228YCG1HG38XX11TMF2.2048.jpg&quot;,&quot;caption&quot;:&quot;desktop overview&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2025&#x2F;2025-08-04-01K1T8KFK8ZD3J78HTXFESH6WB.2048.jpg&quot;,&quot;caption&quot;:&quot;it fits two (2) different mugs!&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2025&#x2F;2025-05-30-01JWFRYGS0WVZJ3CC73CBN9MJH.2048.png&quot;,&quot;caption&quot;:&quot;CAM screenshot from FreeCAD&quot;}];

      &#x2F;&#x2F; Get the current script&#x27;s parent album container to scope queries
      const currentScript = document.currentScript;
      const albumContainer = currentScript.closest(&#x27;.plop-album&#x27;);

      let currentIndex = 0;
      const imgEl = albumContainer.querySelector(&#x27;.album-photo&#x27;);
      const captionEl = albumContainer.querySelector(&#x27;.album-caption&#x27;);
      const counterEl = albumContainer.querySelector(&#x27;.album-counter&#x27;);
      const prevBtn = albumContainer.querySelector(&#x27;.album-prev&#x27;);
      const nextBtn = albumContainer.querySelector(&#x27;.album-next&#x27;);

      function showPhoto(index) {
        const photo = photos[index];
        imgEl.src = photo.url;
        imgEl.alt = photo.caption || &#x27;&#x27;;

        if (photo.caption) {
          captionEl.textContent = photo.caption;
          captionEl.style.display = &#x27;block&#x27;;
        } else {
          captionEl.style.display = &#x27;none&#x27;;
        }

        counterEl.textContent = `${index + 1} &#x2F; ${photos.length}`;

        prevBtn.disabled = index === 0;
        nextBtn.disabled = index === photos.length - 1;
      }

      prevBtn.addEventListener(&#x27;click&#x27;, function() {
        if (currentIndex &gt; 0) {
          currentIndex--;
          showPhoto(currentIndex);
        }
      });

      nextBtn.addEventListener(&#x27;click&#x27;, function() {
        if (currentIndex &lt; photos.length - 1) {
          currentIndex++;
          showPhoto(currentIndex);
        }
      });

      &#x2F;&#x2F; Keyboard navigation only works when this album is focused&#x2F;hovered
      let isActive = false;

      albumContainer.addEventListener(&#x27;mouseenter&#x27;, function() {
        isActive = true;
      });

      albumContainer.addEventListener(&#x27;mouseleave&#x27;, function() {
        isActive = false;
      });

      document.addEventListener(&#x27;keydown&#x27;, function(e) {
        if (!isActive) return;

        if (e.key === &#x27;ArrowLeft&#x27; &amp;&amp; currentIndex &gt; 0) {
          currentIndex--;
          showPhoto(currentIndex);
        } else if (e.key === &#x27;ArrowRight&#x27; &amp;&amp; currentIndex &lt; photos.length - 1) {
          currentIndex++;
          showPhoto(currentIndex);
        }
      });

      imgEl.addEventListener(&#x27;click&#x27;, function(e) {
        const rect = imgEl.getBoundingClientRect();
        const clickY = e.clientY - rect.top;
        const midpoint = rect.height &#x2F; 2;

        if (clickY &lt; midpoint) {
          &#x2F;&#x2F; Clicked top half - go to next
          if (currentIndex &lt; photos.length - 1) {
            currentIndex++;
            showPhoto(currentIndex);
          }
        } else {
          &#x2F;&#x2F; Clicked bottom half - go to previous
          if (currentIndex &gt; 0) {
            currentIndex--;
            showPhoto(currentIndex);
          }
        }
      });

      if (photos.length &gt; 0) {
        showPhoto(0);
      }
    })();
  &lt;&#x2F;script&gt;
&lt;&#x2F;div&gt;
</description>
      </item>
      <item>
          <title>versiona non grata</title>
          <pubDate>Fri, 06 Jun 2025 01:28:31 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2025/06/versiona-non-grata/</link>
          <guid>https://peterlyons.com/problog/2025/06/versiona-non-grata/</guid>
          <description xml:base="https://peterlyons.com/problog/2025/06/versiona-non-grata/">&lt;p&gt;I propose to coin a new term: &lt;strong&gt;versiona non grata&lt;&#x2F;strong&gt;. This is to be used in software update mechanisms when a released version has severe problems and a forced update is warranted. You excommunicate that version and it becomes &lt;strong&gt;versiona non grata&lt;&#x2F;strong&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Blogging this in case I actually created this phrase because it is great. But maybe it already exists and I just independently coined it.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Kipra Keyboard v2 Build Video</title>
          <pubDate>Sun, 25 Aug 2024 16:38:55 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2024/08/kipra-keyboard-v2-build-video/</link>
          <guid>https://peterlyons.com/problog/2024/08/kipra-keyboard-v2-build-video/</guid>
          <description xml:base="https://peterlyons.com/problog/2024/08/kipra-keyboard-v2-build-video/">&lt;p&gt;I made another version of the kipra &quot;kinda pragmatic&quot; split ergonomic keyboard. It&#x27;s a minor incremental evolution of the first version. Key changes are:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;thumb arc moved forward and outward quite a bit
&lt;ul&gt;
&lt;li&gt;the extreme placement of v1 turned out to be not as great ergonomically as I predicted&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;don&#x27;t bother with hot swap sockets for the switches
&lt;ul&gt;
&lt;li&gt;I&#x27;m ride or die for the moment on Kailh Choc Ambient Nocturnal 20g silent linear switches&lt;&#x2F;li&gt;
&lt;li&gt;I doubt there will be any switch available that I will prefer in the next few years&lt;&#x2F;li&gt;
&lt;li&gt;direct soldering is more reliable and fewer components&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;simplify the perimeter geometry of the PCB
&lt;ul&gt;
&lt;li&gt;more pragmatic generally for fitting into cases, boxes, shelves&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;leave a blank area along the PCB perimiter for a bottom plate
&lt;ul&gt;
&lt;li&gt;also makes routing traces easier&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;reduced pinky stagger
&lt;ul&gt;
&lt;li&gt;standardized stagger unit of 1&#x2F;4 of key size&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;no mounting holes
&lt;ul&gt;
&lt;li&gt;I ended up not needing them&lt;&#x2F;li&gt;
&lt;li&gt;my new caseless approach is working great for low profile use&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;even-moar-pcbs-from-pcbway&quot;&gt;Even MOAR PCBs from PCBWay&lt;&#x2F;h2&gt;
&lt;p&gt;Once again the great &lt;a href=&quot;https:&#x2F;&#x2F;www.pcbway.com&#x2F;&quot;&gt;PCBWay&lt;&#x2F;a&gt; was supportive enough to comp me these PCBs. I recommend them for your next ergogen keeb or any custom PCB project. I switched it up this time for the white color with black silkscreen and went on-theme for me travel case with matte black &amp;amp; white PLA filament as well as printing some tilted keycaps in matte black polytera PLA.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;full-build-video&quot;&gt;Full Build Video&lt;&#x2F;h2&gt;
&lt;p&gt;I filmed a full build of my 3rd assembled kipra v2. Mostly this will be for my own reference if I do another iteration, but it&#x27;s there if anyone wants to watch.&lt;&#x2F;p&gt;
&lt;div class=&quot;youtube-video-container&quot;&gt;
&lt;iframe src=&quot;https:&#x2F;&#x2F;www.youtube-nocookie.com&#x2F;embed&#x2F;4qQcrFWpdw8&quot; allowfullscreen&gt;&lt;&#x2F;iframe&gt;
&lt;&#x2F;div&gt;
&lt;h2 id=&quot;key-point-solder-microcontroller-first-and-test-frequently&quot;&gt;Key Point: Solder Microcontroller First and Test Frequently&lt;&#x2F;h2&gt;
&lt;p&gt;The main idea I&#x27;d like to see adopted into the keeb zeitgeist is:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Test the keyboard works frequently during the build&lt;&#x2F;li&gt;
&lt;li&gt;As a corollary of that, &lt;strong&gt;the standard advice of doing diodes first is bad for beginners&lt;&#x2F;strong&gt;.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;It&#x27;s very easy to solder diodes, hotswap sockets, rotary encoders, interconnect jacks, etc and only as the last step of the build plug in the keyboard to find it does not type. It doesn&#x27;t show up as a USB device to the OS.&lt;&#x2F;p&gt;
&lt;p&gt;This is super disappointing and just no fun. I don&#x27;t want folks to have to feel that feeling and then experience the chaotic troubleshooting it necessitates.&lt;&#x2F;p&gt;
&lt;p&gt;But it&#x27;s straightforward to avoid this by changing around the order of operations to focus on testability throughout the build.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;recommended-order-of-operations-for-building-a-keyboard&quot;&gt;Recommended Order of Operations for Building a Keyboard&lt;&#x2F;h2&gt;
&lt;p&gt;The build video above demonstrates this as well as showing several mid-build tests that failed and how I was able to focus my troubleshooting exclusively on the previous step and quickly correct problems.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;flash firmware on the MCUs first
&lt;ul&gt;
&lt;li&gt;confirm they work as keyboards&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;solder pin headers to MCU (left)
&lt;ul&gt;
&lt;li&gt;confirm again it still works as a keyboard&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;solder jumper pads on the PCB (left bottom side)
&lt;ul&gt;
&lt;li&gt;test with multimeter that jumpers are properly soldered&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;solder the MCU onto the PCB (left side, MCU on top side, solder on bottom side)
&lt;ul&gt;
&lt;li&gt;(optional) could test again here it still works as a keyboard&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;solder first diode closest in the matrix to the MCU (left bottom side)&lt;&#x2F;li&gt;
&lt;li&gt;solder first switch (switch on left top, solder on bottom)
&lt;ul&gt;
&lt;li&gt;confirm it still works as a keyboard and that 1 switch types&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;now proceed with remaining diodes (left)
&lt;ul&gt;
&lt;li&gt;confirm each row with a multimeter and visually confirming all diodes are oriented correctly&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;solder remaining switches (left)
&lt;ul&gt;
&lt;li&gt;confirm every key types correctly using a keystroke debugger like &lt;code&gt;xev&lt;&#x2F;code&gt; or &lt;code&gt;Karabiner-Events-Viewer&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;solder the TRRS (mount to left top, solder on left bottom)&lt;&#x2F;li&gt;
&lt;li&gt;solder pin headers to MCU (right)
&lt;ul&gt;
&lt;li&gt;confirm again it still works as a keyboard&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;solder jumper pads on the PCB (right bottom side)
&lt;ul&gt;
&lt;li&gt;test with multimeter that jumpers are properly soldered&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;solder the MCU onto the PCB (right side, MCU on top side, solder on bottom side)
&lt;ul&gt;
&lt;li&gt;(optional) could test again here it still works as a keyboard&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;solder first diode closest in the matrix to the MCU (right bottom side)
&lt;ul&gt;
&lt;li&gt;careful that the diode orientation will be the opposite direction now&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;solder first switch (switch on right top, solder on bottom)
&lt;ul&gt;
&lt;li&gt;confirm it still works as a keyboard and that 1 switch types&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;now proceed with remaining diodes (right)
&lt;ul&gt;
&lt;li&gt;confirm each row with a multimeter and visually confirming all diodes are oriented correctly&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;solder remaining switches (right)
&lt;ul&gt;
&lt;li&gt;confirm every key types correctly using a keystroke debugger like &lt;code&gt;xev&lt;&#x2F;code&gt; or &lt;code&gt;Karabiner-Events-Viewer&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;solder the TRRS (mount to right top, solder on right bottom)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</description>
      </item>
      <item>
          <title>Dev Logs Without the Noise</title>
          <pubDate>Sat, 24 Aug 2024 20:12:16 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2024/08/dev-logs-without-the-noise/</link>
          <guid>https://peterlyons.com/problog/2024/08/dev-logs-without-the-noise/</guid>
          <description xml:base="https://peterlyons.com/problog/2024/08/dev-logs-without-the-noise/">&lt;p&gt;For local development, json logs often have a bunch of fields you might want for production observability, but almost never care about for local development. The script below is a template you can use to filter out fields you know are noise while allowing all other fields to come through unchanged. It&#x27;ll also pretty-print for easier reading. The basic approach is using &lt;code&gt;jq&lt;&#x2F;code&gt; to delete noise fields with some looseness encoded to handle unexpected schema.&lt;&#x2F;p&gt;
&lt;p&gt;The exact filters and selects will need to be tailored for your specific logs, but a project I recently worked on had things roughly as below that were usually noise I wanted to hide.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# Delete the noisy fields we usually don&amp;#x27;t care about
# in dev and let jq pretty print and colorize
jq &amp;#x27;. |
    del(.request.ip?) |
    del(.duration?) |
    del(.time?) |
    del(.env?) |
    del(.user_id?) |
    del(.msg? | select(. == &amp;quot;request served&amp;quot;)) |
    del(.level? | select(. == &amp;quot;INFO&amp;quot;)) |
    if .msg? | contains(&amp;quot;[core]&amp;quot;) then empty else . end |
    del(.revision?)&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;To use this, pipe your server&#x27;s json log output to this script. You can throw &lt;code&gt;tee&lt;&#x2F;code&gt; in the pipeline too if you want the full unfiltered log to disk, but the pretty version in your terminal.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Paginaton for Product Managers</title>
          <pubDate>Sat, 27 Jul 2024 12:26:06 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2024/07/paginaton-for-product-managers/</link>
          <guid>https://peterlyons.com/problog/2024/07/paginaton-for-product-managers/</guid>
          <description xml:base="https://peterlyons.com/problog/2024/07/paginaton-for-product-managers/">&lt;h2 id=&quot;hi-product-managers&quot;&gt;Hi product managers&lt;&#x2F;h2&gt;
&lt;p&gt;This post is written primarily for product managers instead of my usual engineering audience. I want to explain why engineers may be reluctant to build traditional numbers-based pagination. By this I mean a list page featuring page numbers counting up from page 1, often including both the total number of pages and links to jump to particular pages. &lt;code&gt;1 2 3 4 5 6 7 8 9 … 537&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-exactly-do-we-mean-by-classic-numbered-pages&quot;&gt;What exactly do we mean by classic numbered pages?&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;The page shows some fixed small-to-medium number of items&lt;&#x2F;li&gt;
&lt;li&gt;There&#x27;s page numbers starting with 1 and integers ascending from there&lt;&#x2F;li&gt;
&lt;li&gt;You can jump to pages by number with a click
&lt;ul&gt;
&lt;li&gt;You can click directly on the 5 to go to page 5&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;There might be some indication of the total number of pages&lt;&#x2F;li&gt;
&lt;li&gt;There might be a way to jump directly to the last page
&lt;ul&gt;
&lt;li&gt;Labeled either with its number of just the word &quot;last&quot;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;The URL is mostly likely shareable&#x2F;bookmarkable although not necessarily&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;functional-reasons-engineers-don-t-like-this&quot;&gt;Functional reasons engineers don&#x27;t like this&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;It&#x27;s a poor fit for changing data&lt;&#x2F;strong&gt;. Many data sets are now closer to a social media feed with a firehose of records being created at all times so the concept of the &quot;page 3&quot; is kind of stale as soon as the user sees it. If records are being bulk deleted or re-ordered, we can&#x27;t easily page through them in a coherent way from both a technical and logical perspective.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Sharing links doesn&#x27;t work&lt;&#x2F;strong&gt;. Bookmarking or sharing URLs can&#x27;t easily made to show the same data. If the user intent is &quot;ooh look at these cool widgets I found on page 4&quot; and wants to have that be the same when sharing the link, the notion of &quot;page 4&quot; isn&#x27;t right and creating a token-based permalink would require a fair bit of machinery.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;It&#x27;s an inferior approach to discovery&lt;&#x2F;strong&gt;. Relying heavily on paging linearly through a long list of data often means there&#x27;s opportunity for better search and browse features and workflows. Maybe higher-quality free text search would work better. (Aside, is any one else stupefied by how utterly bad we are at free text search, like for anything anywhere?). Maybe category-based browsing would help. Maybe curated lists would help. If users are clicking page 8, guaging some piece of data there like the price of an items, then estimating their item might be around page 20, we could probably build a better tool to facilitate their search tactic.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;It&#x27;s lazy and sad&lt;&#x2F;strong&gt;. I get &quot;contractors built a paginated list page with zero care or thought, emailed us a huge invoice, and bounced&quot; vibes. Don&#x27;t let agencies that don&#x27;t care about user experience and quality software do work for you!&lt;&#x2F;p&gt;
&lt;h2 id=&quot;technical-reasons-engineers-don-t-like-this&quot;&gt;Technical reasons engineers don&#x27;t like this&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;It can be catastrophically slow in most databases&lt;&#x2F;strong&gt;. I think most of the engineering resistance originates here. Most databases cannot perform these queries efficiently. Or at least, the naive implementation cannot - see below for more technical notes. Having to know the total number of results (and therefore pages) means the database usually has to scan every single record. If there are filters in place, and there ususally are, then the database may not be able to use an index for the query and it will have to slowly scan every row and test whether it matches the filters or not. Even if the end user has not specified any filters, there might be system-level filters for things like &quot;is not deleted&quot; or &quot;user has permission to see this&quot; or &quot;is not a draft&quot; etc. Especially as page numbers get higher, even if computing the total number of records&#x2F;pages has been taken out of scope, the DB will still have to scan every row prior to the current page and the performance nosedives. This can reduce performance across the application as the DB has fewer resources available to serve other queries. Sometimes there are clever ways to implement the queries to avoid the performance problem, but they are definitely not techniques found on the first few pages of google results. They also may not manifest until record counts hit 25K or 100K and the odds of a developer working on a net new app testing that are near zilch.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-do-we-end-up-implementing&quot;&gt;What do we end up implementing?&lt;&#x2F;h2&gt;
&lt;p&gt;We usually end up on a &quot;load more&quot; button, possibly the infinite scroll manifestation.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;other-technical-solutions&quot;&gt;Other technical solutions&lt;&#x2F;h2&gt;
&lt;p&gt;I think there are ways to implement this with good performance by avoiding SQL &lt;code&gt;OFFSET&lt;&#x2F;code&gt; in favor of carefully-designed &lt;code&gt;WHERE&lt;&#x2F;code&gt; clause using an index, but this post is not about the technical aspect. A web search will show several varieties of alternatives that might be what you need.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Tips for Switching from Linux to Mac</title>
          <pubDate>Sat, 20 Jul 2024 13:51:11 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2024/07/tips-for-switching-from-linux-to-mac/</link>
          <guid>https://peterlyons.com/problog/2024/07/tips-for-switching-from-linux-to-mac/</guid>
          <description xml:base="https://peterlyons.com/problog/2024/07/tips-for-switching-from-linux-to-mac/">&lt;p&gt;I&#x27;ve run the full matrix of combinations of linux and macos between my work and personal setups. At this point I&#x27;m pretty comfortable switching. Linux will always have my heart but periodically I get fed up and just want a laptop that goes to sleep when you close the lid. I&#x27;ve recently seen some posts and since I did my most recent round of switching from an all-linux setup to linux-at-home + mac-at-work, I wanted to share a short list of key apps. These are mostly from my personal toolbelt with a few picked from some online discussions I saw recently.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;terminal
&lt;ul&gt;
&lt;li&gt;I run &lt;a href=&quot;https:&#x2F;&#x2F;sw.kovidgoyal.net&#x2F;kitty&#x2F;&quot;&gt;kitty&lt;&#x2F;a&gt; and really enjoy having the exact same terminal across OSes&lt;&#x2F;li&gt;
&lt;li&gt;The Terminal.app that ships with macos is also a fine choice.&lt;&#x2F;li&gt;
&lt;li&gt;Things are in very good state at the moment with numerous high-quality choices avalible including alacritty, wezterm, etc&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;zsh
&lt;ul&gt;
&lt;li&gt;Just note that for license reasons the default shell on macos is zsh, which is good&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;brew.sh&quot;&gt;homebrew&lt;&#x2F;a&gt; is the most common package manager, although I think there are alternatives for the brave
&lt;ul&gt;
&lt;li&gt;Also &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;mas-cli&#x2F;mas&quot;&gt;mas&lt;&#x2F;a&gt; is a CLI to help with the Mac App Store&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;clipboard manager
&lt;ul&gt;
&lt;li&gt;I use &lt;a href=&quot;https:&#x2F;&#x2F;apps.apple.com&#x2F;us&#x2F;app&#x2F;flycut-clipboard-manager&#x2F;id442160987?mt=12&quot;&gt;flycut&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;keyboard remapping
&lt;ul&gt;
&lt;li&gt;macos built-in keyboard settings can handle basic layout swapping as well as the common key remaps like caps lock is control&lt;&#x2F;li&gt;
&lt;li&gt;for advanced tricks, &lt;a href=&quot;https:&#x2F;&#x2F;karabiner-elements.pqrs.org&#x2F;&quot;&gt;Karabiner Elements&lt;&#x2F;a&gt; is great&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;formulae.brew.sh&#x2F;formula&#x2F;cliclick#default&quot;&gt;cliclick&lt;&#x2F;a&gt; is a handy tool for scripting simulated keyboard events (similar to xdotool for linux)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;mouse and touchpad tricks
&lt;ul&gt;
&lt;li&gt;I use &lt;a href=&quot;https:&#x2F;&#x2F;folivora.ai&#x2F;&quot;&gt;BetterTouchTool&lt;&#x2F;a&gt; mostly to get &quot;natural scrolling&quot; on the touchpad while keeping a physical mouse wheel working correctly. It has some other good knobs and dials too, and I think even a clipboard manager, but I don&#x27;t use most of its features anymore.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;pilotmoon.com&#x2F;scrollreverser&#x2F;&quot;&gt;Scroll Reverser&lt;&#x2F;a&gt; can fix this too&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;screenshots
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;flameshot.org&#x2F;&quot;&gt;flameshot&lt;&#x2F;a&gt; works on macos and linux and is great&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;If you use dmenu or rofi on linux, you might want &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;chipsenkbeil&#x2F;choose&quot;&gt;choose&lt;&#x2F;a&gt; (&quot;choose-gui&quot; is the homebrew package name)&lt;&#x2F;li&gt;
&lt;li&gt;Many command line and terminal tools you may use on linux are cross-platform and available on macos as well
&lt;ul&gt;
&lt;li&gt;bat&lt;&#x2F;li&gt;
&lt;li&gt;direnv&lt;&#x2F;li&gt;
&lt;li&gt;eza&lt;&#x2F;li&gt;
&lt;li&gt;fd&lt;&#x2F;li&gt;
&lt;li&gt;fzf&lt;&#x2F;li&gt;
&lt;li&gt;gitui&lt;&#x2F;li&gt;
&lt;li&gt;htop&lt;&#x2F;li&gt;
&lt;li&gt;jless&lt;&#x2F;li&gt;
&lt;li&gt;jq&lt;&#x2F;li&gt;
&lt;li&gt;lazygit&lt;&#x2F;li&gt;
&lt;li&gt;lf&lt;&#x2F;li&gt;
&lt;li&gt;neovim&lt;&#x2F;li&gt;
&lt;li&gt;pandoc&lt;&#x2F;li&gt;
&lt;li&gt;prettier&lt;&#x2F;li&gt;
&lt;li&gt;ripgrep&lt;&#x2F;li&gt;
&lt;li&gt;sd&lt;&#x2F;li&gt;
&lt;li&gt;shellcheck&lt;&#x2F;li&gt;
&lt;li&gt;shfmt&lt;&#x2F;li&gt;
&lt;li&gt;starship&lt;&#x2F;li&gt;
&lt;li&gt;tokei&lt;&#x2F;li&gt;
&lt;li&gt;etc&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;window-management&quot;&gt;Window Management&lt;&#x2F;h1&gt;
&lt;p&gt;Since I run &lt;a href=&quot;https:&#x2F;&#x2F;awesomewm.org&#x2F;&quot;&gt;awesomewm&lt;&#x2F;a&gt; on linux, which is configured with lua code, I run &lt;a href=&quot;http:&#x2F;&#x2F;www.hammerspoon.org&#x2F;&quot;&gt;Hammerspoon&lt;&#x2F;a&gt; on mac. It&#x27;s the same philosophy and approach - use real code to get highly precise customization. The hammerspoon API is way way nicer than awesomewm though. When I joined Mailchimp in 2020 and had to use mac for work, I ended up doing a nearly feature-for-feature port of my awesomewm config to Hammerspoon and surprisingly my basic approach has been so stable that I have been maintaining both of them in parallel for about 4 years now. I know how I want my desktop environment to work and they both can acheive it.&lt;&#x2F;p&gt;
&lt;p&gt;If you just want basic window arrangements there are many good tools available including the ones below. I don&#x27;t personally use these since Hammerspoon handles window management for me, but I know many folks like these:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;rectangleapp.com&#x2F;&quot;&gt;rectangle&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;alt-tab-macos.netlify.app&#x2F;&quot;&gt;Alt-Tab&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;koekeishiya&#x2F;yabai&quot;&gt;yabai&lt;&#x2F;a&gt; if you prefer tiling window managers a la i3
&lt;ul&gt;
&lt;li&gt;pairs well with &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;koekeishiya&#x2F;skhd&quot;&gt;skhd&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;launchers-omnibar-thingies&quot;&gt;Launchers &amp;amp; Omnibar Thingies&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.raycast.com&#x2F;&quot;&gt;Raycast&lt;&#x2F;a&gt; and &lt;a href=&quot;https:&#x2F;&#x2F;www.alfredapp.com&#x2F;&quot;&gt;Alfred&lt;&#x2F;a&gt; are popular. I have a home-grown thing based on scripts and choose-gui which works identically for me on linux and mac so I haven&#x27;t looked at this category much.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;goat-meeting-reminder-app-meetingbar&quot;&gt;GOAT Meeting Reminder App: MeetingBar&lt;&#x2F;h1&gt;
&lt;p&gt;Get &lt;a href=&quot;https:&#x2F;&#x2F;meetingbar.app&#x2F;&quot;&gt;MeetingBar&lt;&#x2F;a&gt;. It&#x27;s amazing. Perfect in every way and solves a longstanding problem for me. I barely need to look at my calendar anymore.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;closing&quot;&gt;Closing&lt;&#x2F;h1&gt;
&lt;p&gt;The differences between linux and mac for developers are as minimal now as they have ever been. We spend so much time in browsers and other cross-platform developer tools that it&#x27;s pretty straightforward to have high similarity. It may take a fair amount of tweaking and futzing, but you should be able to get something that feels comfortable.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Where I Get Tech News in 2024</title>
          <pubDate>Sat, 29 Jun 2024 15:12:07 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2024/06/where-i-get-tech-news-in-2024/</link>
          <guid>https://peterlyons.com/problog/2024/06/where-i-get-tech-news-in-2024/</guid>
          <description xml:base="https://peterlyons.com/problog/2024/06/where-i-get-tech-news-in-2024/">&lt;p&gt;I share links to tech articles and projects somewhat regularly in work slack, and a teammate recently asked how I keep up with industry pulse and trends. I&#x27;ll snapshot my current resources even though it will quickly become outdated, but I also want to talk a little bit about the how things changed drastically in the last two years or so.&lt;&#x2F;p&gt;
&lt;p&gt;So the basic narrative is:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;I got pretty deep into node.js back end development between 2011 and 2018 and was very dialed in ~2016&lt;&#x2F;li&gt;
&lt;li&gt;Things changed after COVID and after ZIRP with twitter, stackoverflow, and reddit all experiencing major permanent disruptions&lt;&#x2F;li&gt;
&lt;li&gt;I have a new set of information sources now but it&#x27;s unstable and chaotic and disappointing&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;my-current-somewhat-disappointing-setup&quot;&gt;My current somewhat disappointing setup&lt;&#x2F;h2&gt;
&lt;p&gt;Key points here, more details below.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;I follow aggregators for both &lt;a href=&quot;https:&#x2F;&#x2F;social.lansky.name&#x2F;@hn50&quot;&gt;Hacker News&lt;&#x2F;a&gt; and &lt;a href=&quot;https:&#x2F;&#x2F;botsin.space&#x2F;@lobsters&quot;&gt;lobsters&lt;&#x2F;a&gt; on mastodon.
&lt;ul&gt;
&lt;li&gt;I kind of like having these in my fediverse feed and generally do not go directly to their web site home pages.&lt;&#x2F;li&gt;
&lt;li&gt;Any links too long to read immediately I save to &lt;a href=&quot;https:&#x2F;&#x2F;omnivore.app&#x2F;&quot;&gt;Omnivore&lt;&#x2F;a&gt; to read later.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;I have a jumble of tech accounts I follow on mastodon but essentially the very good set I built up over 14 years on twitter is gone
&lt;ul&gt;
&lt;li&gt;Anyone who has given a talk at Strangeloop or !!Con is probably worth following at least for a while to sample&lt;&#x2F;li&gt;
&lt;li&gt;Some thought leaders have come over from twitter and I managed to find them&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;I pay a fair amount of attention to &lt;a href=&quot;https:&#x2F;&#x2F;randsinrepose.com&#x2F;welcome-to-rands-leadership-slack&#x2F;&quot;&gt;Rands Leadership Slack&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;I subscribe to the &lt;a href=&quot;https:&#x2F;&#x2F;golangweekly.com&#x2F;&quot;&gt;Golang Weekly&lt;&#x2F;a&gt; email newsletter&lt;&#x2F;li&gt;
&lt;li&gt;I have a kind of roundabout way of tracking blog posts coming out of &lt;a href=&quot;https:&#x2F;&#x2F;www.recurse.com&#x2F;&quot;&gt;Recurse Center&lt;&#x2F;a&gt; and their internal community Zulip chat surfaces good links and interesting discussions regularly. I don&#x27;t track it very closely as it&#x27;s too active to stay on top of.&lt;&#x2F;li&gt;
&lt;li&gt;I subscribe to some tech podcasts. Mostly long-form interviews but some news bulletin types.
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;pca.st&#x2F;podcast&#x2F;1f1720a0-5ab3-013a-d70c-0acc26574db2&quot;&gt;Software Unscripted&lt;&#x2F;a&gt; with Richard Feldman is my current favorite.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;changelog.com&#x2F;podcast&quot;&gt;The Changelog&lt;&#x2F;a&gt; is good for general open source and tech industry news&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;changelog.com&#x2F;gotime&quot;&gt;Go Time&lt;&#x2F;a&gt; from the same team behind The Changelog is their go-specific podcast.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.backendbanter.fm&#x2F;&quot;&gt;Backend Banter&lt;&#x2F;a&gt; is good for backend interviews and topics.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;cupogo.dev&#x2F;&quot;&gt;Cup o&#x27; Go&lt;&#x2F;a&gt; is short coverage of go news&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Noticeably absent from this suite of tools is any kind of RSS reader. I don&#x27;t have one I like anymore and it&#x27;s tragic. I&#x27;m also much more OK with keeping looser tabs on things these days as I have both a full-time job and a very involved side business running &lt;a href=&quot;https:&#x2F;&#x2F;focusretreatcenter.com&quot;&gt;Focus Retreat Center&lt;&#x2F;a&gt; so I just can&#x27;t dial in like I did a decade ago.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;a-brief-description-of-the-peak-circa-2018&quot;&gt;A brief description of the peak circa 2018&lt;&#x2F;h2&gt;
&lt;p&gt;Most of my tech news came from twitter and I followed a great set of accounts that meant almost every project, startup, product ever discussed in work slack or at a tech conference was something I had at the very least already heard about and most likely read a few paragraphs to have the basic elevator pitch. I&#x27;m sure at this point I could have named from memory 50 people active in the node.js ecosystem and mapped dozens of companies and relationships.&lt;&#x2F;p&gt;
&lt;p&gt;I had a very dialed-in set of RSS blogs subscriptions and used feedly extensively to track dozens and dozens of high-quality tech blogs.&lt;&#x2F;p&gt;
&lt;p&gt;I was very active on stackoverflow answering about 1600 questions mostly about node.js and used it extensively for technical problems but also for finding experts in particular topics and sometimes following them on social media directly.&lt;&#x2F;p&gt;
&lt;p&gt;I was on dozens of smaller slack communities affiliated with either specific meetup groups, specific industry groups like independent consultants or freelancers, particular topics of interest, etc. I didn&#x27;t track all of these super closely, but I could scan across them periodically and find interesting stuff.&lt;&#x2F;p&gt;
&lt;p&gt;I went to conferences more regularly including 4 trips to Strangeloop, a couple of node.js conferences, a few rust conferences, and some for work. I belonged to many meetup groups and went somewhat frequently. I lived in Boulder at the time so the local tech scene was small but robust enough. I frequented Code &amp;amp; Coffee at Dojo4 and Github in Boulder.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;elon-and-enshittification&quot;&gt;Elon and enshittification&lt;&#x2F;h2&gt;
&lt;p&gt;Elon ruined it. Twitter kind of imploded and the ads are now intolerable and it&#x27;s way too much politics and arguing. I still have my account but very rarely read it and basically stopped posting. Several people I love reading still seem to only post to twitter and I&#x27;m basically waiting and hoping to find a better way to get their writing.&lt;&#x2F;p&gt;
&lt;p&gt;Reddit also had a meltdown right around the same time. I didn&#x27;t really use reddit for tech stuff though. I know about a few subreddits but they were kind of a last resort for me, usually on very niche projects like awesomewm or hammerspoon. I did use it a ton for maker stuff and specifically &#x2F;r&#x2F;ergomechkeyboards. It went away when reddit leadership screwed with moderators&#x2F;API leading up to the IPO circa 2023 and I never migrated to any of the proposed alternatives. It&#x27;s kind of back to normal now but feels slightly less active.&lt;&#x2F;p&gt;
&lt;p&gt;Stackoverflow is kinda done and LLMs are mostly replacing it for coding work. It&#x27;s still a useful repository knowledge base but I think it&#x27;s mostly a read-only archive at this point.&lt;&#x2F;p&gt;
&lt;p&gt;Feedly got enshittified with bizarre UI, a steady stream of nonsense no one wanted nor asked for in what was at one point a polished, text-forward web app with excellent keyboard shortcuts. I bailed out when I literally could not understand the UI because it was so design-heavy bizarre with strange expand&#x2F;collapse behavior and lost all its visual hierarchy. I have tried a bunch of terminal-based TUI RSS clients but they&#x27;re all quite lacking. Even the oldest most mature ones would consistently crash for me on basic use cases. All the new ones in rust or go usually show some promise but have a long roadmap to build before they are conveniently usable. There probably is a good RSS reader out there but I have not yet found one or had enough time and motivation to re-curate a good blog feed.&lt;&#x2F;p&gt;
&lt;p&gt;I work from home and only do long drives every few weeks, so my podcast info is usually months old by the time I listen to it.&lt;&#x2F;p&gt;
&lt;p&gt;It&#x27;s kind of sad at the moment. I would love to get another good RSS setup going and fill out my mastodon feed better, but time will tell.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Kipra Keyboard Build Guide</title>
          <pubDate>Sat, 18 May 2024 12:33:41 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2024/05/kipra-keyboard-build-guide/</link>
          <guid>https://peterlyons.com/problog/2024/05/kipra-keyboard-build-guide/</guid>
          <description xml:base="https://peterlyons.com/problog/2024/05/kipra-keyboard-build-guide/">&lt;p&gt;This guide will walk you through the process of building a kipra keybord.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;parts-list&quot;&gt;Parts List&lt;&#x2F;h2&gt;
&lt;p&gt;Caveat: parts availability changes quickly and vendor inventory tends to be come and go. Probably, you&#x27;ll be able to get everything from 3 vendors, but some struggle may be involved.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;A pair of kipra v1 PCBs.
&lt;ul&gt;
&lt;li&gt;I suggest &lt;a href=&quot;https:&#x2F;&#x2F;www.pcbway.com&#x2F;&quot;&gt;PCBWay&lt;&#x2F;a&gt; to fabricate boards for you&lt;&#x2F;li&gt;
&lt;li&gt;The exact .zip file I uploaded to them is available &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;focusaurus&#x2F;kipra-keyboard&quot;&gt;in the focusaurus&#x2F;kipra-keyboard github repo&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;It&#x27;s &lt;code&gt;kipra-v1.gerber.zip&lt;&#x2F;code&gt; in the root of the repo&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;a pair of RP2040 MCUs. I bought &lt;a href=&quot;https:&#x2F;&#x2F;www.aliexpress.us&#x2F;item&#x2F;3256805923036572.html?spm=a2g0o.order_detail.order_detail_item.3.24e8f19crLknnv&amp;amp;gatewayAdapt=glo2usa#nav-specification&quot;&gt;these on aliexpress&lt;&#x2F;a&gt; - the 16M &quot;color&quot;
&lt;ul&gt;
&lt;li&gt;these will come with pin headers&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;44 kailh choc switches
&lt;ul&gt;
&lt;li&gt;I strongly endorse the new amazing &lt;a href=&quot;https:&#x2F;&#x2F;lowprokb.ca&#x2F;collections&#x2F;switches&#x2F;products&#x2F;ambients-silent-choc-switches?variant=44873446391972&quot;&gt;Kailh Choc Ambient Nocturnal 20g linear silent switches&lt;&#x2F;a&gt; from the fantastic lowprokb.ca store.&lt;&#x2F;li&gt;
&lt;li&gt;lowprokb stocks most of the parts you need for this build&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;44 kailh choc hotswap sockets&lt;&#x2F;li&gt;
&lt;li&gt;44 keycaps of your choosing&lt;&#x2F;li&gt;
&lt;li&gt;44 diodes
&lt;ul&gt;
&lt;li&gt;I suggest surface mount (SMD), but the board supports through-hole as well (in theory)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;a pair of TRRS connectors&lt;&#x2F;li&gt;
&lt;li&gt;A TRRS cable. I suggest about 1 meter so it can run behind a laptop and 90-degree connectors make the most sense for that. If you know you want your cable to go straight across between the halves then a straight cable connector is fine.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;tools-and-supplies&quot;&gt;Tools and Supplies&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;soldering iron and solder&lt;&#x2F;li&gt;
&lt;li&gt;small breadboard&lt;&#x2F;li&gt;
&lt;li&gt;flush cutters&lt;&#x2F;li&gt;
&lt;li&gt;masking tape and marker&lt;&#x2F;li&gt;
&lt;li&gt;high quality precision tweezers&lt;&#x2F;li&gt;
&lt;li&gt;safety gear if you are using leaded solder
&lt;ul&gt;
&lt;li&gt;nitrile gloves&lt;&#x2F;li&gt;
&lt;li&gt;fume extractor and&#x2F;or respirator&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;bins to hold tiny parts are very helpful&lt;&#x2F;li&gt;
&lt;li&gt;excellent lighting and&#x2F;or headlamp&lt;&#x2F;li&gt;
&lt;li&gt;safety glasses&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;study-soldering-technique-and-safety&quot;&gt;Study soldering technique and safety&lt;&#x2F;h2&gt;
&lt;p&gt;If you are new to soldering, head over to youtube and watch some tutorials for technique and safety.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;flash-the-mcu-ahead-of-time&quot;&gt;Flash the MCU ahead of time&lt;&#x2F;h2&gt;
&lt;p&gt;I suggest flashing the MCU as soon as you receive them. Better to get this sorted and out of the way so when you finish assembly, you get the satisfaction of having a working keyboard without any flashing frustrations spoiling the experience.&lt;&#x2F;p&gt;
&lt;p&gt;I have not yet published my QMK and Vial ports. If someone other than me ever actually builds one of these, hopefully I will have figured out how to do that either by merging the keeb into QMK and&#x2F;or Vial or just adding them to the kipra repo on my github or something.&lt;&#x2F;p&gt;
&lt;p&gt;If you flash the bare MCU, you should be able to type letters by shorting through holes from the end of the left column to the end of the right column.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2024&amp;#x2F;2024-04-02-01HTE3FKRRXZNF2ABCQ41NBAPK.2048.mov&quot;&gt;
  &lt;figcaption&gt;Typing on the bare MCU&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;build-one-half-at-a-time&quot;&gt;Build one half at a time&lt;&#x2F;h2&gt;
&lt;p&gt;I recommend building only one half start to finish first unless you have a lot of experience building keyboard kits. This way you are more likely to make a mistake on only 1 PCB not both. Each half will work as a keyboard when connected to the computer via USB so you&#x27;ll know everything is correct.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;steady-hands&quot;&gt;Steady hands&lt;&#x2F;h2&gt;
&lt;p&gt;Pro tip: you might want to avoid caffeine on build day. My hands get shakier if I&#x27;ve had too much caffeine and these SMD diodes are tiny!&lt;&#x2F;p&gt;
&lt;h2 id=&quot;label-the-pcbs-halves-and-sides&quot;&gt;Label the PCBs halves and sides&lt;&#x2F;h2&gt;
&lt;p&gt;Take your PCBs, lay them out so you have a paired left and right, and put a piece of masking tape on them labeling all 4 sides: &quot;left top&quot;, &quot;left bottom&quot;, &quot;right top&quot;, &quot;right bottom&quot;. It&#x27;s easy at the beginning of the build to lose track and solder something to the wrong side.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2024&amp;#x2F;2024-04-20-01HVXZMEM0RA07KRTNCM9V7Z9P.2048.jpg&quot;&gt;
  &lt;figcaption&gt;label the PCB all 4 sides&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2024&amp;#x2F;2024-04-20-01HVXZPD40C8HBBE726KZ9FF47.2048.jpg&quot;&gt;
  &lt;figcaption&gt;labels on the bottom&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;solder-the-diodes&quot;&gt;Solder the diodes&lt;&#x2F;h2&gt;
&lt;p&gt;Set your iron to 350°C (or whatever works best with your solder, but as low as will work). The diodes go on the BOTTOM so make sure you are working on the bottom side. The stripe on the diode needs to align with the direction the arrow points on the PCB silkscreen. This is not symmetric so for the left half the text on the diode will be upside down, and on the right hand the text will be right side up.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2024&amp;#x2F;2024-04-20-01HVY00DE098V54YMKJYZ69V8Y.2048.jpg&quot;&gt;
  &lt;figcaption&gt;pinecil soldering iron at 350°C&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;For surface mount, heat one pad and put the tiniest amount of solder you can onto it. Then get the diode ready in tweezers butting up to that solder blob. Reheat it and slide the diode so the lead is in the solder and it&#x27;s properly centered on the footprint. Hold it steady while you remove the iron. For the other lead, heat the pad &amp;amp;  lead and solder them as normal.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2024&amp;#x2F;2024-04-20-01HVY03B605VW8FFZRR6XBHJBC.2048.jpg&quot;&gt;
  &lt;figcaption&gt;first dots of solder on diode pads&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2024&amp;#x2F;2024-04-20-01HVY06TGGECB6S6M8ZW893VDG.2048.jpg&quot;&gt;
  &lt;figcaption&gt;getting the diode alignment ready&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2024&amp;#x2F;2024-04-20-01HVY088D85PBDDEA0ZC76VJXT.2048.jpg&quot;&gt;
  &lt;figcaption&gt;both pads of a diode soldered&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;Continue for the rest of the diodes. After each row or column, do a scan to visually confirm they are all facing the same way.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2024&amp;#x2F;2024-04-20-01HVY12XXRMBS4WYPNRVXTYRK0.2048.jpg&quot;&gt;
  &lt;figcaption&gt;diodes done on left side&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;solder-the-hotswap-sockets&quot;&gt;Solder the hotswap sockets&lt;&#x2F;h2&gt;
&lt;p&gt;Still working on the bottom, install a hotswap socket. Heat one pad by shoving your iron into the metal bit of the hotswap socket and flood it with a decent amount of solder. Being careful not to burn yourself, use the tip of a finger to press the other side of the socket firmly down to be sure it&#x27;s evenly seated before retracting the iron from the other side. Use tweezers or a pencil eraser or something if you are worried, but I was able to just use my finger and suitable amount of caution. Solder the other side and repeat for each key.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2024&amp;#x2F;2024-04-20-01HVY1BFBRWVBEXNT3BXC2GV7F.2048.jpg&quot;&gt;
  &lt;figcaption&gt;hotswap sockets going on left bottom&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2024&amp;#x2F;2024-04-20-01HVY1SJH8ZW52M8JQD91SDEAX.2048.jpg&quot;&gt;
  &lt;figcaption&gt;left side hotswap sockets done&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;solder-the-mcu-jumper-pads&quot;&gt;Solder the MCU jumper pads&lt;&#x2F;h2&gt;
&lt;p&gt;Turn the PCB over so you are looking at the left top side. We are going to connect each pair of jumper pads within the MCU footprint. Heat both pads with your iron tip if possible and get a small dot of solder to cover both pads. Make sure when it cools there&#x27;s enough solder so it doesn&#x27;t shrink to 2 disconnected dots, but not a huge amount that might spill to a nearby pad or through hole. Note that the silkscreen instructions say &quot;R. Side - Jumper Here&quot; and this is the left side for us. This is OK. We want our MCU mounted face up so the buttons are accessible, but the footprint was designed primarily for folks who like to mount their MCU face down.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2024&amp;#x2F;2024-05-11-01HXK5DGK8HK3VND71MY7PNBCK.2048.jpg&quot;&gt;
  &lt;figcaption&gt;Diagram of orientation for MCUs and jumper pads&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2024&amp;#x2F;2024-04-20-01HVY1YDT06AE23W66VXR3TCY2.2048.jpg&quot;&gt;
  &lt;figcaption&gt;working on the left top MCU footprint jumper pads&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;Continue until each pair of jumpers is connected.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;solder-pin-headers-to-the-mcu&quot;&gt;Solder pin headers to the MCU&lt;&#x2F;h2&gt;
&lt;p&gt;You can use socket headers if you like, but I&#x27;m not very concerned about moving my $4 MCU to another keyboard, so I just use normal pin headers. The MCU has 1 extra through hole compared to our PCB footprint, so you&#x27;ll need to snip 1 pin header off the strip to have the right number.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2024&amp;#x2F;2024-04-20-01HVY21P9R25FJ11R5X1TEXSXC.2048.jpg&quot;&gt;
  &lt;figcaption&gt;Snipping off the extra pin&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;Set 2 columns of pin headers into a breadboard, longer section of the pin in the breadboard, shorter section sticking up in the air. Mount the MCU onto the pins with the empty pair of through holes closer to the USB connector. The through holes at the corners furthest from the USB connector should have pins in them. The MCU should be face up with the buttons visible.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2024&amp;#x2F;2024-04-20-01HVY23ZHGDWYE43CJFPJ4ZC58.2048.jpg&quot;&gt;
  &lt;figcaption&gt;Pin headers in the breadboard to hold everything properly while soldering&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2024&amp;#x2F;2024-04-20-01HVY24J38XYHXCV7J69ZEC684.2048.jpg&quot;&gt;
  &lt;figcaption&gt;Take care to align the correct through holes&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;Solder the pin headers.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2024&amp;#x2F;2024-04-20-01HVY2C67RGC0JKTTT0M52DP4F.2048.jpg&quot;&gt;
  &lt;figcaption&gt;That 4th one is no bueno and needs a reflow&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;solder-the-mcu-to-the-pcb&quot;&gt;Solder the MCU to the PCB&lt;&#x2F;h2&gt;
&lt;p&gt;Put the MCU into the PCB from the top then turn it upside down to solder from the bottom side.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2024&amp;#x2F;2024-04-20-01HVY2FJMG0PHC2308WMQ9RSFR.2048.jpg&quot;&gt;
  &lt;figcaption&gt;placing the MCU onto the PCB top side&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2024&amp;#x2F;2024-04-20-01HVY2GC105P0T9M8P4285T1TF.2048.jpg&quot;&gt;
  &lt;figcaption&gt;flipped over so we can solder the pin headers from the bottom&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;Solder the pin headers to the through holes.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2024&amp;#x2F;2024-04-20-01HVY2P4K8B2KF44M1M5YTNG70.2048.jpg&quot;&gt;
  &lt;figcaption&gt;pin headers soldered&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;Next we&#x27;ll trim these flush BUT these tend to fly off and really want to poke into your eyeball. Wear safety glasses and&#x2F;or put something on top of the header to hold it in place while you trim it so it doesn&#x27;t fly off. I used a little piece of dense packing foam that was lying around.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2024&amp;#x2F;2024-04-20-01HVY2RXF04ASGJY53MCC1BPAH.2048.jpg&quot;&gt;
  &lt;figcaption&gt;flush cutting the pin headers without eye boo-boos&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;solder-the-trrs-connector&quot;&gt;Solder the TRRS connector&lt;&#x2F;h2&gt;
&lt;p&gt;This mounts to the top side of the PCB and solders on the bottom. I rested the top of the TRRS connector on my breadboard to hold the connector flush against the PCB while soldering.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2024&amp;#x2F;2024-04-20-01HVYMA8BGQQFTHZ5YZ61CWTJ7.2048.jpg&quot;&gt;
  &lt;figcaption&gt;TRRS connector pins getting soldered from the bottom of the left half&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;install-some-switches&quot;&gt;Install some switches&lt;&#x2F;h2&gt;
&lt;p&gt;You can now install switches into the hotswap sockets. Make sure both switch pins are not bent and aligned properly to seat into the socket. I like to support the underside of the socket with my finger and press firmly to get each switch fully seated and flush but not put too much force on the solder joint for the hotswap socket.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2024&amp;#x2F;2024-04-20-01HVY36VR8XBF2SNTDSCREPXY0.2048.jpg&quot;&gt;
  &lt;figcaption&gt;putting in some switches&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;plug-it-in-and-see-if-it-types&quot;&gt;Plug it in and see if it types&lt;&#x2F;h2&gt;
&lt;p&gt;You can now connect USB (assuming your MCU is already flashed with firmware) and it should type letters. If something doesn&#x27;t work, check diode orientation. You may need to reflow some solder joints if anything is not working. Note the USB connector on this MCU is not mechanically very strong, so be gentle when plugging and unplugging it and take care you are moving the plug straight and level.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2024&amp;#x2F;2024-04-20-01HVY3JPN8QZ4XZQEGRAR6C74X.2048.jpg&quot;&gt;
  &lt;figcaption&gt;checking typing with xev&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;take-break&quot;&gt;Take break&lt;&#x2F;h2&gt;
&lt;p&gt;Rest your mind a bit if you intend to do both halves the same day.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;proceed-to-the-right-half&quot;&gt;Proceed to the right half&lt;&#x2F;h2&gt;
&lt;h2 id=&quot;right-side-diodes&quot;&gt;Right side diodes&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;NOTE&lt;&#x2F;strong&gt; the diode orientation will feel different on the right half. Make sure you are soldering them to the right half bottom side. The diode arrows will point left which means the diode label text will likely be right side up on this half of the keyboard.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2024&amp;#x2F;2024-04-20-01HVY3WF58NDXR0P05FZG86D9X.2048.jpg&quot;&gt;
  &lt;figcaption&gt;right side bottom showing diode direction&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;right-side-hotwsap-sockets&quot;&gt;Right side hotwsap sockets&lt;&#x2F;h2&gt;
&lt;p&gt;No surprises here.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2024&amp;#x2F;2024-04-20-01HVY58K8R9N0Q53DSS4BP1BBB.2048.jpg&quot;&gt;
  &lt;figcaption&gt;right side bottom with diodes and hotswap sockets soldered&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;right-side-mcu-jumper-pads&quot;&gt;Right side MCU jumper pads&lt;&#x2F;h2&gt;
&lt;p&gt;You will solder the MCU footprint jumper pads on the TOP RIGHT side. This is essentially &quot;the same&quot; as the left side, but it&#x27;s tricky so I want to be clear about this.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2024&amp;#x2F;2024-04-20-01HVY5ENKGJ0F5AR4GF5MD48WQ.2048.jpg&quot;&gt;
  &lt;figcaption&gt;MCU footprint jumper pads soldered on top right side of the PCB&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;right-side-mcu-to-pcb&quot;&gt;Right side MCU to PCB&lt;&#x2F;h2&gt;
&lt;p&gt;No surprises here. Buttons face up.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2024&amp;#x2F;2024-04-20-01HVY5XQ1RZWACKNHBE2G8P8T6.2048.jpg&quot;&gt;
  &lt;figcaption&gt;about to solder the MCU pins to the PCB from the right side bottom&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;flush-cut-the-pin-headers&quot;&gt;Flush cut the pin headers&lt;&#x2F;h2&gt;
&lt;p&gt;No surprises here.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2024&amp;#x2F;2024-04-20-01HVY5Z9TRDAD6RS02R7R3WNYY.2048.jpg&quot;&gt;
  &lt;figcaption&gt;flush cutting the MCU pin headers on the right half bottom side&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;right-side-trrs-connector&quot;&gt;Right side TRRS connector&lt;&#x2F;h2&gt;
&lt;p&gt;No surprises here.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2024&amp;#x2F;2024-04-20-01HVYMC8T0K63NXTJ7K2F4DRJJ.2048.jpg&quot;&gt;
  &lt;figcaption&gt;TRRS pins getting soldered on the right side&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;test-the-full-keyboard&quot;&gt;Test the full keyboard&lt;&#x2F;h2&gt;
&lt;p&gt;TRRS has an annoyance that the connector shape slides across a short circuit as you put the jack into the plug, so it&#x27;s best to only connect&#x2F;disconnect TRRS when USB is already disconnected. I&#x27;ve never seen this actually cause problems even after doing this wrong all the time on my ergodox for years, but I think it&#x27;s a real thing that could damage your keyboard.&lt;&#x2F;p&gt;
&lt;p&gt;So connect the halves with TRRS, then connect the left half to USB. Both halves should work properly.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2024&amp;#x2F;2024-04-20-01HVYMEVT8JTHAK7RZ5Y6F0MAM.2048.jpg&quot;&gt;
  &lt;figcaption&gt;Assembly done. Time to plug in and type.&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;add-bottom-liners&quot;&gt;Add bottom liners&lt;&#x2F;h2&gt;
&lt;p&gt;Perhaps the lowest-profile option for a case is no case. You may want to glue shelf liners to the bottom side for a little electrical shielding and a non-stick, non-scratch material for the desktop.&lt;&#x2F;p&gt;
&lt;p&gt;I cut my liners just a few mm smaller than the PCB shape and used some gorilla glue. I put the tiniest drop I could manage into the center of each hotswap socket, taking care not to get any glue into the actual socket. I spritzed the shelf liner with water which is how this glue says to do it. I left it with a little weight pressing it overnight to cure.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2024&amp;#x2F;2024-05-07-01HXA8H8W0JKEGMM587771CN62.2048.jpg&quot;&gt;
  &lt;figcaption&gt;cutting shelf liner&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2024&amp;#x2F;2024-05-08-01HXBMV7W0KEP33YJGKMZ0P6VT.2048.jpg&quot;&gt;
  &lt;figcaption&gt;I recommend covering the entire bottom of the board unlike I did in this first attempt to keep the contacts shielded&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;done&quot;&gt;Done!&lt;&#x2F;h2&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2024&amp;#x2F;2024-05-11-01HXMRG0A0VZY2EDBCHBVVVGEF.2048.jpg&quot;&gt;
  &lt;figcaption&gt;Here&amp;#x27;s a build in 3D printed case with palm rests clipped on&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;May your thumbs forever be untucked and your switches be silent!&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Kipra Keyboard</title>
          <pubDate>Sat, 11 May 2024 13:00:10 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2024/05/kipra-keyboard/</link>
          <guid>https://peterlyons.com/problog/2024/05/kipra-keyboard/</guid>
          <description xml:base="https://peterlyons.com/problog/2024/05/kipra-keyboard/">&lt;h2 id=&quot;tl-dr-this-post-is-over-4500-words-sorry-not-sorry&quot;&gt;TL;DR (This post is over 4500 words. Sorry. Not sorry.)&lt;&#x2F;h2&gt;
&lt;p&gt;I made a split ergonomic keyboard called the kipra v1 using a generator tool called &lt;a href=&quot;https:&#x2F;&#x2F;ergogen.cache.works&quot;&gt;ergogen&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2024&amp;#x2F;2024-05-11-01HXMRG0A0VZY2EDBCHBVVVGEF.2048.jpg&quot;&gt;
  &lt;figcaption&gt;kipra. It&amp;#x27;s kinda pragmatic.&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;introduction&quot;&gt;Introduction&lt;&#x2F;h2&gt;
&lt;p&gt;I&#x27;ve built a new split ergonomic mechanical keyboard called the &quot;kipra&quot;. My daily driver for most of the past year has been a sofle choc, which is overall quite good and adequate for my current needs. However, I wanted to make something even more tailored to my specific preferences in terms of features and hand shape. The sofle was kind of bought on a whim when the vendor had a business closing sale so it had a bunch of stuff I wouldn&#x27;t choose starting from scratch.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;requirements&quot;&gt;Requirements&lt;&#x2F;h2&gt;
&lt;p&gt;After going pretty far toward the end (beyond the end?) of the distribution for &lt;a href=&quot;https:&#x2F;&#x2F;peterlyons.com&#x2F;problog&#x2F;2021&#x2F;06&#x2F;squeezebox-keyboard-v2105&#x2F;&quot;&gt;custom scooped and tented split keyboards&lt;&#x2F;a&gt;, I was in the mood for something much more &lt;strong&gt;portable, durable, replaceable, and generally pragmatic&lt;&#x2F;strong&gt;. Tenting is a real pain for this, and I have been using my sofle flat for a year with no RSI pain, so I felt confident I could abandon tenting at least for now and hope I stay pain-free.&lt;&#x2F;p&gt;
&lt;p&gt;I also wanted the &lt;strong&gt;thumb arc way lower and further inward&lt;&#x2F;strong&gt; than almost any other keyboard I have seen. I find tucking my thumb under my palm quite uncomfortable and I don&#x27;t understand why so many designs put thumb keys under the palm. So I wanted my innermost thumb key just at the edge of my palm and an arc away from there. I also like to thwack my thumb keys with my thumb knuckle, not the tip, so positioned the thumb arc accordingly.&lt;&#x2F;p&gt;
&lt;p&gt;I wanted &lt;strong&gt;dedicated modifier&lt;&#x2F;strong&gt; keys. I have tried home row mods and all the tweaking to try to make them work and I don&#x27;t like them. I type using the dvorak layout which has tons of rolls on the home row and that makes home row mods even trickier. So 4 dedicated modifier keys per hand. I also wanted to try putting the modifier row at the bottom vs my sofle layout which has them on the top row. Mods on the top row requires shifting my whole arm and losing home row, but I can  curl my fingers enough to reach the bottom row without a full reposition so I think it will be better, but time will tell.&lt;&#x2F;p&gt;
&lt;p&gt;I don&#x27;t want a pinky reach column as it&#x27;s an RSI risk, so I eliminated that column entirely in comparison to my sofle. I also think I can handle function keys as tap-holds on my numpad layer, so that&#x27;s a whole row I don&#x27;t need also.&lt;&#x2F;p&gt;
&lt;p&gt;I didn&#x27;t want anything fancy or fragile. Just the basics. No rotary encoders, no LEDs, no screens, no pointing devices, etc. Just a basic keyboard I could rely on and toss in a bag without snapping any parts off.&lt;&#x2F;p&gt;
&lt;p&gt;Thus I introduce to you &lt;strong&gt;The kipra keyboard: It&#x27;s kinda pragmatic.&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Use a PCB and have the keys on a flat plane&lt;&#x2F;li&gt;
&lt;li&gt;Nothing but switches&lt;&#x2F;li&gt;
&lt;li&gt;Don&#x27;t bother with any tenting&lt;&#x2F;li&gt;
&lt;li&gt;Add a few extra keys to solve the modifier problem&lt;&#x2F;li&gt;
&lt;li&gt;Make it wired so it&#x27;s reliable&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;So in summary:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;split with column stagger tailored to my finger lengths&lt;&#x2F;li&gt;
&lt;li&gt;choc switches with minimal vertical spacing and just a bit of horizontal spacing&lt;&#x2F;li&gt;
&lt;li&gt;main finger grid is 5 rows by 3 columns&lt;&#x2F;li&gt;
&lt;li&gt;below that are 4 dedicated modifier keys&lt;&#x2F;li&gt;
&lt;li&gt;3-key thumb arc&lt;&#x2F;li&gt;
&lt;li&gt;PCB needs only 3 distinct electronic footprints: switches, MCU, and TRRS&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;preliminary-physical-physical-prototyping&quot;&gt;Preliminary physical physical prototyping&lt;&#x2F;h2&gt;
&lt;p&gt;I printed up some 1x4 columns of switch housings and a thumb arc so I could test my column stagger positions and figure out exactly where I wanted my thumb arc. This will give me some confidence before I get PCBs manufactured, but it&#x27;s never a guarantee. Things feel different when your actually typing on a working keyboard (spoiler alert for later!). But since this is mostly small tweaks to my daily driver sofle choc, I feel pretty confident this will be at the very least as usable as that is.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2023&amp;#x2F;2023-05-26-01H1CABM8G1BRJTXY34XB3F2AY.2048.jpg&quot;&gt;
  &lt;figcaption&gt;Prototype rig with calipers measuring thumb arc offset&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2023&amp;#x2F;2023-05-26-01H1CA1G1G1W4BZVT7Q8QND0FC.2048.jpg&quot;&gt;
  &lt;figcaption&gt;Prototype rig showing housings where switches fit&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;designing-with-ergogen&quot;&gt;Designing with Ergogen&lt;&#x2F;h2&gt;
&lt;p&gt;I used &lt;a href=&quot;https:&#x2F;&#x2F;ergogen.cache.works&#x2F;&quot;&gt;ergogen&lt;&#x2F;a&gt; for this build. It&#x27;s pretty easy to go from nothing to damn near a full keyboard in a single sitting. Here&#x27;s where I was after the first session in ergogen.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2023&amp;#x2F;2023-05-29-01H1JVRGZ8XJKABZ9Z9167PNE4.2048.png&quot;&gt;
  &lt;figcaption&gt;screenshot of ergogen for the kipra after the first day&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2024&amp;#x2F;2024-05-11-01HXK5A46GEA6XH8ZR682ZEF9X.2048.png&quot;&gt;
  &lt;figcaption&gt;KiCAD 3D rendering of a very early version of kipra in ergogen&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;Oh yeah, this was like back in August 2023. Project was mostly squeezed into the margins of my life over the ensuing 8 months.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;printing-accurate-fit-test-prototypes&quot;&gt;Printing accurate fit test prototypes&lt;&#x2F;h2&gt;
&lt;p&gt;It&#x27;s really fun to be able to layout the switches with some pretty simple yaml, then 3D print a mock-up that switches can be fit into and test out how it feels to type. Altogether I think I printed and did fit&#x2F;layout testing across at least 6 iterations. The process consists of:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Set up ergogen yaml for a rectangle for each switch footprint&lt;&#x2F;li&gt;
&lt;li&gt;Add the overall outline of the edge of the PCB
&lt;ul&gt;
&lt;li&gt;see the excerpt below&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;run ergogen to output &lt;code&gt;build&#x2F;outlines&#x2F;test_print.dxf&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;In FreeCAD, start a new file and use Draft workbench&lt;&#x2F;li&gt;
&lt;li&gt;Import the &lt;code&gt;.dxf&lt;&#x2F;code&gt; file. It will create many shapes.&lt;&#x2F;li&gt;
&lt;li&gt;Select them all and hit the blue up arrow to upgrade them to wires&lt;&#x2F;li&gt;
&lt;li&gt;Select them all and click the squiggly icon to convert to sketch&lt;&#x2F;li&gt;
&lt;li&gt;Switch to Part Design Workbench&lt;&#x2F;li&gt;
&lt;li&gt;Make a new body&lt;&#x2F;li&gt;
&lt;li&gt;put the sketch in the body&lt;&#x2F;li&gt;
&lt;li&gt;select the sketch and pad up 6mm
&lt;ul&gt;
&lt;li&gt;6mm is enough for the switch posts to clear the desk surface so the rig sits flat&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;Export it to &lt;code&gt;.stl&lt;&#x2F;code&gt;, load it into your slicer, and print it out&lt;&#x2F;li&gt;
&lt;li&gt;pop some switches in to test&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The ergogen bit looks like this:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;yaml&quot; class=&quot;language-yaml &quot;&gt;&lt;code class=&quot;language-yaml&quot; data-lang=&quot;yaml&quot;&gt;outlines:
  plate_shape:
    main:
      what: rectangle
      where: true
      size: kx
      bound: true
      fillet: 2
  switch_cutouts:
    main:
      what: rectangle
      where: true
      size: 13.8
  test_print:
    - name: plate_shape
    - name: switch_cutouts
      operation: subtract
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2024&amp;#x2F;2024-02-25-01HQGSS3DGG8WPZJ8Y6DE9SDNC.2048.jpg&quot;&gt;
  &lt;figcaption&gt;Many fit tests compliments of ergogen&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;This was in February 2024. I blame the holidays and traveling for that interval.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;my-first-printed-circuit-board&quot;&gt;My first printed circuit board&lt;&#x2F;h2&gt;
&lt;p&gt;I&#x27;ve built a few keyboard kits with their own PCBs, and done hand wired builds with no PCBs, but never had a custom PCB fabricated before. Luckily ergogen makes this overall pretty straightforward. You add the additional components, which for the kipra was just:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;a reversible MCU footprint for my pro-micro compatible MCU&lt;&#x2F;li&gt;
&lt;li&gt;a TRRS jack&lt;&#x2F;li&gt;
&lt;li&gt;a reset button footprint which I didn&#x27;t end up needing because the one built into my MCU works great&lt;&#x2F;li&gt;
&lt;li&gt;mounting holes for support posts&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Ergogen sets up the electronics properly so your keyboard matrix is well-defined and the manual work you need to do in KiCAD to route the tracks is nearly foolproof because the Design Rules Checker will tell you if you forget anything, and KiCAD won&#x27;t even let you wire something to the wrong place.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2024&amp;#x2F;2024-05-11-01HXK418JRMD2X70G67JVWGNKH.2048.png&quot;&gt;
  &lt;figcaption&gt;KiCAD  highlighting the net for a single row&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;There&#x27;s a tricky hand-off in the process where you&#x27;ve done all you can in ergogen and generated a KiCAD file which you then start to modify. If you discover any changes you want that require you to go back into ergogen, you&#x27;ll need to re-do all the KiCAD work. So I put off routing traces for a while as I gradually ran out of tweaks to make in ergogen and started to get bored and keen to get on with the KiCAD bit.&lt;&#x2F;p&gt;
&lt;p&gt;It took a while for me to learn KiCAD and get the track routing done. I didn&#x27;t know about &quot;nudge mode&quot; in the track tool until I paired with someone from Recurse Center who told me about it and then things were a lot easier.&lt;&#x2F;p&gt;
&lt;p&gt;I was super nervous about having a mistake in the KiCAD manual work that would render the PCBs unusable, so it took me a long time and lots of re-dos to practice and get slightly more confident that everything would work. The reversible PCBs can be mind-benders when things get tricky. I also used a fancy reversible MCU footprint which was important since I wanted both of my MCUs mounted face up so the buttons were accessible. Many footprints require one or both of the MCUs to be mounted face down to get the pinout to align properly on both halves.&lt;&#x2F;p&gt;
&lt;p&gt;I got a lot of help and checking, but eventually the Design Rules Checker in KiCAD had no errors and it was time to generate some gerber files and upload them to PCBWay for fabrication.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2024&amp;#x2F;2024-05-10-01HXHRWTR8GWN4W7AAMS15B6W6.2048.png&quot;&gt;
  &lt;figcaption&gt;screenshot of the kipra PCB open in the KiCAD PCB editor&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;fabrication-with-pcbway&quot;&gt;Fabrication with PCBWay&lt;&#x2F;h2&gt;
&lt;p&gt;My friends at &lt;a href=&quot;https:&#x2F;&#x2F;www.pcbway.com&#x2F;&quot;&gt;PCBWay&lt;&#x2F;a&gt; offered to support this project so big thanks to them! I uploaded my gerber zip file, followed their documentation about using settings that would work properly, chose blue for my PCB color and white text set in Hack Nerd Font. Then I fired off an order for 10 reversible PCBs which can make 5 split keyboards. About 10 days later I had a package on my doorstep with a bundle of beautiful circuit boards!&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2024&amp;#x2F;2024-04-10-01HV4RXFGRA1SBT7KQPAENN7DA.2048.jpg&quot;&gt;
  &lt;figcaption&gt;PCBWay order arrived with 10 reversible circuit boards&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2024&amp;#x2F;2024-04-20-01HVXZHPQGRWCME3JTHKJG1571.2048.jpg&quot;&gt;
  &lt;figcaption&gt;a pair of kipra v1 PCBs in their resplendent glory&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;some-custom-text-added-in-kicad&quot;&gt;Some custom text added in KiCAD&lt;&#x2F;h2&gt;
&lt;p&gt;It&#x27;s silly, but I am solidly at the place where having a PCB with bespoke silkscreen text in my hands still feels special and awesome. The charm will likely wear off in a few more builds, but I&#x27;m enjoying this moment.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;soldering-and-the-oh-fuck-cycle&quot;&gt;Soldering and the oh fuck cycle&lt;&#x2F;h2&gt;
&lt;p&gt;Soldering the diodes, hot swap, and TRRS were familiar to me so I was pretty confident all that would work out correctly. But for this reversible MCU I needed to solder across pairs of jumper pads on the correct side, and due to wanting both MCUs facing up, I ended up having to do exactly the opposite of what the instructions on the PCB  silkscreen said. So this contributed to many rounds of what I call the &quot;oh fuck...whew cycle&quot;. This starts with me soldering something and then either thinking or being told on discord that I just fucked it up. Typically this kind of thing is all or nothing. You get all the MCU pins wired correctly and you have a keyboard, or you make one mistake and you have a drink coaster.&lt;&#x2F;p&gt;
&lt;p&gt;(The reality is not that stark especially if you do electronics work regularly, but in my anxious mind all this felt very high-stakes)&lt;&#x2F;p&gt;
&lt;p&gt;But each time within a few minutes and with posting KiCAD files or screenshots or photos, eventually we hit the &quot;whew&quot; phase of the cycle where we figure out actually it&#x27;s fine and correct. Keep in mind this is a custom one-off project. It&#x27;s not a kit that dozens (dozens of us!) have built before. So asking for help requires friendly folks on discord to go the extra mile to learn your custom design and provide guidance.&lt;&#x2F;p&gt;
&lt;p&gt;I went through a few rounds of &quot;of fuck...whew&quot; on the PCB jumpers, a few rounds on the MCU mounting, a few rounds on the MCU pinout, a round about whether I had been referencing the wrong pinout for the entire project (I had), and a few rounds on the TRRS QMK firmware stuff. Exciting times!&lt;&#x2F;p&gt;
&lt;h2 id=&quot;mcu-mounting-footgun&quot;&gt;MCU mounting footgun&lt;&#x2F;h2&gt;
&lt;p&gt;Mounting the MCU revealed that my MCU had one extra through hole per side compared to the footprint on my PCB. So I had to figure out if the extra empty hole was supposed to be the one closest to the USB connector or the one furthest away. I also had to snip one of my pin headers off to make it match the PCB footprint. I studied the pinout a bunch and tried to reason that reset should map to reset, etc, but it&#x27;s tricky because my MCU footprint is reversible and has jumpers so everything is like &quot;this is reset, unless you soldered one of these jumper pads, in which case it is no longer reset&quot;, but exactly how all that cleverness works out is really tricky to keep track of. This was extra fun because the microscopic hole labels on the MCU are mostly between the through holes and it&#x27;s not clear if the label applies to the hole above or below the label, and even studying the ends of the row don&#x27;t really clear it up. But in the end I was pretty sure I needed my rows and columns starting on the holes farthest from USB, so I mounted it that way, and it ended up being correct.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2024&amp;#x2F;2024-05-11-01HXK5DGK8HK3VND71MY7PNBCK.2048.jpg&quot;&gt;
  &lt;figcaption&gt;Planing the details and getting confirmation before soldering the MCUs to the PCBs&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2024&amp;#x2F;2024-04-20-01HVY1YDT06AE23W66VXR3TCY2.2048.jpg&quot;&gt;
  &lt;figcaption&gt;soldering jumper pads on the top side of the PCB (doing the opposite of what the silk screen says)&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;I also chose to &lt;strong&gt;NOT&lt;&#x2F;strong&gt; socket my MCU, flouting all the advice online. I&#x27;ve socketed a lot of MCUs and it&#x27;s extra fiddly work and difficult to prevent the headers from bending. These days MCUs and PCBs are cheap, so I&#x27;m comfortable just using regular pin headers and not futzing with socketing.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;BUT&lt;&#x2F;strong&gt; in the end it all worked out. No mistakes. No parts wasted. The first 2 kits I have built are fully working.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Whew!&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2024&amp;#x2F;2024-04-20-01HVYMEVT8JTHAK7RZ5Y6F0MAM.2048.jpg&quot;&gt;
  &lt;figcaption&gt;first build done with switches and keycaps installed&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2024&amp;#x2F;2024-04-21-01HW173XDG4AQA9DYAW1H5GVCJ.2048.jpg&quot;&gt;
  &lt;figcaption&gt;the closest thing you&amp;#x27;re going to get from me to a glamour shot&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;rp2040-mcus-are-great&quot;&gt;RP2040 MCUs are great&lt;&#x2F;h2&gt;
&lt;p&gt;For this project I had ordered some MCUs from aliexpress that were on sale for something under $4 each I think. These are pretty great.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;RP2040 chips which are great&lt;&#x2F;li&gt;
&lt;li&gt;pro-micro compatible footprint which makes them usable in many existing kits&#x2F;projects&lt;&#x2F;li&gt;
&lt;li&gt;have an on-board reset&#x2F;bootloader button which makes flashing super easy&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;I have to say my early forays into QMK in 2019 were traumatic in how much a fiddly pain everything about flashing was. I bought a bunch of terrible cheap promicros with micro USB connectors that were a complete nightmare to get to show up on the USB bus and to boot into the bootloader and to successfully flash without bricking them. I tried linux, windows, and mac and went on many extended docs&#x2F;forum reading sprees. It was a nightmare of rapidly shorting pins with tweezers, no error messages, etc.&lt;&#x2F;p&gt;
&lt;p&gt;Even the stemcell MCUs I bought recently for the sofles were a giant confusing pain.&lt;&#x2F;p&gt;
&lt;p&gt;In contrast, these RP2040s are amazing. Hold the bootloader button while plugging in USB then release it. They show up immediately in your OS as a mounted thumb drive. To flash them you copy a &lt;code&gt;.uf2&lt;&#x2F;code&gt; file to the root directory and that&#x27;s it. The actual flashing requires zero special software, although compiling a firmware image still requires a working QMK toolset. They are fantastic.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2024&amp;#x2F;2024-04-20-01HVY24J38XYHXCV7J69ZEC684.2048.jpg&quot;&gt;
  &lt;figcaption&gt;soldering pin headers onto this promicro compatible RP2040 MCU&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;the-struggle-to-trrs-split&quot;&gt;The struggle to TRRS split&lt;&#x2F;h2&gt;
&lt;p&gt;I got both halves of the keyboard working when directly plugged into the computer via USB fine basically on the first try with no errors. But I couldn&#x27;t get the TRRS connection to work so the secondary half would type. I spent a day reading QMK&#x27;s extremely-confusing docs about serial, i2c, uart, usart, pio and flailing around until clutch help on discord arrived to guide me through the exact right settings for &lt;code&gt;config.h&lt;&#x2F;code&gt;, &lt;code&gt;rules.mk&lt;&#x2F;code&gt;, etc. There were dozens of rounds of recompiling and reflashing both halves then carefully recabling and seeing yet again the right side did nothing. TRRS comes with the risk of short circuits so care must be taken to always disconnect USB before working with the TRRS cable.&lt;&#x2F;p&gt;
&lt;p&gt;I got some clutch help on discord from someone who actually understands what the RP2040 has built in and that most of the QMK docs are for older, less capable chips, and most of what I needed was default. Shout out to casuanoob! Oh and ALSO there was a super confusing MCU pinout off by one error. As discussed above during mounting and soldering, my MCUs have 1 more through hole per side than the PCB footprint has through holes, so that casts doubt into every &quot;count the holes&quot; step in the docs and creates confusion between the MCU and footprint docs. I later learned that I was studying a slightly incorrect pinout because my assumption that the product detail page on aliexpress is for one exact product. In actual fact, the exact same page sells several different MCUs with different pinouts. So for the entire project I had been referencing a pinout that was similar enough to correct to not be obviously wrong, but was in fact not correct for the exact MCU part number I bought. So the better part of a day&#x27;s flailing ended with incrementing pin numbers by 1 in the firmware and both halves started working together properly!&lt;&#x2F;p&gt;
&lt;h2 id=&quot;vial-configuration-gui-for-real-time-remapping&quot;&gt;Vial configuration GUI for real-time remapping&lt;&#x2F;h2&gt;
&lt;p&gt;While waiting for PCB fabrication, I got a head start on configuring QMK firmware and flashing my RP2040 MCUs. My goal was to have this board supported in &lt;a href=&quot;https:&#x2F;&#x2F;get.vial.today&quot;&gt;Vial&lt;&#x2F;a&gt; which is a nice GUI that allows real-time changes to your keymap without reflashing. As per the Vial docs, I started by making sure I had a stock QMK firware compiling, flashing, and working as a keyboard. I confirmed this by just shorting a row hole to a column hole with a jumper wire and seeing a letter get typed.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2024&amp;#x2F;2024-04-02-01HTE3FKRRXZNF2ABCQ41NBAPK.2048.mov&quot;&gt;
  &lt;figcaption&gt;Typing on a raw MCU&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;Then I ported my keyboard into Vial. This is fairly straightforward using the online keyboard layout editor GUI to specify your switch positions and mapping the matrix is mostly trial and error that&#x27;s clear enough when you have things reversed by mistake. This is an approach I actually recommend for newcomers to the custom keyboard hobby: as soon as your MCU arrives, try to get it flashed and working as a keyboard by just dangling it off a USB cable and shorting a row to a column pin with a jumper wire. Much better to have this part sorted early. If things become difficult, this can require a level of patience and perseverence that are easier to muster at the start of a project.&lt;&#x2F;p&gt;
&lt;p&gt;So now I can change my keymap on the fly using the Vial WYSIWYG GUI which is great.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2024&amp;#x2F;2024-05-10-01HXHRWTR8AKXTB6TXPSQHSQ9C.2048.png&quot;&gt;
  &lt;figcaption&gt;Vial GUI with a kipra connected for realtime key remapping and easy JSON export&amp;#x2F;import&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;3d-modeling-the-case-in-janky-mode&quot;&gt;3D modeling the case in janky mode&lt;&#x2F;h2&gt;
&lt;p&gt;As I understand it presently, ergogen doesn&#x27;t provide that much to help with creating a case. I got it to spit out the basic outline of my PCB shape, but the rest of the process from the flatfootfox tutorial seemed daunting given the amount of geometry in my design. I also didn&#x27;t have experience in FreeCAD with the Draft and Part workbenches which seemed to be essential to this workflow. But with a bunch of research and experimentation, I hacked something together roughly as below. I&#x27;m not sure I can even do this again because I tried so many sequences of workbenches and tools. But logically, the flow goes:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;start with the PCB outline from ergogen, imported into FreeCAD Draft workbench as a &lt;code&gt;.dxf&lt;&#x2F;code&gt; file&lt;&#x2F;li&gt;
&lt;li&gt;Create a 2D offset from that allowing roughly 0.4mm for clearance so the PCB fits inside&lt;&#x2F;li&gt;
&lt;li&gt;Then do another 2D offset at 2.4mm for wall thickness (4 perimeters with my 0.6mm nozzle installed)
&lt;ul&gt;
&lt;li&gt;Select the &lt;code&gt;fill the offset&lt;&#x2F;code&gt; option on this one so you get a face&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;Extrude that up 12mm. We now have a wall our PCB will fit beautifully into, but no floor&lt;&#x2F;li&gt;
&lt;li&gt;Upgrade the first offset (where the floor should meet the interior wall) to a face and extrude it up 2mm&lt;&#x2F;li&gt;
&lt;li&gt;We now have walls and a floor&lt;&#x2F;li&gt;
&lt;li&gt;Use some standard geometry cubes and boolean difference in the Part workbench to make cutouts for the USB and TRRS cables
&lt;ul&gt;
&lt;li&gt;I did this very crudely so far&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;Ideally I would make 4 13mm diameter (not radius!) by 2mm depth cylindrical cutouts in the bottom for rubber bumpers at this point, but
I was mostly winging this and so excited when I got something working that I ended up doing those in Prusa Slicer instead.&lt;&#x2F;li&gt;
&lt;li&gt;Combine those shapes into a mesh and export to an STL you can print&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The case is essentially exactly what I hoped for but it&#x27;s a bit janky because I&#x27;m just eyeballing the MCU and TRRS positions since I have no reference geometry from ergogen available in this toolchain.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2024&amp;#x2F;2024-04-29-01HWMCD29R32Y79BK7X1WPHACZ.2048.jpg&quot;&gt;
  &lt;figcaption&gt;3D printed cases&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;I would also ideally model the support posts exactly underneath the mounting holes in the PCB. These will get m2 threaded inserts. But I don&#x27;t have a good process for this from ergogen, so I have decided to model individual posts, print them individually as distinct objects, screw them to the PCB, then just glue them to the floor of the case. I&#x27;ve tried to 3D model these without exact positioning information before and there&#x27;s no fudging it. If you&#x27;re off by a fraction of a millimeter, the bolt won&#x27;t thread into the insert.&lt;&#x2F;p&gt;
&lt;p&gt;I did several attempts using threaded inserts with no success. The 3mm thickness is not enough material to properly bottom out the heat-set insert and not warp the entire thing so it won&#x27;t sit flat. It&#x27;s super easy for the threads to fill with molten PLA during inserting so the bolt won&#x27;t thread. I did about 4 attempts before pivoting.&lt;&#x2F;p&gt;
&lt;p&gt;The next version I made little 3mmx7mm rectangles with a cutout to hold an m2 hex nut. So I just threaded the stack of bolt, PCB, printed standoff, and hex nut. Then I put a dab of superglue on each side of the bottom of the standoffs and set the keyboard in the case with some weight to hold it down while the glue cured. There&#x27;s 5 standoffs and not much force so I think it&#x27;ll be fine to keep the PCB in the case and also not have it resting on the hotswap sockets.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2024&amp;#x2F;2024-05-01-01HWTW7XEGD5EP7NDRV9JDA9VS.2048.jpg&quot;&gt;
  &lt;figcaption&gt;Standoffs for 3mm height with a captured m2 hex nut&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;adding-palm-rests&quot;&gt;Adding Palm Rests&lt;&#x2F;h2&gt;
&lt;p&gt;I have been using my sofle without palm rests comfortably, but the novelty of the kipra and the very different placement of the thumb arc made me feel like palm rests might be ergonomically necessary. I have some store bought cushy ones that actually fit nicely, but I don&#x27;t like how they can slide around independently from the keyboard. Since I had a FreeCAD sketch of the case outline, I modeled some palm rests to mate up exactly to the edge of the case. I put some clips on them so the easily clip on then won&#x27;t drift away from the keyboard.&lt;&#x2F;p&gt;
&lt;p&gt;I did 2 iterations so far and there&#x27;s still more tweaking to do. The shape is still not quite ideal and I&#x27;d like to maybe add magnets, but these are workable for the moment.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2024&amp;#x2F;2024-05-01-01HWSKAYQGDYQABQ44QNFDBSNB.2048.jpg&quot;&gt;
  &lt;figcaption&gt;palm rest clips to the case wall but can easily detach for travel&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;removing-palm-rests-and-going-caseless&quot;&gt;Removing palm rests and going caseless&lt;&#x2F;h2&gt;
&lt;p&gt;After a few days of trying some variations, I thought I might be able to get away without a case, and hopefully without a palm rest, by attaching some shelf liner material directly to the underside of the keyboard. I cut some scrap pieces we had lying around to roughly the right shape, spritzed the interior side lightly with water which the gorilla glue I used needs to activate, put the tiniest partial drop of glue I could manage in the center of each hotswap socket, placed them together, and left them weighted overnight for the glue to cure.&lt;&#x2F;p&gt;
&lt;p&gt;I love that this is very low profile, provides some shielding for the contacts on the PCB, and provides enough friction to keep the keyboard steady on the desktop. I&#x27;m still testing but I think this approach might be OK, or possibly with a fairly low palm rest.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2024&amp;#x2F;2024-05-08-01HXBMV7W0KEP33YJGKMZ0P6VT.2048.jpg&quot;&gt;
  &lt;figcaption&gt;shelf liners glued to the bottom&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;I later covered the exposed contacts with some electrical tape. I&#x27;ll still need to make a travel case but all I need to figure out is some kind of lid mechanism.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;hoping-for-firmware-bliss&quot;&gt;Hoping for firmware bliss&lt;&#x2F;h2&gt;
&lt;p&gt;My sofle has some nuisances that I suspect are either firmware or MCU issues. The one-shot shift key has a bug that makes it a 2-shot if you roll 2 letters quickly, which I do constantly starting sentences with &quot;The&quot; and it&#x27;s a nuisance. Also the right half goes comatose somewhat regularly and needs a reset. Also my one-shot layer key stops working for 2 specific keys after a while which is bizarre. Actually debugging those I think would be pretty tricky, so I&#x27;m hopefully these new kipras don&#x27;t have those issues. So far the evidence is that the 2-shot mod bug seems to still be there but the other issues are gone.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ambient-choc-switches-a-few-days-too-late&quot;&gt;Ambient choc switches a few days too late&lt;&#x2F;h2&gt;
&lt;p&gt;For these builds I ordered mostly enough choc pro red linear 35g switches to build 5 split keyboards. This was on April 5, 2024. On April 23, a brand new flavor of choc switches engineered to be silent went on sale for the first time over at &lt;a href=&quot;https:&#x2F;&#x2F;lowprokb.ca&#x2F;products&#x2F;ambients-silent-choc-switches?variant=44873446391972&quot;&gt;lowprokb.ca&lt;&#x2F;a&gt;. Of course I found out about this project just a few days after placing my order. Sad trombone.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2024&amp;#x2F;2024-04-13-01HVBPTEZ0Q7PBAKQWRK3B69QQ.2048.jpg&quot;&gt;
  &lt;figcaption&gt;Oops a huge quantity of what very recently transitioned from my favorite switches to my 3rd favorite switches&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;But anyway, I ordered enough ambient nocturnal (20g linear silent) and ambient twilight (35g linear silent) for a keyboard each and they are &lt;strong&gt;amazing&lt;&#x2F;strong&gt;! Absolutely the nocturnals are my favorite switch by a wide margin. They are even quieter than my thinkpad or macbook laptop switches. I commented on &lt;code&gt;&#x2F;r&#x2F;ergomechkeyboards&lt;&#x2F;code&gt; that typing on them feels like whispering and like my brain thinks a word and my fingers create it automatically. With louder switches it feels like I have to strain to enunciate my typing &quot;speech&quot; to be heard. I love this intimate feeling they have and the silent operation feels almost magical.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;source-files-repo&quot;&gt;Source files repo&lt;&#x2F;h2&gt;
&lt;p&gt;See &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;focusaurus&#x2F;kipra-keyboard&quot;&gt;focusaurus&#x2F;kipra-keyboard&lt;&#x2F;a&gt; on github for all the files you need for ergogen, kicad, freecad, and PCBWay if you want to make a variation of this.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;build-guide-coming-soon&quot;&gt;Build guide coming soon&lt;&#x2F;h2&gt;
&lt;p&gt;When I built the second kipra, I took photos throughout the process so I could create a detailed build guide. This is probably mostly going to be for me to reference, but if anyone else ends up ordering some PCBs, this will help them get it soldered up correctly.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;thanks-to-my-internet-heros&quot;&gt;Thanks to my internet heros&lt;&#x2F;h2&gt;
&lt;p&gt;I definitely could not have ever done this without both the cool open source projects people have built and shared, some online tutorials, discussion forums generally, and of course specific personal help for me with my many questions and struggles.&lt;&#x2F;p&gt;
&lt;p&gt;Special thanks and shout outs to my buds:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;zealot.hu&#x2F;&quot;&gt;mrzealot&lt;&#x2F;a&gt; who is the lead on ergogen and moderates the &lt;a href=&quot;https:&#x2F;&#x2F;zealot.hu&#x2F;absolem&#x2F;&quot;&gt;Absolem Club&lt;&#x2F;a&gt; discord&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;flatfootfox.com&quot;&gt;flatfootfox&lt;&#x2F;a&gt; who wrote &lt;a href=&quot;https:&#x2F;&#x2F;flatfootfox.com&#x2F;ergogen-introduction&#x2F;&quot;&gt;this amazing in-depth ergogen guide&lt;&#x2F;a&gt; which I studied constantly throughout this project&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;ceoloide&#x2F;&quot;&gt;ceoloide&lt;&#x2F;a&gt; who designed the reversible MCU footprint and provided tons of detailed specific help to me to get this build designed and working&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;casuanoob&quot;&gt;causuanoob&lt;&#x2F;a&gt; who diagnosed the QMK split keyboard firmware issues and helped get my RP2040 settings correct so the right half started working&lt;&#x2F;li&gt;
&lt;li&gt;hand_le for expert guidance and troubleshooting help&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;jmgrosen&quot;&gt;Jessie Grosen&lt;&#x2F;a&gt; and &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;cceckman&quot;&gt;Charles Eckman&lt;&#x2F;a&gt; for the KiCAD tutorial and pairing&lt;&#x2F;li&gt;
&lt;li&gt;All the &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;qmk&#x2F;qmk_firmware&#x2F;graphs&#x2F;contributors&quot;&gt;QMK contributors&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;All the &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;vial-kb&#x2F;vial-qmk&#x2F;graphs&#x2F;contributors&quot;&gt;Vial contributors&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</description>
      </item>
      <item>
          <title>Range Daily Checkins</title>
          <pubDate>Tue, 16 Jan 2024 00:31:48 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2024/01/range-daily-checkins/</link>
          <guid>https://peterlyons.com/problog/2024/01/range-daily-checkins/</guid>
          <description xml:base="https://peterlyons.com/problog/2024/01/range-daily-checkins/">&lt;p&gt;I&#x27;ve started a new position as Staff Engineer at Float Health. We&#x27;re using a nice SaaS tool called &lt;a href=&quot;https:&#x2F;&#x2F;www.range.co&#x2F;&quot;&gt;Range&lt;&#x2F;a&gt; for daily checkins as well as meeting agendas and notes. The workflow encourages everyone to write a small handful of bullets about the main things they worked on each day. We are lucky enough to have Range founder Dan Pupius working with us at Float Health, and I had to smile and send him this ancient screenshot. This is essentially my daily check-in&#x2F;log built from unix tools and a corporate wiki server..checks notes..18 years ago.&lt;&#x2F;p&gt;
&lt;p&gt;I hope Dan got a chuckle out of it. Needless to say, I&#x27;m a big fan of the tool and this is still a key part of my workflow for staying in good communication with my team and for activity records that can be used to build brag docs when promotion season rolls around.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;..&#x2F;..&#x2F;..&#x2F;..&#x2F;leveling-up&#x2F;daily_activity_log_redacted_cropped.png&quot; alt=&quot;Daily Activity Log&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Cuddle Up to KDL</title>
          <pubDate>Thu, 16 Nov 2023 15:13:25 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2023/11/cuddle-up-to-kdl/</link>
          <guid>https://peterlyons.com/problog/2023/11/cuddle-up-to-kdl/</guid>
          <description xml:base="https://peterlyons.com/problog/2023/11/cuddle-up-to-kdl/">&lt;p&gt;I&#x27;ve been trying out a relatively new config file format called &lt;a href=&quot;https:&#x2F;&#x2F;kdl.dev&quot;&gt;KDL&lt;&#x2F;a&gt;. It&#x27;s pronounced &quot;cuddle&quot; and here&#x27;s a very quick run-down of what it&#x27;s for and why I find it worth experimenting with. It&#x27;s a general purpose data language that feels mostly like a blend of JSON, command line, and XML. So it would be useable for use cases including configuration files, data interchange, and data storage, but so far I&#x27;ve only been considering it for the use case of a configuration file intended for manual editing by me primarily.&lt;&#x2F;p&gt;
&lt;p&gt;Read &lt;a href=&quot;https:&#x2F;&#x2F;kdl.dev&quot;&gt;the main kdl.dev&lt;&#x2F;a&gt; site for a very nice intro and full docs, but to just give you the gist of it:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;It has nice syntax
&lt;ul&gt;
&lt;li&gt;Nesting with curly braces works fine even for deep trees&lt;&#x2F;li&gt;
&lt;li&gt;No need to quote every property like JSON&lt;&#x2F;li&gt;
&lt;li&gt;More terse than JSON without getting into YAML ambiguity and gotchas&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;It supports comments properly&lt;&#x2F;li&gt;
&lt;li&gt;It has a nice spec, a test suite, and docs around consistent interchange between JSON and KDL&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;don-t-link-to-xkcd-don-t-do-it&quot;&gt;Don&#x27;t link to xkcd. Don&#x27;t do it.&lt;&#x2F;h1&gt;
&lt;p&gt;Yes, I&#x27;ve seen the comic. You&#x27;ve seen the comic. Everyone has seen the comic. There are real and painful issues with JSON, YAML, TOML, XML, and bespoke formats that make exploring new formats interesting and compelling &lt;strong&gt;to me&lt;&#x2F;strong&gt;. If you are hoping for a world where someone created the one true syntax and everyone just used that and lived happily ever after, I&#x27;m sorry to disappoint you. You might be best served by closing this browser tab and moving on at this point.&lt;&#x2F;p&gt;
&lt;p&gt;The &lt;a href=&quot;https:&#x2F;&#x2F;kdl.dev&#x2F;#faq&quot;&gt;KDL FAQ&lt;&#x2F;a&gt; addresses the common &quot;Why don&#x27;t you just use language X?&quot; questions.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;using-kdl-in-my-day-to-day&quot;&gt;Using KDL in my day to day&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;rofi-and-launch-lag&quot;&gt;rofi and launch lag&lt;&#x2F;h2&gt;
&lt;p&gt;I am a huge fan of a utility program called &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;davatorium&#x2F;rofi&quot;&gt;rofi&lt;&#x2F;a&gt; which I use to build many pop-up windows in my desktop environment that are absolutely the core of my workflow: switching programs, running scripts, opening bookmarks, grabbing emoji, etc. I ended up building my own variant because I wanted slightly different behavior (stay running permanently instead of do-then-exit) and I couldn&#x27;t find a way to do it with the popular rofi project. So I set out to write a basic version very tailored to my needs, which ended up being an interesting journey.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;false-start-in-go&quot;&gt;false start in go&lt;&#x2F;h2&gt;
&lt;p&gt;I started in go because I love the static binaries and not mucking with cloning source repos, installing interpreters, installing dependencies, etc.&lt;&#x2F;p&gt;
&lt;p&gt;My config file started as JSON and has a nested tree structure. I quickly realized how annoying this is to handle in go and struggled to model the types nicely as go structs that could be unmarshaled.&lt;&#x2F;p&gt;
&lt;p&gt;Because I was keen to just get something working to confirm or deny my hypothesis that a custom rofi variant would actually solve my problem, I knew I could do this way faster in node.js. This type of thing is cake in node, so I switched to node using commonjs modules and vanilla javascript.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;node-js-nofi-and-the-elm-architecture&quot;&gt;node.js: nofi and the elm architecture&lt;&#x2F;h2&gt;
&lt;p&gt;I got that version working well enough pretty quickly. I named the project &quot;nofi&quot;. I haven&#x27;t done much building of true terminal TUI style apps (and it&#x27;s been mostly bash scripts), so I wasn&#x27;t sure whether I&#x27;d need a terminal library to get some widgets, screen layout stuff, something to deal with the terminal and treat it essentially like a GUI toolkit. I decided pretty early to try a basic &lt;a href=&quot;https:&#x2F;&#x2F;guide.elm-lang.org&#x2F;architecture&#x2F;&quot;&gt;elm architecture&lt;&#x2F;a&gt; which at this point has proven itself to be an amazing balance of utility and beauty&#x2F;simplicity. I just hand coded a very basic &lt;code&gt;update()&lt;&#x2F;code&gt; &amp;amp; &lt;code&gt;view()&lt;&#x2F;code&gt; API and keystroke event handling. No library for this although I&#x27;m sure many exist, I just coded them up loosely, not worrying to much initially about doing things like &lt;code&gt;console.log&lt;&#x2F;code&gt; during the &lt;code&gt;update(model, message)&lt;&#x2F;code&gt; totally in dirty personal project mode.&lt;&#x2F;p&gt;
&lt;p&gt;I got the thing fully functioning way faster than I expected and it was working and clocking in under 200 lines of code. I added some unit tests since the elm architecture makes that so very easy.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;replacing-json-with-kdl&quot;&gt;replacing JSON with KDL&lt;&#x2F;h2&gt;
&lt;p&gt;I think it was at exactly this point that I discovered KDL on mastodon and immediately thought &quot;Hmm. I bet this would be nice for nofi so I could add comments&quot;. It would also be way easier to quickly add to my config file without too much friction since my use case maps so nicely to the KDL syntax. So I grabbed the js parsing library and ported my config file from JSON to KDL and swapped out &lt;code&gt;JSON.parse()&lt;&#x2F;code&gt; for the &lt;code&gt;kdl.parse()&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;a-porting-frenzy&quot;&gt;A porting frenzy&lt;&#x2F;h2&gt;
&lt;p&gt;Because of my current circumstances of keeping my tech skills up to date and job searching, things then got interesting. It went down roughly like this.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;I ported nofi to ES modules&lt;&#x2F;li&gt;
&lt;li&gt;I ported nofi to typescript&lt;&#x2F;li&gt;
&lt;li&gt;I ported nofi from node to bun&lt;&#x2F;li&gt;
&lt;li&gt;I ported the unit tests to bun&#x27;s built-in library&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;So now I had a nicer config file syntax that was very concise but not something bespoke and specific to nofi, just a generic syntax I could learn once and use a lot.&lt;&#x2F;p&gt;
&lt;p&gt;I also now had modeled the data structure in Typescript types, so maybe I could try porting it to go using that as a reference?&lt;&#x2F;p&gt;
&lt;h2 id=&quot;oops-no-go-support&quot;&gt;Oops, no go support&lt;&#x2F;h2&gt;
&lt;p&gt;At this time in Oct 2023, there were no go implementations of KDL parsing listed on the KDL website. Ah ha! I exclaimed, I will write the first go parser! So I set out to do just that.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;parsing-peg-and-pidgeon&quot;&gt;Parsing, PEG, and pidgeon&lt;&#x2F;h2&gt;
&lt;p&gt;In my brief research about parsing, lexing, recursive descent, hand-tuned parsers, parser combinators, etc, I stumbled upon the Parsing Expression Grammar (PEG) technology and found a go implementation called &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;mna&#x2F;pigeon&#x2F;tree&#x2F;master&#x2F;examples&quot;&gt;mna&#x2F;pigeon&lt;&#x2F;a&gt;. So I played around with that. The basic workflow is you model your language in the special PEG syntax, and then codegen builds you a library that can parse it. And the great thing was that KDL already publishes the PEG syntax in their spec (albeit with cosmetic syntax differences which seems to be a nuisance across the whole PEG ecosystem - they are all similar but not identical). So great - I could in theory take that grammar, tweak the syntax to match what pigeon needs, and get a go parser built? Maybe?&lt;&#x2F;p&gt;
&lt;p&gt;I got a lot of stuff working with this approach. The only other key thing is after the generated code has built an abstract syntax tree, it&#x27;s basically a sea of &lt;code&gt;[]interface{}&lt;&#x2F;code&gt; in go and you need to write little snippets of go code manually to map that to nice proper types which you can design. So I modeled the basic elements of KDL - a &lt;code&gt;Document&lt;&#x2F;code&gt; which is basically a tree of &lt;code&gt;Node&lt;&#x2F;code&gt;s and nodes have &lt;code&gt;Properties&lt;&#x2F;code&gt; and &lt;code&gt;Args&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;I was able to get through the bulk of KDL by porting a bit of PEG, writing some go snippets, and working my way around the language incrementally. I got a lot of stuff working this way:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;node lists and node identifiers&lt;&#x2F;li&gt;
&lt;li&gt;key&#x2F;value properties&lt;&#x2F;li&gt;
&lt;li&gt;positional arguments&lt;&#x2F;li&gt;
&lt;li&gt;scalar data types
&lt;ul&gt;
&lt;li&gt;booleans&lt;&#x2F;li&gt;
&lt;li&gt;numbers&lt;&#x2F;li&gt;
&lt;li&gt;quoted strings&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;nested nodes&lt;&#x2F;li&gt;
&lt;li&gt;slashdash comments&lt;&#x2F;li&gt;
&lt;li&gt;node type annotations&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;This was my night&#x2F;weekend hobby project for a while, but as I got all the low hanging fruit working, I started to hit issues building the remaining bits of the PEG grammar that I was unable to debug in go. The generated code and sea of deeply nested untyped &lt;code&gt;[]interface{}&lt;&#x2F;code&gt; eventually stopped me in my tracks.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;open-source-provides&quot;&gt;Open Source Provides&lt;&#x2F;h2&gt;
&lt;p&gt;At this point as I was circling back to the KDL docs to see if I missed anything, I just happened to notice that in just the past few weeks, suddenly instead of zero go implementations, there were now two (2!) separate implementations listed on the KDL site. &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;sblinch&#x2F;kdl-go&quot;&gt;kdl-go&lt;&#x2F;a&gt; and &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;lunjon&#x2F;gokdl&quot;&gt;gokdl&lt;&#x2F;a&gt;. It seems I wasn&#x27;t the only go developer interested, and these folks had more success. They had both written scanners and parsers by hand, which involves a lot of fiddly unicode&#x2F;rune stuff. One of them claimed to pass the full suite of tests from the KDL spec. So while I still want to get my pigeon version finished, I could try to use one of these libraries to get unblocked on porting nofi to go. This timing has &lt;a href=&quot;https:&#x2F;&#x2F;peterlyons.com&#x2F;problog&#x2F;2017&#x2F;12&#x2F;recurse-center-21-nom-and-combinators&#x2F;&quot;&gt;happened to me before&lt;&#x2F;a&gt; (except an open source library for what I needed literally appeared during my subway commute into Recurse Center that time), and it&#x27;s such an awesome feeling! It makes me want to high-five these people through the Internet.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;gofi-and-bubbletea&quot;&gt;gofi and bubbletea&lt;&#x2F;h2&gt;
&lt;p&gt;So I added kdl-go and felt some confirmation since the type model almost exactly matched what was in the javascript library as well as what I was working on in my kdlpigeon project.&lt;&#x2F;p&gt;
&lt;p&gt;OK so I had my data loaded in from my config file, but how should I TUI in go? I was aware that there are great libraries for this in go, and aware of &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;charmbracelet&#x2F;bubbletea&quot;&gt;bubbletea&lt;&#x2F;a&gt; in particular, but oh boy was I pleased to read the docs on bubble tea and discover that it&#x27;s actually the elm architecture for TUI programs in go. :chef-kiss:.&lt;&#x2F;p&gt;
&lt;p&gt;I felt immediately at home and blazed through porting my typescript implementation with my home grown elm architecture to go and bubbletea. There was basically no impedance mismatch and everything came over fast and easy. In pretty short order, I had all the basic nuts and bolts of dynamic key mapping across a nested menu structure working in go. I ported over the unit tests and then started using code coverage analysis to add unit tests to cover more paths through the big main &lt;code&gt;switch&lt;&#x2F;code&gt; statement within the &lt;code&gt;update()&lt;&#x2F;code&gt; method which is where the sausage is always made in the elm architecture.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;daily-driving-gofi&quot;&gt;Daily driving gofi&lt;&#x2F;h2&gt;
&lt;p&gt;Here&#x27;s a screenshot of gofi at the moment.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;problog&#x2F;images&#x2F;2023&#x2F;gofi.png&quot; alt=&quot;gofi screenshot&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;So gofi is now functionally complete and I have updated all my awesomewm keybindings and integrations to use it in my daily workflow. Keep in mind, I run this thing almost every time I switch applications so we&#x27;re talking for sure hundreds if not thousands of times in a given day. It has to be blazing fast and functionally flawless.&lt;&#x2F;p&gt;
&lt;p&gt;One technical glitch in debugging TUI code in go is so far I don&#x27;t know of a good way to launch the debugger from with VS Code and have a proper TTY available and way to send keystrokes. So there&#x27;s some friction there which requires me to launch a &lt;code&gt;dlv&lt;&#x2F;code&gt; debugger process from a terminal with a TTY where the TUI can show and I can use it, then attach to that from VS Code and switch back and forth while debugging. It does work though. It&#x27;s great to have a breakpoint at the beginning of &lt;code&gt;update()&lt;&#x2F;code&gt; and just examine all the calls there to understand your app state and how bubbletea represents the terminal as messages coming into your program&#x27;s core logic.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;aside-on-dynamic-and-static-typing&quot;&gt;Aside on dynamic and static typing&lt;&#x2F;h1&gt;
&lt;p&gt;At the beginning of this project, I was not yet clear on how to structure my configuration file. I didn&#x27;t know exactly what data shape to use to represent my application. I needed a few cycles of writing a flavor, trying to code against that, realizing some issue, revising the structure to address, and so on. Go being statically typed presents pretty much a hard wall here. Either you have some totally sound representation of all this and you can compile and run your program, or you cannot compile at all. Whereas in dynamic land, I can parse a document into whatever the library gives me and then interactively look at it in the debugger to learn its shape and how bits of syntax in the file get represented within the javascript program. A few sessions later, after I had a working Typescript program, porting it to go was easy and useful, but using dynamic types as a stepping stone was highly useful and effective for me.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;fin&quot;&gt;Fin&lt;&#x2F;h1&gt;
&lt;p&gt;So go check out &lt;a href=&quot;https:&#x2F;&#x2F;kdl.dev&quot;&gt;KDL&lt;&#x2F;a&gt; and play around with it. I would still like to get closure on my kdlpigeon project by squashing the current bug and then completing the remaining bits of the PEG grammar. I&#x27;m curious how it fares compared to the hand-coded state machine scanners and parsers in the other open source libraries.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Skeletyl Keyboard Build</title>
          <pubDate>Sun, 23 Apr 2023 15:14:56 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2023/04/skeletyl-keyboard-build/</link>
          <guid>https://peterlyons.com/problog/2023/04/skeletyl-keyboard-build/</guid>
          <description xml:base="https://peterlyons.com/problog/2023/04/skeletyl-keyboard-build/">&lt;h2 id=&quot;motivation-for-this-project&quot;&gt;Motivation for this project&lt;&#x2F;h2&gt;
&lt;p&gt;So back in November 2022 there was a &lt;a href=&quot;https:&#x2F;&#x2F;kbd.news&#x2F;giveaway&quot;&gt;giveaway contest run by kbd.news&lt;&#x2F;a&gt;. I entered and was one of the winners and we got a choice of various discount coupons, so I requested a discount over at &lt;a href=&quot;https:&#x2F;&#x2F;bastardkb.com&#x2F;&quot;&gt;bastardkb&lt;&#x2F;a&gt;. I was very curious about the flexible PCBs Quentin uses in his low-profile dactyl-inspired builds. I had already made a DIY  TBK Mini which is his 3x6+3 flavor. I 3D printed the case, glued in kailh choc brown tactile low-profile switches, hand wired the matrix, and used a pair of elite-c microcontrollers. I realize now that I never even blogged about that build. I forget exactly when I did it, but the pertinent takeaway is that it is a pretty great form factor and it was my daily driver for a while just prior to the &lt;a href=&quot;&#x2F;problog&#x2F;2022&#x2F;01&#x2F;squeezebox-keyboard-v2112&#x2F;&quot;&gt;squeezebox v2112&lt;&#x2F;a&gt; being ready for action.&lt;&#x2F;p&gt;
&lt;p&gt;After daily driving my &lt;a href=&quot;https:&#x2F;&#x2F;shop.keyboard.io&#x2F;products&#x2F;model-100&quot;&gt;Keyboardio Model 100&lt;&#x2F;a&gt; while traveling this winter, when I got back home I was really missing QMK&#x27;s combo support and the lower travel of choc switches, so I busted out the tbkmini again. It&#x27;s very pleasant to type on. My only real issue with this one is I would now prefer linear switches as opposed to the brown tactiles which are in there. They are feeling quite sticky and inconsistent. The other janky part of my tbkmini is I 3D printed a small MCU holder but did not have the modeling skills to make it mount properly to the threaded inserts in the case, so instead I cut out a slot for it and glued in some terrible hacky adapters. It works fine but it&#x27;s a hack job for sure.&lt;&#x2F;p&gt;
&lt;p&gt;So my hope was to build a skeletyl which would be same basic shape with a few improvements:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Remove the outer pinky column which is an RSI injury factory. I don&#x27;t use it anyway, but just get rid of it.&lt;&#x2F;li&gt;
&lt;li&gt;Use some linear choc silver switches&lt;&#x2F;li&gt;
&lt;li&gt;Get the fancy flex PCBs so I don&#x27;t have to hand wire it&lt;&#x2F;li&gt;
&lt;li&gt;Get the MCU holder so that part is solid and not janky.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;the-order-and-the-case&quot;&gt;The order and the case&lt;&#x2F;h2&gt;
&lt;p&gt;So I used the coupon to order the skeletyl kit which has the flex PCBs, MCUs, diodes, screws, threaded inserts, TRRS jacks, etc. But no case, switches, or keycaps. So while traveling and visiting a friend I got access to a 3D printer and cranked out the 2 case tops and bottom parts so those were already done and came out really well. The order was here waiting for me when I got home.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-first-build-session-first-oopsie&quot;&gt;The first build session, first oopsie&lt;&#x2F;h2&gt;
&lt;p&gt;I sat down to study the build guide and a few youtube video build vlogs. When I test fit a kailh choc silver switch into the case it fits fine since choc and MX slot into the same size square cutout, but the PCB would not engage. The choc housing does not protrude beyond the bottom of the case and there&#x27;s no way to mate it into the PCB. I searched around bastardkb for &quot;choc&quot; and was surprised to find that my fears were true: if you build with the flex PCBs, they only support MX switches and not chocs! Oh no! I&#x27;ve seen some clever PCB footprints that support both so it&#x27;s possible, but even the switch pin through holes are not in the right spot.&lt;&#x2F;p&gt;
&lt;p&gt;I was a bit despondent about this. After a long sigh of &quot;why do I do this keeb life to myself&quot; and some navel gazing, I considered two possible paths forward.&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Research silent linear MX switches with the shortest activation travel and total travel, order them, and use MX for this build&lt;&#x2F;li&gt;
&lt;li&gt;Hack through it by running tiny sections of wire from the choc switch pins through the PCB through holes.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;I thought the choc hack might run into mechanical difficulties actually getting the sections of wire to thread through the PCB through holes when you account for it needing to be mounted into the case while this happens and there might not be any clearance to get tweezers in there to guide the wires through the holes and just might be terribly frustrating.&lt;&#x2F;p&gt;
&lt;p&gt;So I asked in my channel on the &lt;a href=&quot;https:&#x2F;&#x2F;discord.gg&#x2F;ddY9UhrM&quot;&gt;Absolem Club Discord&lt;&#x2F;a&gt; and someone recommended akko custom silver for short activation travel. So I ordered a box of those sufficient to build this 36-key keyboard.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;mx-switch-modification-side-quest&quot;&gt;MX Switch Modification Side Quest&lt;&#x2F;h2&gt;
&lt;p&gt;In discord discussions around my fruitless quest for low-travel switches, folks pointed me to both o-rings as a possible mod as well as the &lt;a href=&quot;https:&#x2F;&#x2F;www.reddit.com&#x2F;r&#x2F;MechanicalKeyboards&#x2F;comments&#x2F;70om2d&#x2F;modification_the_ball_bearing_mod_reducing_travel&#x2F;&quot;&gt;ball bearing mod&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;I&#x27;ve never truly grokked the mechanism by which folks claim o-rings work and the whole thing seems dubious, but I ordered sets of ball bearings in 0.8mm and 1.2mm diameters and a bag of o-rings without extensively searching&#x2F;shopping&#x2F;understanding the available varieties.&lt;&#x2F;p&gt;
&lt;p&gt;So the build gets paused for a week or so then I proceed with experimental modding to reduce travel.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2023&amp;#x2F;2023-04-14-01GY0N5WZGR2R81MDJRH1Y28D4.2048.jpg&quot;&gt;
  &lt;figcaption&gt;Very small ball bearings&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;While experimenting with different size ball bearing mods to reduce travel, we didn&#x27;t have a great test rig set up. I guess ideally you&#x27;d have a hotswap MX keyboard at hand and swap in your modded key to do some careful quality control tests. Being low-profile enthusiasts, we have little to no MX gear around here, so our test rig consisted of holding the modded switch in the skeletyl PCB and trying to rotate it where both switch pins would make good contact with the PCB through holes then while not letting that wiggle out of place, try to type. We basically got &quot;it never types&quot; or &quot;it probably types OK&quot; result accuracy.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2023&amp;#x2F;2023-04-14-01GY0SFN60GJRRQWAE3JKP6X36.2048.jpg&quot;&gt;
  &lt;figcaption&gt;Opening the switch and studying the hollow stem post where the ball bearing will get dropped&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2023&amp;#x2F;2023-04-14-01GY0VD4YRYSP02S7GWRD6BN9B.2048.jpg&quot;&gt;
  &lt;figcaption&gt;Realizing that 1.75mm diameter PLA 3D printer filament fits well into the stem post hole&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2023&amp;#x2F;2023-04-14-01GY0VP058Z78MD2A7020XPNG5.2048.jpg&quot;&gt;
  &lt;figcaption&gt;Trying to slice with a utility razor a cylinder of filament exactly 1.5mm long&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;We discovered you can put as many as 8 0.8mm ball bearings in the switch post and still have the switch activate. We tried all the basics and some weird things like combinations of ball bearings of different sizes. The main learning from the first session was that 1.2mm still activated the switch (* footnote spoiler read ahead), so we decided to proceed and order 1.5mm ball bearings even though the original post we were studying said it was too large. Maybe because these akko CS silver switches have a high activation point our were still activating?&lt;&#x2F;p&gt;
&lt;p&gt;We didn&#x27;t have any 1.8mm ball bearings, but we found the little brass spheres from a keychain were about that size so we tried that and found the switch wouldn&#x27;t activate. So that was our upper bound.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2023&amp;#x2F;2023-04-16-01GY55724RDWCZCP01QYAF91S5.2048.jpg&quot;&gt;
  &lt;figcaption&gt;Display of our experimental specimens&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;We got a nice shot of the travel reduction of each mod by removing the springs so gravity would leave the stem in the lowest position for the photo. The rightmost switch isn&#x27;t fully seated into the plate. Sorry about that. I didn&#x27;t notice until I had already torn down the rig. But the 1.5mm ball bearing and 1.5mm hunk of PLA filament both do exactly the same thing to the stem travel.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2023&amp;#x2F;2023-04-22-01GYMW5TC0TXCARE3D3FKZF1Y1.2048.jpg&quot;&gt;
  &lt;figcaption&gt;Here&amp;#x27;s how the mods affected bottom out position&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2023&amp;#x2F;2023-04-22-01GYMW6PP87VPFVMJZK1KRQCFZ.2048.jpg&quot;&gt;
  &lt;figcaption&gt;We got this shot by removing the springs so gravity would leave the stem at the bottom for the photo&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;build-modded-switches-into-the-pcb&quot;&gt;Build modded switches into the PCB?&lt;&#x2F;h2&gt;
&lt;p&gt;So yesterday we proceeded to insert a modded switch into the skeletyl 3D printed case, bend the flexible PCB into position over the post, and solder the first switch into the case. Once that was done, we had a much more reliable setup from which to carefully test for clean typing.&lt;&#x2F;p&gt;
&lt;p&gt;AC went into production line mode opening switches and preparing to work through a full batch of 18 switches, but I insisted we do a round of careful tests first. It turned out to be the right approach because as we tested, we easily found clear double-press misfires with sometimes as much as like 5% probability. So we kind of panicked a bit and soldered I think 3 total switches in: one stock with no modifications, one with a 1.2mm ball bearing, and one with a 1.5mm ball bearing.&lt;&#x2F;p&gt;
&lt;p&gt;The unmodded switch worked without issue. Both of the modded switches had frequent double-fire errors. I got pretty despondent and felt like all this time researching, ordering, waiting for parts, 3D printing custom tools, etc was  a waste. I mean, we learned what doesn&#x27;t work though, right?&lt;&#x2F;p&gt;
&lt;p&gt;So then I had to desolder the modded switches so we could unmod them. I got them out but desoldering is tricky business.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2023&amp;#x2F;2023-04-22-01GYMZVZM0W5T15CEMWGN61XDK.2048.jpg&quot;&gt;
  &lt;figcaption&gt;Desoldering the unreliable modded switches. Tricky business.&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;So next there was like a camera pan to a high overhead shot of me in the lab where this project was supposed to yield a choc skeletyl and instead here I am building with unmodified MX switches with a mile of travel and a super high profile like a pleb. So I decided to just finish the build, play around with vial firmware which I haven&#x27;t used yet, and try it out. If it&#x27;s OK for me to type on, it will be in theory less janky than my current daily driver hand wired tbkmini.&lt;&#x2F;p&gt;
&lt;p&gt;Getting the switches installed, clicked into the &quot;flexible&quot; PCBs and soldered on a skeletyl is hard. Like really hard. Like, it would be easier to hand wire this hard. The &quot;flex&quot; mechanism doesn&#x27;t really handle the z-axis variation that well. I bent switch pins on a bunch of switches in trying to get the PCB pressed into place.&lt;&#x2F;p&gt;
&lt;p&gt;Eventually I found solutions combining helping hands clamped to the case, and either a tweezer wedged in holding the PCB down or a spring clamp if there is access from the side to place it.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2023&amp;#x2F;2023-04-22-01GYNAF54GWSY89KGXZ4VAH7BF.2048.jpg&quot;&gt;
  &lt;figcaption&gt;Tweezer wedged against the case holding the PCB in position so I can solder the switch pins&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;When I got one side finished, I mounted the MCU holder PCB to the case. This was also very tricky to get both ports fully aligned with their holes in the case and both of the threaded inserts exactly below the through holes so the screws can thread in. I eventually got it to work, but this could easily have been 15 minutes of futzing.&lt;&#x2F;p&gt;
&lt;p&gt;When I went to attach the bottom plate, I had another sad trombone moment when I realized I had soldered the ribbon cables to the wrong side of the MCU PCB. I think the electronic connections are still right because the legends lined up, but there&#x27;s also a chance that the wiring matrix is now messed up and I&#x27;ll have to adjust the firmware to compensate for that. But in the end, I just bent the ribbon cables a bit closer to the PCB and a bit sharper of an angle than is probably ideal, but the case does now fit and doesn&#x27;t bulge out at that spot. I could try to fix this before finishing the other half but I think it&#x27;ll be easier to manage if both halves are symmetric. Plus, I did probably my best ever soldering job on this project and I don&#x27;t want to mess that up with a bunch of desoldering and resoldering on the ribbon cables.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2023&amp;#x2F;2023-04-22-01GYND24D8EZ118YEZ2X6MSKE3.2048.jpg&quot;&gt;
  &lt;figcaption&gt;Realizing I soldered the ribbon cables to the wrong side of the MCU holder&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;I went through and finished soldering all of the switches into both halves. When that was done I tested them and of course found that my home row index finger switch was busted, and of course accessing the underside of that one to desolder it requires removing the MCU holder again. So I undid all those bolts, managed to get the switch desoldered and slurp off the excess solder, find a switch from our stack of experiments that I could restore to unmodded operable state, and solder that into place.&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;OK so I&#x27;m now typing on the new skeletyl build. I threw some MX DSA keycaps on it that we had lying around from the early days when we were trying to build dactyl manuforms. I put some rubber bumper feet on it and connected it to my laptop. I have not used Vial before this and it&#x27;s very awesome. The keyboard was recognized and kind of to my surprise both halves worked with nothing about the matrix busted (at least if I connect USB to the right hand half).&lt;&#x2F;p&gt;
&lt;p&gt;So I very quickly set up my layers and keymaps and Vial applies them in real time with no flashing which is amazing.&lt;&#x2F;p&gt;
&lt;p&gt;So I&#x27;ve already done 88 WPM on monkeytype. How loud these switches are is going to take some adjustment, but I think it&#x27;ll be fun to wade into the world of MX true mechanical keyboards for a while at least.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2023&amp;#x2F;2023-04-23-01GYQQEW08EG7CWKM0G149XMNH.2048.jpg&quot;&gt;
  &lt;figcaption&gt;skeletyl set up and working&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
</description>
      </item>
      <item>
          <title>Markdown to PDF with weasyprint</title>
          <pubDate>Tue, 21 Feb 2023 00:01:47 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2023/02/markdown-to-pdf-with-weasyprint/</link>
          <guid>https://peterlyons.com/problog/2023/02/markdown-to-pdf-with-weasyprint/</guid>
          <description xml:base="https://peterlyons.com/problog/2023/02/markdown-to-pdf-with-weasyprint/">&lt;p&gt;I have a lengthy history of cobbling together tools for working with documents written in markdown and getting them into PDF when interfacing with the default world who are not always plain text die hards. And I&#x27;m super excited about this post because this is finally actually both very easy and pretty nice. So I&#x27;ll save some more of the journey and story for the bottom of this post, but here&#x27;s the new tooling stack.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;markdown-to-html-with-pandoc&quot;&gt;Markdown to HTML with pandoc&lt;&#x2F;h2&gt;
&lt;p&gt;I use the trusty old &lt;a href=&quot;https:&#x2F;&#x2F;pandoc.org&#x2F;&quot;&gt;pandoc&lt;&#x2F;a&gt; for this bit. And the beautiful part is all the defaults are fine so something as simple as &lt;code&gt;echo &quot;# Test 1&quot; | pandoc&lt;&#x2F;code&gt; spits out the expected HTML. For readability and reliability purposes, in my actual shell function, I have:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;md-to-html() {
  pandoc --from=markdown --to=html
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;html-to-pdf-with-weasyprint&quot;&gt;HTML to PDF with weasyprint&lt;&#x2F;h2&gt;
&lt;p&gt;A colleague pointed me to &lt;a href=&quot;https:&#x2F;&#x2F;weasyprint.org&#x2F;&quot;&gt;weasyprint&lt;&#x2F;a&gt; which is the hero we all need in this workflow. It does require file paths for input and output, but handles &lt;code&gt;-&lt;&#x2F;code&gt; meaning stdio just fine, so &lt;code&gt;weasyprint - -&lt;&#x2F;code&gt; makes it a standard unix filter. It can take a stylesheet to make the PDF pretty, so here&#x27;s my final shell function.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;html-to-pdf() {
  weasyprint --stylesheet ~&amp;#x2F;.config&amp;#x2F;markdown&amp;#x2F;screen.css - -
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Of course you can customize the styles, but setting the page size to letter if you are in the US and making smaller margins is a reasonable starting point. Here&#x27;s my stylesheet.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;css&quot; class=&quot;language-css &quot;&gt;&lt;code class=&quot;language-css&quot; data-lang=&quot;css&quot;&gt;@page {
  margin: 1cm;
  size: Letter;
}
body {
  margin: 0;
  font-family: sans-serif;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;lazy-pdf-on-filesystem-ready-to-email-upload&quot;&gt;Lazy PDF on filesystem ready to email&#x2F;upload&lt;&#x2F;h2&gt;
&lt;p&gt;Here&#x27;s my final convenience wrapper that lets me have some markdown in a file or copied to my clipboard and easily get a PDF on disk I can upload to whatever agency I&#x27;m having to send a PDF to.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;md-to-pdf-file() {
  input=&amp;quot;&amp;#x2F;dev&amp;#x2F;stdin&amp;quot;
  output=$(mktemp &amp;#x2F;tmp&amp;#x2F;file-XXX.pdf)
  case $# in
  0) ;;
  1)
    case &amp;quot;$1&amp;quot; in
    *.md)
      input=&amp;quot;$1&amp;quot;
      ;;
    *.pdf)
      output=&amp;quot;$1&amp;quot;
      ;;
    *)
      echo &amp;quot;Expecting either a markdown file ending in .md or a PDF file ending in .pdf when run with one argument&amp;quot; 1&amp;gt;&amp;amp;2
      return 10
      ;;
    esac
    ;;
  2)
    input=&amp;quot;$1&amp;quot;
    output=&amp;quot;$2&amp;quot;
    ;;
  *)
    cat &amp;lt;&amp;lt;EOF
Converts markdown text into a PDF file.

Input and output are determined based on number of command line arguments provided.

With no arguments, markdown will be read from standard input and a PDF file will be generated in &amp;#x2F;tmp&amp;#x2F;file-XXX.pdf with a unique temporary filename. The temporary filename will be written to standard output.

With one argument, the file name will be matched based on .md or .pdf extension and treated as input or output accordingly. The input will come from standard input if a .pdf argument is provided, otherwise the .md argument will be processe as input to a temporary output PDF file.

In all cases, the name of the output .pdf file will be written to standard output.
EOF
    return 10
    ;;
  esac
  md-to-html &amp;lt;&amp;quot;${input}&amp;quot; | html-to-pdf &amp;gt;&amp;quot;${output}&amp;quot;
  echo &amp;quot;${output}&amp;quot;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;hot-reloading&quot;&gt;Hot reloading&lt;&#x2F;h2&gt;
&lt;p&gt;I don&#x27;t quite have hot reloading really working, but &lt;code&gt;entr&lt;&#x2F;code&gt; is great for watching the source markdown file for changes and automatically generating the PDF, which I can have open in &lt;code&gt;mupdf&lt;&#x2F;code&gt; and hit &quot;r&quot; to reload it. It&#x27;s pretty effective while iterating on the stylesheet or checking layout, etc.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;other-tools-for-markdown-rendering&quot;&gt;Other tools for markdown rendering&lt;&#x2F;h2&gt;
&lt;p&gt;Since I&#x27;m biased toward command line tools coded in rust, I previously used &lt;code&gt;pulldown-cmark&lt;&#x2F;code&gt; for the markdown to HTML conversion. But arch linux is kind of annoying with rust compiling and there&#x27;s no binary package for it, so sticking with pandoc for simplicity.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;other-tools-for-pdf-generation&quot;&gt;Other tools for PDF generation&lt;&#x2F;h2&gt;
&lt;p&gt;For a while I tried to work with the oldie &lt;code&gt;wkhtmltopdf&lt;&#x2F;code&gt;. I tried various headless chrome thingies, most recently &lt;a href=&quot;https:&#x2F;&#x2F;www.npmjs.com&#x2F;package&#x2F;html-pdf-chrome&quot;&gt;html-pdf-chrome&lt;&#x2F;a&gt; which can automatically fire up a headless chrome process and have it render the PDF. But this needed a small custom javascript file and node and npm and weasyprint has an AUR arch linux package that&#x27;s ready to go, so I adopted that instead.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;bonus-formatting-email-messages&quot;&gt;Bonus: Formatting Email Messages&lt;&#x2F;h2&gt;
&lt;p&gt;Even though I&#x27;ve grumped and grumped for years on the Internet that copy and paste should NOT retain formatting by default and what a scourge &quot;Paste without formatting&quot; not being the default is, I do like to send a lot of bullet lists in email and being able to compose those as markdown is nice. So my workflow is draft an email in neovim in markdown, copy it to the clipboard, then convert it to HTML and copy that to the clipboard with formatting. Then I can paste into an email and it&#x27;s formatted correctly. Works in gmail and fastmail.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;~&amp;#x2F;bin&amp;#x2F;paste | pandoc --from=markdown | xclip -t text&amp;#x2F;html -selection clipboard
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;kinda-bonus-slack-bullet-lists&quot;&gt;Kinda bonus: Slack bullet lists&lt;&#x2F;h2&gt;
&lt;p&gt;Slack now kinda converts markdown bullet lists to formatted if you paste or type them in in markdown. It&#x27;s still mostly slack&#x27;s weird custom format and there&#x27;s still no way to just let slack let us use markdown directly, and it doesn&#x27;t work for all syntax, but it&#x27;s better than it used to be when it would just send the message as plain text.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Tolerable HTML and CSS</title>
          <pubDate>Wed, 07 Dec 2022 01:17:26 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2022/12/tolerable-html-and-css/</link>
          <guid>https://peterlyons.com/problog/2022/12/tolerable-html-and-css/</guid>
          <description xml:base="https://peterlyons.com/problog/2022/12/tolerable-html-and-css/">&lt;h2 id=&quot;back-story&quot;&gt;Back Story&lt;&#x2F;h2&gt;
&lt;p&gt;For my entire time as a web developer, front end web layout has been a kludgey messy nightmare. I think about every few years since 1999 or so I spend a few days reading the latest CSS features that are the new hotness and advertised as finally going to solve pervasive problems with layout, centering, alignment, etc. I&#x27;ve just been through so many rounds of this nonsense: rounded corners, sticky headers and footers, gradients, wrapping text around images, weird magic number font sizes, negative margins, margin collapsing, battling horizontal scrollbars, parallax nonsense, full-width background images. I have the scars. Every time without exception, I&#x27;ve come away disappointed and bitter and without an actually workable solution.&lt;&#x2F;p&gt;
&lt;p&gt;Anyway, when flexbox turned out to be complex and bad and front end devs seemed to still be doing bootstrap or various terrible grid systems, I noped out again to focus on back end concerns and continue my wait-25-years-if-thats-what-it-takes approach to wanting semantic HTML with no extra wrapper divs and expressive CSS that matches how we think about layout.&lt;&#x2F;p&gt;
&lt;p&gt;For the late 2022 round of this self-flagellation, I&#x27;m taking a look at CSS grid in hopes that I can 1. Not use a 3rd party grid system and 2. be rid of non-semantic wrapper row&#x2F;column grids. I watched some great tutorials by Jen Simmons on CSS grid, and I think I&#x27;m ready to declare the project a success. There&#x27;s still a handful of nuisance issues, but most of my modest layout desires are straightforward to implement with CSS grid and other core features both old and new.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;tidy-site&quot;&gt;Tidy Site&lt;&#x2F;h2&gt;
&lt;p&gt;To showcase how I like to split my SCSS files up and which ones I can re-use wholesale between similar site projects, I did a little demo project called &lt;a href=&quot;https:&#x2F;&#x2F;focusaurus.github.io&#x2F;tidy-site&quot;&gt;Tidy Site&lt;&#x2F;a&gt;. It&#x27;s up on github pages for the live version and the source code is on github. Have a look!&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;focusaurus.github.io&#x2F;tidy-site&quot;&gt;https:&#x2F;&#x2F;focusaurus.github.io&#x2F;tidy-site&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Squeezebox Scorecard</title>
          <pubDate>Sat, 03 Dec 2022 16:03:27 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2022/12/squeezebox-scorecard/</link>
          <guid>https://peterlyons.com/problog/2022/12/squeezebox-scorecard/</guid>
          <description xml:base="https://peterlyons.com/problog/2022/12/squeezebox-scorecard/">&lt;p&gt;This article was written for the &lt;a href=&quot;https:&#x2F;&#x2F;kbd.news&#x2F;Squeezebox-Scorecard-1763.html&quot;&gt;Keyboard Builder&#x27;s Digest Advent Calendar&lt;&#x2F;a&gt;. I&#x27;m posting it to my blog below as well.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;squeezebox-scorecard&quot;&gt;Squeezebox Scorecard&lt;&#x2F;h1&gt;
&lt;p&gt;In this article I&#x27;ll review some of the design experiments I&#x27;ve conducted in my squeezebox prototyping efforts and discuss each individually through the lens of an experimental hypothesis, trial in a prototype build, and results.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-scooped-column-3-switches-at-100deg-160deg-angles&quot;&gt;The scooped column: 3 switches at 100° &amp;amp; 160° angles&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2022&#x2F;2022-01-02-01FRDM8WYG13R9WRF9REBXNM67.2048.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;This is probably the most fundamental idea that drove me to build the squeezebox. I played saxophone most of my life and it can be played very fast and accurately using primarily the pads of the fingers in a trigger motion, not the points of the fingers in a typing motion. I thought these should be combined with the pad resting on one &quot;home corner&quot; switch and the point resting on another switch, either of which could be pressed without moving the hand or the base knuckle.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Result&lt;&#x2F;strong&gt;: Yup, it&#x27;s good. Certainly for index, middle, and ring fingers. It&#x27;s less clear for the pinky and not applicable to the thumb.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-home-corner-1-finger-chord&quot;&gt;The home corner 1-finger chord&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-04-17-01F3FY53HGBS7XN9Q87F94MBX1.2048.jpg&quot; alt=&quot;Chording the pinkie corner on v2104&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;By poking at the corner between row 1 and 2, both keys could be chorded down with a single finger.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Result:&lt;&#x2F;strong&gt; Abandoned with caveat. Not reliable on current QMK. Might be possible with more carefully tuned&#x2F;specialized handling in firmware, but too many misfires during testing.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;type-directly-on-choc-stems-without-keycaps&quot;&gt;Type directly on choc stems without keycaps&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2022&#x2F;2022-01-02-01FRDKZK38WS6FDSGM3HNCJ9Q8.2048.jpg&quot; alt=&quot;v2112 with capless choc red switches&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;This was not really a design intention, it was just a kludge&#x2F;shrug that fell out of early prototyping of the scooped column where tight spacing made it impossible to fit keycaps in and get the corner mechanics that I wanted.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Result&lt;&#x2F;strong&gt;: Works fine. Has been in every squeezebox prototype.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;mechanical-adjustments-with-slots-and-bolts-for-tailor-fit&quot;&gt;Mechanical adjustments with slots and bolts for tailor fit&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-12-25-01FQRVCCA8YR0GR971N30XVSWS.2048.jpg&quot; alt=&quot;prototype with slots for adjustment&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;I wanted to adjust the column offset near&#x2F;far on a per-finger basis and be able to adjust fit without reprinting any parts or rebuilding any wiring. Early prototypes had the slots on the keywells and later slots were moved to the base plate instead.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Result&lt;&#x2F;strong&gt;: Yup, works fine within a certain narrow range&lt;&#x2F;p&gt;
&lt;h2 id=&quot;custom-near-far-tailor-fit&quot;&gt;Custom near&#x2F;far tailor fit&lt;&#x2F;h2&gt;
&lt;p&gt;Hypothesis was that each column should be adjusted with fine granularity to fit the varying lengths of fingers and distance between the knuckle and the finger tip.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Result&lt;&#x2F;strong&gt;: This is only really true for the pinky. For my hand, index, middle, and ring the offset is so small that even having them all the same is fine for me.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;custom-column-height-for-each-finger&quot;&gt;Custom column height for each finger&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-12-17-01FQ3VM6CGFBSS9NHPES2ARK0C.2048.jpg&quot; alt=&quot;Posts for custom height on a prototype&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;The hypothesis was that each column should have a finely tuned height to exactly fit each finger&#x27;s natural resting position.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Result&lt;&#x2F;strong&gt;: Only really true for the pinky finger. For the index, middle, and ring fingers it&#x27;s actually better to have all the same height so proprioception can sense where the bottom is across fingers.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;height-adjustment-with-threaded-bolt&quot;&gt;Height adjustment with threaded bolt&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2022&#x2F;2022-04-06-01FZZ88HK0375WXWYHDGSFGF63.2048.jpg&quot; alt=&quot;Post with threaded bolt and captured nut&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;The hypothesis was each keywell could sit atop a post incorporating a nut and bolt such that by twisting the post you could easily adjust the height.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Result&lt;&#x2F;strong&gt;: Abandoned. Height changes are not that necessary and it&#x27;s really just about getting the pinky right. This was a bit too fiddly and easy to go out of adjustment so I abandoned it. The idea still seems kind of neat though.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;per-finger-granular-splay-tailor-fit&quot;&gt;Per-finger granular splay tailor fit&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2022&#x2F;2022-01-02-01FRDJ60AGX3RVXRN40HMNREAP.2048.jpg&quot; alt=&quot;Side view showing splay angles&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;The hypothesis was as the fingers extend, each column should closely follow the natural splay angle.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Result&lt;&#x2F;strong&gt;: Mixed. I don&#x27;t like any on the ring finger, a tiny bit for index is OK but probably not necessary, and a moderate amount on the pinky I think is legit but also might be unnecessary.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;3d-print-many-small-components-and-assemble-into-a-product-mechanically&quot;&gt;3D print many small components and assemble into a product mechanically&lt;&#x2F;h2&gt;
&lt;p&gt;The hypothesis was iteration will be faster and cheaper and easier to motivate with a componentized approach instead of dactyl-style large complex parts to print.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Results&lt;&#x2F;strong&gt;: Holds true for prototyping for sure. If this were ever to get to production run quality, there&#x27;s probably a flavor where at least the 4 fingers per hand type on a single large part containing all the columns and switches.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;mechanical-adjustments-with-magnets-on-a-steel-plate&quot;&gt;Mechanical adjustments with magnets on a steel plate&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-06-02-01F75PW0JREN5A3RFN4KP017TA.2048.jpg&quot; alt=&quot;The prototype with magnets and steel base plate&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;The hypothesis was granular adjustment of keywell position across both axes of a base plate can be achieved by each keywell having strong magnets in the base and mounting them on a steel base plate.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Results&lt;&#x2F;strong&gt;: Abandoned. It&#x27;s too fragile and easy to accidentally reposition. Won&#x27;t hold up for transportation, etc.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;chopped-chocs-for-tighter-spacing&quot;&gt;Chopped chocs for tighter spacing&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2022&#x2F;2022-04-14-01G0N048Z8520RYBEX4G6J8MQ9.2048.jpg&quot; alt=&quot;2x3 column with chopped chocs for tight spacing side to side&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;I observed that even the smallest commercially-available switches seemed too far apart for my aspirations. I learned on discord that at least 30% of the area of a choc is non-essential space for housing a LED. That section of the switch can be cut off and the switch will still work fine. I did not invent this approach and probably never would have thought to do so. I learned about this on discord from others who had already pioneered it.&lt;&#x2F;p&gt;
&lt;p&gt;The hypothesis was to adopt this technique to pack switches closer together. This was first incorporated as an swap-in index column component (yay modularity!) in a 2x3 index column so the innermost reach column could be as close as possible to the home column.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Result&lt;&#x2F;strong&gt;: Validated. It&#x27;s a lot more work, hard to do consistently with DIY tools, and makes it much harder to seat a switch into a housing once it&#x27;s chopped, but it&#x27;s worth it in my opinion. It&#x27;s a close call though. The tight spacing does not cause me any issues in typing; No mistypes by hitting neighbor keys by accident. So the technique works, but whether the tighter spacing is worth the significant extra work in building the keyboard isn&#x27;t clear. For now, yes, it&#x27;s worth it. 3 or 4 prototypes further down the road, I wouldn&#x27;t be surprised if my motivation runs out.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wide-split-shoulder-width&quot;&gt;Wide split (shoulder width)&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2022&#x2F;2022-10-13-01GF95JGZ8W92CG3JEAQWNG1AK.2048.jpg&quot; alt=&quot;v2209 with wide split stand&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;The hypothesis of split keyboards: put them further apart.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Result&lt;&#x2F;strong&gt;: Yup, it&#x27;s great. I&#x27;ve been doing this since my first ergodox in 2013 and no regrets.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;steep-tent-80deg&quot;&gt;Steep tent (80°)&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2022&#x2F;2022-01-02-01FRDM0AH8FCXK7Y0XF856JP05.2048.jpg&quot; alt=&quot;v2112 with steep tenting on Z camera stands&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;The hypothesis is that hands should be turned with palms facing inward, similar to how they are when resting at your side while standing and relaxing. This reduces ulnar rotation and is a straighter path for muscles and tendons when compared to traditional position with palms down facing the desktop surface.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Result:&lt;&#x2F;strong&gt; Still unclear. Ergonomics seem clearly superior but logistics are very hard. It requires a large stand that is not very portable. It&#x27;s really easy for the halves to want to rotate in place, slide around, get misaligned, or topple over. Tenting also gets in the way of reaching around the desk for mouse, coffee, etc. Easy to smack into the keyboard.&lt;&#x2F;p&gt;
&lt;p&gt;I Spent a lot of energy and time on this one:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;custom tenting posts&lt;&#x2F;li&gt;
&lt;li&gt;Z camera stands&lt;&#x2F;li&gt;
&lt;li&gt;bolting things to the desk&lt;&#x2F;li&gt;
&lt;li&gt;various ways of attaching to chair armrests&lt;&#x2F;li&gt;
&lt;li&gt;etc&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;I&#x27;m thinking about trying to go back to flat on the desk as long as the ulnar rotation doesn&#x27;t hurt.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;low-activation-distance&quot;&gt;Low activation distance&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2022&#x2F;2022-05-07-01G2G1NVD8EZFS76N2C7NG5Q10.2048.jpg&quot; alt=&quot;A prototype with mouse switches&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Hypothesis is that (warning: keeb heresy) laptop keyboards with their low activation distance and solid bottom out are better for me to type on. Chocs are better than MX, and choc minis are a teensy bit better than that even, but I still want even lower. On choc minis I still push the switch thinking it typed, but didn&#x27;t press down far enough and it doesn&#x27;t actuate.&lt;&#x2F;p&gt;
&lt;p&gt;I did some prototyping with mouse switches and plan to continue some more in the future, but the truly tiny size of those things is really hard to work with in a DIY situation.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-scooped-column-4-switches-at-100deg-160deg-angles&quot;&gt;The scooped column: 4 switches at 100° &amp;amp; 160° angles&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2022&#x2F;2022-06-22-01G666S8XR6RYX44RY20DQSPRP.2048.jpg&quot; alt=&quot;A 1x4 scooped column prototype&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;The motivation came out of daily driver work on v2112 noticing my index reaching for the innermost column was throwing my hand off position and causing de-homing and typos. The hypothesis is that each finger should stay in exactly one column and never reach across to a neighbor column. I&#x27;ve eliminated the outermost pinky reach column from my builds since just after v2112, but the innermost index column remains on my daily driver setup still.&lt;&#x2F;p&gt;
&lt;p&gt;The challenge is once you remove that column, you get deep into obscure layout land and can no longer implement any popular layout including qwerty, dvorak, colemak, etc. But the premise is with 2 1x4 columns on each hand that&#x27;s 13 switches per hand which is enough for the entire English alphabet without any punctuation.&lt;&#x2F;p&gt;
&lt;p&gt;I have a prototype build and mapped with a variation on the &lt;a href=&quot;https:&#x2F;&#x2F;engram.dev&quot;&gt;engram layout&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2022&#x2F;2022-10-13-01GF8ZG85RM2HVAB52VC82EMZY.2048.jpg&quot; alt=&quot;v2209 key layout&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Results&lt;&#x2F;strong&gt;: I am still practicing and learning the engram layout, so no true results yet, but likely will be fine for index and middle but not ring nor pinky.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;hand-wiring-on-columns-to-pcb-inside-the-case&quot;&gt;Hand wiring on columns to PCB inside the case&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2022&#x2F;2022-10-02-01GECWB4XG1SV6JWBY2K882CCG.2048.jpg&quot; alt=&quot;Inside of case with column cables wired to the PCB&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;The challenge here is the scooped columns don&#x27;t really work with flat PCBs. They make flexible PCBs which BastardKB incorporates nicely, but I&#x27;m mostly at too early stages of prototyping to wait for flexible PCB iterations, so I have gotten by with hand wiring so far. Early builds were wired straight to the MCU and only recently have I added a small PCB to help tidy things up.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Result:&lt;&#x2F;strong&gt; Seems OK. My more recent builds are down to 32 switches total, so I don&#x27;t think I actually need diodes anymore if I switch to MCUs with more GPIO pins. I could probably get away with a very simple PCB that just houses some TBD plug connectors: one for each finger&#x27;s column and the MCU footprint plus some mounting holes.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;future-aspirations&quot;&gt;Future Aspirations&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;strong&gt;Pointing device&lt;&#x2F;strong&gt;: I&#x27;d love to get one or more pointing devices integrated. Either a touchpad (there&#x27;s a very interesting prototype doing the rounds on reddit lately), or a small trackball, or a trackpoint or some combination of these, plus a scroll wheel would be great.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;your-ideas-and-suggestions&quot;&gt;Your Ideas and Suggestions&lt;&#x2F;h1&gt;
&lt;p&gt;I&#x27;m always inspired by suggestions from the community so feel free to send me any ideas you have!&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Engram Plans</title>
          <pubDate>Sat, 22 Oct 2022 19:40:36 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2022/10/engram-plans/</link>
          <guid>https://peterlyons.com/problog/2022/10/engram-plans/</guid>
          <description xml:base="https://peterlyons.com/problog/2022/10/engram-plans/">&lt;p&gt;So now that the Squeezebox v2209 is up and running, and given it&#x27;s very unusual arrangement of keys (2 for pinky, 3 for ring, 4 for middle and index, 3 for thumb), I have to figure out which keymap to use for my base&#x2F;alpha layer. Since I currently use dvorak both on my thinkpad keyboard and on my daily driver squeezebox v2112, there&#x27;s some justification for keeping close to dvorak and then sort of shoving some keys over to the strong fingers. Honestly, removing keys from one finger and adding them to a different finger throws a huge amount of chaos at one&#x27;s typing universe. There&#x27;s no truly great solution because the hardware difference is always going to be there. This is all sort of to say that the design of v2209 may in retrospect prove to be truly unpragmatic, but exploring that line is one of the duties that comes with building custom stuff. And yes, I do have enough self awareness to admit that any reasonable definition of &quot;pragmatic&quot; was exceeded much much shallower down this rabbit hole than my current depth. I&#x27;m using &quot;pragmatic&quot; in a purely relative sense here amongst options that are all individually ridiculous.&lt;&#x2F;p&gt;
&lt;p&gt;So I&#x27;m intrigued by the &lt;a href=&quot;https:&#x2F;&#x2F;engram.dev&quot;&gt;engram layout&lt;&#x2F;a&gt; because it&#x27;s use of punctuation on the inner columns aligns nicely with the v2209 hardware so I can map engram to the squeezebox with only 4 extra letters to shuffle. All the punctuation can be on my symbols layer. So I&#x27;m currently starting to learn engram squeezebox variant on v2209 on a dedicated spare laptop set up just for this purpose. I&#x27;ll only make a daily driver switchover if I fully learn to touch type the layout and it seems like a net win and doesn&#x27;t introduce any unexpected RSI pain. If I do so, I&#x27;ll probably also learn stock engram on my laptop keyboard. There will be those pesky 4 key differences to deal with, but so be it. No way around it I can see.&lt;&#x2F;p&gt;
&lt;p&gt;Hardware wise, I have a lot going on now. Just today my Keyboardio Model 100 arrived after months of delays due to COVID, supply chain, customs, etc. I also have all the hardware I need to build my first wireless split keyboard: a &lt;a href=&quot;https:&#x2F;&#x2F;www.tzcl.me&#x2F;blog&#x2F;rae-dux#org0b16131&quot;&gt;rae-dux&lt;&#x2F;a&gt;. I may do the rae-dux hardware build while I&#x27;m still practicing engram, but I&#x27;ll most likely hold off putting either of these keyboards into rotation until I&#x27;ve switched to engram.&lt;&#x2F;p&gt;
&lt;p&gt;So &lt;em&gt;maybe&lt;&#x2F;em&gt; I&#x27;ll end up using an arrangement like this:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Home office daily driver: squeezebox v2209 or Model 100 with engram&lt;&#x2F;li&gt;
&lt;li&gt;Laptop built-in keyboard with engram&lt;&#x2F;li&gt;
&lt;li&gt;rae-dux with engram for bluetooth to my phone&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;But I could see any of those 3 boards becoming my daily driver.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Squeezebox Keyboard v2209</title>
          <pubDate>Thu, 13 Oct 2022 13:33:05 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2022/10/squeezebox-keyboard-v2209/</link>
          <guid>https://peterlyons.com/problog/2022/10/squeezebox-keyboard-v2209/</guid>
          <description xml:base="https://peterlyons.com/problog/2022/10/squeezebox-keyboard-v2209/">&lt;p&gt;I have completed a new revision of the Squeezebox Keyboard - v2209.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2022&amp;#x2F;2022-10-13-01GF8ZHBARS1ASF66RKJV9NS7S.2048.jpg&quot;&gt;
  &lt;figcaption&gt;Side view of Squeezebox v2209&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2022&amp;#x2F;2022-10-13-01GF8ZJX4GTE0H8RV1BE3V7CAA.2048.jpg&quot;&gt;
  &lt;figcaption&gt;Top view&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;some-quick-project-history-checkpoints&quot;&gt;Some quick project history checkpoints&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;April 2021: First fully functional squeezebox prototype, v2104, is built
&lt;ul&gt;
&lt;li&gt;Slots for column stagger adjustment are on the keywells&lt;&#x2F;li&gt;
&lt;li&gt;Keywells mounted onto sawtooth wall for tailor fit height&lt;&#x2F;li&gt;
&lt;li&gt;No splay&lt;&#x2F;li&gt;
&lt;li&gt;Used flat (no tenting)&lt;&#x2F;li&gt;
&lt;li&gt;45 keys: 3x5 finger matrix plus a 6-key (!) thumb cluster&lt;&#x2F;li&gt;
&lt;li&gt;Printed on green PLA&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;June 2021: v2105 is done
&lt;ul&gt;
&lt;li&gt;Mounted on tenting stands&lt;&#x2F;li&gt;
&lt;li&gt;Slots for column stagger adjustment remain on the keywells&lt;&#x2F;li&gt;
&lt;li&gt;Splay supported with steel base plate and magnets in the keywell posts&lt;&#x2F;li&gt;
&lt;li&gt;Component based design where each keywell can be tested individually as a working keyboard&lt;&#x2F;li&gt;
&lt;li&gt;Hot swap sockets incorporated&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;December 2021: v2112 is done
&lt;ul&gt;
&lt;li&gt;Slots for column stagger moved to the base plate&lt;&#x2F;li&gt;
&lt;li&gt;Splay adjustment handled by grid of slots on the base plate&lt;&#x2F;li&gt;
&lt;li&gt;Tried some different connectors for wiring across the columns&lt;&#x2F;li&gt;
&lt;li&gt;Printed in red and black PLA and looks pretty snazzy (still spaghetti wires visible though)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;February 2022: Squeezebox v2112 becomes my daily driver, replacing the TBK Mini&lt;&#x2F;li&gt;
&lt;li&gt;April 2022: Built narrow index keywells with chopped choc switches&lt;&#x2F;li&gt;
&lt;li&gt;September 2022: Hit personal record 85 WPM with 99% accuracy on v2112&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;driving-factors-beyond-previous-version&quot;&gt;Driving Factors beyond previous version&lt;&#x2F;h2&gt;
&lt;p&gt;The previous build, v2112, has been my daily driver since February 2022. The build is holding up fine mechanically. It feels very comfortable and my typing speed and accuracy are in the general vicinity of my personal record on any keyboard ever. It&#x27;s not very consistent, but I can regularly hit 85 WPM with good accuracy on a 1-minute monkeytype test including uppercase and punctuation.&lt;&#x2F;p&gt;
&lt;p&gt;Below I&#x27;ll outline the various goals I had for the v2209 revision.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2022&amp;#x2F;2022-07-13-01G7WMERKGG9Y7KR208A7BX0AC.2048.jpg&quot;&gt;
  &lt;figcaption&gt;Full set of 3D printed parts prior to assembly&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h3 id=&quot;no-inner-reach-column-for-index-fingers&quot;&gt;No inner reach column for index fingers&lt;&#x2F;h3&gt;
&lt;p&gt;I&#x27;m noticing that reaching inward for the index inner neighbor column I tend to move the whole hand a bit not just the finger and it throws my home position off. So I wanted to try a layout without any sideways reaching and giving more work to index and middle fingers and continuing to reduce work on the pinky. Thus I wanted to build a 1x4 keywell with chopped choc switches to try to get 4 switches crammed into a radius that can still easily be reached without moving the hand forward. I call this &quot;stay in your lane&quot; layout.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;no-pinky-top-column&quot;&gt;No pinky top column&lt;&#x2F;h3&gt;
&lt;p&gt;I noticed a similar issue when I had to reach for the top switch on my pinky column, so I wanted to reduce the pinky to just the &quot;home corner&quot; 2 switches.I felt OK leaving 3 switches for the ring finger and by coincidence that just happens to be 13 switches per hand so 26 for all the fingers which is the length of the English alphabet and I dig that.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;chopped-choc-for-very-tight-spacing&quot;&gt;Chopped Choc for very tight spacing&lt;&#x2F;h3&gt;
&lt;p&gt;Now that I had experience successfully slicing about 1&#x2F;3 of the material off of a Kailh Choc switch (the bits dealing with LEDs), I wanted to incorporate that into the main keywell so I could fit 4 keys under a finger and have them be easy to reach.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2022&amp;#x2F;2022-09-04-01GC477TNGNM0H4Y3QS3R6MPE8.2048.jpg&quot;&gt;
  &lt;figcaption&gt;Chopping a choc mini with a rotary tool&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2022&amp;#x2F;2022-09-04-01GC47DP5GB9S4N4PG1ASFWVWE.2048.jpg&quot;&gt;
  &lt;figcaption&gt;Chopped choc showing the cut&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2022&amp;#x2F;2022-09-04-01GC488GJ860KBWVZAYVJZ8RQR.2048.jpg&quot;&gt;
  &lt;figcaption&gt;Mounted in a 1x4 keywell&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;(Dearest Internet Commenters: Safety Goggles were worn during the operation of this power tool. Chill.)&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2022&amp;#x2F;2022-09-10-01GCM02368VXFYZW6E2F902RD8.2048.mov&quot;&gt;
  &lt;figcaption&gt;Chopping a Choc Mini keyboard switch&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h3 id=&quot;choc-minis-for-shorter-actuation-travel&quot;&gt;Choc Minis for shorter actuation travel&lt;&#x2F;h3&gt;
&lt;p&gt;I still am searching for the shortest available actuation travel. The discord community is pretty convinced that actual laptop switches are very difficult to solder into DIY projects, which is a bummer. So the only switch I have identified which I can actually buy with less travel than the current Kailh Choc v2 is the &lt;a href=&quot;https:&#x2F;&#x2F;www.aliexpress.us&#x2F;item&#x2F;2255800091079572.html?spm=a2g0o.order_list.0.0.30ad1802bxxiW4&amp;amp;gatewayAdapt=glo2usa&amp;amp;_randl_shipto=US&quot;&gt;Kailh Choc Mini PG1232&lt;&#x2F;a&gt;. This switch has just a tiny bit less actuation travel, so I bought a bunch of linear blacks.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;strain-relief-for-the-wiring&quot;&gt;Strain Relief for the Wiring&lt;&#x2F;h3&gt;
&lt;p&gt;I added some simple bolted strain relief bars to the keywells. These actually work great despite being dead simple. Pulling on the wires won&#x27;t break the solder joints. Really pleased with these.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2022&amp;#x2F;2022-09-05-01GC7310X0NBYGS02F3ZVQ38SP.2048.jpg&quot;&gt;
  &lt;figcaption&gt;Strain relief bar on the wiring&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2022&amp;#x2F;2022-09-10-01GCKYBWT82BDE1QJBS2C9KWR6.2048.jpg&quot;&gt;
  &lt;figcaption&gt;Wires running through strain relief and heatshrink tubing terminating in a DuPont jumper connector&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h3 id=&quot;new-tenting-mount&quot;&gt;New Tenting Mount&lt;&#x2F;h3&gt;
&lt;p&gt;Using v2112, I got tired of the camera z-stands wanting to slide around on the desk, so I built a wooden stand for them. I carried this idea forward but I wanted to 3D print more of it and just have a single flat board be the bulky wood part. So I designed some 80° tenting mounts and assembly such that they can screw onto the stand and the Squeezebox bottom case can bolt onto them with 1&#x2F;4-20 bolts. I also printed some bolt hex handles so the bolts can be tightened by hand without a wrench.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2022&amp;#x2F;2022-09-05-01GC6QQNDRPP75K40WHKRG34QA.2048.jpg&quot;&gt;
  &lt;figcaption&gt;Bottom box with mounting bolts and magnets&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2022&amp;#x2F;2022-09-05-01GC6TGM3GZFMAWPNDPVFQR0TM.2048.jpg&quot;&gt;
  &lt;figcaption&gt;Mounted onto the stand&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h3 id=&quot;printed-circuit-board&quot;&gt;Printed Circuit Board&lt;&#x2F;h3&gt;
&lt;p&gt;To help reduce the handwiring spaghetti situation, I wanted to try to make an actual circuit board. If the main switch matrix circuitry could go there, and the diodes could live there instead of by the switches on the keywells, that would tidy things up a lot. I would just need a max of 5 wires coming out of each keywell and going only to the PCB. In v2112 the keywells have to daisy chain to their neighbors so there&#x27;s a cable for incoming and another one for outgoing which is a fiddly mess.&lt;&#x2F;p&gt;
&lt;p&gt;I didn&#x27;t know anything about PCB design or KiCAD. As I sat down to learn and started discussing my plans on the Absolem Club discord, Petr Viktorin (&lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;encukou&quot;&gt;encukou on github&lt;&#x2F;a&gt;) saw that a small form factor PCB could be useful for many keyboard builds that were too heavily 3D to put the switches directly onto the PCB, and created an absolutely brilliant and compact PCB design he calls the &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;encukou&#x2F;selfish&#x2F;tree&#x2F;c975633adc153e1b267ada8de5742252208aacc2&#x2F;pcb&#x2F;matrixbar&quot;&gt;matrixbar&lt;&#x2F;a&gt;. Aside: it was originally called the squeezebar which I preferred for obvious reasons.&lt;&#x2F;p&gt;
&lt;p&gt;With just a few exchanges on discord, we found a layout that would fit comfortably in the Squeezebox case and account for clearances we need for the adjustment bolts, cables connecting to the PCB, etc. Petr was also able to make it such that each keywell could connect to single row of pin headers meaning the cables would be so tidy and a standard DuPont jumper wire housing would easily fit through the slot to get to the interior of the case.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2022&amp;#x2F;2022-09-18-01GD8Y9QD8R71K52R2RD6VPPAM.2048.jpg&quot;&gt;
  &lt;figcaption&gt;4 keywells plus the thumb cluster mounted to the slot box with cables routed to inside the case&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;This was such an exciting and fun development! It saved me months of learning and skill building. And from there I got another unexpected and very pleasant gift from other enthusiasts on the Internet...&lt;&#x2F;p&gt;
&lt;h3 id=&quot;pcb-fabrication-with-pcbway&quot;&gt;PCB Fabrication with PCBWay&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.pcbway.com&quot;&gt;&lt;img src=&quot;&#x2F;problog&#x2F;images&#x2F;2022&#x2F;pcbway-logo.png&quot; width=&quot;300&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.pcbway.com&#x2F;&quot;&gt;PCBWay&lt;&#x2F;a&gt; reached out to me to offer to fabricate a PCB for the squeezebox at no cost. So I took the files that Petr created and uploaded them to PCBWay&#x27;s online shopping tool. In a few minutes, I had submitted an order for 10 PCBs (enough for 5 split keyboards). Ten days later, the PCBs had been fabricated and shipped across the world to me. It was so exciting to have something with an air of legitimacy giving the project a lot of polish instead of just an embarrassing pile of wires.&lt;&#x2F;p&gt;
&lt;p&gt;Thanks PCBWay and Petr Viktorin! The boards look great, fit in the case perfectly, and work great.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2022&amp;#x2F;2022-10-13-01GF8K4G3R0PXPR6J9EYDGX5PA.2048.jpg&quot;&gt;
  &lt;figcaption&gt;Custom MatrixBar PCB with pin headers, diodes, and MCU sockets&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h3 id=&quot;new-case-approach&quot;&gt;New Case Approach&lt;&#x2F;h3&gt;
&lt;p&gt;The PCB enabled me to put all the electronics into the top case, which I call the &quot;slot box&quot; part. That now nests into the bottom case and some magnets snap it into position. The nice thing about this is all the electronics are housed in that single unit and I can remove it from the stand to adjust either the ergonomic fit or the wiring and it&#x27;s a unit without any wires that need to be disconnected.&lt;&#x2F;p&gt;
&lt;p&gt;The main driver for this was v2112 had a box w&#x2F; lid design where the lid bolts to the box with 4 m3 bolts. This is somewhat tedious to remove all the bolts to make an adjustment, so I wanted something easier. The new design is way easier: I can just grab it and remove it without undoing any fasteners, but it is noticeably jankier where the bolts were rock solid. I&#x27;m not convinced this is an improvement overall and probably need another iteration to find something that really delivers here.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2022&amp;#x2F;2022-10-13-01GF8KBBV01409GR1KTYMRN1SC.2048.jpg&quot;&gt;
  &lt;figcaption&gt;Case halves show open&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;what-about-keymaps-engram&quot;&gt;What about keymaps? (engram)&lt;&#x2F;h2&gt;
&lt;p&gt;So I&#x27;m down to 26 total keys on the fingers and the fingers don&#x27;t all have the same number of keys, so sticking with dvorak doesn&#x27;t really apply. In fact, most of the existing layouts can&#x27;t really be morphed to apply to this hardware. So I went shopping for keymaps briefly. I did not do a huge research effort, so there could be a lot more to explore here, but quickly skimming through some discord conversations and online summaries of keymap evolution, I discovered a fairly new layout called &lt;a href=&quot;https:&#x2F;&#x2F;engram.dev&quot;&gt;engram&lt;&#x2F;a&gt; which appealed to me for 2 main reasons:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;The inner reach columns for the index fingers all had punctuation not letters
&lt;ul&gt;
&lt;li&gt;This meant I could use the alpha layout without heavily compromising it&lt;&#x2F;li&gt;
&lt;li&gt;I could stick all symbols on a layer anyway&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;It had the same basic idea of vowels on left hand home row and important consonants on right hand home row which is how dvorak is and I like that.&lt;&#x2F;li&gt;
&lt;li&gt;It didn&#x27;t move any alphas to the thumb which I don&#x27;t quite feel ready for so I could probably learn engram on my thinkpad built in keyboard and use that while traveling and keep my window manager keybinds consistent&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;After some discussion in discord, I was encouraged to swap q and z out to combos to make space to fit period and comma on the base layout, so my layout is now basically take engram, swap a few silly letters around to account for my hardware weirdness, and add period and commma.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;problog&#x2F;images&#x2F;2022&#x2F;squeezebox-v2209-engram.png&quot; alt=&quot;Engram layout for Squeezebox v2209&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;quick-video&quot;&gt;Quick Video&lt;&#x2F;h2&gt;
&lt;div class=&quot;youtube-video-container&quot;&gt;
&lt;iframe src=&quot;https:&#x2F;&#x2F;www.youtube-nocookie.com&#x2F;embed&#x2F;VH0nU8ttMY4&quot; allowfullscreen&gt;&lt;&#x2F;iframe&gt;
&lt;&#x2F;div&gt;
&lt;h2 id=&quot;hackaday-project-log&quot;&gt;Hackaday Project Log&lt;&#x2F;h2&gt;
&lt;p&gt;As I did this build, I posted smaller build logs for almost each session over on &lt;a href=&quot;https:&#x2F;&#x2F;hackaday.io&#x2F;project&#x2F;187158-squeezebox-keyboard-v2209&quot;&gt;Hackaday&lt;&#x2F;a&gt;. Feel free to read through those for more blow-by-blow commentary and thoughts.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;stl-files-to-3d-print-this&quot;&gt;STL Files to 3D Print This&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.printables.com&#x2F;model&#x2F;294609-squeezebox-keyboard-v2209&quot;&gt;Squeezebox v2209 on printables.com&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;reddit-discussion&quot;&gt;Reddit Discussion&lt;&#x2F;h2&gt;
&lt;p&gt;Discussed on &lt;a href=&quot;https:&#x2F;&#x2F;www.reddit.com&#x2F;r&#x2F;ErgoMechKeyboards&#x2F;comments&#x2F;y38ebu&#x2F;squeezebox_v2209&#x2F;&quot;&gt;&#x2F;r&#x2F;ergomechkeyboards here&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;thoughts-and-learnings-at-the-moment&quot;&gt;Thoughts and Learnings at the Moment&lt;&#x2F;h2&gt;
&lt;p&gt;I still can&#x27;t type on v2209 and am slowly learing one key at a time on keybr.com. I think I&#x27;ll need to practice a lot and get fluent and then maybe in a few months I&#x27;ll try to use v2209 as my daily driver. We&#x27;ll see. Here&#x27;s some thoughts on the overall project.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;crimping-dupont-jumper-connectors-sucks&quot;&gt;Crimping DuPont Jumper Connectors Sucks&lt;&#x2F;h3&gt;
&lt;p&gt;I bought a crimper so I could make DuPont jumper connectors the exact right length with the exact right number of wires and consistent wire colors. But sheesh, this is terrible business. Crimping them is extremely fiddly and frustrating. The crimping goes wrong easily. The metal connector doesn&#x27;t reliably snap into the locked position in the plastic housing. I think the actual connector form factor is more or less OK-ish, and being slim enough to fit through the slot box slots is great, but I need to do it with factory cables somehow. Ultimately these connectors are going to be one of the least reliable aspects of this build. I may need to glue the wires into the housings to keep them from degrading and I actually thought about cutting the connectors off entirely and soldering the wires to the header pins. It would be a lot less plug-n-play but at least they switches would always type.&lt;&#x2F;p&gt;
&lt;p&gt;I&#x27;m still searching for other small but robust connectors. Folks have linked me to various shape JST connectors, but they don&#x27;t fit through the slots, so I&#x27;d need to design holes specifically for them to get down into the slot box. That&#x27;s fine and there&#x27;s enough space available to do it. It&#x27;s just nice that DuPonts are so slim they can fit through the m3 slots.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;nested-boxes-for-the-case-is-kinda-bad&quot;&gt;Nested Boxes for the Case Is Kinda Bad&lt;&#x2F;h3&gt;
&lt;p&gt;The nested boxes with magnets for the case is workable but mostly bad. There&#x27;s some slop due to print warping that I have to address with a rubber band or the case won&#x27;t be stable when closed. Something based around a smaller number of bolts is probably better.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;pla-is-tough-for-adhesives&quot;&gt;PLA is Tough for Adhesives&lt;&#x2F;h3&gt;
&lt;p&gt;The DAP RapidFuse adhesive I use tends to leech into the PLA and dries visibly white which is a bummer. Anything I can do to reduce this or avoid adhesives altogether I&#x27;ll try.&lt;&#x2F;p&gt;
&lt;p&gt;My initial design for mounting the PCB to the case was some smallish PLA standoffs glued to the inside of the slot box. At one point I knocked one half of the keyboard off my workbench onto the floor and that failed in 2 ways. One standoff popped the glue, and the other one sheared the standoff itself. So I had to seek advise and designed a 1-piece rectangular mount which uses bolts to attach to both the PCB and the slot box. That is much sturdier.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;keywells-overall-feel-good&quot;&gt;Keywells Overall Feel Good&lt;&#x2F;h3&gt;
&lt;p&gt;The keywell layout feels good. I think I may ultimately like typing on this best so far but need a lot of practice.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;choc-minis&quot;&gt;Choc Minis?&lt;&#x2F;h3&gt;
&lt;p&gt;The Choc minis are slightly better than choc v2s do to actuation travel but it&#x27;s by such a small amount that it&#x27;s probably negligible.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;pcb-learnings&quot;&gt;PCB Learnings&lt;&#x2F;h3&gt;
&lt;p&gt;I ruined at least 3 PCBs in not really knowing what I was doing. The first one I didn&#x27;t understand how pin headers should be mounted and I soldered the wrong side. Somewhere in the socketing of the MCU created a short on 2 other PCBs and I thought for like a week that I had fried 2 Elite-C MCUs. Only when I pried them out of the sockets so they were disconnected from the PCB was I then able to flash them and they were fine. So I think 2 of the PCBs had a solder short somewhere (probably the reset button through holes) and I had to just grab spares and move on to avoid spending too much time debugging.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;case-hole-layout-is-tricky&quot;&gt;Case Hole Layout Is Tricky&lt;&#x2F;h3&gt;
&lt;p&gt;I tried to do CAD for the holes in the case for the USB cable, RJ9 connector, and reset button. But it turns out predicting the wiring constraints is hard. My USB hole ended up not accounting for the extra height socketing the MCU adds so it didn&#x27;t initially line up and I had to print more slot boxes to correct it. I also realized the reset button could go on the outer face which worked out fine in terms of easy to reach, hard to reach by accident though, and easy to route the wires to the PCB pin headers from the button.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;other-misc-mistakes&quot;&gt;Other Misc Mistakes&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;I wired one reset button wrong so it was always completing the circuit. Oops.&lt;&#x2F;li&gt;
&lt;li&gt;Of course I glued at least one magnet in with the polarity wrong. I do this every time despite attempting to mark them with tape or sharpie.&lt;&#x2F;li&gt;
&lt;li&gt;The magnets would look cleaner if I made the case walls like 1mm thicker so the magnets sit in a recess not a through hole.&lt;&#x2F;li&gt;
&lt;li&gt;I soldered a bunch of diodes onto the wrong side of one PCB. I didn&#x27;t realize the Elite-C pinouts were different on the front and back so everything has to go on a particular side for the build to work in my case.&lt;&#x2F;li&gt;
&lt;li&gt;Some of the wires I tried to pre-crimp I didn&#x27;t account for the length variations running to different switches so they didn&#x27;t group nicely at the end&lt;&#x2F;li&gt;
&lt;li&gt;Initially I routed the thumb cluster wires dead center same as the post, but ultimately this interfered with how the thumb cluster mounts to the slot box so in the next tweak I offset the strain relief to the exterior considerable so the wire would route properly.&lt;&#x2F;li&gt;
&lt;li&gt;I tried to directly crimp DuPont connectors to the wires that come with my RJ9 connectors, but discovered their gauge was too small and the crimp was unreliable. So I had to splice on some store bought jumper wires, and this hack had to be done after the connector was already glued into the case, so it was fiddly and annoying.&lt;&#x2F;li&gt;
&lt;li&gt;One of the cables I pre-fabricated while on vacation away from my workshop ended up being just a TINY bit too short to reach the pin headers on the PCB so I had to make a short janky extension cable in order to connect to the pin headers.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</description>
      </item>
      <item>
          <title>Focus Retreat Center Programming Retreat</title>
          <pubDate>Tue, 28 Jun 2022 00:00:00 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2022/06/focus-retreat-center-programming-retreat/</link>
          <guid>https://peterlyons.com/problog/2022/06/focus-retreat-center-programming-retreat/</guid>
          <description xml:base="https://peterlyons.com/problog/2022/06/focus-retreat-center-programming-retreat/">&lt;p&gt;We over at my new project Focus Retreat Center have announced the dates for our inaugural &lt;a href=&quot;https:&#x2F;&#x2F;focusretreatcenter.com&#x2F;2022&#x2F;programming&#x2F;&quot;&gt;Programming Retreat&lt;&#x2F;a&gt;. We are so excited for this event! We have interesting applications rolling in and have already awarded several Residencies.&lt;&#x2F;p&gt;
&lt;p&gt;If you are at all curious, please get in touch with us at &lt;strong&gt;team@focusretreatcenter.com&lt;&#x2F;strong&gt; or &lt;a href=&quot;https:&#x2F;&#x2F;focusretreatcenter.com&#x2F;contact&quot;&gt;our contact form here&lt;&#x2F;a&gt;. We are happy to answer any questions you may have about what a retreat experience is like. If you have a friend who might be a good fit, you can nominate them for a Residency as well.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Squeezebox Tweaks and Plans</title>
          <pubDate>Sat, 12 Mar 2022 13:23:06 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2022/03/squeezebox-tweaks-and-plans/</link>
          <guid>https://peterlyons.com/problog/2022/03/squeezebox-tweaks-and-plans/</guid>
          <description xml:base="https://peterlyons.com/problog/2022/03/squeezebox-tweaks-and-plans/">&lt;p&gt;Typing on the Squeezebox keyboard is coming along nicely. No real issues with it as my daily driver at the moment. I&#x27;m still tweaking my keymap but only in incremental ways.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;keymap-tweaks&quot;&gt;Keymap tweaks&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;reduce-keys-from-42-to-36&quot;&gt;Reduce keys from 42 to 36&lt;&#x2F;h3&gt;
&lt;p&gt;My main goal right now is to shrink from 42 keys to 36 by eliminating the outer most pinky columns. The reach there requires taking most fingers away from the home corner and is really disruptive. Luckily, earlier versions of the squeezebox didn&#x27;t even have this column and there&#x27;s not too many important keys in the keymap there anyway so I was able to find good alternatives to everything. As of now none of the layers in my keymap have anything in the outermost column that isn&#x27;t also available somewhere else, and I&#x27;m practicing to learn the alternative ones and leave the outermost column behind entirely.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;home-row-mods-on-number-layer&quot;&gt;Home row mods on number layer&lt;&#x2F;h3&gt;
&lt;p&gt;I&#x27;ve never really embraced home row mods since they interfere with combos and I can&#x27;t get them to play nicely with dvorak rolls (particularly &quot;ch&quot; when I had h as a mod-tap was giving me mods instead of taps most of the time). However, I recently realized there&#x27;s no such issue when on my numbers layer, and so I can do home row mods there on the right hand while the left hand has arrow keys and thus have better access to ctrl+arrows to move by words in non-vim text editors. I&#x27;ll probably start using this a lot now that I have a workable way to hold the modifiers.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;symbols-layer&quot;&gt;Symbols layer&lt;&#x2F;h3&gt;
&lt;p&gt;My symbols layer has evolved nicely to fit in my new 36 key layout so no issues there either. The most interesting thing is I finally found how to make QMK swap colon and semicolon so colon is the regular key and semi is shifted.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;future-plans&quot;&gt;Future Plans&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;switch-to-zmk&quot;&gt;Switch to ZMK&lt;&#x2F;h3&gt;
&lt;p&gt;Watching Ben Vallack&#x27;s youtube videos plus a few reddit comments here and there have made me interested in porting my firmware from QMK to ZMK and see if some of the more advanced features work better there. I&#x27;d also really love to get off the QMK git fork workflow and stop editing C source code files for my keymap.&lt;&#x2F;p&gt;
&lt;p&gt;Due to the &quot;bunch of daisy chained macro pads&quot; design of squeezebox, I can start with a single 3-key column and see how it goes.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;bring-innermost-column-closer&quot;&gt;Bring innermost column closer&lt;&#x2F;h3&gt;
&lt;p&gt;I&#x27;ve recently discovered the &lt;a href=&quot;https:&#x2F;&#x2F;zealot.hu&#x2F;absolem&#x2F;&quot;&gt;Absolem&lt;&#x2F;a&gt; project and the associated discord. Folks in there have experimented with slicing off the part of Kailh Choc switches that house LEDs normally to get ultra-tight spacing. I&#x27;d like to try that to get the index finger X-axis travel shorter when reaching inward to its neighbor column.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;make-slot-plate-into-a-pcb&quot;&gt;Make slot plate into a PCB&lt;&#x2F;h3&gt;
&lt;p&gt;I&#x27;m pretty excited to design a PCB for this which could also serve as the slot plate that is the foundation of the structure and adjustability. It doesn&#x27;t &lt;strong&gt;have&lt;&#x2F;strong&gt; to be that way and a regular PCB could be attached to a 3D-printed slot plate, but it seems like a nice duality to pursue. I don&#x27;t know kicad or any PCB software though, but I&#x27;m much better connected to communities that can guide me through that. Finding sufficient free time to do it remains a separate concern, however.&lt;&#x2F;p&gt;
&lt;p&gt;Another tweak is instead of bolting onto the bottom box, the slot plate should slide into grooves along the bottom box. The bolts are too tedious to undo if you want to adjust the position of stuff.&lt;&#x2F;p&gt;
&lt;p&gt;A PCB would let me only have wires running from the underside of each column to a connector (probably a JST SH connector) on the PCB directly below it and all of the interconnections would be on the circuit board itself so that solves the wiring mess. I&#x27;m not exactly sure how I would want the USB-C and RJ-9 connectors to work. Would they be mounted onto the PCB and poked through the wall of the bottom box or would they just be mounted in the wall of the bottom box and then have wires to the PCB?&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Squeezebox Daily Driver Update</title>
          <pubDate>Fri, 25 Feb 2022 23:46:03 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2022/02/squeezebox-daily-driver-update/</link>
          <guid>https://peterlyons.com/problog/2022/02/squeezebox-daily-driver-update/</guid>
          <description xml:base="https://peterlyons.com/problog/2022/02/squeezebox-daily-driver-update/">&lt;p&gt;So I&#x27;ve been using the squeezebox as my daily driver keyboard for two weeks now. Here&#x27;s my thoughts on this experience. But first a typing video:&lt;&#x2F;p&gt;
&lt;h2 id=&quot;typing-video&quot;&gt;Typing video&lt;&#x2F;h2&gt;
&lt;div class=&quot;youtube-video-container&quot;&gt;
&lt;iframe src=&quot;https:&#x2F;&#x2F;www.youtube-nocookie.com&#x2F;embed&#x2F;EgOgtf3WEmI&quot; allowfullscreen&gt;&lt;&#x2F;iframe&gt;
&lt;&#x2F;div&gt;
&lt;h2 id=&quot;the-home-corner-works&quot;&gt;The Home Corner Works&lt;&#x2F;h2&gt;
&lt;p&gt;The design element of the middle and bottom rows being a steep 100 degree angle feels totally validated to me and I&#x27;d recommend other experimenters out there incorporate it into their designs. Even though accidental bottom row presses are still some of my more common errors, I think with time that will improve and the benefits of that saxophone-like squeeze action are worth it.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;per-finger-ergonomics-are-really-about-the-pinkies&quot;&gt;Per-finger ergonomics are really about the pinkies&lt;&#x2F;h2&gt;
&lt;p&gt;So I have small variations in the height and columnar offset for every finger, and I&#x27;m beginning to really doubt whether this is worthwhile for the index, middle, and ring fingers. I may print a few more sets of standoffs and set them all to be the same height and remove the columnar stagger. I think the proprioception of those 3 fingers understanding when they will all come to rest on a flat surface is pretty strong and each finger having a different height key to press I think counteracts that. Both are definitely necessary and worthwhile for the pinky but for the other 3 fingers even after all this ergonomic prototyping work I&#x27;m skeptial.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;i-still-want-less-travel&quot;&gt;I still want less travel&lt;&#x2F;h2&gt;
&lt;p&gt;The kailh chocs I&#x27;m using say 1.5mm travel for activation and it feels like I would prefer much less than that. Even kailh butterfly keys (which I have never tried) only get that down to 1.2mm which seems disappointing. It still feels like a long way to press and I have to consciously think about pressing deep enough to avoid presses that fail to activate the switch.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;tenting-is-a-nightmare&quot;&gt;Tenting is a nightmare&lt;&#x2F;h2&gt;
&lt;p&gt;Even after all the nonsense design and prototyping work I did to make this thing exactly fit my hand, getting a tenting setup that is stable and correct is a massive hassle. I ended up having to buy a keyboard tray to attach to my desk and then drill holes through that so I could bolt the camera stands that hold my keyboard to it down solidly. Otherwise they squish together, rotate inward, and lift up while typing. Any of that instability I think is super bad for typing confidence.&lt;&#x2F;p&gt;
&lt;p&gt;I&#x27;m expecting a Keyboardio Model 100 in the next few months and if I can comfortably type on that without tenting I&#x27;ll likely try to switch to it. The availability of a flat sturdy desk surface is just so good and not having to deal with the complexity that comes with tenting would be a nice relief. Of course the ergonomics of tenting are significantly better but it all comes down to whether or not typing becomes painful in any given position.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;typing-is-ok-but-inconsistent&quot;&gt;Typing is OK but inconsistent&lt;&#x2F;h2&gt;
&lt;p&gt;My typing speed and accuracy sometimes bump up pretty close to my previous level, but when either take a dip, they can dip pretty far before bottomming out. I feel like my range on my TBK mini was like 50-70 WPM and fairly tight whereas it&#x27;s like 25-70 WPM on the squeezebox and currently I&#x27;m still far less accurate, especially on longer typing tests.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;chair-and-body-position-are-locked&quot;&gt;Chair and body position are locked&lt;&#x2F;h2&gt;
&lt;p&gt;When I&#x27;m seated, the bolted-down tenting setup pretty much requires my chair be exactly centered an exactly in the right position and my butt all the way back and punishes any kind of leaning or slouching or half-sitting on my chair. Any of that causes it to become hard to squeeze the bottom row properly. I don&#x27;t know if this is all-in-all a benefit, but it&#x27;s definitely a change and less forgiving than my TBK mini setup.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-compromise-in-this-design-shows&quot;&gt;The compromise in this design shows&lt;&#x2F;h2&gt;
&lt;p&gt;Ultimately the key layout I have is a compromise between two fundamentally incompatible approaches:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Hands stay still and fingers do the typing&lt;&#x2F;li&gt;
&lt;li&gt;Hands move to get fingers over the keys then the fingers type&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Because I have extra columns outward from both my pinky and index fingers, it means I have to move my hands a bit to reach the outermost keys, and that kind of screws up the beauty of the home corner which is really about keeping your hands almost exactly still and your finger movements very small from there.&lt;&#x2F;p&gt;
&lt;p&gt;Ultimately I&#x27;d have to switch to a layout with like 24 main keys in a 3x8 grid to really go all-in on stable hands. But even a layout like BEAKL doesn&#x27;t do that. It would require 2 combos for the least common letters and every symbol would need to be off the base layer. Maybe someday, but not right now. Of course, just because the physical switches are there doesn&#x27;t prevent me from changing to a keymap where they are not mapped to anything.&lt;&#x2F;p&gt;
&lt;p&gt;I think if I were to get up the learning curve to a 24-key layout, it would really let the strengths of the squeezebox&#x27;s ultra-tight spacing shine through. But as it is with the outer column reaches, things are hampered and I probably would be just as well off on a more dactyl type shape or even back to flat columnar like an ergodox.&lt;&#x2F;p&gt;
&lt;p&gt;Now, some clever redditor told me I could get these neighbor columns even closer by cutting my Kailh choc switches to chop off the bit where the LED goes because all the switch electronics are on the other side. That might help a lot and if I could basically get choc switch stems really tight next to each other the reaching to neighbor columns might be less disruptive. BUT that&#x27;s a fairly big hack to undertake. Maybe someday. As of now I&#x27;d lean toward building a butterfly design as more likely to be an improvement.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;1-finger-chords&quot;&gt;1-Finger chords&lt;&#x2F;h2&gt;
&lt;p&gt;My keymap doesn&#x27;t have many 1-finger chords because I was holding on to a keymap that was identical between my TBK mini and the squeezebox just in case I needed to switch back. It&#x27;s been long enough now though that I added one: left index finger middle+bottom chord is escape and it&#x27;s working nicely.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;thumb-clusters&quot;&gt;Thumb clusters&lt;&#x2F;h2&gt;
&lt;p&gt;The thumb clusters feel pretty optimal to me, at least for this key mapping. They are not mechanically as rock-solid stable as I&#x27;d like but it&#x27;s OK enough as long as there&#x27;s adequate torque on their bolts.&lt;&#x2F;p&gt;
&lt;p&gt;I could see myself trying a 5-way switch for each thumb for a bunch of layers, and of course I&#x27;d love to get a track point or track ball going at some point, but for keys-only, these clusters are good.&lt;&#x2F;p&gt;
&lt;p&gt;Surprisingly, one of my most persistent typing errors is activation failure on space, which I type with the right thumb on its home key. My intuition would be this would be the absolute most reliable key. The strongest finger on my dominant hand on its home key, but for some reason it doesn&#x27;t fire reliably. This being a DIY hand-wired prototype, I can&#x27;t rule out that maybe the hardware itself is flakey for that key. Actually, maybe the hardware is flakey for every key but I notice it most on space because it&#x27;s the most common key. But going on the assumption the hardware works properly, it&#x27;s an open question why space doesn&#x27;t fire consistently.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-case&quot;&gt;The case&lt;&#x2F;h2&gt;
&lt;p&gt;Since the previous blog post, I added a big empty box to the bottom of the case to provide much more rigidity. The bare slot plate was pretty thin and with all those slots it was getting more deflection when I typed than I would like. So I printed a much thicker version then bolted them to a box and now it&#x27;s good. The box is deep enough so I can set the keyboard down flat on the desk and it rests on the box without resting on the edge of the thumb cluster, which always felt like it was going to bend or snap the thumb cluster post eventually.&lt;&#x2F;p&gt;
&lt;p&gt;Next case would have the slot plate attach to the bottom by sliding into a groove instead of being bolted on. The bolts are too much of a pain to undo when you want to adjust the columns. Of course, this is only supposed to happen rarely, but it&#x27;s so easy to just go &quot;eh, nevermind&quot;.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Squeezebox Keyboard v2112</title>
          <pubDate>Sun, 02 Jan 2022 16:42:04 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2022/01/squeezebox-keyboard-v2112/</link>
          <guid>https://peterlyons.com/problog/2022/01/squeezebox-keyboard-v2112/</guid>
          <description xml:base="https://peterlyons.com/problog/2022/01/squeezebox-keyboard-v2112/">&lt;h2 id=&quot;overview&quot;&gt;Overview&lt;&#x2F;h2&gt;
&lt;p&gt;The Squeezebox Keyboard is a custom-designed ergonomic computer keyboard I have been working on for nearly a year now. The primary design driving factors include:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Wanting the keys really close together both within a column and across columns
&lt;ul&gt;
&lt;li&gt;If there was a nice switch smaller than the Kailh chocs, I&#x27;d be interested&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;&quot;Home corner&quot; concept for the home row and bottom row taking curvature to an extreme and making a straight-up corner (100 degree angle)
&lt;ul&gt;
&lt;li&gt;Each finger can rest on 2 keys and hit either without &quot;moving&quot; by using the fingertip and pad, respectively&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;Mechanical tailor fit without many rounds of long and perilous 3D prints&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2022&amp;#x2F;2022-01-02-01FRDKZK38WS6FDSGM3HNCJ9Q8.2048.jpg&quot;&gt;
  &lt;figcaption&gt;Squeezebox keyboard v2112 right hand steeply tented&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;earlier-prototypes&quot;&gt;Earlier Prototypes&lt;&#x2F;h2&gt;
&lt;p&gt;This post describes my latest fully functional build&#x2F;prototype. Earlier builds include the &lt;a href=&quot;&#x2F;problog&#x2F;2021&#x2F;04&#x2F;squeezebox-keyboard&#x2F;&quot;&gt;original non-tented version&lt;&#x2F;a&gt; from April 2021 and the &lt;a href=&quot;&#x2F;problog&#x2F;2021&#x2F;06&#x2F;squeezebox-keyboard-v2105&#x2F;&quot;&gt;steeply tented version with magnetic posts on a steel base plate&lt;&#x2F;a&gt; from June 2021.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;review-of-existing-aspects&quot;&gt;Review of existing aspects&lt;&#x2F;h2&gt;
&lt;p&gt;If you are just seeing this project for the first time, here&#x27;s a recap of some other things to note about the design.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;No keycaps! Naked switch stems. Kailh choc switches don&#x27;t need no stinking caps.&lt;&#x2F;li&gt;
&lt;li&gt;Super tight spacing. Even the pinky can reach all 3 rows without stretching&#x2F;straining and without having to move the whole arm. I have to move my arm to type the top pinky row on most keyboards I&#x27;ve tried, and for years I pressed that key with my ring finger without consciously realizing it.&lt;&#x2F;li&gt;
&lt;li&gt;Allow chording the middle and bottom row with a single finger from the home position
&lt;ul&gt;
&lt;li&gt;Whether this is really practical with QMK, whose chording support is not that great, is still kind of TBD. A custom firmware with carefully implemented first class support for this might make this a central feature, but as of now I can&#x27;t yet do it consistently&#x2F;accurately enough to use it for important stuff.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;Switches can be hot swapped out&lt;&#x2F;li&gt;
&lt;li&gt;Each finger can be independently finely adjusted in column stagger, splay angle, and height&lt;&#x2F;li&gt;
&lt;li&gt;Modular design facilitates things like swapping in a different thumb cluster, for example&lt;&#x2F;li&gt;
&lt;li&gt;Designed around steep tenting split usage&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;what-s-new-in-this-version&quot;&gt;What&#x27;s new in this version&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;slots-on-the-base-plate&quot;&gt;Slots on the base plate&lt;&#x2F;h3&gt;
&lt;p&gt;The primary mechanism of adjustment near&#x2F;far has been changed to moving the finger columns along a grid of slots in the base plate, which has been custom designed and 3D printed to support this. The previous build used a steel base plate and magnets. That was kind of neat in the arbitrary precision of it and using the same mechanism for both columnar offset (near&#x2F;far) and splay angle (finger columns like rays emanating from a center point vs 4 parallel lines). However, it was a bit too difficult to get everything in an optimal position and then lock it in and know it wouldn&#x27;t accidentally get moved around or knocked loose.&lt;&#x2F;p&gt;
&lt;p&gt;Now the slots on the base plate afford columnar offset by sliding posts along the slots, and splay by putting the front and rear post for a column in different lane of slots. You can think of this like the front of the finger column can be swimming in a different lane than the back of the finger column. I generally put the middle finger straight ahead, splay the index finger by 2 rows, and splay the ring and pinky by 1 row. The degree of precision for this adjustment is not very granular, but it&#x27;s adequate for sure.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;new-standoff-columns&quot;&gt;New standoff columns&lt;&#x2F;h3&gt;
&lt;p&gt;The standoffs&#x2F;posts that afford the height variance across fingers are now hexagonal posts, so they don&#x27;t roll around when loose and have heat-set threaded inserts in both the top and the bottom. They get bolted to the base plate and to the key column. I&#x27;m very happy with this mechanism. I have some tiny washers I 3D printed and when tightened down just a little snug on the bolts everything feels very mechanically stable and secure.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2022&amp;#x2F;2022-01-02-01FRDKXMK8DEPDGWTSQ188JJRM.2048.jpg&quot;&gt;
  &lt;figcaption&gt;Each finger is elevated by a hexagonal column with tailor fit ergonomic height&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h3 id=&quot;new-thumb-cluster-post&quot;&gt;New thumb cluster post&lt;&#x2F;h3&gt;
&lt;p&gt;Same basic mechanism of slots and bolts is used to attach the thumb cluster, but as it is oriented 90 degrees off from the fingers, it goes just on a post. I made the stem longer to allow greater range of motion on the open&#x2F;close lid type action. I mount mine basically as low as the base plate will allow and that ends up feeling about right for me. The center of the thumb plate ends up about aligned with the base plate of the main keyboard, so there&#x27;s one key inward from home and one key outward.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;base-plate-camera-mount&quot;&gt;Base plate camera mount&lt;&#x2F;h3&gt;
&lt;p&gt;The base plate has a housing where I&#x27;ve glued a standard 1&#x2F;4-20 hex nut so I can mount this on my Neewer camera mounts and similar gear. There are 5 total positions to allow some experimentation with balance and stability but for me dead center seems to be OK.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2022&amp;#x2F;2022-01-02-01FRDM4YZ8HG9V597J849V7NYS.2048.jpg&quot;&gt;
  &lt;figcaption&gt;Mounting onto Neewer Z-fold stands via bolt&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h3 id=&quot;base-plate-hex-wrench-holders&quot;&gt;Base plate hex wrench holders&lt;&#x2F;h3&gt;
&lt;p&gt;The base plates have a little slot where I can store a hex wrench for adjusting things. I got tired of losing track of the only one I had so I ordered like 20 of them and 2 can just live on the keyboard now.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2022&amp;#x2F;2022-01-02-01FRDM8WYG13R9WRF9REBXNM67.2048.jpg&quot;&gt;
  &lt;figcaption&gt;Little slots to hold your hex wrenches&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h3 id=&quot;new-brain-box&quot;&gt;New brain box&lt;&#x2F;h3&gt;
&lt;p&gt;The Elite-C microcontroller, reset button, and RJ9 connector are housed in a custom designed box. This has a 2-tone lid which attaches with my go-to heat-set threaded inserts and M3 bolts. The brain box mounts to the base plate again with a pair of M3 bolts. The brain box is much improved over all earlier versions, but that bar was set really low. &quot;Case? What case? The microcontroller is perfectly happy just flapping around at the end of some soldered wires.&quot;. That said, I remain terrible at remembering at design time that wires actually occupy physical space, so both in the case and outside of the case the wiring is super crowded and a pain in the butt to work with. But it&#x27;s workable and the lid does close and everything can reach where it needs to reach. But the idea of like easily swapping in a different column is not easy. It&#x27;s a lot of tedious mucking with hex wrench, bolts, tiny connectors, and very cramped places where fingers can&#x27;t reach, so it&#x27;s a pain. Now that I have it all built, I&#x27;m probably going to do some very small adjustments to columnar stagger for exact tailor fit and symmetry across the hands and then tighten down the bolts and try to not mess with it again if possible.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2022&amp;#x2F;2022-01-02-01FRDM0AH8FCXK7Y0XF856JP05.2048.jpg&quot;&gt;
  &lt;figcaption&gt;brain box holds the microcontroller, reset button, and RJ9 connector&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h3 id=&quot;new-wiring-connectors&quot;&gt;New wiring connectors&lt;&#x2F;h3&gt;
&lt;p&gt;As with the previous version and &lt;a href=&quot;https:&#x2F;&#x2F;youtu.be&#x2F;HcfCOohTDrY&quot;&gt;explained in this video&lt;&#x2F;a&gt;, each finger column is a standalone 3-key macro pad that can work entirely on its own when wired to the microcontroller. The keys connect up the 3 row wires in this version using &lt;a href=&quot;https:&#x2F;&#x2F;amzn.com&#x2F;dp&#x2F;B07GHG9QVD&quot;&gt;4-pin JST connector&lt;&#x2F;a&gt;. I was excited about this during the thinking phase, and I like the nice secure connection these make, but they are just too bulky relative to the Dupont jumper wires I used in the previous version, so I probably will move away from these in futures designs. More on future designs below.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;new-wiring-design&quot;&gt;New wiring design&lt;&#x2F;h3&gt;
&lt;p&gt;In the last build I wired the rows from pinky through thumb then to the microcontroller, which meant the thumb had both incoming and outgoing cables. To tidy that up, I now have a custom made Y cable coming out of the brain box and one side daisy chains to middle-index-thumb and the other side goes to ring-pinky side. It keeps the thumb a bit tidier in exchange for the cables near the middle finger being more crowded.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;choc-red-color-scheme&quot;&gt;choc red color scheme&lt;&#x2F;h3&gt;
&lt;p&gt;I switched back to Kailh choc reds for this and decided to print it in red and black PLA to match the switches. I&#x27;m really pleased with the look and it gives me that 80s TV &quot;A-Team&quot;&#x2F;&quot;Knight Rider&quot; vibe. Let&#x27;s be honest, this device will probably spend most of its life on my desk not actually being used, so at least it can be kind of pleasing and slick to look at. :-&#x2F;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;one-more-pinky-column&quot;&gt;One more pinky column&lt;&#x2F;h3&gt;
&lt;p&gt;My current daily driver is a &lt;a href=&quot;https:&#x2F;&#x2F;bastardkb.com&#x2F;product&#x2F;tbk-mini-kit&#x2F;&quot;&gt;TBK Mini&lt;&#x2F;a&gt; which has 6 columns for the fingers, and although I&#x27;m trying to not use the pinky very much, that extra row does come in really handy and simplify layouts, so I added an extra column for the pinky to reach to. This also gives me better hope of switching to make the Squeezebox my daily driver since now it has exactly the same number of finger keys and the same thumb cluster arrangement as the TBK mini so all I have to deal with is the form factor switch itself but my keymap setup can be identical. So each hand now has a pair of 6-key double wide units (pinky and index), and a pair of 3-key units (middle and index) and a 3-key thumb cluster.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;files-for-3d-printing&quot;&gt;Files for 3D Printing&lt;&#x2F;h2&gt;
&lt;p&gt;If you want to print one of these I have uploaded the files and instructions to &lt;a href=&quot;https:&#x2F;&#x2F;www.prusaprinters.org&#x2F;prints&#x2F;112655-squeezebox-keyboard-v2112&quot;&gt;prusaprinters here&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;notes-and-learnings&quot;&gt;Notes and Learnings&lt;&#x2F;h2&gt;
&lt;p&gt;I had a huge struggle and delay this time due to adhesive issues. I started out using Gorilla Glue brand Superglue Gel (cyanoacrylate or &quot;CA glue&quot;). The gel makes it reasonably achievable to get glue where it needs to go to hold the Kailh hot swap sockets onto the PLA part, but it takes way longer to cure than advertised and I don&#x27;t really want to use accelerant from a spritzer bottle and get it over way too large an area when I really only want it on like a single square millimeter. I guess I could try like spraying accelerant onto a Q-Tip and then applying to the socket then putting it into place on the gel, but it&#x27;s already a tedious process. Once cured, the superglue does not hold well. The sockets were popping off like mad, especially if the key column flexed at all. I probably had to re-glue at least 10 of them.&lt;&#x2F;p&gt;
&lt;p&gt;So I tried Loctite 2-part epoxy but also had issues. It took forever to cure as well, like more than a day, and even then it was still visibly gooey. I live in a very cold and humid climate, so it may be challenging conditions for adhesives in general. But it&#x27;s also a pain to get a small amount mixed, emits a lot of nasty fumes, suggests you waste a mixing nozzle for every application which I don&#x27;t really like. All I can say is epoxy cures clear and looks tidy relative to superglue, which cures to crusty ugly white and can really ruin the appearance of an otherwise stylin&#x27; part.&lt;&#x2F;p&gt;
&lt;p&gt;I may try to add some walls for the hot swap sockets to the 3D model so it will have a floor, 2 post holes, and 2 walls to hold it as well as more surface area to glue, but that&#x27;s going to introduce bridging to a part of the print that is high precision and a little sagging there will prevent the socket from fitting so I&#x27;m not sure. I might just give up on hot swap sockets. They are neat and it&#x27;s cool to list hot swap as a feature, but realistically I&#x27;m not going to be hot swapping switches and they add a lot of complexity to the CAD and assembly and soldering so I&#x27;ll likely just ditch them and run soldered wires from the switch pins to a PCB with a dupont connector in future versions.&lt;&#x2F;p&gt;
&lt;p&gt;Another design goal that did not work out was I intended for each key column to have a &quot;port&quot; like a store bought piece of electronics so I was going to glue the JST female connectors firmly to the front of the column (that&#x27;s the entire reason the columns have a &quot;nose&quot; part at the end) but I couldn&#x27;t get any adhesive to bond strongly enough for that to work out. I got it kinda working for the dupont wires on the double wide parts but on the single wide parts I ran into an unforeseen collision between where the standoff post mounts and where the dupont connectors would be so I had to abandon that feature and just mostly have small wire connectors dangling off here and there. That makes the wire splices even more prone to failure.&lt;&#x2F;p&gt;
&lt;p&gt;I was inconsistent with remembering to use heat shrink tubing to insulate and support my wire splices in this one and I think it helps a lot, so I&#x27;ll try to remember to do that more consistently. After the initial build I broke 2 wires at splice points during assembly. The 2nd custom Y cable I made I did it and it came out pretty nice. Generally my splices have large solder blobs as I&#x27;m not good at it, so I need heat shrink tubing of much higher diameter relative to the wire I&#x27;m splicing.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;next-version-plans-custom-pcb&quot;&gt;Next version plans: Custom PCB&lt;&#x2F;h2&gt;
&lt;p&gt;So now that I&#x27;ve done all these iterations, I can see how this could evolve to a hybrid partially hand wired but mostly using a printed circuit board.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;I&#x27;d not put any diodes on the key columns. Instead, I&#x27;d use surface-mounted diodes on the PCB.&lt;&#x2F;li&gt;
&lt;li&gt;Each column would get the following solders&#x2F;wires
&lt;ul&gt;
&lt;li&gt;A wire from one of the switch pins on each switch to the PCB, which will then go through a diode (the row wires)&lt;&#x2F;li&gt;
&lt;li&gt;A wire connecting each of the other switch pins together (the column wire). This then routes to the PCB and on to a microcontroller pin.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;The PCB would be designed to take the place of the 3D-printed base plate. I think PCB material (which I gather is called FR4) is sturdy enough. It&#x27;s probably much more rigid than this print which is only I think 2mm thick.&lt;&#x2F;li&gt;
&lt;li&gt;Behind each switch column would be through hole connections, 4 per column. 3 for the rows and 1 for the column.
&lt;ul&gt;
&lt;li&gt;The rows all connect across to each other and then on the pins on the microcontoller&lt;&#x2F;li&gt;
&lt;li&gt;The columns route directly to the microcontroller&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;A standard Elite-C through hole housing, probably still on the far side roughly where the brain box is now&lt;&#x2F;li&gt;
&lt;li&gt;Reset button is easy and can go kind of anywhere&lt;&#x2F;li&gt;
&lt;li&gt;3 through holes for wiring the RJ9 for inter-hand connection&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;I think that&#x27;s it. That would enable me to bundle the 4 wires from each switch column into some heat-shrink and make it pretty tidy, while still allowing mechanical repositioning of the columns and removal&#x2F;replacement independently.&lt;&#x2F;p&gt;
&lt;p&gt;Anyone want to teach me some kicad? I&#x27;ve never designed a circuit board.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Exploring Editors</title>
          <pubDate>Mon, 30 Aug 2021 23:36:08 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2021/08/exploring-editors/</link>
          <guid>https://peterlyons.com/problog/2021/08/exploring-editors/</guid>
          <description xml:base="https://peterlyons.com/problog/2021/08/exploring-editors/">&lt;p&gt;Content warning: programmer discussing text editors.&lt;&#x2F;p&gt;
&lt;p&gt;I have been down a path spurred primarily by 2 events:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Development of the Atom editor becoming uncertain after microsoft acquired github and thinking it likely there was no room for both VS Code and Atom, and thus Atom would be sunset&lt;&#x2F;li&gt;
&lt;li&gt;My rage quitting of workflowy after staring at the damn loading spinner and general disgust&#x2F;contempt for the web as a platform that takes a todo list app in which my lifetime full data set could fit in memory 100 times over and makes me wait 20 seconds to see the grocery list I was looking at just a moment ago.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;So I sampled some workflowy alternatives including roam research, nvalt, a vim org-mode plugin, emacs org mode, and spacemacs org mode.&lt;&#x2F;p&gt;
&lt;p&gt;I eventually concluded &lt;a href=&quot;https:&#x2F;&#x2F;orgmode.org&#x2F;&quot;&gt;emacs org mode&lt;&#x2F;a&gt; was a suitable replacement for workflowy and fully converted my entire data set. I learned just enough spacemacs&#x2F;emacs to use org mode with moderate efficiency, but it became clear to me that my go-to editor would still be neovim. I did use a vim org-mode plugin for a few weeks, but its feature set was incredibly paltry and there was no active development, so I went for the real thing.&lt;&#x2F;p&gt;
&lt;p&gt;My distaste for emacs stems I think primarily from:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;terrible chorded keyboard shortcut sequences which the community just cannot stop doubling down on every decade&lt;&#x2F;li&gt;
&lt;li&gt;Arcane docs and terminology which again the community cannot just admit that for every keyboard with a meta key ever built, there have been like 20,000 keyboards built without a key labeled &quot;meta&quot; and update their stupid confusing nonsense docs.&lt;&#x2F;li&gt;
&lt;li&gt;Slow startup time which reflects performance as not important and the &quot;emacs is a place you live&quot; approach which does not really provide a good path to gradual adoption.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;So I doubled down a bit on neovim. I did find &lt;a href=&quot;https:&#x2F;&#x2F;www.spacemacs.org&#x2F;&quot;&gt;spacemacs&lt;&#x2F;a&gt; overall a breath of fresh air and am now also test driving &lt;a href=&quot;https:&#x2F;&#x2F;spacevim.org&#x2F;&quot;&gt;spacevim&lt;&#x2F;a&gt;, but it&#x27;s docs are also a bit of a struggle for me to use.&lt;&#x2F;p&gt;
&lt;p&gt;I&#x27;m also watching &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;vhyrro&#x2F;neorg&quot;&gt;neorg&lt;&#x2F;a&gt; as an update on the org mode file format with a real grammar that programs other than emacs can reasonably read and manipulate. Doesn&#x27;t quite look ready to try yet but hopefully soon.&lt;&#x2F;p&gt;
&lt;p&gt;In my editor experimentation I was aware of &lt;a href=&quot;https:&#x2F;&#x2F;kakoune.org&#x2F;&quot;&gt;kakoune&lt;&#x2F;a&gt; and thought about taking it for a spin but...&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;I am very skeptical of viability of a C++ codebase these days as not being an unstable insecure nightmare&lt;&#x2F;li&gt;
&lt;li&gt;There seems to be fairly good user community interest but the ratio of user requests to maintainer&#x2F;implementer activity seems way skewed.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;That led me to my signature line of &quot;found a new version in rust&quot; in &lt;a href=&quot;https:&#x2F;&#x2F;helix-editor.com&#x2F;&quot;&gt;helix&lt;&#x2F;a&gt;. It seems very nascent still but I&#x27;m curious to play around with the multicursor model and am very encourage by on-screen keybinding menus. God bless modern TUI apps that put the stupid keybinds on the damn screen, and hats off to old school ones like the PINE email client and nano text editor that have had on-screen keybinding menus since the beginning.&lt;&#x2F;p&gt;
&lt;p&gt;So I think I&#x27;ll keep an eye on helix development and occasionally fire it up to check on progress. I&#x27;ll try spacevim a little but if I can&#x27;t grok how to properly customize it for thinks like a plugin to manage quotes&#x2F;parens, I&#x27;ll likely abandon it for basic neovim.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Squeezebox Keyboard v2105</title>
          <pubDate>Fri, 11 Jun 2021 21:39:11 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2021/06/squeezebox-keyboard-v2105/</link>
          <guid>https://peterlyons.com/problog/2021/06/squeezebox-keyboard-v2105/</guid>
          <description xml:base="https://peterlyons.com/problog/2021/06/squeezebox-keyboard-v2105/">&lt;p&gt;I&#x27;ve made a second prototype of the Squeezebox Keyboard. It&#x27;s a huge improvement over the first working prototype.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2021&amp;#x2F;2021-06-13-01F82HZ3J0ECCRPM37A1TSH528.2048.jpg&quot;&gt;
  &lt;figcaption&gt;Detail of assembled right hand side&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;improvements-included-in-this-version&quot;&gt;Improvements included in this version&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;Key switches are hot swappable courtesy of Kailh hot-swap sockets
&lt;ul&gt;
&lt;li&gt;This required a major level-up in my FreeCAD skills as there&#x27;s a LOT of precise geometry&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;Per-finger splay angle, adjustable for tailor fit&lt;&#x2F;li&gt;
&lt;li&gt;Each keywell column is a separate independently-operable 3-key macropad
&lt;ul&gt;
&lt;li&gt;This means if one breaks or fails you only need to print a single part and handwire 3 switch&#x2F;diode pairs&lt;&#x2F;li&gt;
&lt;li&gt;This is also extremely useful for the DIY process because each small session of handwiring work can be positively confirmed working and correct without the psychological crush of spending one or more full days wiring and soldering only to connect USB and have precisely nada happen when you press a key&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;Angle between rows A and B increased to solve problem of some fingers scraping it on the fingernail side&lt;&#x2F;li&gt;
&lt;li&gt;Smaller individual 3D printed components avoids frustration of multi-hour print job failing and delivering no usable parts&lt;&#x2F;li&gt;
&lt;li&gt;reset button is actually mounted into a housing instead of floating in the air at the end of some wires&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;wiring-and-electronics&quot;&gt;Wiring and Electronics&lt;&#x2F;h2&gt;
&lt;p&gt;Here&#x27;s a video explaining the wiring system which I believe is fairly novel and strikes me as the &quot;classic Jeep&quot; approach of DIY handwired keyboards. As you&#x27;ll see in the video, the electronics are made really clear and easy to build, understand, disassemble, reassemble.&lt;&#x2F;p&gt;
&lt;div class=&quot;youtube-video-container&quot;&gt;
&lt;iframe src=&quot;https:&#x2F;&#x2F;www.youtube-nocookie.com&#x2F;embed&#x2F;HcfCOohTDrY&quot; allowfullscreen&gt;&lt;&#x2F;iframe&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;Key points from the video for the TL;DW crowd&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Each finger column can be connected to the microcontroller with dupont wires and work as a 3-key macropad&lt;&#x2F;li&gt;
&lt;li&gt;The fingers daisy chain together with dupont wires to link up the columns into the full half of a split keyboard&lt;&#x2F;li&gt;
&lt;li&gt;Thumb cluster works the same way&lt;&#x2F;li&gt;
&lt;li&gt;Don&#x27;t like the layout of the thumb cluster? You can disconnect it, build a different one, and connect that up and Bob&#x27;s your father&#x27;s brother.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;mechanics-assembly-and-adjustment&quot;&gt;Mechanics, Assembly, and Adjustment&lt;&#x2F;h2&gt;
&lt;p&gt;This video demonstrates how the individual parts are assembled into a system and tailor fit for the user&#x27;s hand.&lt;&#x2F;p&gt;
&lt;div class=&quot;youtube-video-container&quot;&gt;
&lt;iframe src=&quot;https:&#x2F;&#x2F;www.youtube-nocookie.com&#x2F;embed&#x2F;yGRIldWoOo8&quot; allowfullscreen&gt;&lt;&#x2F;iframe&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;&lt;strong&gt;Key points&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Heat-set threaded inserts and M3 bolts continue to be heavily used&lt;&#x2F;li&gt;
&lt;li&gt;This version incorporates magnets holding the base pillars to a metallic bottom plate covered in gaffer tape&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;ergonomics-and-tenting&quot;&gt;Ergonomics and Tenting&lt;&#x2F;h2&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2021&amp;#x2F;2021-06-13-01F82HVAF0SEVPWT96JSS4T7D1.2048.jpg&quot;&gt;
  &lt;figcaption&gt;Squeezebox Keyboard v2105 split and tented for standing use&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;There&#x27;s been some dramatic back and forth on this R&amp;amp;D effort regarding tenting. My original vision was a truly vertical orientation (like a joystick with your hand in &quot;tamborine position&quot; not &quot;bongo position&quot; - thus the name &quot;squeezebox&quot; which is held in that position). Test typing on my first prototype, my fingers felt pretty good but my thumb began quickly to experience fatigue and shaking due to floating unsupported hovering over its home key on the thumb cluster. That would probably be addressable if instead of floating I was really gripping something joystick-shaped or roughly soda can shape but I didn&#x27;t want to add yet another discipline like modeling clay to my curriculum and my 3D CAD skills were definitely not ready to make some organic curvy thing, so I yielded to pragmatism and changed my design to target using flat on a desk with no tenting like a standard keyboard. So the first working prototype from back in April was used flat with tall wrist rests and had a thumb cluster on the same basic plane of orientation as the main keywells.&lt;&#x2F;p&gt;
&lt;p&gt;This version was completed with that same basic approach and I was either going to try to use it on my desk top or maybe mount it to the arms of my chair. But once I had the working version with the diamond-pattern metal base plate in hand, I had a eureka moment realizing I could easily and securly mount this to the camera stands I bought initially to tent my Keyboardio Model 01. That keyboard comes with a threaded insert in the base sized to match up with camera mounting gear which is absolutely brilliant. All keyboard designers take note, please. Every split keyboard should have that.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2021&amp;#x2F;2021-06-13-01F82TGMFR39DFHRKR9XCMY1A9.2048.jpg&quot;&gt;
  &lt;figcaption&gt;Standing desk posture with floating arms&amp;#x2F;hands&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;So I was able to mount this just by cutting a hole in the gaffer tape and running the bolt through the existing hole in the diamond plate pattern and tightening it down with a bolt and it worked great. I have nice fancy adjustable mounts (these are not at all DIY - video folks don&#x27;t mess around when it comes to gear) and I can tent them anywhere from 0 degrees to full 90 if I want. Here&#x27;s a video talking a little bit about this.&lt;&#x2F;p&gt;
&lt;div class=&quot;youtube-video-container&quot;&gt;
&lt;iframe src=&quot;https:&#x2F;&#x2F;www.youtube-nocookie.com&#x2F;embed&#x2F;yJ7hvMvGCOM&quot; allowfullscreen&gt;&lt;&#x2F;iframe&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;And one random ergonomic note is that the separate fingers and splay angle combined with the white key switch stems makes this thing look a lot like a skeleton hand, which I take as a pretty good indication of the bio-mimickry going on in the ergonomics of it.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;so-you-type-real-fast-now-right&quot;&gt;So You Type Real Fast Now, Right?&lt;&#x2F;h2&gt;
&lt;p&gt;Ha ha, no. This is a one-of-a-kind prototype. I can sort of barely use it but I am very very slow and very very inaccurate. But at the moment I&#x27;ve truly only spent a few minutes even trying. This is mostly a passion project and adopting it as my daily driver would be great, but I&#x27;m many dozens of hours of practice away from seriously considering that. That said, my keymap has undergone a fairly thorough overhaul as a result of the explorations involved with this project, so even on my daily driver ergodox, my layout is better now and I&#x27;m gradually reducing pinky usage and other straining movements. Also even on my ergodox which I&#x27;ve been using primarily since 2013 I&#x27;m just not very fast or accurate or consistent. 70-90 WPM ballpark. Keyboard ergonomics for me is an accommodation to reduce RSI of which I&#x27;ve had either mild or severe on and off again for years. Look for impressive WPM stats elsewhere.&lt;&#x2F;p&gt;
&lt;div class=&quot;youtube-video-container&quot;&gt;
&lt;iframe src=&quot;https:&#x2F;&#x2F;www.youtube-nocookie.com&#x2F;embed&#x2F;AaEdjcQJLuc&quot; allowfullscreen&gt;&lt;&#x2F;iframe&gt;
&lt;&#x2F;div&gt;
&lt;h2 id=&quot;diy-keyboard-kit&quot;&gt;DIY Keyboard Kit&lt;&#x2F;h2&gt;
&lt;p&gt;The files for this are posted on &lt;a href=&quot;https:&#x2F;&#x2F;www.prusaprinters.org&#x2F;prints&#x2F;69209-squeezebox-keyboard-v2105&quot;&gt;prusaprinters.org&lt;&#x2F;a&gt; if you are ambitious enough to attempt this. It is not at all to the level of documentation to expect not-me people to be able to successfully build one, but have at it if you are inspired.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;plans-for-next-prototype&quot;&gt;Plans for next prototype&lt;&#x2F;h2&gt;
&lt;p&gt;Circumstances in my personal life are likely to no longer afford time to work as much on this project going forward, but if by some means I am able to work on a third prototype, here&#x27;s my todo list&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;If the extreme tenting and floating arms&#x2F;hands seems long-term viable, I will need to design and print a different connector for the thumb cluster so it can have its main plane be more horizontal when the keyboard is tented up to like 80 degrees&lt;&#x2F;li&gt;
&lt;li&gt;Thumb cluster key layout itself may want to tweak slightly when tenting&lt;&#x2F;li&gt;
&lt;li&gt;The PCB mount needs a redesign in light of metal base plate and magnets. I&#x27;ll probably make a very simple rectangular box held to the plate with a magnet and that will help with a tidier wire layout and more stability when connecting&#x2F;disconnecting the USB cable.
&lt;ul&gt;
&lt;li&gt;might even get really fancy and try to have a top so it&#x27;s not just a hood-open bouquet of wires :-)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;I&#x27;d like to make a chair arm rest mount. Possibly magnetic. This would also benefit from a long non-coiled RJ-9 cable that could go around the back of the chair instead of across the front where it is totally in the way.&lt;&#x2F;li&gt;
&lt;li&gt;I have already found and bought some much nicer connectors for daisy chaining the columns. The plastic connector is more robust and snaps into place more solidly, and all the colors will match up nicely. It is more bulky than the dupont connectors though so it&#x27;ll maybe need a bit more space to live under the keywells.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;thanks&quot;&gt;Thanks&lt;&#x2F;h2&gt;
&lt;p&gt;I continue to be very grateful for people across the Internet helping me with this project: answering questions, providing feedback and encouragement, etc. I&#x27;ve posted to many slacks, subreddits, discords, github repos, etc to get help on this project. Especially check out &lt;a href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;user&#x2F;ModOfLow&quot;&gt;Jan Lunge&#x27;s Youtube Channel&lt;&#x2F;a&gt;. You&#x27;ll find a link to his discord there too.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Squeezebox Keyboard</title>
          <pubDate>Sat, 17 Apr 2021 01:32:05 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2021/04/squeezebox-keyboard/</link>
          <guid>https://peterlyons.com/problog/2021/04/squeezebox-keyboard/</guid>
          <description xml:base="https://peterlyons.com/problog/2021/04/squeezebox-keyboard/">&lt;p&gt;I designed and built a split ergonomic keyboard with some ideas I&#x27;ve been wanting to try for over a year. Here are the main features of the keyboard:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Split design, obvs&lt;&#x2F;li&gt;
&lt;li&gt;staggered linear columns&lt;&#x2F;li&gt;
&lt;li&gt;customizable tailor fit column offsets&lt;&#x2F;li&gt;
&lt;li&gt;tailor fit per-finger vertical offsets&lt;&#x2F;li&gt;
&lt;li&gt;thumb cluster with 6 keys&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2021&amp;#x2F;2021-04-17-01F3G77AN0CB6SGVNNMVM65J4A.2048.jpg&quot;&gt;
  &lt;figcaption&gt;Squeezebox split keyboard&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2021&amp;#x2F;2021-04-17-01F3G07V98APTQBNACS0D1H4QP.2048.jpg&quot;&gt;
  &lt;figcaption&gt;The Squeezebox Keyboard rev 21a left side top view&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;the-home-corner&quot;&gt;The Home Corner&lt;&#x2F;h2&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2021&amp;#x2F;2021-04-17-01F3FY4TR8KP1EWJN0B2Q52RPF.2048.jpg&quot;&gt;
  &lt;figcaption&gt;The home position resting in contact with the middle and bottom rows&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;I think the main design element that sets this apart for other similar keebs is the shape of the middle and bottom rows. They are arranged in a steep 100-degree corner and the home position for the finger rests simultaneously with the point of the finger resting on the middle row and the pad of the finger resting on the bottom row. You can type the middle row while maintaining contact with the bottom row, and vice versa. So there&#x27;s 2 separate keys that can be typed without any reaching.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2021&amp;#x2F;2021-04-17-01F3FY50KRH8D6S1EYWRPC341P.2048.jpg&quot;&gt;
  &lt;figcaption&gt;Pressing the middle row&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2021&amp;#x2F;2021-04-17-01F3FY4WPR0XQ5FHAD4NK01TNQ.2048.jpg&quot;&gt;
  &lt;figcaption&gt;Pressing the bottom row&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;Additionally, because the switches are in such a tight corner, it&#x27;s possible to press both of them with a single finger by poking into the corner. Both keys press and release in perfect unison. So each finger has 3 separate characters it can type without any reaching when you add a QMK combo into the keymap.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2021&amp;#x2F;2021-04-17-01F3FY53HGBS7XN9Q87F94MBX1.2048.jpg&quot;&gt;
  &lt;figcaption&gt;1-finger chording the middle and bottom rows&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;There is an upper row which is positioned and angled for very small reach so when you add that it&#x27;s 4 keys per finger. The upper row is so tightly spaced that if you reach for it, you&#x27;ll overshoot it. You don&#x27;t need to reach. Just &quot;think up&quot; slightly and you hit it.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2021&amp;#x2F;2021-04-17-01F3FY55G0D3P2A718KV98KSR6.2048.jpg&quot;&gt;
  &lt;figcaption&gt;Pressing the top row&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;physical-layout-of-switches&quot;&gt;Physical layout of switches&lt;&#x2F;h2&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2021&amp;#x2F;2021-04-17-01F3G07V98APTQBNACS0D1H4QP.2048.jpg&quot;&gt;
  &lt;figcaption&gt;Main finger grid and thumb cluster&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;So we have a 3x5 grid for the fingers plus 6 thumb keys so it&#x27;s 21 keys per hand for a total of 42 (nice!). The thumb cluster has pairs of keys in a &quot;lounge chair&quot; arrangement with one flat and one reclined at an angle making it easy to hit them separately or to chord them. One column is the home position for the thumb and then there&#x27;s one you move your thumb inward to hit and another you move outward for.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ergonomics-and-custom-tailor-fitting&quot;&gt;Ergonomics and Custom Tailor Fitting&lt;&#x2F;h2&gt;
&lt;p&gt;The row spacing is designed essentially to be as tight as possible given Kailh choc switches. I knew from the outset I wanted much denser switch spacing, and if there were smaller switches available I might even go a tiny bit smaller, but this is probably close to ideal.&lt;&#x2F;p&gt;
&lt;p&gt;The height of each column is controlled in parameterized CAD for the front&#x2F;back walls so to adjust that you dial the parameters and print a new wall. Each column has a custom shelf height supporting it, essentially.&lt;&#x2F;p&gt;
&lt;p&gt;For near&#x2F;far adjustment, each finger column is on slots that provide about 15mm of movement
for each finger independently. Tweak the positions with the bolts slightly loose until it fits perfectly then tighten the bolts and you&#x27;re set.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2021&amp;#x2F;2021-04-18-01F3JJZ45RD6WFMB2VR6DF79T4.2048.jpg&quot;&gt;
  &lt;figcaption&gt;Thumb cluster standoff for height and X&amp;#x2F;Y position&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;The thumb cluster can be raised&#x2F;lowered by printing different length standoff cylinder. It&#x27;s placement left&#x2F;right and near&#x2F;far is adjusted by way of the grid of threaded insert holes. I&#x27;ve only tried 2 positions so far so I only bothered installing the threaded inserts in 2 of the holes, but in theory each of those little circular holes below the thumb cluster could house a threaded insert.&lt;&#x2F;p&gt;
&lt;p&gt;The thumb cluster will also rotate around the axis of the bolt attaching it to the main case.&lt;&#x2F;p&gt;
&lt;p&gt;My goal at the outset was to make this a kit that most folks could use and tailor to their hand. Probably with some kind of LEGO style brick stack for the column heights that could be achieved. But at the moment adjusting the column heights requires a custom printed part. Everything else is mechanically adjustable, at least within the limits of slack in the wiring.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;naked-switches&quot;&gt;Naked Switches&lt;&#x2F;h2&gt;
&lt;p&gt;No keycaps! Choc switches have a perfectly fine flat-top stem. The tight geometry I wanted requires an extremely small gap between the middle and bottom row switches and keycaps would get in the way. When you look at it from the side you&#x27;ll notice the bottom row stem actually overlaps just above the middle row stem slightly. Keycaps could technically go on the top row and most of the thumb cluster but they don&#x27;t really add much and I think it&#x27;s kind of a hipster flex to not have them. It&#x27;s like the fixie bike of the keeb world. The switches are installed rotated 90 degrees from standard to reduce the typing gap within a single finger&#x27;s column.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wiring-and-soldering&quot;&gt;Wiring and Soldering&lt;&#x2F;h2&gt;
&lt;p&gt;Some of my early prototypes were super exciting to me. Like this one that was a vertical stack and adjusted in 2 directions with a series of aligning slots.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2021&amp;#x2F;2021-02-17-01EYRMM6Y09H14P24V5N7VX2CT.2048.jpg&quot;&gt;
  &lt;figcaption&gt;This prototype seemed great but soldering inside these housings would be a nightmare&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;That is until I realized there was no way to get in there with a soldering iron and actually wire them up. I had to discard several variations before landing on something that was actually possible to solder.&lt;&#x2F;p&gt;
&lt;p&gt;The current design with the walls detachable from the finger columns also facilitates hand wiring while keeping the frustration level manageable. I did 3 solder operations on each switch with the switch completely out of the case, and only after that did I glue them into their positions. That helps a lot. Then, wiring can proceed with only one wall bolted on at a time so the other side has plenty of clearance for the soldering iron and manipulating the wires by hand.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2021&amp;#x2F;2021-03-21-01F1AEKCK86HCYH3Z9Z6PJT0KM.2048.jpg&quot;&gt;
  &lt;figcaption&gt;Pre-assembly solder operations&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2021&amp;#x2F;2021-03-21-01F1AT7JR8MGPPC9TKYXGA0XS5.2048.jpg&quot;&gt;
  &lt;figcaption&gt;Wiring up the finger columns with only 1 wall attached&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;keywells-below-wrist-rest&quot;&gt;Keywells below wrist rest&lt;&#x2F;h2&gt;
&lt;p&gt;For the most comfortable geometry given the need to do a trigger pull motion to type the bottom row, the squeezebox requires a tall wrist rest that allows your fingers to hang over the edge and contact the keywells in what would be a recessed area below the desk surface if this was on a desk.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;lap-tray&quot;&gt;Lap tray&lt;&#x2F;h2&gt;
&lt;p&gt;Somewhat accidentally while dealing with the clutter of 2 split keyboards on my desk, I set the squeezebox&#x27;s tray across my lap to make room and in that process realized I could line up my chair&#x27;s arm wrests with the wrist rests and get a very comfortable posture going. The chair arm wrests support my forearms and the wrist wrests are directly in front of them at the same height. The tray also affords a surface for the mouse in the middle. So for now I&#x27;m planning to use the lap tray as my primary posture. But it works well enough on the desk too.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2021&amp;#x2F;2021-04-17-01F3G77AN0CB6SGVNNMVM65J4A.2048.jpg&quot;&gt;
  &lt;figcaption&gt;Lap tray and chair arm wrests&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;3d-printing&quot;&gt;3D Printing&lt;&#x2F;h2&gt;
&lt;p&gt;This version is 6 distinct parts. I connect them up by putting holes in the surfaces where I can install a metal threaded insert and connect things up with M3 bolts. I&#x27;ve never used threaded inserts in 3D printing before and they work great are super easy to model and super easy to install with a soldering iron. I love them and will be incorporating them a lot in future projects.&lt;&#x2F;p&gt;
&lt;p&gt;Nothing here needs supports when sliced. I print the finger columns on their sides and my Prusa Mini+ can bridge across the 13.8mm switch square just fine. There&#x27;s some extra&#x2F;redundant symmetry in the staggered walls such that a single part works in any of the 4 positions. I printed a little washer for the bolt that holds the Elite-C microcontroller in place so the bolt head wouldn&#x27;t short circuit the PCB pads below it.&lt;&#x2F;p&gt;
&lt;p&gt;The growth in my modeling skills during the project was highly noticeable. Early parts and prototypes took hours and hours in FreeCAD and many restarts and a smattering of file revisions in git. By the end I think I modeled the inner wall that has housings for the RJ-9 jack, the microcontroller, and the USB-C port in under and hour and the second print&#x2F;iteration was good enough to keep and use.&lt;&#x2F;p&gt;
&lt;p&gt;One thing that is super handy, and probably fairly obvious if you are clever, but I definitely did not understand at the beginning is that for a symmetric split keyboard, almost all the right&#x2F;left differences can be handled by mirroring the Z axis in the slicer software. That means in the CAD program you can just model one half and you&#x27;re done.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-learning-curve&quot;&gt;The Learning Curve&lt;&#x2F;h2&gt;
&lt;p&gt;Overall this is not wildly different than my current daily driver ergodox, especially since I&#x27;ve gradually been reducing the number of keys I use on my ergodox over the years as I learn more QMK features and keymap techniques. However, it&#x27;s radical enough to reduce my typing speed down from like 60 WPM to 20 WPM. I haven&#x27;t spent much time at all yet typing and I&#x27;m very much still figuring out my keymap around punctuation and a few other areas. I&#x27;m confident I&#x27;ll get the speed and accuracy to switch to this as my daily driver but it may take a week or two of typing practice in the evening after work.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;more-parts-details&quot;&gt;More parts details&lt;&#x2F;h2&gt;
&lt;p&gt;The switches are Kaihl Choc Reds which are linear, low-profile, low activation force, and quiet. The microcontrollers are Elite-C. The hands are interconnected via RJ-9 cable and soft serial configuration. Threaded inserts are for M3 bolts. Mostly it&#x27;s a by-the-book handwired keeb approach.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;key-mappings&quot;&gt;Key Mappings&lt;&#x2F;h2&gt;
&lt;p&gt;I&#x27;ve been getting RSI pains from pinky reaching to the outer column and chording shift on my ergodox, so I have been very motivated to reduce use of pinky drastically and move more work to the thumb, so there&#x27;s no column outward from the pinky. Shift and control&#x2F;escape go to the thumb (and&#x2F;or QMK combos. Still WIP.) and tab goes on my navigation layer so the pinky only has basic letter duty. I&#x27;m likely to switch to a pinky-minimizing layout like BEAKL sometime soon but I didn&#x27;t want to tackle that while also adjusting to the new hardware.&lt;&#x2F;p&gt;
&lt;p&gt;I kept one reachy innermost column because I wanted to keep a dvorak base layer and not have to immediately switch to something different. So that innermost column is really only there to ease my transition but I ultimately might make a layout that doesn&#x27;t require it and ditches it. To reduce the distance I need to reach sideways, the 2 innermost columns are a single plate so the gap can be quite small about 2mm.&lt;&#x2F;p&gt;
&lt;p&gt;I&#x27;m still in flux with a lot of key things including modifiers. I&#x27;ve tried home row mods but with dvorak they are really prone to misfires on the right hand for rolls like th and ns, so I moved them to the bottom layer on my ergodox. But on the squeezebox putting mod-taps on the bottom row makes them ineligible for combos which cancels one of the main areas of potential for this design. I think next I&#x27;ll try just mod-taps for ctrl and alt on the top row so I can do 1-finger combos across the middle and bottom rows. Having so many thumb keys provides a lot of great possibilities especially for stuff that&#x27;s only occasionally used.&lt;&#x2F;p&gt;
&lt;p&gt;My &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;focusaurus&#x2F;qmk_firmware&#x2F;tree&#x2F;focusaurus&#x2F;keyboards&#x2F;squeezebox&quot;&gt;QMK fork&lt;&#x2F;a&gt; has my keymap if you want to dig into the details. There will be a lot of churn there in the coming weeks.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;typing-video&quot;&gt;Typing video&lt;&#x2F;h2&gt;
&lt;div class=&quot;youtube-video-container&quot;&gt;
&lt;iframe src=&quot;https:&#x2F;&#x2F;www.youtube-nocookie.com&#x2F;embed&#x2F;chsqAaH_KqI&quot; allowfullscreen&gt;&lt;&#x2F;iframe&gt;
&lt;&#x2F;div&gt;
&lt;h2 id=&quot;overall-thoughts-at-the-milestone-of-a-working-custom-keyboard&quot;&gt;Overall thoughts at the milestone of a working custom keyboard&lt;&#x2F;h2&gt;
&lt;p&gt;This has been a super fun project. I&#x27;ve been wanting to do this since I first described the concept of the corner key to my fellow keeb&#x2F;ergonomic tinkerer &lt;a href=&quot;https:&#x2F;&#x2F;mgsloan.com&#x2F;posts&#x2F;supine-computing&#x2F;&quot;&gt;Michael Sloan&lt;&#x2F;a&gt; back in January 2020. My CAD and 3D printing experience was very beginner level and I had never done proper parameterized CAD prior to this, just some TinkerCAD and SketchUp. Parameterization is absolutely required for this type of project so I studied the dactyl manuform software stack a bit (clojure, openscad) but ultimately modeled this in FreeCAD with good results.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2021&amp;#x2F;2021-04-17-01F3GGTZC8Z34E8N41E220HTSS.2048.png&quot;&gt;
  &lt;figcaption&gt;FreeCAD project showing spreadsheet setup and a model of the main wall&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;This project took at least 2 months of substantial night&#x2F;weekend attention and a huge amount of prototyping (details on that below). I had soldered a few keyboard kits before but never done any hand wiring and never done a custom QMK firmware config. I struggled a lot trying to flash the microcontrollers and understanding how the wiring maps to the firmware. I&#x27;m grateful to many folks on discord forums and slack who answered questions for me and enabled me to overcome obstacles on a weekly basis.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;plans-for-the-next-revision&quot;&gt;Plans for the next revision&lt;&#x2F;h2&gt;
&lt;p&gt;The main thing I&#x27;m dissatisfied with in this version is the angle between top and middle row for ring and middle fingers is too steep. I can&#x27;t properly type the top row with the point of my finger as intended. Instead, my fingernail ends up scraping the keycap. This is a consequence of starting out designing for a vertical joystick style orientation, where that angle works OK. But I switched back to horizontal in the process and didn&#x27;t realize that would actually affect the geometry here. It&#x27;s great for index and pinky which is a bit odd but anyway in the next revision the ring and middle finger top row will be nearly flat like 170 degrees.&lt;&#x2F;p&gt;
&lt;p&gt;I&#x27;ve added slots for bar magnets so I can mount the keyboard and wrist rests to a steel plate which will be adhered to a 1&#x2F;2&quot; plywood board for the lap tray. I may switch to a flat RJ-9 cable instead of coiled so I can staple it to the board and route it along the edge leaving the center part of the tray fully clear for the mouse (or coffee when on the desk).&lt;&#x2F;p&gt;
&lt;p&gt;I didn&#x27;t really account for a physical reset button in this design as I figured I would have a reset key in my keymap. That turned out to be quite optimistic so I just hacked one in there and it&#x27;s dangling around on wires. I&#x27;ll give it a proper home in the next version. I was expecting the QMK tweaking and flashing to be a fairly quick process needing just a few flashes to get it and hoo boy was I wrong. I spent several full weekends debugging weird behavior and going from things like 39 of the 42 keys work to suddenly no keys work to suddenly only the bottom row works, to everything works but it&#x27;s all backward, etc.&lt;&#x2F;p&gt;
&lt;p&gt;I&#x27;ll add a simple outer wall that will bolt onto the main walls with threaded inserts. This will ensure the whole assembly goes together square.&lt;&#x2F;p&gt;
&lt;p&gt;I think I&#x27;m going to give the thumb cluster a proper full revision. I think I want the top row keys right next to each other like a tri-fold science fair display.&lt;&#x2F;p&gt;
&lt;p&gt;I have a spool of black prusament PLA set aside for the next printing and I think it&#x27;s going to look great with the choc red switch stems.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;weeks-and-weeks-of-prototyping&quot;&gt;Weeks and weeks of prototyping&lt;&#x2F;h2&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2021&amp;#x2F;2021-02-14-01EYGHW6007780K6SDP80R694N.2048.jpg&quot;&gt;
  &lt;figcaption&gt;An early vertical prototype with 2 degrees of adjustment via slots and notches&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;So my original conception was for this to be a vertical design used in joystick orientation similar to Victor Eikman&#x27;s &lt;a href=&quot;https:&#x2F;&#x2F;viktor.eikman.se&#x2F;gallery&#x2F;the-concertina&#x2F;&quot;&gt;Concertina&lt;&#x2F;a&gt;. After a &lt;strong&gt;lot&lt;&#x2F;strong&gt; of prototyping I decided to back off that requirement which added a lot of challenges and go for typical horizontal layout flat on the desk with optional tenting.&lt;&#x2F;p&gt;
&lt;p&gt;So the name &quot;squeezebox&quot; was originally because of the hand orientation and tight grid of buttons made it a lot like an accordion or concertina. Squeezebox is an informal name for an instrument like that. Even after switching to a horizontal design I decided to keep the name because the cases ended up very boxy and the corner keys have a trigger squeeze actuation motion so it still seems like an appropriate name.&lt;&#x2F;p&gt;
&lt;p&gt;I was also really interested in vertical orientation combined with more trigger-squeeze activation as I have a background in saxophone and I wanted to bring some of that motion to my typing.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2021&amp;#x2F;2021-02-17-01EYRMM6Y09H14P24V5N7VX2CT.2048.jpg&quot;&gt;
  &lt;figcaption&gt;A vertical prototype adjustable with rows of slots and bolts&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;Getting the main finger columns laid out right was really tricky. I was just learning parametric 3D modeling with FreeCAD too so I was struggling a bit with how to get my ideas into the software. The adjustable fit in 2 dimensions: forward and back as well as lower&#x2F;higher went through a large number of prototypes and close to 2 full spools of 3D printer PLA filament. I had slotted designs, bolted designs, magnetic designs, glue designs, LEGO brick designs, etc. Some of these would have been difficult to wire up so making it feasible to hand wire became a major constraint once I got close to the positioning I wanted.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2021&amp;#x2F;2021-02-28-01EZMDE5GGFSWT55NDV4C2MY63.2048.jpg&quot;&gt;
  &lt;figcaption&gt;2 joystick prototypes with magnetic bases&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;figure&gt;
  &lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;photos.peterlyons.com&amp;#x2F;2021&amp;#x2F;2021-04-17-01F3G4JXFGED40K0AMYEDN2TD9.2048.jpg&quot;&gt;
  &lt;figcaption&gt;Garden of prototypes&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;full-album-of-build-photos&quot;&gt;Full album of build photos&lt;&#x2F;h2&gt;
&lt;p&gt;Uh, there are over 150 photos here. If you want to scan them more easily click through to open the gallery on a full flickr gallery page.&lt;&#x2F;p&gt;



&lt;div class=&quot;plop-album&quot;&gt;
  &lt;figure class=&quot;album-viewer&quot;&gt;
    &lt;img class=&quot;album-photo&quot; src=&quot;&quot; alt=&quot;&quot;&gt;
  &lt;&#x2F;figure&gt;
  &lt;div class=&quot;album-controls&quot;&gt;
    &lt;button class=&quot;album-btn album-prev&quot; aria-label=&quot;Previous photo&quot;&gt;&amp;larr; Prev&lt;&#x2F;button&gt;
    &lt;span class=&quot;album-counter&quot;&gt;&lt;&#x2F;span&gt;
    &lt;button class=&quot;album-btn album-next&quot; aria-label=&quot;Next photo&quot;&gt;Next &amp;rarr;&lt;&#x2F;button&gt;
  &lt;&#x2F;div&gt;
  &lt;figcaption class=&quot;album-caption&quot;&gt;&lt;&#x2F;figcaption&gt;

  &lt;script&gt;
    (function() {
      &#x2F;&#x2F; Load photos from JSON data
      const photos = [{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-02-11-01EY9D550GPSQWTGVWBA840JTT.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-02-11-01EY9D550GCFXBGSJ6BSWAZH2R.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-02-14-01EYFSBHK8DYRWBMT4MSJ0SN0W.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-02-14-01EYG74MTR7BQJBXN8WPD6ZK3G.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-02-14-01EYGHW6007780K6SDP80R694N.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-02-15-01EYKBDKBGKFRB1VB99KAP34NK.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-02-16-01EYMRZVDGX7CET7EH1C3VMDX1.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-02-16-01EYP3QK00K1A6QHXPXDYRHZ3J.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-02-16-01EYP3R9ER8R5BPM6TACYR1M9J.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-02-16-01EYPA72MR6ZYDFJ3WAYM5HGXG.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-02-17-01EYRMHB4GV7948FB8AGEEV4FY.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-02-17-01EYRMM6Y09H14P24V5N7VX2CT.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-02-20-01EYZF2X2RM7DWYYH7T19PD3PN.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-02-20-01EZ082G00R9A50AHJ30MAH4QR.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-02-22-01EZ5NP0BRQJ4HY1G01R5YCE8Z.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-02-23-01EZ7DNCJRP39Z9BG4M1ZCPP7F.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-02-25-01EZD8DB1RBWFBJJW94AZARTZG.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-02-26-01EZFSP1PRS8BNAP0T7Q6FK857.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-02-27-01EZHVKKJ89YRTHZXJ7JTVP7AD.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-02-27-01EZHVKVC8TENXEAHN4KH0N10Y.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-02-27-01EZHW7YY05BSED1QM9AP35DXV.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-02-27-01EZHWJNPR3XTKB0XZ5MC7B445.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-02-27-01EZHWR1JR196BA9DB0EKPJZMC.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-02-27-01EZHXH96GZVSTBT4J0DW4W1GJ.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-02-27-01EZJ3QRRGTNRE74B55BEAR6PF.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-02-27-01EZJ8SDKR41PDMREX2SATKPQS.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-02-27-01EZJCY7KRFYYREQ1SEPX4Z903.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-02-27-01EZJD1TV834ZY68SW6EH9CCTJ.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-02-27-01EZJDJ87RZ22QJF2R4FDJ67M4.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-02-28-01EZM6Z5FRQ2HM9VZ59CHAXPA0.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-02-28-01EZMB4ZQ0RPQYWQNFVZ37XEHC.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-02-28-01EZMB5EBR27ZJ6GQKW8F5JGEM.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-02-28-01EZMDCWG065SM2N2VTF7Z6HM6.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-02-28-01EZMDD77RM9EBJENHRQFW9DXE.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-02-28-01EZMDDHZGS3NF07HX8J279KJX.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-02-28-01EZMDDTRRK5DA2ZT0AW9VR2XW.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-02-28-01EZMDDZN0GC5JDYFJGBR0PVN3.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-02-28-01EZMDE5GGFSWT55NDV4C2MY63.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-03-01-01EZQCSF3R85X261NPXHHW0MYE.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-03-04-01EZZD6018E4670MCH8RV8CN5H.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-03-06-01F03PW448EPPG46GHKFDQFQNY.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-03-06-01F03QEH00QS2P70K9H9ME1MWB.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-03-07-01F06BA128C47NPFFAG5GGAG2R.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-03-07-01F06BADRG2X087ZKWWFV2G06B.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-03-13-01F0P4MPZG9ZFPW9BE1KTPV37S.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-03-14-01F0R74VJ82EN8EF85GHJ45P1S.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-03-14-01F0R7GRDRQTKDDK2PWWB3FNZG.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-03-14-01F0R8EPE0M72D691HR8JWQD6R.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-03-14-01F0R8MGYR3K1PA8Y1FNZKTQ3P.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-03-14-01F0RA4A88ZGFAMK7QNVX41AFB.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-03-14-01F0RABSGGWXD3XFEZHFRN5EPC.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-03-14-01F0RHDVWG558KAF0H21PRQKXF.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-03-14-01F0RKG8WG3D0PJ9ZB7BHEKBA1.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-03-14-01F0RX47C8E8EAMYBD97AYQCFA.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-03-14-01F0RXVFGRQVG6PRPXF4WBWFQ7.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-03-14-01F0S2FXGRR9YSP9RVD7C9C53K.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-03-14-01F0S46RD0GBJ6ZY77CD4JNSZ2.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-03-14-01F0S5AEZG006FV2315WAW6P3G.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-03-15-01F0TGFP6GGYPMNGZD2JB7GS98.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-03-16-01F0YAPBSRE9M2A6Y3263F7QSB.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-03-16-01F0YBPK1RD6RRVDBQWTKRVBH0.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-03-16-01F0YCMVSRKDJFJ7WQEADBBGJX.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-03-16-01F0YDM1V8TV86FCKC8K9WKGFF.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-03-16-01F0YEGGZG98F40JFWH45ABM01.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-03-17-01F0YTNH78QSMZA7QJCAQAVN83.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-03-17-01F0YTNWY8XC8VFRF1GZV18RZ8.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-03-18-01F1348PK0YGD30AFC89WPAV86.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-03-18-01F13EB02899Z0G902291RAK80.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-03-18-01F13EB9TRAA0JK3HZKNFSFQGC.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-03-20-01F184EQSGZECJ92AAB8B64WDN.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-03-20-01F184F2H8SYDSEGR61AY6952E.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-03-20-01F18AGJXGWYTAGV6AESC9526X.2048.mp4&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-03-21-01F1A5EDK8H04BAA5BZATR8PCC.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-03-21-01F1A5JBJGRNP9V8W06KHVAHA6.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-03-21-01F1A62E78WGC1DZB972B9CRJE.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-03-21-01F1AEKCK86HCYH3Z9Z6PJT0KM.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-03-21-01F1AFH6PG0NQDK0Q67PYVM4X4.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-03-21-01F1AT7JR8MGPPC9TKYXGA0XS5.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-03-27-01F1SW99SR8MGMW6YSRT6NESE9.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-03-27-01F1SWHDJ8MQPM3HCSBBV8223D.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-03-27-01F1T7NM08NKFAXTT9VKZQWC8H.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-03-27-01F1T9EN6GHZCC560SM53SACAE.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-03-27-01F1TFPKMGFQ74DV41G9BW98F7.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-03-27-01F1TGQ8J0M63W7RXFKF66M2A9.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-03-27-01F1THDDHGR183EHDGDF2XT3FQ.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-03-27-01F1THGK3GKBHFA6QHEBWR2SVT.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-03-28-01F1W3S5Q0ZCHGWV42J6N5AD8D.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-03-28-01F1W3VEYRM25G3ZH85J0QYHEJ.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-03-29-01F1YFNZCR0XGHCNDAQZPAFYY9.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-04-02-01F29WY4W82TED2EDTG5KY9JT1.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-04-10-01F2Y27TXGJ7YMVR9MPQZFGSFT.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-04-17-01F3FY4TR8KP1EWJN0B2Q52RPF.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-04-17-01F3FY4WPR0XQ5FHAD4NK01TNQ.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-04-17-01F3FY50KRH8D6S1EYWRPC341P.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-04-17-01F3FY53HGBS7XN9Q87F94MBX1.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-04-17-01F3FY55G0D3P2A718KV98KSR6.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-04-17-01F3G07V98APTQBNACS0D1H4QP.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-04-17-01F3G4JXFGED40K0AMYEDN2TD9.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-04-17-01F3G75KZ0RVER0B2M423ZEC3W.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-04-17-01F3G776R0S895JJDZ36JACSFJ.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-04-17-01F3G77AN0CB6SGVNNMVM65J4A.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-04-17-01F3GGD03R6D9XBDTADWKZ1N20.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-04-17-01F3GGTZC8Z34E8N41E220HTSS.2048.png&quot;,&quot;caption&quot;:&quot;screen-2021-04-17-19:19:25-squeezebox-freecad-setup&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-04-18-01F3JJZ45RD6WFMB2VR6DF79T4.2048.jpg&quot;,&quot;caption&quot;:&quot;thumb-cluster-standoff&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2021&#x2F;2021-04-18-01F3K2QGSRVRN74Z8GCZDQ0CJ6.2048.mp4&quot;}];

      &#x2F;&#x2F; Get the current script&#x27;s parent album container to scope queries
      const currentScript = document.currentScript;
      const albumContainer = currentScript.closest(&#x27;.plop-album&#x27;);

      let currentIndex = 0;
      const imgEl = albumContainer.querySelector(&#x27;.album-photo&#x27;);
      const captionEl = albumContainer.querySelector(&#x27;.album-caption&#x27;);
      const counterEl = albumContainer.querySelector(&#x27;.album-counter&#x27;);
      const prevBtn = albumContainer.querySelector(&#x27;.album-prev&#x27;);
      const nextBtn = albumContainer.querySelector(&#x27;.album-next&#x27;);

      function showPhoto(index) {
        const photo = photos[index];
        imgEl.src = photo.url;
        imgEl.alt = photo.caption || &#x27;&#x27;;

        if (photo.caption) {
          captionEl.textContent = photo.caption;
          captionEl.style.display = &#x27;block&#x27;;
        } else {
          captionEl.style.display = &#x27;none&#x27;;
        }

        counterEl.textContent = `${index + 1} &#x2F; ${photos.length}`;

        prevBtn.disabled = index === 0;
        nextBtn.disabled = index === photos.length - 1;
      }

      prevBtn.addEventListener(&#x27;click&#x27;, function() {
        if (currentIndex &gt; 0) {
          currentIndex--;
          showPhoto(currentIndex);
        }
      });

      nextBtn.addEventListener(&#x27;click&#x27;, function() {
        if (currentIndex &lt; photos.length - 1) {
          currentIndex++;
          showPhoto(currentIndex);
        }
      });

      &#x2F;&#x2F; Keyboard navigation only works when this album is focused&#x2F;hovered
      let isActive = false;

      albumContainer.addEventListener(&#x27;mouseenter&#x27;, function() {
        isActive = true;
      });

      albumContainer.addEventListener(&#x27;mouseleave&#x27;, function() {
        isActive = false;
      });

      document.addEventListener(&#x27;keydown&#x27;, function(e) {
        if (!isActive) return;

        if (e.key === &#x27;ArrowLeft&#x27; &amp;&amp; currentIndex &gt; 0) {
          currentIndex--;
          showPhoto(currentIndex);
        } else if (e.key === &#x27;ArrowRight&#x27; &amp;&amp; currentIndex &lt; photos.length - 1) {
          currentIndex++;
          showPhoto(currentIndex);
        }
      });

      imgEl.addEventListener(&#x27;click&#x27;, function(e) {
        const rect = imgEl.getBoundingClientRect();
        const clickY = e.clientY - rect.top;
        const midpoint = rect.height &#x2F; 2;

        if (clickY &lt; midpoint) {
          &#x2F;&#x2F; Clicked top half - go to next
          if (currentIndex &lt; photos.length - 1) {
            currentIndex++;
            showPhoto(currentIndex);
          }
        } else {
          &#x2F;&#x2F; Clicked bottom half - go to previous
          if (currentIndex &gt; 0) {
            currentIndex--;
            showPhoto(currentIndex);
          }
        }
      });

      if (photos.length &gt; 0) {
        showPhoto(0);
      }
    })();
  &lt;&#x2F;script&gt;
&lt;&#x2F;div&gt;
</description>
      </item>
      <item>
          <title>Staff Engineer Solver Archetype</title>
          <pubDate>Tue, 23 Mar 2021 01:12:49 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2021/03/staff-engineer-solver-archetype/</link>
          <guid>https://peterlyons.com/problog/2021/03/staff-engineer-solver-archetype/</guid>
          <description xml:base="https://peterlyons.com/problog/2021/03/staff-engineer-solver-archetype/">&lt;p&gt;So since I became a Staff Engineer last year, I&#x27;ve been studying up on the role. Mostly I&#x27;ve been devouring Will Larson&#x27;s writing in article&#x2F;blog form, and now all that material has been published in his book &lt;a href=&quot;https:&#x2F;&#x2F;staffeng.com&#x2F;book&quot;&gt;Staff Engineer&lt;&#x2F;a&gt;. The writing is lucid and insightful and very narrowly on-topic so I&#x27;ve been reading it very closely. I just set up a book club at work to go through it as well.&lt;&#x2F;p&gt;
&lt;p&gt;So early on one of the things Will does is break the role down into 4 archetypes (direct quote below)&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The Tech Lead&lt;&#x2F;strong&gt; guides the approach and execution of a particular team. They partner closely with a single manager, but sometimes they partner with two or three managers within a focused area.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;The Architect&lt;&#x2F;strong&gt; is responsible for the direction, quality, and approach within a critical area.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;The Solver&lt;&#x2F;strong&gt; digs deep into arbitrarily complex problems and finds an appropriate path forward. Some focus on a given area for long periods. Others bounce from hotspot to hotspot as guided by organizational leadership.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;The Right Hand&lt;&#x2F;strong&gt; extends an executive&#x27;s attention, borrowing their scope and authority to operate particularly complex organizations. They provide additional leadership bandwidth to leaders of large-scale organizations.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;For me at this job, it&#x27;s pretty clear that The Solver is best suited to me. But first let me quickly discuss why the others are not as good of a fit for me at my current job.&lt;&#x2F;p&gt;
&lt;p&gt;I think so far my role has been The Tech Lead archetype. It&#x27;s been OK but I&#x27;m finding it increasingly difficult and frustrating. Communication is difficult and I often find myself attempting to provide &quot;guidance&quot; which is both too late in the project lifecycle and too drastic of a departure from the current course, and that&#x27;s not particularly effective. I feel like I can and have led efforts fairly well from the start, but providing &quot;guidance&quot; to an ongoing effort that I don&#x27;t ultimately hold primary engineering responsibility for is not a position I enjoy. I have thought to myself that there&#x27;s an &quot;invisible hand&quot; at work and it&#x27;s incredibly difficult to write or say anything that will alter the course of the hand in any nontrivial way.&lt;&#x2F;p&gt;
&lt;p&gt;The Architect is an easy no for me. I&#x27;ve never liked this kind of work and always felt ill-equiped to do it and grateful when anyone else has the confidence to make these kind of 3-year bets. My approaches would be so far out of current industry trends and norms that no one would listen to me anyway.&lt;&#x2F;p&gt;
&lt;p&gt;The Right Hand presupposes top-down leadership which my department does not have nor want at the moment. Adjusting to our bottom-up approach to work has been a struggle for me for sure. I&#x27;ve actually thought about proposing to management splitting off a self-selected subteam of people who prefer to work in a top-down fashion and I think there&#x27;s enough of us for it to be viable. We&#x27;ll see if I end up trying to wrangle those folks together or not.&lt;&#x2F;p&gt;
&lt;p&gt;OK so that leaves just The Solver. I generally like this kind of work and can do it effectively. It&#x27;s not always been my best way to contribute so at times when I have been more Tech Lead I would leave solver type work to others, but at the moment I think this would alleviate a lot of my communication struggles and also allow me to deliver more value by being able to work more independently and therefore much more quickly. I&#x27;ve already written a document of roughly 8 problems of this type and delivered solutions to 2 so far. So this would mostly just be an acknowledgement of my existing role. I hope formalizing this would allow me to disengage from our main scrum team processes and just focus on this work.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Home LAN Improvements</title>
          <pubDate>Sun, 24 Jan 2021 19:55:20 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2021/01/home-lan-improvements/</link>
          <guid>https://peterlyons.com/problog/2021/01/home-lan-improvements/</guid>
          <description xml:base="https://peterlyons.com/problog/2021/01/home-lan-improvements/">&lt;p&gt;With two of us working from home, we were having trouble making due with a single wifi access point covering the whole house, especially considering our offices are at opposite sides. So we made a few attempts to address the problem and learned a bit along the way.&lt;&#x2F;p&gt;
&lt;p&gt;Since the Internet service is coaxial and there were already cable runs splitting at the ISP point of entry and terminating in each of the office rooms, my first thought was maybe we can use that wiring. A web search revealed a LAN tech called Multimedia Over Coaxial (MOCA), which despite my using coax broadband since the 90s I had never heard of. So I bought a pair of adapters that were supposedly able to extend our LAN between the rooms over the coax and at the same time use that coax for the ISP link. We spent a full day trying to make it work in various configurations. If we just ran a short coax cable directly between the 2 moca adapters, they both lit up green meaning everything is working fine. We could link them at various partial lengths of our network and get a link, but the full run between the 2 offices would never show a green link light. We tried 3 types of spiltters, point-of-entry filter, etc but it just wouldn&#x27;t work. It&#x27;s also super tedious to test because every coax connect&#x2F;disconnect operation is at least 30s of awkward work with a pair wrenches.&lt;&#x2F;p&gt;
&lt;p&gt;So we returned that gear and tried some powerline boxes. These, as we hoped, were truly plug and play and we had a functioning network literally 90s after opening the box. But it was much lower throughput and higher latency than wifi and was therefore not going to really make things better. Another order return.&lt;&#x2F;p&gt;
&lt;p&gt;I had my property manager do some research and replace our existing single coax wall plates with plates that had both a coax and an ethernet jack. He then ran a 100&#x27; length of cat8 cable alongside the coax that ran outside our house. That length just by chance was close enough to exact that we didn&#x27;t bother cutting and crimping a new end. So with now a fully wired link Stella was able to get good speed and stability for her work laptop. I also bought a second Turris Omnia wifi router to act as both an ethernet switch and additional wifi dumb access point for that side of the house.&lt;&#x2F;p&gt;
&lt;p&gt;We are now getting consistently good speeds both wired and wireless. I had strong suspicions that both MOCA and powerline ethernet were both &quot;hooey&quot; with severe limitations to their practical application, and for this situation, those suspicions were confirmed to be true.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>The Problem Mindset</title>
          <pubDate>Sat, 28 Nov 2020 18:32:03 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2020/11/the-problem-mindset/</link>
          <guid>https://peterlyons.com/problog/2020/11/the-problem-mindset/</guid>
          <description xml:base="https://peterlyons.com/problog/2020/11/the-problem-mindset/">&lt;p&gt;Everywhere I look in the tech world, I see everything everyone does being framed as &quot;solving problems&quot;. &quot;What problem are you trying to solve?&quot; is a question asked at the start of many project inception documents. It occupies an early slide in most presentations. Whole company missions are framed as solving a particular problem. Many engineers list problem solving as one of their core skills.&lt;&#x2F;p&gt;
&lt;p&gt;My motivation for this post is to simply state that I don&#x27;t think this mindset should be taken as a given and I personally don&#x27;t think this way about everything. It&#x27;s not that clearly defining a problem isn&#x27;t a useful approach to planning&#x2F;strategy. I just think about the word &quot;problem&quot; in a more specific way meaning something is broken, troublesome, or has gone wrong somehow. I think the world needs people and organizations doing things in addition to solving problems. It&#x27;s perfectly valid to be refining something that is not problematic. It&#x27;s fine to be creating a new capability from scratch without having to think of the &quot;I don&#x27;t have X capability&quot; as the definition of the problem.&lt;&#x2F;p&gt;
&lt;p&gt;OK let&#x27;s go through some concrete examples.&lt;&#x2F;p&gt;
&lt;p&gt;Are ride sharing apps like Lyft and Uber really solving a problem? I think of them as providing a particular kind of transportation. It&#x27;s an update to a centuries-old core business in the world of smart phones, GPS, gig economy, etc. There&#x27;s a raft of interesting changes in their financial models, approach to vehicle inventory, driver relations, driver compensation, driver competition, etc, but fundamentally I don&#x27;t think of what they are doing of solving the problem of &quot;I would rather use an app than a telephone to summon a taxicab&quot;. For that matter I don&#x27;t think &quot;transportation&quot; when thought of as a problem has a science-fact solution. We can make it better, but not really solve it as a problem. Thus I would think about it more like a need.&lt;&#x2F;p&gt;
&lt;p&gt;WTF problem is instagram solving? It&#x27;s a thing that exists and has utility for some and is also problematic but I can&#x27;t bend my brain to think of instagram as solving any particular problem. I guess if arch-capitalists think of &quot;Advertising dollars are being spent with companies I do not own&quot; as a problem, then Instagram solves for that I guess?&lt;&#x2F;p&gt;
&lt;p&gt;Does AirBnB really solve the &quot;It&#x27;s hard for me to book short-term tenants into my guest room&quot; problem? It makes a lot more sense to me to think of this as creating a marketplace facilitated by the web and smartphone apps.&lt;&#x2F;p&gt;
&lt;p&gt;Here&#x27;s a couple examples of companies&#x2F;products that it seems logical to think about as solving a problem.&lt;&#x2F;p&gt;
&lt;p&gt;Insurance comes to mind. Something rare and costly just went wrong and I otherwise would be in financial disaster, but insurance prevents that disaster for me. Roadside assistance. A flat tire is a problem that sometimes happens and while roadside service doesn&#x27;t &quot;solve&quot; it as in prevent it from ever being a thing that happens, it reduces the impact significantly.&lt;&#x2F;p&gt;
&lt;p&gt;An extension cord solves a problem. In some ways WiFi solves a similar problem for your network cable, but it seems weird to me to think about WiFi as a solution to the problem &quot;my gigantic ethernet cable is annoying to manage while I carry my laptop around the house&quot;. It&#x27;s just a new technology and I don&#x27;t feel any need to frame it in relationship to a specific problem.&lt;&#x2F;p&gt;
&lt;p&gt;If any of this resonates with you, please join me in rolling our eyes in our next strategic planning meeting.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Novelty Makes Novices</title>
          <pubDate>Sat, 19 Sep 2020 16:07:15 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2020/09/novelty-makes-novices/</link>
          <guid>https://peterlyons.com/problog/2020/09/novelty-makes-novices/</guid>
          <description xml:base="https://peterlyons.com/problog/2020/09/novelty-makes-novices/">&lt;p&gt;My experience in the last decade have been that software developers are asked to learn new technologies with such frequency that novice level skills have become a semi-permanent norm that is expected and accepted.&lt;&#x2F;p&gt;
&lt;p&gt;Here&#x27;s some technologies:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;clojure&lt;&#x2F;li&gt;
&lt;li&gt;edn&lt;&#x2F;li&gt;
&lt;li&gt;kotlin&lt;&#x2F;li&gt;
&lt;li&gt;maven&lt;&#x2F;li&gt;
&lt;li&gt;VS Code&lt;&#x2F;li&gt;
&lt;li&gt;vim&lt;&#x2F;li&gt;
&lt;li&gt;elasticsearch&lt;&#x2F;li&gt;
&lt;li&gt;kafka&lt;&#x2F;li&gt;
&lt;li&gt;kafka connect&lt;&#x2F;li&gt;
&lt;li&gt;kafka streams&lt;&#x2F;li&gt;
&lt;li&gt;mongodb replication&lt;&#x2F;li&gt;
&lt;li&gt;event sourcing, change data capture architecture, CQRS&lt;&#x2F;li&gt;
&lt;li&gt;avro&lt;&#x2F;li&gt;
&lt;li&gt;graphql&lt;&#x2F;li&gt;
&lt;li&gt;graphql schema delegation&lt;&#x2F;li&gt;
&lt;li&gt;apollojs&lt;&#x2F;li&gt;
&lt;li&gt;nextjs&lt;&#x2F;li&gt;
&lt;li&gt;postgresql ltree plugin&lt;&#x2F;li&gt;
&lt;li&gt;yarn&lt;&#x2F;li&gt;
&lt;li&gt;advanced circleci configuration&lt;&#x2F;li&gt;
&lt;li&gt;advanced docker&lt;&#x2F;li&gt;
&lt;li&gt;mozilla sops&lt;&#x2F;li&gt;
&lt;li&gt;kustomize&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Is that list everything I&#x27;ve used in my 20 years in the industry? No! That&#x27;s just tech I had to learn and use in my 18 months at Reaction Commerce! That&#x27;s more than one each month. Alomst every one of these represents &quot;probably should read at least 1 whole book on this&quot; depth.&lt;&#x2F;p&gt;
&lt;p&gt;Then of course we go through an acquisition and our tech stack changes pretty much completely. 2 new programming languages, 2 new databases, a new public cloud, a new RPC system, in-house frameworks and libraries, and so on.&lt;&#x2F;p&gt;
&lt;p&gt;In terms of my personal job satisfaction, I&#x27;m hoping that now that the acquisition transition is pretty much complete, I&#x27;ll start to have at least a few stable bits of my tech stack: mostly the same DBs, programming languages, and cloud managed services. I enjoy doing work with intermediate or advanced skills a lot more than I enjoy learning new things and fumbling around as a novice. Of course others will crave and flourish with a series of new stuff to learn in a cursory way.&lt;&#x2F;p&gt;
&lt;p&gt;Thinking more broadly, I have some vague concerns about this might have an indirect causal relationship with the generally terrible state of technology at large, but I have no way to really test this, and the even second level more cynical side of me suspects maybe even if expert implementations were the norm, tech would still be mostly terrible because we don&#x27;t really care about making it good.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Original ErgoDox is Better than Infinity</title>
          <pubDate>Thu, 17 Sep 2020 13:37:26 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2020/09/original-ergodox-is-better-than-infinity/</link>
          <guid>https://peterlyons.com/problog/2020/09/original-ergodox-is-better-than-infinity/</guid>
          <description xml:base="https://peterlyons.com/problog/2020/09/original-ergodox-is-better-than-infinity/">&lt;p&gt;I own both an original ErgoDox (full hand acrylic case, from I think the 2nd massdrop) and a newer ErgoDox Infitity. The original works better for me and I wanted to share my experience and reasoning. There&#x27;s a few things that are just simpler&#x2F;nicer:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;flashing the original just requires a paperclip button press on the top of the keyboard and 1 command in the OS&lt;&#x2F;li&gt;
&lt;li&gt;Flashing the infinity is a dance of a paperclip press on the underside of one half, an OS command that requires sudo, changing a USB cable around, a second paperclip on the bottom of the other half, a second OS command, and finally putting the USB cable back.&lt;&#x2F;li&gt;
&lt;li&gt;2 of the infinity USB cables have the wiggly USB 3.0 micro shape which is a pain to connect&lt;&#x2F;li&gt;
&lt;li&gt;the cable connecting the 2 halves of the infinity is thicker than the ergodox&#x27;s TRRS so its more of an intrusion into my desktop&lt;&#x2F;li&gt;
&lt;li&gt;The infinity full-hand case has outer bottom shape isn&#x27;t a straight line. There&#x27;s a tiny angle and curve that makes trying to tent it near impossible vs the original where you jam a piece of all-thread into one of the bolt holes and you&#x27;re done.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</description>
      </item>
      <item>
          <title>ErgoThink T420 Laptop Tray</title>
          <pubDate>Thu, 10 Sep 2020 01:23:21 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2020/09/ergothink-t420-laptop-tray/</link>
          <guid>https://peterlyons.com/problog/2020/09/ergothink-t420-laptop-tray/</guid>
          <description xml:base="https://peterlyons.com/problog/2020/09/ergothink-t420-laptop-tray/">&lt;p&gt;Last weekend I built a custom laptop tray design to fit my ThinkPad T480 laptop, my ergodox split keyboard, and my vertical mouse comfortably on my lap. We did 3 rounds of build&#x2F;prototypes and I&#x27;m really happy with the one we have.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;motivation-and-context&quot;&gt;Motivation and Context&lt;&#x2F;h2&gt;
&lt;p&gt;I have two main setups where I do my computing. I have a sit&#x2F;stand desk in my office well-appointed with tented split keyboard, vertical mouse, laptop stand putting the screen at proper height (which I also custom designed and built), etc. I also like to just use the laptop directly for brief sessions of reading and browsing where I don&#x27;t have to do extensive typing.&lt;&#x2F;p&gt;
&lt;p&gt;I wanted a way to work from the couch while also still having the great typing experience afforded by my ergodox keyboard. My ThinkPad T480 screen opens to a full 180 degrees which also puts the screen at a much more ergonomic position. I had a thought that if I could put the laptop fully open that might allow me to set up the ergodox where the laptop keyboard would normally be, turning the laptop keyboard into effectively just a monitor stand.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;conception&quot;&gt;Conception&lt;&#x2F;h2&gt;
&lt;p&gt;As these things generally go, I started to get ideas on a design while lying in bed trying unsuccessfully to fall asleep, so I got up and did some sketches in my notebook. The basic idea was to build a pair of C-shaped slots to hold the corners of the laptop down and then some curbs to keep the ergodox in a fixed position.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;prototyping&quot;&gt;Prototyping&lt;&#x2F;h2&gt;
&lt;p&gt;I was pretty excited to try it out and casting about in the shop we found some corner clamps we recently 3D printed just for fun that actually kind of worked to hold the laptop up. So that gave us a shortcut to a v1 prototype which we made out of 3&#x2F;4&quot; plywood for the base and some stacks of 1&#x2F;2&quot; plywood for the keyboard curbs.&lt;&#x2F;p&gt;
&lt;p&gt;I was able to test that on Friday night and it worked, but the big piece of 3&#x2F;4&quot; plywood was really heavy and the slack in the corner clamps allowed the laptop to flap front to back quite a bit which made it a bit terrifying to carry around.&lt;&#x2F;p&gt;
&lt;p&gt;But it was good enough to know the basic approach would be fine.&lt;&#x2F;p&gt;
&lt;p&gt;So Saturday we did another version this time out of 1&#x2F;2&quot; plywood for everything. I designed some laptop clamps which had a pretty cool construction method:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;rough cut some oversized rectangles big enough to make both sides of the clamp&lt;&#x2F;li&gt;
&lt;li&gt;stack that 4 layers high and glue it up into a big block&lt;&#x2F;li&gt;
&lt;li&gt;Then cut it down to final dimensions and square up the corners on the table saw sled
&lt;ul&gt;
&lt;li&gt;Cutting the long edge just barely fit in our sled&#x27;s capacity&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;The use a forstner bit to drill holes to fit where the laptop bottom pads go. Locate this so in the end it will be a semicircular cylinder slot instead of a full hole.&lt;&#x2F;li&gt;
&lt;li&gt;Cut that in half&lt;&#x2F;li&gt;
&lt;li&gt;On the table saw sled with the blade quite high, hog out the interior of the &quot;C&quot; shape and do test fits until sneaking up on a nice fit for the laptop.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2020&#x2F;2020-09-03-01EHAV7T60C3X2ZPF2NJ9YAYS5.2048.jpg&quot; alt=&quot;v1 prototype&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;We didn&#x27;t get the construction fully dialed until v3 but once we did it&#x27;s a good balance of easy to put the laptop in and take it out, but it&#x27;s stable in use and also if you are walking around carrying the tray with you.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;v3-prototype&quot;&gt;v3 prototype&lt;&#x2F;h2&gt;
&lt;p&gt;v2 was much better but I wanted another crack at the laptop slots and a much smoother mouse area, so the next day we made v3 out of nice baltic birch plywood and sanded most surfaces at 80, 120, and 240 grit to get really smooth surfaces.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2020&#x2F;2020-09-06-01EHHK9EGG739N9SRK424J61AR.2048.jpg&quot; alt=&quot;v2 prototype&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;I bought the smallest hinges they sell at Lowe&#x27;s and made some collapsible keyboard tenting supports. These ended up feeling really stable and solid.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;finishing-touches&quot;&gt;Finishing touches&lt;&#x2F;h2&gt;
&lt;p&gt;The gap between the ergodox and the laptop is small. Not even 2&quot;, so with the TRRS and USB cables coming out of the ergodox in that direction, they were feeling and looking cramped. I found a 9&quot; TRRS cable with 90 degree connectors that fit the space perfectly and also a 6&quot; mini USB to USB A 90-degree cable so now everything has just a tiny bit of slack so they can be comfortably connected and disconnected but are so well-sized that they don&#x27;t dangle down onto the tray at all and have no need for coiling or cable management of any kind.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2020&#x2F;2020-09-08-01EHQSTED8R68CKYDMQJG0P5A9.2048.jpg&quot; alt=&quot;over shoulder view&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;putting-it-to-use&quot;&gt;Putting it to Use&lt;&#x2F;h2&gt;
&lt;p&gt;I&#x27;m planning a writing retreat later this month so this setup should get a lot of use. I&#x27;m looking forward to it!&lt;&#x2F;p&gt;
&lt;h2 id=&quot;dual-purpose&quot;&gt;Dual-Purpose&lt;&#x2F;h2&gt;
&lt;p&gt;The icing on this cake was sort of a surprise. I can leave the whole tray&#x2F;rig set up, place it on my desk, and it works fine there too. So this will replace the custom stand I built earlier and all I do to &quot;undock&quot; from the desk is disconnect the power cable and carry the tray away.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;build-photo-gallery&quot;&gt;Build Photo Gallery&lt;&#x2F;h2&gt;



&lt;div class=&quot;plop-album&quot;&gt;
  &lt;figure class=&quot;album-viewer&quot;&gt;
    &lt;img class=&quot;album-photo&quot; src=&quot;&quot; alt=&quot;&quot;&gt;
  &lt;&#x2F;figure&gt;
  &lt;div class=&quot;album-controls&quot;&gt;
    &lt;button class=&quot;album-btn album-prev&quot; aria-label=&quot;Previous photo&quot;&gt;&amp;larr; Prev&lt;&#x2F;button&gt;
    &lt;span class=&quot;album-counter&quot;&gt;&lt;&#x2F;span&gt;
    &lt;button class=&quot;album-btn album-next&quot; aria-label=&quot;Next photo&quot;&gt;Next &amp;rarr;&lt;&#x2F;button&gt;
  &lt;&#x2F;div&gt;
  &lt;figcaption class=&quot;album-caption&quot;&gt;&lt;&#x2F;figcaption&gt;

  &lt;script&gt;
    (function() {
      &#x2F;&#x2F; Load photos from JSON data
      const photos = [{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2020&#x2F;2020-09-03-01EHAV7T60C3X2ZPF2NJ9YAYS5.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2020&#x2F;2020-09-04-01EHDB36MR0JWWNWZHSYYE8JAV.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2020&#x2F;2020-09-04-01EHDESDV85XRRMY7WMSMCH7A9.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2020&#x2F;2020-09-05-01EHEMCVKR5KCVWGJ7C3AP4A2V.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2020&#x2F;2020-09-05-01EHEQM3AG5VC15Y9X2927TR77.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2020&#x2F;2020-09-06-01EHHHZ40RNE2Q9N9PFZREVHMQ.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2020&#x2F;2020-09-06-01EHHHZCT08P3SWXD9D2XJY6NG.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2020&#x2F;2020-09-06-01EHHK9EGG739N9SRK424J61AR.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2020&#x2F;2020-09-06-01EHHPSGEGFJMHVJE7A568XAGP.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2020&#x2F;2020-09-06-01EHHQA1R0NM88WJ6A4PY65K97.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2020&#x2F;2020-09-06-01EHHVTDVRN9RS2Q7TE5T8BY3T.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2020&#x2F;2020-09-06-01EHHVX3SRX8S6FSH7PBYSGERE.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2020&#x2F;2020-09-06-01EHHVXBKR17ANB1HE7VAZBCYA.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2020&#x2F;2020-09-06-01EHHVZPT0JR4NHNDAKQ7TA2BY.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2020&#x2F;2020-09-06-01EHHVZTQ0651J5K5B0S6HDZPF.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2020&#x2F;2020-09-06-01EHHW0D8RB72GGYCNJRWCAGJC.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2020&#x2F;2020-09-06-01EHHZ2WMG5PTWPME2GQYVN0F1.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2020&#x2F;2020-09-06-01EHJ0A1J8CFJS180S9DV2SJCB.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2020&#x2F;2020-09-08-01EHQSSM1GY9JTCX87SN9VS1HR.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2020&#x2F;2020-09-08-01EHQSTED8R68CKYDMQJG0P5A9.2048.jpg&quot;},{&quot;url&quot;:&quot;https:&#x2F;&#x2F;photos.peterlyons.com&#x2F;2020&#x2F;2020-09-08-01EHQSTW2RPET174HCNNT8RM6D.2048.jpg&quot;}];

      &#x2F;&#x2F; Get the current script&#x27;s parent album container to scope queries
      const currentScript = document.currentScript;
      const albumContainer = currentScript.closest(&#x27;.plop-album&#x27;);

      let currentIndex = 0;
      const imgEl = albumContainer.querySelector(&#x27;.album-photo&#x27;);
      const captionEl = albumContainer.querySelector(&#x27;.album-caption&#x27;);
      const counterEl = albumContainer.querySelector(&#x27;.album-counter&#x27;);
      const prevBtn = albumContainer.querySelector(&#x27;.album-prev&#x27;);
      const nextBtn = albumContainer.querySelector(&#x27;.album-next&#x27;);

      function showPhoto(index) {
        const photo = photos[index];
        imgEl.src = photo.url;
        imgEl.alt = photo.caption || &#x27;&#x27;;

        if (photo.caption) {
          captionEl.textContent = photo.caption;
          captionEl.style.display = &#x27;block&#x27;;
        } else {
          captionEl.style.display = &#x27;none&#x27;;
        }

        counterEl.textContent = `${index + 1} &#x2F; ${photos.length}`;

        prevBtn.disabled = index === 0;
        nextBtn.disabled = index === photos.length - 1;
      }

      prevBtn.addEventListener(&#x27;click&#x27;, function() {
        if (currentIndex &gt; 0) {
          currentIndex--;
          showPhoto(currentIndex);
        }
      });

      nextBtn.addEventListener(&#x27;click&#x27;, function() {
        if (currentIndex &lt; photos.length - 1) {
          currentIndex++;
          showPhoto(currentIndex);
        }
      });

      &#x2F;&#x2F; Keyboard navigation only works when this album is focused&#x2F;hovered
      let isActive = false;

      albumContainer.addEventListener(&#x27;mouseenter&#x27;, function() {
        isActive = true;
      });

      albumContainer.addEventListener(&#x27;mouseleave&#x27;, function() {
        isActive = false;
      });

      document.addEventListener(&#x27;keydown&#x27;, function(e) {
        if (!isActive) return;

        if (e.key === &#x27;ArrowLeft&#x27; &amp;&amp; currentIndex &gt; 0) {
          currentIndex--;
          showPhoto(currentIndex);
        } else if (e.key === &#x27;ArrowRight&#x27; &amp;&amp; currentIndex &lt; photos.length - 1) {
          currentIndex++;
          showPhoto(currentIndex);
        }
      });

      imgEl.addEventListener(&#x27;click&#x27;, function(e) {
        const rect = imgEl.getBoundingClientRect();
        const clickY = e.clientY - rect.top;
        const midpoint = rect.height &#x2F; 2;

        if (clickY &lt; midpoint) {
          &#x2F;&#x2F; Clicked top half - go to next
          if (currentIndex &lt; photos.length - 1) {
            currentIndex++;
            showPhoto(currentIndex);
          }
        } else {
          &#x2F;&#x2F; Clicked bottom half - go to previous
          if (currentIndex &gt; 0) {
            currentIndex--;
            showPhoto(currentIndex);
          }
        }
      });

      if (photos.length &gt; 0) {
        showPhoto(0);
      }
    })();
  &lt;&#x2F;script&gt;
&lt;&#x2F;div&gt;
</description>
      </item>
      <item>
          <title>Reading Pseudogrammars</title>
          <pubDate>Fri, 12 Jun 2020 15:57:21 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2020/06/reading-pseudogrammars/</link>
          <guid>https://peterlyons.com/problog/2020/06/reading-pseudogrammars/</guid>
          <description xml:base="https://peterlyons.com/problog/2020/06/reading-pseudogrammars/">&lt;p&gt;In my software development work, I encounter new syntaxes truly almost every day.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;&amp;lt;grumplezone&amp;gt;&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;It&#x27;s a gripe for me that our contemporary way of working introduces a huge new ecosystem of commands every 6 months.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;We&#x27;re on AWS, here&#x27;s the CLI with 3 dozen subcommands you&#x27;ll need to learn.&lt;&#x2F;li&gt;
&lt;li&gt;Oops, sorry it&#x27;s GCP now. A lot of similar stuff but the CLI is completely distinct.&lt;&#x2F;li&gt;
&lt;li&gt;Oh yeah it&#x27;s kubernetes so there&#x27;s a huge YAML grammar.&lt;&#x2F;li&gt;
&lt;li&gt;Wait, no it&#x27;s helm charts which have a grammar built on the kubernetes grammar but also golang templating which you should also learn now.&lt;&#x2F;li&gt;
&lt;li&gt;Oh wait helm is terrible, we&#x27;re using kustomize instead.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;&amp;lt;&#x2F;grumplezone&amp;gt;&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;This is usually command line program arguments, but sometimes it&#x27;s file syntax or structured data in a common syntax. This means I read a lot of man pages and similar documentation. One thing I struggle with is the pseudogrammar block often found at the top of such documentation. For example, here&#x27;s the pseudogrammar for the &lt;code&gt;COPY&lt;&#x2F;code&gt; command in &lt;code&gt;Dockerfile&lt;&#x2F;code&gt; syntax:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;COPY [--chown=&amp;lt;user&amp;gt;:&amp;lt;group&amp;gt;] &amp;lt;src&amp;gt;... &amp;lt;dest&amp;gt;
COPY [--chown=&amp;lt;user&amp;gt;:&amp;lt;group&amp;gt;] [&amp;quot;&amp;lt;src&amp;gt;&amp;quot;,... &amp;quot;&amp;lt;dest&amp;gt;&amp;quot;]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;My old habit would be to skip straight past that as noise and look for examples. When I see examples, I can immediately and with no conscious cognitive effort derive what the syntax is. I guess this comes from decades of using command line programs but when I see&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;count-floopers --mode terse projects&amp;#x2F;blog&amp;#x2F;settings.json
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;I don&#x27;t think &quot;Oh dang, it only works on json files at that particular relative path, let me make some directories and move my json file around so the path matches&quot;. I also infer there&#x27;s other possible values for &lt;code&gt;--mode&lt;&#x2F;code&gt; and it&#x27;s an enum of string names. Thus I usually reach for examples but I want to get better at actually parsing and grokking the pseudogrammar.&lt;&#x2F;p&gt;
&lt;p&gt;So for the Dockerfile COPY example above, here&#x27;s how I&#x27;d slowly break it down in my mind.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;OK it seems like square brackets are indicating optional stuff and angle brackets mean required
&lt;ul&gt;
&lt;li&gt;This is somewhat conventional but not in a rigorous or consistent way. For example, I&#x27;m pretty sure the group identifier is optional but there&#x27;s not a nested pair of brackets indicating that.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;So there&#x27;s a long-form option &lt;code&gt;--chown&lt;&#x2F;code&gt; and it takes user and group separate by colon
&lt;ul&gt;
&lt;li&gt;BUT because there&#x27;s no example I have no idea if those need to be names or numbers&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;Then comes a required src argument which I infer must be a filesystem path&lt;&#x2F;li&gt;
&lt;li&gt;The &lt;code&gt;...&lt;&#x2F;code&gt; means there can be more than 1 &amp;lt;src&amp;gt; argument and I presume they are space separated and delivered as distinct entries in the underlying&lt;code&gt;argv&lt;&#x2F;code&gt; array.&lt;&#x2F;li&gt;
&lt;li&gt;Then comes a required &amp;lt;dest&amp;gt; which I have to infer from context (which I probably don&#x27;t have if I&#x27;m just learning this command) that this is a filesystem path inside the docker image filesystem.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;So I guess I managed to grok this one, but it&#x27;s pseudogrammar was pretty small. When I start to see pipe symbols. For the next few pseudogrammars I encounter, I&#x27;m going to practice reading slowly character-by-character and writing out in long form what I think each element denotes. Hopefully with a little practice I&#x27;ll become more fluent in grokking these.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Which files do they edit</title>
          <pubDate>Fri, 05 Jun 2020 02:02:45 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2020/06/which-files-do-they-edit/</link>
          <guid>https://peterlyons.com/problog/2020/06/which-files-do-they-edit/</guid>
          <description xml:base="https://peterlyons.com/problog/2020/06/which-files-do-they-edit/">&lt;p&gt;When hopping in to an unfamiliar git repo, if it&#x27;s a big sprawling repo or even a monorepo, it can be hard to know which parts are relevant. Well, if you know someone on your team&#x27;s name, I have a script that can give you a pretty solid clue.&lt;&#x2F;p&gt;
&lt;p&gt;Here&#x27;s the pipeline and explanations.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sh&quot; class=&quot;language-sh &quot;&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;# Show me the hash of every commit by someone named Doe
# for the last 3 months
git log --author=&amp;#x27;Doe&amp;#x27; --pretty=format:%H --since=&amp;quot;3 months ago&amp;quot;

# Now show me the files changed in each of those commits
  xargs -n 1 git diff-tree --no-commit-id --name-only -r |

# now remove duplicates and count how many commits per file
  sort |
  uniq -c |
  sort -nr
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;--since&lt;&#x2F;code&gt; has pretty intuitive natural language processing so you can easily adjust that. &lt;code&gt;--author&lt;&#x2F;code&gt; seems to work on portions of either name or email.&lt;&#x2F;p&gt;
&lt;p&gt;For example, here&#x27;s a snippet of the files Eric Dobbertin has been changing in the main reaction commerce repo the last 6 months.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;git log --author=&amp;#x27;Dobbertin&amp;#x27; --pretty=format:%H --since=&amp;quot;3 months ago&amp;quot; |
  xargs -n 1 git diff-tree --no-commit-id --name-only -r |
  sort |
  uniq -c |
  sort -nr

     19 package-lock.json
     14 package.json
     10 plugins.json
     10 jest.config.cjs
      7 tests&amp;#x2F;integration&amp;#x2F;api&amp;#x2F;mutations&amp;#x2F;createProduct&amp;#x2F;createProduct.test.js
      6 tests&amp;#x2F;util&amp;#x2F;factory.js
      6 tests&amp;#x2F;integration&amp;#x2F;api&amp;#x2F;mutations&amp;#x2F;createProductVariant&amp;#x2F;createProductVariant.test.js
      6 src&amp;#x2F;mockTypes.graphql
      5 tests&amp;#x2F;integration&amp;#x2F;api&amp;#x2F;queries&amp;#x2F;taxServices&amp;#x2F;taxServices.test.js
      5 tests&amp;#x2F;integration&amp;#x2F;api&amp;#x2F;queries&amp;#x2F;ordersByAccountId&amp;#x2F;ordersByAccountId.test.js
      5 tests&amp;#x2F;integration&amp;#x2F;api&amp;#x2F;queries&amp;#x2F;catalogItemProduct&amp;#x2F;catalogItemProduct.test.js
      5 tests&amp;#x2F;integration&amp;#x2F;api&amp;#x2F;queries&amp;#x2F;anonymousCartByCartId&amp;#x2F;anonymousCartByCartId.test.js
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</description>
      </item>
      <item>
          <title>GraphQL Pagination and Sorting</title>
          <pubDate>Wed, 15 Apr 2020 20:01:45 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2020/04/graphql-pagination-and-sorting/</link>
          <guid>https://peterlyons.com/problog/2020/04/graphql-pagination-and-sorting/</guid>
          <description xml:base="https://peterlyons.com/problog/2020/04/graphql-pagination-and-sorting/">&lt;p&gt;&lt;strong&gt;Editor&#x27;s Note&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;This article was originally written by Chris Potter and me when we worked at Reaction Commerce. It was published on April 15, 2020 on the Reaction Commerce Blog. Reaction Commerce was acquired by Mailchimp and the blog has disappeared, so I&#x27;m republishing here to keep the content available. I will need to reference it myself if I ever have to do graphql pagination again. To my knowledge, this is the most in-depth and technically detailed resource on the topic.&lt;&#x2F;p&gt;
&lt;p&gt;Another note is that OFFSET&#x2F;LIMIT pagination MAY have severe performance problems due to full table scans to count the number of rows. So if your table has 100K rows or more you&#x27;ll need to benchmark and may need an alternate implementation based on cursors.&lt;&#x2F;p&gt;
&lt;hr&gt;
&lt;p&gt;When building client&#x2F;server applications, data sets that are too large
to reasonably transmit and process in a single request must be made
manageable via pagination. All databases and APIs typically provide a
mechanism for this. This tutorial will walk you through implementing
GraphQL pagination with PostgreSQL, but first let&#x27;s explain and review
the concepts needed for SQL and pagination.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;conceptual-overview-and-glossary&quot;&gt;Conceptual Overview and Glossary&lt;&#x2F;h2&gt;
&lt;p&gt;The following are common terms when describing GraphQL and SQL
pagination. Some terms are taken from GraphQL pagination specifications
and supported with GraphQL connections information from Facebook. For
SQL terminology we&#x27;ll use the &lt;a href=&quot;https:&#x2F;&#x2F;web.archive.org&#x2F;web&#x2F;20210416212650&#x2F;https:&#x2F;&#x2F;www.postgresql.org&#x2F;docs&#x2F;9.3&#x2F;queries-limit.html&quot;&gt;Postgres
definitions&lt;&#x2F;a&gt;
but they are relatively generic between SQL implementations.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;offset-limit-pagination-terms&quot;&gt;Offset + limit pagination terms&lt;&#x2F;h3&gt;
&lt;p&gt;This is a pagination method that starts from the first record of a
sorted dataset and &quot;pages&quot; forward a number of offsets depending on the
limit of records requested. If there are 20 records in the result,
offset 1 with limit 5 will start from limit × offset + 1 or from the 6th
record, since offsets are 0 based. The downside of offset pagination
stems from the fact it always starts from the first record in a data
set, meaning to get the last page in a result, the database query needs
to create and scan past all pages before it. This becomes less efficient
as your data size increases.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;limit&lt;&#x2F;strong&gt; - a number that specifies the &lt;em&gt;maximum&lt;&#x2F;em&gt; records to be returned
in a query. If there are not enough records to fill the limit, all
records are returned.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;offset&lt;&#x2F;strong&gt; - informs SQL how many rows to skip before returning the next
set of records.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;cursor-pagination-terms&quot;&gt;Cursor pagination terms&lt;&#x2F;h3&gt;
&lt;p&gt;This pagination is based upon opaque identifiers which map to records
within your data set. Cursors act as a bookmark to identify positions
from which to start or end pagination. Cursors solve some efficiency
problems because they will load only the data requested, starting or
ending with the given opaque identifier.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;cursor&lt;&#x2F;strong&gt; - a unique identifier for a record, generally an opaque
value.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;Note: A cursor is not a specific column in your database, but should be
considered a function that serves to locate a particular position in a
data set. In the most simple case your function might be cursor = field,
but this should not be an universal expectation across cursor
implementations&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;after&lt;&#x2F;strong&gt; - GraphQL argument indicating the unique identifier we want to
start our page from and get all results after not including this
identifier.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;first&lt;&#x2F;strong&gt; - coupled with &lt;strong&gt;after&lt;&#x2F;strong&gt;, this is the number of records to
return after the supplied cursor. If no cursor is supplied, then this
will be the number of records to return from the start of the
collection. Can also be used in GraphQL with an &lt;strong&gt;offset&lt;&#x2F;strong&gt; to implement
traditional offset pagination where &lt;strong&gt;first&lt;&#x2F;strong&gt; represents &lt;strong&gt;limit&lt;&#x2F;strong&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;before&lt;&#x2F;strong&gt; - Complement of &lt;strong&gt;after&lt;&#x2F;strong&gt;. Indicates the unique identifer we
want to &quot;end&quot; our page with and get all results before, not including,
the cursor.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;last&lt;&#x2F;strong&gt; - coupled with &lt;strong&gt;before&lt;&#x2F;strong&gt;, this is the number of records to
return before the supplied cursor. If no cursor is supplied, then this
will be the number of records to return from the end of the collection.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;psa-regarding-cursor-pagination&quot;&gt;PSA regarding cursor pagination&lt;&#x2F;h3&gt;
&lt;p&gt;Specifications for cursor pagination assume a stable sort and direction
on a unique field from the collection. Cursor pagination assumes that
all data will be in the same direction and listed&#x2F;sorted by the same
value &lt;strong&gt;every time&lt;&#x2F;strong&gt;. This might be practical for timeline based live
streams (a la Twitter or Facebook) where you can &lt;strong&gt;always sort by most
recent timestamp&lt;&#x2F;strong&gt;, but at Reaction Commerce we deal with customizable
product lists within categories, search parameters that need higher
levels of merchandising and sorting (e.g. list products from lowest to
highest price or alphebetically by name). Therefore we have introduced
two new terms which let us specify list orders discretely but add higher
orders of complexity:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;sortBy&lt;&#x2F;strong&gt;: the pivot field from the database to sort the paginated
results by.(e.g &quot;name&quot; &quot;breed&quot; &quot;fur-color&quot;)&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;sortOrder&lt;&#x2F;strong&gt;: the order in which to send the returned results. Options
are &lt;code&gt;ASC&lt;&#x2F;code&gt; or &lt;code&gt;DESC&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;Aside: our cursor pagination will always fallback to a stable sort on a
unique field from the collection. This ensures that requested &lt;code&gt;sortBy&lt;&#x2F;code&gt;
fields that match (everyone named &quot;Jane Doe&quot;) will return the same way
with subsequent requests.&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;cursor-pagination-information-pageinfo&quot;&gt;Cursor pagination information (pageInfo)&lt;&#x2F;h2&gt;
&lt;p&gt;For GraphQL pagination there is a data structure that is commonly
returned relating to the position of the returned results, defined from
Facebook&#x27;s cursor connections specificaion (see further reading). This
structure is as follows&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;graphql&quot; class=&quot;language-graphql &quot;&gt;&lt;code class=&quot;language-graphql&quot; data-lang=&quot;graphql&quot;&gt;&amp;quot;pageInfo&amp;quot;: {
    &amp;quot;startCursor&amp;quot;: &amp;quot;&amp;lt;opaque-cursor&amp;gt;&amp;quot;,
    &amp;quot;endCursor&amp;quot;: &amp;quot;&amp;lt;opaque-cursor&amp;gt;&amp;quot;,
    &amp;quot;hasPreviousPage&amp;quot;: boolean,
    &amp;quot;hasNextPage&amp;quot;: boolean
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;startCursor&lt;&#x2F;strong&gt; and &lt;strong&gt;endCursor&lt;&#x2F;strong&gt; simply map to the first and last items
in the collection of results returned from the query. No matter if the
results match the limit requested, we always return a &lt;code&gt;startCursor&lt;&#x2F;code&gt; and
&lt;code&gt;endCursor&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;pages-are-tricky&quot;&gt;Pages are tricky&lt;&#x2F;h3&gt;
&lt;p&gt;Depending on the sort order of a collection, &lt;code&gt;before&lt;&#x2F;code&gt; and &lt;code&gt;after&lt;&#x2F;code&gt; will
infer whether a returned collection &lt;code&gt;hasPreviousPage&lt;&#x2F;code&gt; or &lt;code&gt;hasNextPage&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;pictures-for-1000-words&quot;&gt;Pictures for 1000 words&lt;&#x2F;h3&gt;
&lt;p&gt;Here is our cursor pagination represented by a pictoral cat collection&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;&#x2F;problog&#x2F;images&#x2F;2020&#x2F;cat-collection-forward.png&quot;&gt; 
  &lt;figcaption&gt;forward&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;figure&gt;
  &lt;img src=&quot;&#x2F;problog&#x2F;images&#x2F;2020&#x2F;cat-collection-backward.png&quot;&gt;
  &lt;figcaption&gt;backward&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;In the context of the first image we would query for the second cat, and
limit our selection to the next 5 cats after it. &lt;code&gt;hasNextPage&lt;&#x2F;code&gt; should
return &lt;code&gt;true&lt;&#x2F;code&gt; since the second to last cat exists (the next cat after
the returned collection). &lt;code&gt;hasPreviousPage&lt;&#x2F;code&gt; will also return true since
there is a record before the &lt;code&gt;after&lt;&#x2F;code&gt; cursor (pink circle)&lt;&#x2F;p&gt;
&lt;p&gt;In the context of the second image we would query for the second to last
cat, and limit our selection to the previous 5 cats before it.
&lt;code&gt;hasNextPage&lt;&#x2F;code&gt; should return &lt;code&gt;true&lt;&#x2F;code&gt; since the last cat exists, since we
exclude the queried record (green circle) from our paging information
(we already know it exists). &lt;code&gt;hasPreviousPage&lt;&#x2F;code&gt; will also return true
since the second cat exists (the cat before the returned collection).&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;Important note from the Facebook connection specifications which is
demonstrated in the above images: the ordering of edges should be the
same when using first&#x2F;after as when using last&#x2F;before, all other
arguments being equal. It should not be reversed when using last&#x2F;before&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;h1 id=&quot;lets-get-to-it-already&quot;&gt;Lets get to it already&lt;&#x2F;h1&gt;
&lt;p&gt;In this tutorial, we&#x27;ll walk through implementing GraphQL-style
pagination on top of a PostgreSQL relational database step by step. In
this case the process of understanding the requirements, implementing
them, and verifying they are working properly is a nice encapsulated
example of the software development process and what kinds of work we as
software engineers typically do.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;a-simple-data-set&quot;&gt;A Simple Data Set&lt;&#x2F;h2&gt;
&lt;p&gt;To give ourselves the best shot at implementing this quickly, and
correctly, we&#x27;ll create a simple synthetic data set that is small enough
to quickly scan and also designed to surface sorting issues in an
easy-to-spot way.&lt;&#x2F;p&gt;
&lt;p&gt;Here&#x27;s some SQL to create our set of test records:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sql&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;CREATE TABLE cats (id int PRIMARY KEY, name text NOT NULL);

INSERT INTO cats (id, name) VALUES
  (1, &amp;#x27;esther&amp;#x27;), (2, &amp;#x27;cookie&amp;#x27;), (3, &amp;#x27;cookie&amp;#x27;),
  (4, &amp;#x27;cookie&amp;#x27;), (5, &amp;#x27;dave&amp;#x27;), (6, &amp;#x27;bosco&amp;#x27;),
  (7, &amp;#x27;frida&amp;#x27;), (9, &amp;#x27;giggles&amp;#x27;), (10, &amp;#x27;jasmine&amp;#x27;),
  (11, &amp;#x27;jerry&amp;#x27;), (12, &amp;#x27;alice&amp;#x27;), (13, &amp;#x27;iggy&amp;#x27;);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;We can see our data with a simple query&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sql&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;SELECT * FROM &amp;quot;cats&amp;quot;;
 id |  name
----+---------
  1 | esther
  2 | cookie
  3 | cookie
  4 | cookie
  5 | dave
  6 | bosco
  7 | frida
  9 | giggles
 10 | jasmine
 11 | jerry
 12 | alice
 13 | iggy
(12 rows)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;PostgreSQL is returning those results in the order they were inserted
(at least so it seems), but to ensure consistent results, we should
explicitly order by a unique column in our collection, in our case we&#x27;ll
use &lt;code&gt;id&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sql&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;SELECT * FROM &amp;quot;cats&amp;quot; ORDER BY &amp;quot;id&amp;quot; ASC;
 id |  name
----+---------
  1 | esther
  2 | cookie
  3 | cookie
  4 | cookie
  5 | dave
  6 | bosco
  7 | frida
  9 | giggles
 10 | jasmine
 11 | jerry
 12 | alice
 13 | iggy
(12 rows)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Also note we have some cats with the same name. This will help us ensure
correct results for some trickier cases later on.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;case-1-1-just-first&quot;&gt;Case 1.1: just first&lt;&#x2F;h2&gt;
&lt;p&gt;GraphQL pagination uses the inputs &lt;code&gt;first&lt;&#x2F;code&gt; or &lt;code&gt;last&lt;&#x2F;code&gt; to specify a
reduced page size. Here&#x27;s the most basic paginated query just asking for
the first 3 results plus the pagination metadata fields.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;GraphQL query&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;graphql&quot; class=&quot;language-graphql &quot;&gt;&lt;code class=&quot;language-graphql&quot; data-lang=&quot;graphql&quot;&gt;query {
  cats(first: 3) {
    edges {
      cursor
      node {
        id
        name
      }
    }
    totalCount
    pageInfo {
      startCursor
      endCursor
      hasPreviousPage
      hasNextPage
    }
  }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;SQL Construction&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Let&#x27;s work through the fields in the graphql response and see how we can
obtain the data we need via SQL.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;The GraphQL argument &lt;code&gt;first&lt;&#x2F;code&gt; is a pagination limit which is going to
map directly to the SQL &lt;code&gt;LIMIT&lt;&#x2F;code&gt; clause for this query&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;hasPreviousPage&lt;&#x2F;code&gt; is &lt;code&gt;false&lt;&#x2F;code&gt; whenever we have &lt;code&gt;first&lt;&#x2F;code&gt; but not
&lt;code&gt;after&lt;&#x2F;code&gt; in our graphql arguments so we can determine this directly
in code and don&#x27;t need any related SQL. This is by definition the
first page.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;hasNextPage&lt;&#x2F;code&gt; is going to be computed with our first technique which
we&#x27;ll call &lt;strong&gt;beyond the limit&lt;&#x2F;strong&gt;. We&#x27;ll add 1 to the provided &lt;code&gt;first&lt;&#x2F;code&gt;
parameter to &quot;peek&quot; ahead in the results and see if we&#x27;re at the
true end or not. Then we can determine &lt;code&gt;hasNextPage&lt;&#x2F;code&gt; with
&lt;code&gt;rows returned &amp;gt; first&lt;&#x2F;code&gt;. So for this query our &lt;code&gt;LIMIT&lt;&#x2F;code&gt; will be &lt;code&gt;4&lt;&#x2F;code&gt;
instead of &lt;code&gt;3&lt;&#x2F;code&gt;. &lt;strong&gt;When the SQL results are returned the last record,
if it exists, is omitted from the GraphQL response.&lt;&#x2F;strong&gt;
&lt;ul&gt;
&lt;li&gt;When results are passed, If the number of results matches the
limit requested (4), we will know that the &lt;code&gt;hasNextPage&lt;&#x2F;code&gt; is
&lt;code&gt;true&lt;&#x2F;code&gt; since a record exists for the start of the next page.
This technique will be used for the extent of querying whether a
page has another page before or after, depending on the GraphQL
request.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;startCursor&lt;&#x2F;code&gt; is the cursor from the first row in the results&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;endCursor&lt;&#x2F;code&gt; is the cursor from the last row in the results (after
omitting the &lt;code&gt;hasNextPage&lt;&#x2F;code&gt; &quot;peek&quot; record if it&#x27;s there)&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;totalCount&lt;&#x2F;code&gt; requires an additional query. For convenience, we&#x27;ll
use the SQL &lt;code&gt;AS&lt;&#x2F;code&gt; alias to present the total_count as a column in our
main result set. It will end up in every row, but they will all be
identical so we&#x27;ll just use the value in the first returned row.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;sql&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;SELECT *,
  (SELECT COUNT(*) FROM &amp;quot;cats&amp;quot;) AS &amp;quot;total_count&amp;quot;
  FROM &amp;quot;cats&amp;quot;
  ORDER BY &amp;quot;id&amp;quot; ASC
  LIMIT 4;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;em&gt;Aside: since this tutorial is focused on pagination, we&#x27;ll always do
&lt;code&gt;SELECT *&lt;&#x2F;code&gt; in our SQL. In a production application if a table had many
columns, you would probably specifically select only the columns
required by the graphql query.&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;SQL Results&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;id |  name  | total_count
----+--------+-------------
  1 | esther |          12
  2 | cookie |          12
  3 | cookie |          12
  4 | cookie |          12
(4 rows)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;GraphQL Response&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;{
  &amp;quot;data&amp;quot;: {
    &amp;quot;cats&amp;quot;: {
      &amp;quot;edges&amp;quot;: [
        {
          &amp;quot;cursor&amp;quot;: &amp;quot;1&amp;quot;,
          &amp;quot;node&amp;quot;: {
            &amp;quot;id&amp;quot;: 1,
            &amp;quot;name&amp;quot;: &amp;quot;esther&amp;quot;
          }
        },
        {
          &amp;quot;cursor&amp;quot;: &amp;quot;2&amp;quot;,
          &amp;quot;node&amp;quot;: {
            &amp;quot;id&amp;quot;: 2,
            &amp;quot;name&amp;quot;: &amp;quot;cookie&amp;quot;
          }
        },
        {
          &amp;quot;cursor&amp;quot;: &amp;quot;3&amp;quot;,
          &amp;quot;node&amp;quot;: {
            &amp;quot;id&amp;quot;: 3,
            &amp;quot;name&amp;quot;: &amp;quot;cookie&amp;quot;
          }
        }
      ],
      &amp;quot;pageInfo&amp;quot;: {
        &amp;quot;endCursor&amp;quot;: &amp;quot;3&amp;quot;,
        &amp;quot;hasNextPage&amp;quot;: true,
        &amp;quot;hasPreviousPage&amp;quot;: false,
        &amp;quot;startCursor&amp;quot;: &amp;quot;1&amp;quot;
      },
      &amp;quot;totalCount&amp;quot;: 12
    }
  }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;So we got 4 rows back from the database, but omitted cookie 4 from the
graphql results and got our &lt;code&gt;has*Page&lt;&#x2F;code&gt; booleans correct.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;case-1-2-just-last&quot;&gt;Case 1.2: just last&lt;&#x2F;h2&gt;
&lt;p&gt;To implement &lt;code&gt;last&lt;&#x2F;code&gt; isn&#x27;t so easy though because SQL doesn&#x27;t directly
support this. &lt;code&gt;LIMIT&lt;&#x2F;code&gt; always implies a limit from the beginning of the
results in SQL.&lt;br &#x2F;&gt;
We need to take a different approach. We have a few options&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Make a separate query to count the number of rows and use that to
calculate an &lt;code&gt;OFFSET&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Have the database reverse the sort direction of the results and use
&lt;code&gt;LIMIT&lt;&#x2F;code&gt;, but then reverse the results in the application before
sending the GraphQL response.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;GraphQL&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;graphql&quot; class=&quot;language-graphql &quot;&gt;&lt;code class=&quot;language-graphql&quot; data-lang=&quot;graphql&quot;&gt;query {
  cats(last: 3) {
    edges {
      cursor
      node {
        id
        name
      }
    }
    totalCount
    pageInfo {
      startCursor
      endCursor
      hasPreviousPage
      hasNextPage
    }
  }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;SQL Option 1: Separate count query&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sql&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;-- First do a query to get the total row count
SELECT count(*) FROM &amp;quot;cats&amp;quot;;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt; count
-------
    12
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;sql&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;-- Compute the offset as total count minus limit
-- Then use that to send the right OFFSET
SELECT * FROM &amp;quot;cats&amp;quot; ORDER BY &amp;quot;id&amp;quot; ASC LIMIT 3 OFFSET (12 - 3);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt; id | name
----+-------
 11 | jerry
 12 | alice
 13 | iggy
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;SQL Option 2: Double Reverse&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;The SQL &lt;code&gt;LIMIT&lt;&#x2F;code&gt; clause only works at the beginning of the result set,
not the end. But if we reverse the direction, the beginning is then the
end, right? So we can hack our way to the right results with&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Use the reverse direction in our SQL &lt;code&gt;ORDER BY&lt;&#x2F;code&gt; clause&lt;&#x2F;li&gt;
&lt;li&gt;Use the &lt;code&gt;LIMIT&lt;&#x2F;code&gt; clause&lt;&#x2F;li&gt;
&lt;li&gt;Reverse the in-memory results in the application while building the
graphql results&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Here&#x27;s how our graphql pagination metadata will be computed&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;hasPreviousPage&lt;&#x2F;code&gt; will use our &lt;strong&gt;beyond the limit&lt;&#x2F;strong&gt; technique again,
but here the results will be reversed&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;hasNextPage&lt;&#x2F;code&gt; Requesting &lt;code&gt;last&lt;&#x2F;code&gt; without &lt;code&gt;before&lt;&#x2F;code&gt; is by definition
requesting the last page of results, so we know &lt;code&gt;hasNextPage&lt;&#x2F;code&gt; must
be &lt;code&gt;false&lt;&#x2F;code&gt; here&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;startCursor&lt;&#x2F;code&gt; is the &lt;code&gt;id&lt;&#x2F;code&gt; from the next-to-last row in the SQL
results
&lt;ul&gt;
&lt;li&gt;This accounts both for the limit + 1 &quot;peeking&quot; and the
double-reversing&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;endCursor&lt;&#x2F;code&gt; is the &lt;code&gt;id&lt;&#x2F;code&gt; from the first row in the results&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;totalCount&lt;&#x2F;code&gt; will use the same subselect&#x2F;alias technique again&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;sql&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;SELECT *,
  (SELECT COUNT(*) FROM &amp;quot;cats&amp;quot;) AS &amp;quot;total_count&amp;quot;
  FROM &amp;quot;cats&amp;quot;
  ORDER BY &amp;quot;id&amp;quot; DESC
  LIMIT 4;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt; id |  name   | total_count
----+---------+-------------
 13 | iggy    |          12
 12 | alice   |          12
 11 | jerry   |          12
 10 | jasmine |          12
(4 rows)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;That gives us the correct 3 rows (13, 12, 11), plus our
&lt;code&gt;hasPreviousPage&lt;&#x2F;code&gt; row count peek row(10), but the rows are in the
incorrect order, so in our application code, we&#x27;d need to:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Check the row count to determine &lt;code&gt;hasPreviousPage&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;discard the extra row if present&lt;&#x2F;li&gt;
&lt;li&gt;reverse the remaining rows for proper ordering&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;clj&quot; class=&quot;language-clj &quot;&gt;&lt;code class=&quot;language-clj&quot; data-lang=&quot;clj&quot;&gt;; In clojure, if the sequence of database result rows is bound to &amp;quot;results&amp;quot;
; we&amp;#x27;d do:
(reverse results)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;js&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&amp;#x2F;&amp;#x2F; In javascript, if the database results was in the &amp;quot;results&amp;quot; array,
&amp;#x2F;&amp;#x2F; we&amp;#x27;d do
results.reverse()
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;GraphQL Response&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;{
  &amp;quot;data&amp;quot;: {
    &amp;quot;cats&amp;quot;: {
      &amp;quot;edges&amp;quot;: [
        {
          &amp;quot;cursor&amp;quot;: &amp;quot;11&amp;quot;,
          &amp;quot;node&amp;quot;: {
            &amp;quot;id&amp;quot;: 11,
            &amp;quot;name&amp;quot;: &amp;quot;jerry&amp;quot;
          }
        },
        {
          &amp;quot;cursor&amp;quot;: &amp;quot;12&amp;quot;,
          &amp;quot;node&amp;quot;: {
            &amp;quot;id&amp;quot;: 12,
            &amp;quot;name&amp;quot;: &amp;quot;alice&amp;quot;
          }
        },
        {
          &amp;quot;cursor&amp;quot;: &amp;quot;13&amp;quot;,
          &amp;quot;node&amp;quot;: {
            &amp;quot;id&amp;quot;: 13,
            &amp;quot;name&amp;quot;: &amp;quot;iggy&amp;quot;
          }
        }
      ],
      &amp;quot;pageInfo&amp;quot;: {
        &amp;quot;endCursor&amp;quot;: &amp;quot;13&amp;quot;,
        &amp;quot;hasNextPage&amp;quot;: false,
        &amp;quot;hasPreviousPage&amp;quot;: true,
        &amp;quot;startCursor&amp;quot;: &amp;quot;11&amp;quot;
      },
      &amp;quot;totalCount&amp;quot;: 12
    }
  }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;case-1-3-first-after-paging-forward&quot;&gt;Case 1.3: first + after: paging forward&lt;&#x2F;h2&gt;
&lt;p&gt;OK back in the forward pagination direction via &lt;code&gt;first&lt;&#x2F;code&gt;, to get
subsequent pages of the query, a graphql client will send the &lt;code&gt;after&lt;&#x2F;code&gt;
parameter. The value of this is an opaque GraphQL &lt;code&gt;cursor&lt;&#x2F;code&gt; which was
provided in the &lt;code&gt;pageInfo.endCursor&lt;&#x2F;code&gt; field of the response for the
previous page. For clarity in this tutorial, we will return the raw ids
of the first and last records returned. In production code, we follow
Facebook&#x27;s suggestion of lightly reinforcing the &quot;opaque&quot; nature of
cursors by base64 encoding them.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;GraphQL query&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;graphql&quot; class=&quot;language-graphql &quot;&gt;&lt;code class=&quot;language-graphql&quot; data-lang=&quot;graphql&quot;&gt;query {
  cats(first: 3, after: &amp;quot;3&amp;quot;) {
    edges {
      cursor
      node {
        id
        name
      }
    }
    totalCount
    pageInfo {
      startCursor
      endCursor
      hasPreviousPage
      hasNextPage
    }
  }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;To make this work in SQL, we need to build a &lt;code&gt;WHERE&lt;&#x2F;code&gt; clause to skip the
first bit of the result set by reference, as opposed to trying to use
&lt;code&gt;OFFSET&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Let&#x27;s work through the fields in the graphql response and see how we can
obtain the data we need via SQL.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Most things for page 2 will work the same as for page 1:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;first&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;hasNextPage&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;startCursor&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;endCursor&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;totalCount&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;after&lt;&#x2F;code&gt; is new and to make this work in SQL, we need to build a &lt;code&gt;WHERE&lt;&#x2F;code&gt; clause to skip the first part of the result set by reference, as opposed to trying to use &lt;code&gt;OFFSET&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;hasPreviousPage&lt;&#x2F;code&gt; we&#x27;ll now have to compute. We will &lt;code&gt;COUNT(*)&lt;&#x2F;code&gt; the results using an inverse &lt;code&gt;WHERE&lt;&#x2F;code&gt; clause and alias that as &lt;code&gt;has_previous_page&lt;&#x2F;code&gt;. By &quot;inverse WHERE clause&quot; we mean use a comparison operator opposite from the one used to skip prior pages. So in this case since our main &lt;code&gt;WHERE&lt;&#x2F;code&gt; clause uses greater than (&lt;code&gt;&amp;gt;&lt;&#x2F;code&gt;), we&#x27;ll use less than or equal to (&lt;code&gt;&amp;lt;&lt;&#x2F;code&gt;) in our &lt;code&gt;has_previous_page&lt;&#x2F;code&gt; subselect &lt;code&gt;WHERE&lt;&#x2F;code&gt; clause.
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;(SELECT COUNT(*) FROM &quot;cats&quot; WHERE &quot;id&quot; &amp;lt; 3) AS &quot;has_previous_page&quot;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;This query returns &lt;code&gt;3&lt;&#x2F;code&gt; and our application logic converts it to a boolean with just &lt;code&gt;&amp;gt; 0&lt;&#x2F;code&gt; logic.&lt;&#x2F;li&gt;
&lt;li&gt;This conversion to boolean could also be done directly in SQL but we declared that out of scope for this post&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;sql&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;SELECT *,
  (SELECT COUNT(*) FROM &amp;quot;cats&amp;quot; WHERE &amp;quot;id&amp;quot; &amp;lt; 3) AS &amp;quot;has_previous_page&amp;quot;,
  (SELECT COUNT(*) FROM &amp;quot;cats&amp;quot;) AS &amp;quot;total_count&amp;quot;
  FROM &amp;quot;cats&amp;quot;
  WHERE &amp;quot;id&amp;quot; &amp;gt; 3
  ORDER BY &amp;quot;id&amp;quot; ASC
  LIMIT 4;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;SQL results&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;id |  name  | has_previous_page | total_count
----+--------+-------------------+-------------
  4 | cookie |                 2 |          12
  5 | dave   |                 2 |          12
  6 | bosco  |                 2 |          12
  7 | frida  |                 2 |          12
(4 rows)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;GraphQL response&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;{
  &amp;quot;data&amp;quot;: {
    &amp;quot;cats&amp;quot;: {
      &amp;quot;edges&amp;quot;: [
        {
          &amp;quot;cursor&amp;quot;: &amp;quot;4&amp;quot;,
          &amp;quot;node&amp;quot;: {
            &amp;quot;id&amp;quot;: 4,
            &amp;quot;name&amp;quot;: &amp;quot;cookie&amp;quot;
          }
        },
        {
          &amp;quot;cursor&amp;quot;: &amp;quot;5&amp;quot;,
          &amp;quot;node&amp;quot;: {
            &amp;quot;id&amp;quot;: 5,
            &amp;quot;name&amp;quot;: &amp;quot;dave&amp;quot;
          }
        },
        {
          &amp;quot;cursor&amp;quot;: &amp;quot;6&amp;quot;,
          &amp;quot;node&amp;quot;: {
            &amp;quot;id&amp;quot;: 6,
            &amp;quot;name&amp;quot;: &amp;quot;bosco&amp;quot;
          }
        }
      ],
      &amp;quot;pageInfo&amp;quot;: {
        &amp;quot;endCursor&amp;quot;: &amp;quot;6&amp;quot;,
        &amp;quot;hasNextPage&amp;quot;: true,
        &amp;quot;hasPreviousPage&amp;quot;: true,
        &amp;quot;startCursor&amp;quot;: &amp;quot;4&amp;quot;
      },
      &amp;quot;totalCount&amp;quot;: 12
    }
  }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;case-1-4-last-before-paging-backward&quot;&gt;Case 1.4: last + before: paging backward&lt;&#x2F;h2&gt;
&lt;p&gt;Lets remember our collection in ascending &lt;code&gt;id&lt;&#x2F;code&gt; order:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sql&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;SELECT * FROM &amp;quot;cats&amp;quot; ORDER BY &amp;quot;id&amp;quot; ASC;

 id |  name
----+---------
  1 | esther
  2 | cookie
  3 | cookie
  4 | cookie
  5 | dave
  6 | bosco
  7 | frida
  9 | giggles
 10 | jasmine
 11 | jerry
 12 | alice
 13 | iggy
(12 rows)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;For paging backward from the end, clients send &lt;code&gt;last&lt;&#x2F;code&gt; and &lt;code&gt;before&lt;&#x2F;code&gt; using
the value from &lt;code&gt;startCursor&lt;&#x2F;code&gt; in the previous query.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;graphql&quot; class=&quot;language-graphql &quot;&gt;&lt;code class=&quot;language-graphql&quot; data-lang=&quot;graphql&quot;&gt;query {
  cats(last: 3, before: &amp;quot;13&amp;quot;) {
    edges {
      cursor
      node {
        id
        name
      }
    }
    totalCount
    pageInfo {
      startCursor
      endCursor
      hasPreviousPage
      hasNextPage
    }
  }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;To implement this, we flip our &lt;code&gt;WHERE&lt;&#x2F;code&gt; operator to be less than (&lt;code&gt;&amp;lt;&lt;&#x2F;code&gt;),
flip our direction to be &lt;code&gt;DESC&lt;&#x2F;code&gt; and then again reverse in the
application code.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sql&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;SELECT *,
  (SELECT count(*) FROM &amp;quot;cats&amp;quot; WHERE &amp;quot;id&amp;quot; &amp;gt; 13) as has_next_page,
  (SELECT COUNT(*) FROM &amp;quot;cats&amp;quot;) AS &amp;quot;total_count&amp;quot;
  FROM &amp;quot;cats&amp;quot;
  WHERE &amp;quot;id&amp;quot; &amp;lt; 13
  ORDER BY &amp;quot;id&amp;quot; DESC
  LIMIT 4;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Notes on the SQL statement and how our graphql pagination metadata will
be computed&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;hasPreviousPage&lt;&#x2F;code&gt; will use our &lt;strong&gt;beyond the limit&lt;&#x2F;strong&gt; technique again,
but here the results will be reversed&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;hasNextPage&lt;&#x2F;code&gt; here we will use the same techinque as &lt;code&gt;after&lt;&#x2F;code&gt; and
&lt;code&gt;first&lt;&#x2F;code&gt; with a similar query
&lt;code&gt;(SELECT count(*) FROM &quot;cats&quot; WHERE &quot;id&quot; &amp;gt; 13) as has_next_page,&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;startCursor&lt;&#x2F;code&gt; is the &lt;code&gt;id&lt;&#x2F;code&gt; from the next-to-last row in the SQL
results
&lt;ul&gt;
&lt;li&gt;This accounts both for the limit + 1 &quot;peeking&quot; and the
double-reversing&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;endCursor&lt;&#x2F;code&gt; is the &lt;code&gt;id&lt;&#x2F;code&gt; from the first row in the results&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;totalCount&lt;&#x2F;code&gt; will use the same subselect&#x2F;alias technique again&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;SQL Result&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt; id |  name   | has_next_page | total_count
----+---------+---------------+-------------
 12 | alice   |             0 |          12
 11 | jerry   |             0 |          12
 10 | jasmine |             0 |          12
  9 | giggles |             0 |          12
(4 rows)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;GraphQL response&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;{
  &amp;quot;data&amp;quot;: {
    &amp;quot;cats&amp;quot;: {
      &amp;quot;edges&amp;quot;: [
        {
          &amp;quot;cursor&amp;quot;: &amp;quot;10&amp;quot;,
          &amp;quot;node&amp;quot;: {
            &amp;quot;id&amp;quot;: 10,
            &amp;quot;name&amp;quot;: &amp;quot;jasmine&amp;quot;
          }
        },
        {
          &amp;quot;cursor&amp;quot;: &amp;quot;11&amp;quot;,
          &amp;quot;node&amp;quot;: {
            &amp;quot;id&amp;quot;: 11,
            &amp;quot;name&amp;quot;: &amp;quot;jerry&amp;quot;
          }
        },
        {
          &amp;quot;cursor&amp;quot;: &amp;quot;12&amp;quot;,
          &amp;quot;node&amp;quot;: {
            &amp;quot;id&amp;quot;: 12,
            &amp;quot;name&amp;quot;: &amp;quot;alice&amp;quot;
          }
        }
      ],
      &amp;quot;pageInfo&amp;quot;: {
        &amp;quot;endCursor&amp;quot;: &amp;quot;12&amp;quot;,
        &amp;quot;hasNextPage&amp;quot;: false,
        &amp;quot;hasPreviousPage&amp;quot;: true,
        &amp;quot;startCursor&amp;quot;: &amp;quot;10&amp;quot;
      },
      &amp;quot;totalCount&amp;quot;: 12
    }
  }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;part-2-handling-client-specified-order&quot;&gt;Part 2: Handling client-specified order&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;case-2-1-first-sortby&quot;&gt;Case 2.1: first + sortBy&lt;&#x2F;h2&gt;
&lt;p&gt;When your GraphQL clients need to specify the order of results, we have
extra complexity to manage.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;graphql&quot; class=&quot;language-graphql &quot;&gt;&lt;code class=&quot;language-graphql&quot; data-lang=&quot;graphql&quot;&gt;query {
  cats(first: 3, sortBy: &amp;quot;name&amp;quot;, sortOrder: ascending) {
    edges {
      cursor
      node {
        id
        name
      }
    }
    totalCount
    pageInfo {
      startCursor
      endCursor
      hasPreviousPage
      hasNextPage
    }
  }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Here we need to map the GraphQL parameters to our &lt;code&gt;ORDER BY&lt;&#x2F;code&gt; clause
which is straightforward.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sql&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;SELECT * FROM &amp;quot;cats&amp;quot; ORDER BY &amp;quot;name&amp;quot; ASC LIMIT 3;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;id&quot; class=&quot;language-id &quot;&gt;&lt;code class=&quot;language-id&quot; data-lang=&quot;id&quot;&gt;----+--------
 12 | alice
  6 | bosco
  3 | cookie
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;We can see our results are in the correct order. However, we do have 3
cats named cookie. We happened to get cookie 3 in this result, but
that&#x27;s not guaranteed to always be the case. To ensure consistent
results, we should add a secondary sort.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sql&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;SELECT * FROM &amp;quot;cats&amp;quot; ORDER BY &amp;quot;name&amp;quot; ASC, &amp;quot;id&amp;quot; ASC LIMIT 3;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt; id |  name
----+--------
 12 | alice
  6 | bosco
  2 | cookie
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Here we&#x27;ll always get cookie 2 in this page (assuming an unchanged data
set).&lt;&#x2F;p&gt;
&lt;h3 id=&quot;paging-forward-with-ordering&quot;&gt;Paging forward with ordering&lt;&#x2F;h3&gt;
&lt;p&gt;Lets remember our collection in name order by both name and id for
consistency.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sql&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;SELECT * FROM &amp;quot;cats&amp;quot; ORDER BY &amp;quot;name&amp;quot; ASC, &amp;quot;id&amp;quot; ASC;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;id |  name
----+---------
 12 | alice
  6 | bosco
  2 | cookie
  3 | cookie
  4 | cookie
  5 | dave
  1 | esther
  7 | frida
  9 | giggles
 13 | iggy
 10 | jasmine
 11 | jerry
(12 rows)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;When it comes time to query the second page of these results, we need a
way to express &quot;when sorted by name ascending, I need the results that
come after X&quot; where X is the &lt;code&gt;endCursor&lt;&#x2F;code&gt; from the first page. One way to
do this is to make an extra query by the id from the cursor to get that
cat&#x27;s name, and use that in our &lt;code&gt;WHERE&lt;&#x2F;code&gt; clause.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;graphql&quot; class=&quot;language-graphql &quot;&gt;&lt;code class=&quot;language-graphql&quot; data-lang=&quot;graphql&quot;&gt;query {
  cats(first: 3, after: &amp;quot;2&amp;quot;, sortBy: &amp;quot;name&amp;quot;, sortOrder: ascending) {
    edges {
      cursor
      node {
        id
        name
      }
    }
    totalCount
    pageInfo {
      startCursor
      endCursor
      hasPreviousPage
      hasNextPage
    }
  }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;sql&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;SELECT &amp;quot;name&amp;quot; FROM &amp;quot;cats&amp;quot; WHERE &amp;quot;id&amp;quot; = 2;
-- Now we know that cat 2 is named cookie

SELECT * FROM &amp;quot;cats&amp;quot;
  WHERE &amp;quot;name&amp;quot; &amp;gt;= &amp;#x27;cookie&amp;#x27;
  AND &amp;quot;id&amp;quot; != 2
  ORDER BY &amp;quot;name&amp;quot; ASC, &amp;quot;id&amp;quot; ASC LIMIT 3;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;id |  name
----+--------
  3 | cookie
  4 | cookie
  5 | dave
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;We need to make sure cookie 2 doesn&#x27;t appear in both page 1 and page 2
of results so we explicitly filter her out by id in our &lt;code&gt;WHERE&lt;&#x2F;code&gt; clause.&lt;&#x2F;p&gt;
&lt;p&gt;SQL is capable of combining these 2 queries, so we take advantage of
that with a subselect.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sql&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;SELECT * FROM &amp;quot;cats&amp;quot;
  WHERE &amp;quot;name&amp;quot; &amp;gt;= (SELECT name FROM &amp;quot;cats&amp;quot; WHERE &amp;quot;id&amp;quot; = 2)
  AND &amp;quot;id&amp;quot; != 2
  ORDER BY &amp;quot;name&amp;quot; ASC, &amp;quot;id&amp;quot; ASC LIMIT 3;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;id |  name
----+--------
  3 | cookie
  4 | cookie
  5 | dave
(3 rows)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Here&#x27;s how our graphql pagination metadata will be computed with our
added complexity:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;hasNextPage&lt;&#x2F;code&gt; will use our &lt;strong&gt;beyond the limit&lt;&#x2F;strong&gt; technique again,
increasing &lt;code&gt;LIMIT&lt;&#x2F;code&gt; from &lt;code&gt;3&lt;&#x2F;code&gt; to &lt;code&gt;4&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;hasPreviousPage&lt;&#x2F;code&gt; is more complicated because it introducts
complexity with ducplicates
&lt;ul&gt;
&lt;li&gt;we want to reverse the direction of the &lt;code&gt;WHERE&lt;&#x2F;code&gt; clause to get
all records to &lt;code&gt;&amp;lt;=&lt;&#x2F;code&gt;
&lt;ul&gt;
&lt;li&gt;we need to account for &lt;strong&gt;cats with the same name but a lower
id than &lt;code&gt;after&lt;&#x2F;code&gt; in the previous page&lt;&#x2F;strong&gt; which is why &lt;code&gt;=&lt;&#x2F;code&gt; is
included&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;after&lt;&#x2F;code&gt; cursor should be excluded from results with
&lt;code&gt;AND &quot;id&quot; != 2&quot;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;we need to exclude all cats of the same name who&#x27;s IDs are
greater than the &lt;code&gt;after&lt;&#x2F;code&gt; cursor with&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;sql&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt; AND &amp;quot;id&amp;quot; NOT IN (SELECT &amp;quot;id&amp;quot; FROM &amp;quot;cats&amp;quot;
     WHERE &amp;quot;name&amp;quot; = (SELECT &amp;quot;name&amp;quot; FROM &amp;quot;cats&amp;quot; WHERE &amp;quot;id&amp;quot; = 2)
     AND &amp;quot;id&amp;quot; &amp;gt; 2))
```

for a final query that looks like:

```sql
  (SELECT * FROM &amp;quot;cats&amp;quot;
    WHERE &amp;quot;name&amp;quot; &amp;lt;= (SELECT name FROM &amp;quot;cats&amp;quot; WHERE &amp;quot;id&amp;quot; = 2)
    AND &amp;quot;id&amp;quot; != 2
    AND &amp;quot;id&amp;quot; NOT IN
      (SELECT &amp;quot;id&amp;quot; FROM &amp;quot;cats&amp;quot;
        WHERE &amp;quot;name&amp;quot; =
          (SELECT &amp;quot;name&amp;quot; FROM &amp;quot;cats&amp;quot;
             WHERE &amp;quot;id&amp;quot; = 2)
        AND &amp;quot;id&amp;quot; &amp;gt; 2));
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;id | name
----+-------
 6 | bosco
12 | alice
(2 rows)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;we will return &lt;code&gt;COUNT(*)&lt;&#x2F;code&gt; and set this as &lt;code&gt;has_previous_page&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;startCursor&lt;&#x2F;code&gt; is the &lt;code&gt;id&lt;&#x2F;code&gt; from the next-to-last row in the SQL
results
&lt;ul&gt;
&lt;li&gt;This accounts both for the limit + 1 &quot;peeking&quot; and the
double-reversing&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;endCursor&lt;&#x2F;code&gt; is the &lt;code&gt;id&lt;&#x2F;code&gt; from the first row in the results&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;totalCount&lt;&#x2F;code&gt; will use the same subselect&#x2F;alias technique again&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;Final Query&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sql&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;SELECT *,
  (SELECT COUNT(*) FROM &amp;quot;cats&amp;quot;
    WHERE &amp;quot;name&amp;quot; &amp;lt;= (SELECT name FROM &amp;quot;cats&amp;quot; WHERE &amp;quot;id&amp;quot; = 2)
    AND &amp;quot;id&amp;quot; != 2
    AND &amp;quot;id&amp;quot; NOT IN
      (SELECT &amp;quot;id&amp;quot; FROM &amp;quot;cats&amp;quot;
        WHERE &amp;quot;name&amp;quot; = (SELECT &amp;quot;name&amp;quot; FROM &amp;quot;cats&amp;quot; WHERE &amp;quot;id&amp;quot; = 2)
        AND &amp;quot;id&amp;quot; &amp;gt; 2))
    AS has_previous_page,
  (SELECT COUNT(*) FROM &amp;quot;cats&amp;quot;) AS &amp;quot;total_count&amp;quot;
FROM &amp;quot;cats&amp;quot;
WHERE &amp;quot;name&amp;quot; &amp;gt;= (SELECT name FROM &amp;quot;cats&amp;quot; WHERE &amp;quot;id&amp;quot; = 2)
AND &amp;quot;id&amp;quot; != 2
ORDER BY &amp;quot;name&amp;quot; ASC, &amp;quot;id&amp;quot; ASC LIMIT 4;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;SQL Results&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sql&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt; id |  name  | has_previous_page | total_count
----+--------+-------------------+-------------
  3 | cookie |                 2 |          12
  4 | cookie |                 2 |          12
  5 | dave   |                 2 |          12
  1 | esther |                 2 |          12
(4 rows)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;GraphQL Response&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;{
  &amp;quot;data&amp;quot;: {
    &amp;quot;cats&amp;quot;: {
      &amp;quot;edges&amp;quot;: [
        {
          &amp;quot;cursor&amp;quot;: &amp;quot;3&amp;quot;,
          &amp;quot;node&amp;quot;: {
            &amp;quot;id&amp;quot;: 3,
            &amp;quot;name&amp;quot;: &amp;quot;cookie&amp;quot;
          }
        },
        {
          &amp;quot;cursor&amp;quot;: &amp;quot;4&amp;quot;,
          &amp;quot;node&amp;quot;: {
            &amp;quot;id&amp;quot;: 4,
            &amp;quot;name&amp;quot;: &amp;quot;cookie&amp;quot;
          }
        },
        {
          &amp;quot;cursor&amp;quot;: &amp;quot;5&amp;quot;,
          &amp;quot;node&amp;quot;: {
            &amp;quot;id&amp;quot;: 5,
            &amp;quot;name&amp;quot;: &amp;quot;dave&amp;quot;
          }
        }
      ],
      &amp;quot;pageInfo&amp;quot;: {
        &amp;quot;endCursor&amp;quot;: &amp;quot;12&amp;quot;,
        &amp;quot;hasNextPage&amp;quot;: true,
        &amp;quot;hasPreviousPage&amp;quot;: true,
        &amp;quot;startCursor&amp;quot;: &amp;quot;10&amp;quot;
      },
      &amp;quot;totalCount&amp;quot;: &amp;quot;12&amp;quot;
    }
  }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;case-2-2-paging-backward-with-ordering-last-and-before&quot;&gt;Case 2.2: Paging backward with ordering: last and before&lt;&#x2F;h2&gt;
&lt;p&gt;Once again a reminder:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sql&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;SELECT * FROM &amp;quot;cats&amp;quot; ORDER BY &amp;quot;name&amp;quot; ASC, &amp;quot;id&amp;quot; ASC;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;id |  name
----+---------
 12 | alice
  6 | bosco
  2 | cookie
  3 | cookie
  4 | cookie
  5 | dave
  1 | esther
  7 | frida
  9 | giggles
 13 | iggy
 10 | jasmine
 11 | jerry
(12 rows)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;To implement paging backward from the end, consider these graphql
inputs:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;graphql&quot; class=&quot;language-graphql &quot;&gt;&lt;code class=&quot;language-graphql&quot; data-lang=&quot;graphql&quot;&gt;query {
  cats(last: 3, before: &amp;quot;13&amp;quot;, sortBy: &amp;quot;name&amp;quot;, sortOrder: ascending) {
    edges {
      cursor
      node {
        id
        name
      }
    }
    totalCount
    pageInfo {
      startCursor
      endCursor
      hasPreviousPage
      hasNextPage
    }
  }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;For this we need to query for the name corresponding to our reference
id, flip the direction, and reverse in application code again.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sql&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;SELECT * FROM &amp;quot;cats&amp;quot;
  WHERE &amp;quot;name&amp;quot; &amp;lt;= (SELECT &amp;quot;name&amp;quot; FROM &amp;quot;cats&amp;quot; WHERE &amp;quot;id&amp;quot; = 13)
  AND &amp;quot;id&amp;quot; != 13
  ORDER BY &amp;quot;name&amp;quot; DESC LIMIT 3;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt; id |  name
----+---------
  9 | giggles
  7 | frida
  1 | esther
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;We reverse that result in application code and we have the correct
results.&lt;&#x2F;p&gt;
&lt;p&gt;Here&#x27;s how our graphql pagination metadata will be computed with our
added complexity:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;hasPreviousPage &lt;&#x2F;code&gt; will use our &lt;strong&gt;beyond the limit&lt;&#x2F;strong&gt; technique
again, increasing &lt;code&gt;LIMIT&lt;&#x2F;code&gt; from &lt;code&gt;3&lt;&#x2F;code&gt; to &lt;code&gt;4&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;hasNextPage&lt;&#x2F;code&gt; just as complicated as Case 2.1 with &lt;strong&gt;query
reversal&lt;&#x2F;strong&gt;. The example will not have duplicate names, but we
account for them nonetheless with one exception&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;whereas in &lt;code&gt;after&lt;&#x2F;code&gt; and &lt;code&gt;first&lt;&#x2F;code&gt; we needed to exclude all options
less than the supplied &lt;code&gt;id&lt;&#x2F;code&gt;, &lt;code&gt;before&lt;&#x2F;code&gt; and &lt;code&gt;last&lt;&#x2F;code&gt; we need to do
the opposite so our has_next_page query becomes&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;sql&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;SELECT * FROM &amp;quot;cats&amp;quot;
  WHERE &amp;quot;name&amp;quot; &amp;gt;= (SELECT name FROM &amp;quot;cats&amp;quot; WHERE &amp;quot;id&amp;quot; = 13)
  AND &amp;quot;id&amp;quot; != 13
  AND &amp;quot;id&amp;quot; NOT IN (SELECT &amp;quot;id&amp;quot; FROM &amp;quot;cats&amp;quot;
      WHERE &amp;quot;name&amp;quot; = (SELECT &amp;quot;name&amp;quot; FROM &amp;quot;cats&amp;quot; WHERE &amp;quot;id&amp;quot; = 13)
      AND &amp;quot;id&amp;quot; &amp;lt; 13)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;startCursor&lt;&#x2F;code&gt; is the &lt;code&gt;id&lt;&#x2F;code&gt; from the next-to-last row in the SQL
results
&lt;ul&gt;
&lt;li&gt;This accounts both for the limit + 1 &quot;peeking&quot; and the
double-reversing&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;endCursor&lt;&#x2F;code&gt; is the &lt;code&gt;id&lt;&#x2F;code&gt; from the first row in the results&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;totalCount&lt;&#x2F;code&gt; will use the same subselect&#x2F;alias technique again&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;Final Query&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sql&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;SELECT *,
  (SELECT COUNT(*) FROM &amp;quot;cats&amp;quot;
    WHERE &amp;quot;name&amp;quot; &amp;gt;= (SELECT name FROM &amp;quot;cats&amp;quot; WHERE &amp;quot;id&amp;quot; = 13)
    AND &amp;quot;id&amp;quot; != 13
    AND &amp;quot;id&amp;quot; NOT IN (SELECT &amp;quot;id&amp;quot; FROM &amp;quot;cats&amp;quot;
      WHERE &amp;quot;name&amp;quot; = (SELECT &amp;quot;name&amp;quot; FROM &amp;quot;cats&amp;quot; WHERE &amp;quot;id&amp;quot; = 13)
      AND &amp;quot;id&amp;quot; &amp;lt; 13))
    AS has_next_page,
  (SELECT COUNT(*) FROM &amp;quot;cats&amp;quot;) AS &amp;quot;total_count&amp;quot;
FROM &amp;quot;cats&amp;quot;
WHERE &amp;quot;name&amp;quot; &amp;lt;= (SELECT name FROM &amp;quot;cats&amp;quot; WHERE &amp;quot;id&amp;quot; = 13)
AND &amp;quot;id&amp;quot; != 13
ORDER BY &amp;quot;name&amp;quot; DESC, &amp;quot;id&amp;quot; DESC LIMIT 4;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;SQL Result&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sql&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt; id |  name   | has_next_page | total_count
----+---------+---------------+-------------
  9 | giggles |             2 |          12
  7 | frida   |             2 |          12
  1 | esther  |             2 |          12
  5 | dave    |             2 |          12
(4 rows)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;GraphQL Result&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;Reminder: results in &lt;code&gt;edges&lt;&#x2F;code&gt; are reversed from SQL Result&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;{
  &amp;quot;data&amp;quot;: {
    &amp;quot;cats&amp;quot;: {
      &amp;quot;edges&amp;quot;: [
        {
          &amp;quot;cursor&amp;quot;: &amp;quot;1&amp;quot;,
          &amp;quot;node&amp;quot;: {
            &amp;quot;id&amp;quot;: 1,
            &amp;quot;name&amp;quot;: &amp;quot;ester&amp;quot;
          }
        },
        {
          &amp;quot;cursor&amp;quot;: &amp;quot;7&amp;quot;,
          &amp;quot;node&amp;quot;: {
            &amp;quot;id&amp;quot;: 7,
            &amp;quot;name&amp;quot;: &amp;quot;frida&amp;quot;
          }
        },
        {
          &amp;quot;cursor&amp;quot;: &amp;quot;9&amp;quot;,
          &amp;quot;node&amp;quot;: {
            &amp;quot;id&amp;quot;: 9,
            &amp;quot;name&amp;quot;: &amp;quot;giggles&amp;quot;
          }
        }
      ],
      &amp;quot;pageInfo&amp;quot;: {
        &amp;quot;endCursor&amp;quot;: &amp;quot;9&amp;quot;,
        &amp;quot;hasNextPage&amp;quot;: true,
        &amp;quot;hasPreviousPage&amp;quot;: true,
        &amp;quot;startCursor&amp;quot;: &amp;quot;1&amp;quot;
      },
      &amp;quot;totalCount&amp;quot;: &amp;quot;12&amp;quot;
    }
  }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;case-2-3-a-final-example-to-work-through&quot;&gt;Case 2.3 A final example to work through&lt;&#x2F;h2&gt;
&lt;p&gt;Let&#x27;s work through this final challenging example&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;graphql&quot; class=&quot;language-graphql &quot;&gt;&lt;code class=&quot;language-graphql&quot; data-lang=&quot;graphql&quot;&gt;query {
  cats(last: 7, before: &amp;quot;3&amp;quot;, sortBy: &amp;quot;name&amp;quot;, sortOrder: descending) {
    edges {
      cursor
      node {
        id
        name
      }
    }
    totalCount
    pageInfo {
      startCursor
      endCursor
      hasPreviousPage
      hasNextPage
    }
  }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;As a reminder, let&#x27;s look at our data set sorted by name descending.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sql&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;SELECT * FROM &amp;quot;cats&amp;quot; ORDER BY &amp;quot;name&amp;quot; DESC, &amp;quot;id&amp;quot; ASC;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;id |  name
----+---------
 11 | jerry
 10 | jasmine
 13 | iggy
  9 | giggles
  7 | frida
  1 | esther
  5 | dave
  2 | cookie
  3 | cookie
  4 | cookie
  6 | bosco
 12 | alice
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Now let&#x27;s understand the graphql pagination request&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;last: 7&lt;&#x2F;code&gt; means the client wants only 7 results&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;last&lt;&#x2F;code&gt; plus &lt;code&gt;before&lt;&#x2F;code&gt; means those results should be prior to the
cursor
&lt;ul&gt;
&lt;li&gt;&quot;prior&quot; is defined here by the client-specified ordering&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;before: &quot;3&quot;&lt;&#x2F;code&gt; means cookie 3 is our reference record&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;sortBy: name&lt;&#x2F;code&gt; means they want them alphabetical by name&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;sortOrder: descending&lt;&#x2F;code&gt; means they want them Z to A&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;So the correct result set looks like this&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;10 | jasmine
13 | iggy
 9 | giggles
 7 | frida
 1 | esther
 5 | dave
 2 | cookie
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;When building our SQL we need:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;a &lt;code&gt;LIMIT 7&lt;&#x2F;code&gt; clause from the &lt;code&gt;last&lt;&#x2F;code&gt; parameter
&lt;ul&gt;
&lt;li&gt;We will bump to &lt;code&gt;8&lt;&#x2F;code&gt; for &lt;code&gt;hasNextPage&lt;&#x2F;code&gt; peeking&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;an &lt;code&gt;ORDER BY name&lt;&#x2F;code&gt; clause but we need to flip the direction from
&lt;code&gt;DESC&lt;&#x2F;code&gt; to &lt;code&gt;ASC&lt;&#x2F;code&gt; because of &lt;code&gt;last&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;our final ordering by id for consistent results
&lt;ul&gt;
&lt;li&gt;This is tricky because both &lt;code&gt;ASC&lt;&#x2F;code&gt; and &lt;code&gt;DESC&lt;&#x2F;code&gt; give us incorrect
results for this case&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;a &lt;code&gt;WHERE &quot;name&quot;&lt;&#x2F;code&gt; clause based on cat 3, which requires a subselect
to get cat 3&#x27;s name&lt;&#x2F;li&gt;
&lt;li&gt;our &lt;code&gt;id !=&lt;&#x2F;code&gt; clause to manage cats with duplicate names&lt;&#x2F;li&gt;
&lt;li&gt;we&#x27;ll also need an &lt;code&gt;id NOT IN &lt;&#x2F;code&gt; subselect to properly exclude the
cats named &#x27;cookie&#x27; that belong to the page after this&lt;&#x2F;li&gt;
&lt;li&gt;The following will be handled with techniques already discussed
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;totalCount&lt;&#x2F;code&gt; (subselect and alias)&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;hasNextPage&lt;&#x2F;code&gt; (beyond the limit)&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;hasPreviousPage&lt;&#x2F;code&gt; will use an inverse where clause subselect as
before&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;SQL Query Option 1&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sql&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;SELECT *,
  (SELECT COUNT(*) FROM &amp;quot;cats&amp;quot;) AS &amp;quot;total_count&amp;quot;,
  (SELECT COUNT(*) FROM &amp;quot;cats&amp;quot; WHERE &amp;quot;name&amp;quot; &amp;lt;
    (SELECT &amp;quot;name&amp;quot; FROM &amp;quot;cats&amp;quot; WHERE &amp;quot;id&amp;quot; = 3)) AS &amp;quot;has_previous_page&amp;quot;
  FROM &amp;quot;cats&amp;quot; WHERE
  &amp;quot;name&amp;quot; &amp;gt;= (SELECT &amp;quot;name&amp;quot; FROM &amp;quot;cats&amp;quot; WHERE &amp;quot;id&amp;quot; = 3)
  AND &amp;quot;id&amp;quot; != 3
  AND &amp;quot;id&amp;quot; NOT IN (SELECT &amp;quot;id&amp;quot; FROM &amp;quot;cats&amp;quot;
       WHERE &amp;quot;name&amp;quot; = (SELECT &amp;quot;name&amp;quot; FROM &amp;quot;cats&amp;quot; WHERE &amp;quot;id&amp;quot; = 3)
       AND &amp;quot;id&amp;quot; &amp;gt; 3)
  ORDER BY &amp;quot;name&amp;quot; ASC, &amp;quot;id&amp;quot; ASC
  LIMIT 8;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;SQL Results&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;id |  name   | total_count | has_previous_page
----+---------+-------------+-------------------
  2 | cookie  |          12 |                 2
  5 | dave    |          12 |                 2
  1 | esther  |          12 |                 2
  7 | frida   |          12 |                 2
  9 | giggles |          12 |                 2
 13 | iggy    |          12 |                 2
 10 | jasmine |          12 |                 2
 11 | jerry   |          12 |                 2
(8 rows)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;We can see the above full query repeats the subquery to find cat 3&#x27;s
name twice. As an optional optimization, we could run this once and
store the result in a temporary table. Perhaps the query planner is
already smart enough to do that optimization automatically though, so
it&#x27;d be best to let benchmarks guide this choice.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;SQL Query Option 2&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sql&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;SELECT name AS pivot_name INTO pivot_cat FROM &amp;quot;cats&amp;quot; WHERE id = 3;
SELECT *,
  (SELECT COUNT(*) FROM &amp;quot;cats&amp;quot;) AS &amp;quot;total_count&amp;quot;,
  (SELECT COUNT(*) FROM &amp;quot;cats&amp;quot; WHERE &amp;quot;name&amp;quot; &amp;lt; pivot_name)
  AS &amp;quot;has_previous_page&amp;quot;
  FROM &amp;quot;cats&amp;quot;, pivot_cat WHERE
  name &amp;gt;= pivot_name
  AND &amp;quot;id&amp;quot; != 3
  AND &amp;quot;id&amp;quot; NOT IN (SELECT &amp;quot;id&amp;quot; FROM &amp;quot;cats&amp;quot;
                   WHERE &amp;quot;name&amp;quot; = pivot_name
                   AND &amp;quot;id&amp;quot; &amp;gt; 3)
  ORDER BY name ASC, &amp;quot;id&amp;quot; ASC
  LIMIT 8;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;We get the same results as with option 1.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;id |  name   | pivot_name | total_count | has_previous_page
----+---------+------------+-------------+-------------------
  2 | cookie  | cookie     |          12 |                 2
  5 | dave    | cookie     |          12 |                 2
  1 | esther  | cookie     |          12 |                 2
  7 | frida   | cookie     |          12 |                 2
  9 | giggles | cookie     |          12 |                 2
 13 | iggy    | cookie     |          12 |                 2
 10 | jasmine | cookie     |          12 |                 2
 11 | jerry   | cookie     |          12 |                 2
(8 rows)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;GraphQL response&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;{
  &amp;quot;data&amp;quot;: {
    &amp;quot;cats&amp;quot;: {
      &amp;quot;edges&amp;quot;: [
        {
          &amp;quot;cursor&amp;quot;: &amp;quot;10&amp;quot;,
          &amp;quot;node&amp;quot;: {
            &amp;quot;id&amp;quot;: 10,
            &amp;quot;name&amp;quot;: &amp;quot;jasmine&amp;quot;
          }
        },
        {
          &amp;quot;cursor&amp;quot;: &amp;quot;13&amp;quot;,
          &amp;quot;node&amp;quot;: {
            &amp;quot;id&amp;quot;: 13,
            &amp;quot;name&amp;quot;: &amp;quot;iggy&amp;quot;
          }
        },
        {
          &amp;quot;cursor&amp;quot;: &amp;quot;9&amp;quot;,
          &amp;quot;node&amp;quot;: {
            &amp;quot;id&amp;quot;: 9,
            &amp;quot;name&amp;quot;: &amp;quot;giggles&amp;quot;
          }
        },
        {
          &amp;quot;cursor&amp;quot;: &amp;quot;7&amp;quot;,
          &amp;quot;node&amp;quot;: {
            &amp;quot;id&amp;quot;: 7,
            &amp;quot;name&amp;quot;: &amp;quot;frida&amp;quot;
          }
        },
        {
          &amp;quot;cursor&amp;quot;: &amp;quot;1&amp;quot;,
          &amp;quot;node&amp;quot;: {
            &amp;quot;id&amp;quot;: 1,
            &amp;quot;name&amp;quot;: &amp;quot;esther&amp;quot;
          }
        },
        {
          &amp;quot;cursor&amp;quot;: &amp;quot;5&amp;quot;,
          &amp;quot;node&amp;quot;: {
            &amp;quot;id&amp;quot;: 5,
            &amp;quot;name&amp;quot;: &amp;quot;dave&amp;quot;
          }
        },
        {
          &amp;quot;cursor&amp;quot;: &amp;quot;2&amp;quot;,
          &amp;quot;node&amp;quot;: {
            &amp;quot;id&amp;quot;: 2,
            &amp;quot;name&amp;quot;: &amp;quot;cookie&amp;quot;
          }
        }
      ],
      &amp;quot;pageInfo&amp;quot;: {
        &amp;quot;endCursor&amp;quot;: &amp;quot;2&amp;quot;,
        &amp;quot;hasNextPage&amp;quot;: true,
        &amp;quot;hasPreviousPage&amp;quot;: true,
        &amp;quot;startCursor&amp;quot;: &amp;quot;10&amp;quot;
      },
      &amp;quot;totalCount&amp;quot;: &amp;quot;12&amp;quot;
    }
  }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;Real Talk Time&lt;&#x2F;strong&gt; in writing this article we found bugs in 2 separate
implementations of graphql pagination in reaction projects. This stuff
is tricky!&lt;&#x2F;p&gt;
&lt;h2 id=&quot;metatopic-the-process-of-software-development&quot;&gt;Metatopic: The Process of Software Development&lt;&#x2F;h2&gt;
&lt;p&gt;This blog post walks step-by-step through a process of implementing
specified behavior. It takes a methodical and use-case driven approach.
There are other approaches as well including test-driven development,
adversarial (pitting QA against development), more academically rigorous
methodologies, etc, but this is a pragmatic approach. Test data was
carefully selected to make correctness and incorrectness easier to spot
at a glance. Key in the data set was including the particular edge cases
that expose bugs in initial attempts at implementation.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;further-reading&quot;&gt;Further Reading&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;web.archive.org&#x2F;web&#x2F;20210416212650&#x2F;https:&#x2F;&#x2F;medium.com&#x2F;@meganchang_96378&#x2F;why-facebook-says-cursor-pagination-is-the-greatest-d6b98d86b6c0&quot;&gt;Is Offset Pagination Dead? Why Cursor Pagination is Taking
Over&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;web.archive.org&#x2F;web&#x2F;20210416212650&#x2F;https:&#x2F;&#x2F;graphql.org&#x2F;learn&#x2F;pagination&#x2F;&quot;&gt;Graphql Pagination
Specifications&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;web.archive.org&#x2F;web&#x2F;20210416212650&#x2F;https:&#x2F;&#x2F;facebook.github.io&#x2F;relay&#x2F;graphql&#x2F;connections.htm#sec-undefined.PageInfo&quot;&gt;Facebook Cursor
Specifications&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</description>
      </item>
      <item>
          <title>Switched to Arch Linux</title>
          <pubDate>Sun, 12 Jan 2020 16:38:33 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2020/01/switched-to-arch-linux/</link>
          <guid>https://peterlyons.com/problog/2020/01/switched-to-arch-linux/</guid>
          <description xml:base="https://peterlyons.com/problog/2020/01/switched-to-arch-linux/">&lt;p&gt;I&#x27;ve switched from Linux Mint to Arch Linux on both my work and personal setups now. The main motivating factors were roughly&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;So many more packages were available in the Arch User Repository
&lt;ul&gt;
&lt;li&gt;On Ubuntu&#x2F;Mint I had accumulated ~100 custom installation scripts for new or obscure packages from git and assorted random web sites. With Arch the number of packages I have to script installation for is much smaller (a handful at most). Plus keeping them up to date is properly automated now.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;I grew tired of certain things in Linux Mint Cinnamon not being in plaintext config files, especially keymappings for the desktop.
&lt;ul&gt;
&lt;li&gt;My new rule is settings in a text file in my dotfiles or GTFO&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;I have access to a work colleague who runs arch, has a highly-tuned setup, and is willing to answer questions. This is so nice. I&#x27;m mostly over the hump now and learned how and where to find information on the web, but at the beginning in particular many things were confusing and being able to ask a question in slack and get opinionated guidance was so nice.&lt;&#x2F;li&gt;
&lt;li&gt;Generally I was feeling a bit stuck and open to massively overhauling my software stack. Let&#x27;s talk about that some more.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;full-stack-overhaul&quot;&gt;Full Stack Overhaul&lt;&#x2F;h2&gt;
&lt;p&gt;I&#x27;m not sure why, but I became motivated to do a major overhaul of my linux stack. I think this started when I wasn&#x27;t able to find any decent clojure tooling in Atom text editor and it seemed like I was going to have to switch to VS Code. Changing editors is a major hassle for me as I get heavily used to the quirks of text selection, multi-cursor support, almost 100 custom keybindings, etc. When I finally accepted that I would need to switch editors, I think I let go of my attachment to anything in my stack and was just like &quot;OK, here we go&quot;.&lt;&#x2F;p&gt;
&lt;p&gt;So in the last few months, I have...&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Finally got a personal Lenovo ThinkPad T480 that closely mirrors the one I use for work.
&lt;ul&gt;
&lt;li&gt;I was using a System76 Lemur which is terrible and my partition scheme was getting full on the root filesystem so I was going to have to reinstall anyway&lt;&#x2F;li&gt;
&lt;li&gt;I found one for like $900 on ebay with enough RAM and SSD for personal use, so I said &quot;What they hey&quot; as my Dad would say&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;Switched from Linux Mint (Ubuntu-based, which is Debian-based) to Arch Linux
&lt;ul&gt;
&lt;li&gt;I have been 100% Debian-derived since I first started running linux circa 1999. I was under the impression that dpkg&#x2F;apt-get was the best package manager available but I wasn&#x27;t aware of how much the high friction for community-contributed obscure&#x2F;new packages was impacting my setup. Arch AUR has a much broader selection of packages and keeping everything up to date continuously is easier&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;Adopted sxhkd (simple X11 hot key daemon) for most of my keybindings. That is, anything that isn&#x27;t specifically window management.
&lt;ul&gt;
&lt;li&gt;This has a nice text config file I can take with me across window managers&lt;&#x2F;li&gt;
&lt;li&gt;It has some advanced syntax features that are nice&lt;&#x2F;li&gt;
&lt;li&gt;The config file doesn&#x27;t require extra quoting&#x2F;escaping (i3 does)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;Switched to the i3wm window manager
&lt;ul&gt;
&lt;li&gt;This was more of a &quot;Ross uses it so I&#x27;ll probably end up there eventually&quot;&lt;&#x2F;li&gt;
&lt;li&gt;I like it OK-ish I guess but I think I&#x27;ll actually switch to something less hard-core&lt;&#x2F;li&gt;
&lt;li&gt;I don&#x27;t really need tiling and i3 just being so goofy is bothering me. Please hold, work zoom I&#x27;m screensharing, while I go read the i3 docs and edit my config file to learn how to close this popup window that&#x27;s occupying my entire screen.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;Switched my dotfiles system
&lt;ul&gt;
&lt;li&gt;I&#x27;ve tried a few variants of the Atlassian-suggested bare repo&lt;&#x2F;li&gt;
&lt;li&gt;There&#x27;s one more step I think I need to configure to get this to a good state&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;Reorganized my dotfiles
&lt;ul&gt;
&lt;li&gt;I separated this into a new dotfiles-public repo where most non-secret&#x2F;non-personal things will live. It&#x27;s on github as a private repo currently but I plan to make it public once it stabilizes a bit&lt;&#x2F;li&gt;
&lt;li&gt;I had public dotfiles in the past but the overhead of trying to keep them clean of client details, credentials, etc got to be too much and for a while I kept everything private because I couldn&#x27;t manage tracking what didn&#x27;t belong in a public repo&lt;&#x2F;li&gt;
&lt;li&gt;Now I&#x27;m going to have 2 repos: dotfiles-public and dotfiles-private
&lt;ul&gt;
&lt;li&gt;Shareable scripts like my fuzzball scripts go in public, but the data directories full of my specific snippets and scripts stay in private&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;New hotkey approach for navigation
&lt;ul&gt;
&lt;li&gt;I&#x27;m pretty jazzed about this but I&#x27;m just going to write it up in my public dotfiles README and publish that when it&#x27;s ready&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;Starting to use systemd timers for periodic scripts
&lt;ul&gt;
&lt;li&gt;There&#x27;s a bunch of low hanging fruit I really should just put on timers and forget about, I just have been pushing it off for a while&lt;&#x2F;li&gt;
&lt;li&gt;I started learning about systemd timers at the airport yesterday and I&#x27;ll be putting a few scripts in there in the coming weeks&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;So mostly happy with my setup. Don&#x27;t get me wrong, it&#x27;s still linux on the desktop with all the associated terribleness and embarrassment, but mostly I feel like my systems are dialed in at this point.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>cargo doc and linux default browser</title>
          <pubDate>Wed, 02 Oct 2019 14:34:39 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2019/10/cargo-doc-and-linux-default-browser/</link>
          <guid>https://peterlyons.com/problog/2019/10/cargo-doc-and-linux-default-browser/</guid>
          <description xml:base="https://peterlyons.com/problog/2019/10/cargo-doc-and-linux-default-browser/">&lt;p&gt;So I recently learned about &lt;code&gt;cargo doc --open&lt;&#x2F;code&gt; while attending the &lt;a href=&quot;https:&#x2F;&#x2F;www.cogoldrust.com&quot;&gt;Colorado Gold Rust&lt;&#x2F;a&gt; conference. However, on my linux mint machine it opened the docs in chrome instead of my default web browser which is firefox. Since I spent most of my morning rust coding time tracking down why that was and how to fix it, I figured I write a blog post about it. So here goes:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;By looking at the cargo source, I was able to see that a separate crate named &lt;code&gt;opener&lt;&#x2F;code&gt; was used for this&lt;&#x2F;li&gt;
&lt;li&gt;By reading the &lt;code&gt;opener&lt;&#x2F;code&gt; docs and source, I determined it runs &lt;code&gt;xdg-open&lt;&#x2F;code&gt; but not the one from my OS package manager, a separate copy it has bundled with the crate. (xdg-open is a shell script).&lt;&#x2F;li&gt;
&lt;li&gt;I tried running &lt;code&gt;xdg-open target&#x2F;doc&#x2F;my-crate&#x2F;index.html&lt;&#x2F;code&gt; directly on the command line and noticed it opened the wrong browser, too.&lt;&#x2F;li&gt;
&lt;li&gt;By editing my system-level xdg-open to add &lt;code&gt;set -x&lt;&#x2F;code&gt; debugging, I was able to determine that my xdg-open was eventually calling &lt;code&gt;gio open&lt;&#x2F;code&gt; which I had never heard of.&lt;&#x2F;li&gt;
&lt;li&gt;So I read a bunch of &lt;a href=&quot;https:&#x2F;&#x2F;developer.gnome.org&#x2F;gio&#x2F;stable&#x2F;gio.html&quot;&gt;gio docs&lt;&#x2F;a&gt; that said the configuration comes from a file called &lt;code&gt;mimeapps.list&lt;&#x2F;code&gt;
&lt;ul&gt;
&lt;li&gt;And of course, there&#x27;s many locations where this file can live and both an old deprecated set of locations that is widely documented online and a new set of actual working locations with scant documentation&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;I tried a bunch of wrong locations where &lt;code&gt;mimeapps.list&lt;&#x2F;code&gt; might need to be with no success&lt;&#x2F;li&gt;
&lt;li&gt;I eventually found &lt;a href=&quot;https:&#x2F;&#x2F;wiki.archlinux.org&#x2F;index.php&#x2F;XDG_MIME_Applications#mimeapps.list&quot;&gt;this Arch Wiki page&lt;&#x2F;a&gt; that described the format and accurate location of mimeapps.list: &lt;code&gt;~&#x2F;.config&#x2F;mimeapps.list&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;In there I found a bunch of stuff and every time I saw &lt;code&gt;google-chrome.desktop&lt;&#x2F;code&gt; I just preceeded it with &lt;code&gt;firefox.desktop;&lt;&#x2F;code&gt; and that seemed to work&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</description>
      </item>
      <item>
          <title>Sorting YAML keys</title>
          <pubDate>Wed, 10 Apr 2019 15:17:20 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2019/04/sorting-yaml-keys/</link>
          <guid>https://peterlyons.com/problog/2019/04/sorting-yaml-keys/</guid>
          <description xml:base="https://peterlyons.com/problog/2019/04/sorting-yaml-keys/">&lt;p&gt;So I had a bunch of YAML files with an array of key&#x2F;value maps representing environment variables. I wanted to sort these to make comparing them across similar files easier. A web search pulled up this nice pipeline to handle the actual sorting:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sh&quot; class=&quot;language-sh &quot;&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;xclip -selection clipboard -o |
  awk &amp;#x27;{printf(&amp;quot;%s%s&amp;quot;,$0,(NR%2)?&amp;quot;\t&amp;quot;:&amp;quot;\n&amp;quot;)}&amp;#x27; |
  sort -k2,2n |
  tr &amp;#x27;\t&amp;#x27; &amp;#x27;\n&amp;#x27; |
  xclip -selection clipboard # sort pairs of yaml lines
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;So what I did was copy just the lines of YAML I wanted to sort (not the whole file) and then run that command, which uses the clipboard for it&#x27;s input&#x2F;output.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;convincing-everyone-the-changes-are-safe&quot;&gt;Convincing everyone the changes are safe&lt;&#x2F;h2&gt;
&lt;p&gt;Great. Now my keys are sorted, but my git diff looks nasty. It looks like a made huge sweeping changes to the files. I wanted to convince myself and my code reviewers that I didn&#x27;t change any keys or values, just sorted the lines.&lt;&#x2F;p&gt;
&lt;p&gt;I futzed around a bit and eventually came up with this bit of bash, which I was really pleased with:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sh&quot; class=&quot;language-sh &quot;&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;git diff-tree --no-commit-id --name-only -r HEAD | {
  while IFS= read -r file_path; do
    git show &amp;quot;HEAD:${file_path}&amp;quot; | sort &amp;gt;new.txt
    git show &amp;quot;HEAD^1:${file_path}&amp;quot; | sort &amp;gt;old.txt
    echo &amp;quot;Diff for ${file_path}:&amp;quot;
    diff old.txt new.txt
    shasum old.txt new.txt
  done
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;What this does is go through each file in the current git commit and compare it to the same file in the previous commit with sorted lines. This shows both versions have the same lines just in different order. It&#x27;s confirmed with an empty &lt;code&gt;diff&lt;&#x2F;code&gt; output and with both files having the same hash via &lt;code&gt;shasum&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Here&#x27;s sample output:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Diff for some&amp;#x2F;path&amp;#x2F;foo&amp;#x2F;file.yaml:
9b45d3510db8e2b85a434f359599a0228848f52e  old.txt
9b45d3510db8e2b85a434f359599a0228848f52e  new.txt
Diff for some&amp;#x2F;path&amp;#x2F;bar&amp;#x2F;file.yaml:
cb340559978a2dc9476c365da9560deb6af43808  old.txt
cb340559978a2dc9476c365da9560deb6af43808  new.txt
Diff for some&amp;#x2F;path&amp;#x2F;baz&amp;#x2F;file.yaml:
47c328231e83b7927b9ba01e5a6a9be9c1b52f24  old.txt
47c328231e83b7927b9ba01e5a6a9be9c1b52f24  new.txt
Diff for some&amp;#x2F;path&amp;#x2F;bux&amp;#x2F;file.yaml:
257ae73f12bf96727402077da2c618174fe16410  old.txt
257ae73f12bf96727402077da2c618174fe16410  new.txt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</description>
      </item>
      <item>
          <title>New Work Journal System</title>
          <pubDate>Tue, 15 Jan 2019 02:55:47 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2019/01/new-work-journal-system/</link>
          <guid>https://peterlyons.com/problog/2019/01/new-work-journal-system/</guid>
          <description xml:base="https://peterlyons.com/problog/2019/01/new-work-journal-system/">&lt;p&gt;I&#x27;ve updated my journaling tooling for my work journal. Here&#x27;s how I do it.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;writing-journal-entries&quot;&gt;Writing Journal Entries&lt;&#x2F;h2&gt;
&lt;p&gt;First, a script computes the path to the specific journal (one per job&#x2F;company) I&#x27;m using.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;#!&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;env bash
# Strict mode boilerplate omitted for the blog, but is really there
# http:&amp;#x2F;&amp;#x2F;redsymbol.net&amp;#x2F;articles&amp;#x2F;unofficial-bash-strict-mode&amp;#x2F;
year=$(date +%Y)
month=$(date +%m)
day=$(date +%d)
journal_path=&amp;quot;${HOME}&amp;#x2F;work&amp;#x2F;journal&amp;#x2F;${year}-${month}&amp;#x2F;${year}-${month}-${day}.md&amp;quot;
exec ~&amp;#x2F;bin&amp;#x2F;append-journal-entry.sh &amp;quot;${journal_path}&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;The directory structure makes it easy to open a text editor in the directory for the current month and browse recent days&lt;&#x2F;li&gt;
&lt;li&gt;easy to restrict searches with &lt;code&gt;rg&lt;&#x2F;code&gt; to certain time periods&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;There&#x27;s a script &lt;code&gt;append-journal-entry.sh&lt;&#x2F;code&gt; that looks like this:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sh&quot; class=&quot;language-sh &quot;&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;#!&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;env bash
# Strict mode boilerplate omitted for the blog, but is really there
# http:&amp;#x2F;&amp;#x2F;redsymbol.net&amp;#x2F;articles&amp;#x2F;unofficial-bash-strict-mode&amp;#x2F;

cd &amp;quot;$(dirname &amp;quot;${BASH_SOURCE[0]}&amp;quot;)&amp;#x2F;..&amp;quot;

main() {
  local journal_path=$1
  shift
  local entry
  entry=&amp;quot;$*&amp;quot;
  if [[ -z &amp;quot;${entry}&amp;quot; ]]; then
    entry=$(~&amp;#x2F;bin&amp;#x2F;prompt-or-clipboard.sh &amp;quot;Entry&amp;quot;)
  fi
  mkdir -p &amp;quot;$(dirname &amp;quot;${journal_path}&amp;quot;)&amp;quot;
  local ts
  ts=$(date +%Y-%m-%dT%H:%M:%S%z)
  cat &amp;lt;&amp;lt;EOF &amp;gt;&amp;gt;&amp;quot;${journal_path}&amp;quot;

# ${ts}
- ${entry}
EOF
}

main &amp;quot;$@&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;That takes the path to the journal file as an argument&lt;&#x2F;li&gt;
&lt;li&gt;It will prompt for the entry content, or if left empty, take the content from the system clipboard
&lt;ul&gt;
&lt;li&gt;I use this to copy code snippets, log entries, etc into my journal&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;Then it just appends them with a markdown header for the timestamp and the content
&lt;ul&gt;
&lt;li&gt;1-liner content is a markdown list, but there&#x27;s so much random syntax pasted into my journals that hoping to have them render properly as markdown is a bit silly&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Here&#x27;s &lt;code&gt;prompt-or-clipboard.sh&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;read -r -p &amp;quot;$1 (ENTER for clipboard): &amp;quot; query
if [[ -z &amp;quot;${query}&amp;quot; ]]; then
  query=$(xclip -clipboard -o)
fi
echo &amp;quot;${query}&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;searching-the-journal&quot;&gt;Searching the Journal&lt;&#x2F;h2&gt;
&lt;p&gt;I use this shell alias&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sh&quot; class=&quot;language-sh &quot;&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;search-work-journal() {
  rg &amp;quot;$*&amp;quot; ~&amp;#x2F;work&amp;#x2F;journal | less
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;reading-the-full-journal&quot;&gt;Reading the full journal&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;sh&quot; class=&quot;language-sh &quot;&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;read-work-journal() {
  fd --type f . ~&amp;#x2F;work&amp;#x2F;journal | xargs cat | less
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</description>
      </item>
      <item>
          <title>Handling optional arguments in bash</title>
          <pubDate>Wed, 19 Dec 2018 22:31:37 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2018/12/handling-optional-arguments-in-bash/</link>
          <guid>https://peterlyons.com/problog/2018/12/handling-optional-arguments-in-bash/</guid>
          <description xml:base="https://peterlyons.com/problog/2018/12/handling-optional-arguments-in-bash/">&lt;p&gt;I learned a nice way of using bash arrays to hold command line arguments that are conditional or will be computed before passing as arguments to a separate program.&lt;&#x2F;p&gt;
&lt;p&gt;There&#x27;s 2 basic parts.&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Use a bash array to hold them&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;code&gt;declare -a opts=(one two three)&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;Expand them properly using this syntax&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;code&gt;&quot;${opts[@]}&quot;&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Here&#x27;s an annotated example:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;#!&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;env bash

# Here&amp;#x27;s our default arguments we intend
# to pass to curl
declare -a opts=(--silent --fail --output &amp;#x2F;dev&amp;#x2F;null)

# Use this for debugging
# opts=(--fail)

if [[ -n &amp;quot;${REMOTE_USER}&amp;quot; ]]; then
# Here we need to change the args, so
# we can prepend some to the variable itself.
  opts=(--user &amp;quot;${REMOTE_USER}:${REMOTE_PASSWORD}&amp;quot; &amp;quot;${opts[@]}&amp;quot;)
fi
url=&amp;quot;${REMOTE_URL}&amp;quot;

# Now pass the on with proper quoting
# using the [@] syntax

curl &amp;quot;${opts[@]}&amp;quot; &amp;quot;${url}&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Note I omitted the &lt;a href=&quot;http:&#x2F;&#x2F;redsymbol.net&#x2F;articles&#x2F;unofficial-bash-strict-mode&quot;&gt;unofficial bash strict mode&lt;&#x2F;a&gt; boilerplate for clarity, but in my production scripts, you&#x27;d see that at the beginning.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Fuzzball Desktop Automation</title>
          <pubDate>Sun, 25 Nov 2018 18:53:13 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2018/11/fuzzball-desktop-automation/</link>
          <guid>https://peterlyons.com/problog/2018/11/fuzzball-desktop-automation/</guid>
          <description xml:base="https://peterlyons.com/problog/2018/11/fuzzball-desktop-automation/">&lt;p&gt;Here&#x27;s a screencast of my fuzzball desktop automation system. The &lt;a href=&quot;https:&#x2F;&#x2F;gist.github.com&#x2F;focusaurus&#x2F;506fff3d849bd167c5c809f2f12815e1&quot;&gt;accompanying gist is here&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;div class=&quot;youtube-video-container&quot;&gt;
&lt;iframe src=&quot;https:&#x2F;&#x2F;www.youtube-nocookie.com&#x2F;embed&#x2F;nI0jIxzc_YQ&quot; allowfullscreen&gt;&lt;&#x2F;iframe&gt;
&lt;&#x2F;div&gt;
</description>
      </item>
      <item>
          <title>tealeaves gets rust and docker updates</title>
          <pubDate>Mon, 17 Sep 2018 01:00:51 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2018/09/tealeaves-gets-rust-and-docker-updates/</link>
          <guid>https://peterlyons.com/problog/2018/09/tealeaves-gets-rust-and-docker-updates/</guid>
          <description xml:base="https://peterlyons.com/problog/2018/09/tealeaves-gets-rust-and-docker-updates/">&lt;p&gt;So I&#x27;ve been trying to do as much of my local development from within docker containers. Each project I tweak things to be a little bit nicer than the last one. Most recently I went to update my tealeaves ssh key parser utility (which is coded in rust). Initially I was getting all manner of frustrating errors trying to get a reproducible version of rust installed with 1.30, clippy, and rustfmt. I bailed one night in frustration only to later learn that I had left my &lt;code&gt;Cargo.toml&lt;&#x2F;code&gt; modified from an attempt to use a local clone of one of my dependencies, and I had never cloned that repo on my new laptop, so rustc couldn&#x27;t find the files it needed. Once I fixed that silly mistake things mostly started to make more sense. I was able to get a configuration where:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;I can do all my local development in the docker container&lt;&#x2F;li&gt;
&lt;li&gt;Filesystem user is the same in the container and the host, no annoying &lt;code&gt;permission denied&lt;&#x2F;code&gt; errors with root-owned files&lt;&#x2F;li&gt;
&lt;li&gt;Cargo properly caches stuff so every time you stop and start the container you don&#x27;t have to redownload and rebuild the universe.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;So after I had my dockerized development setup working, I went to continue my long-postponed effort to upgrade to the nom v4 crate. I was delighted to discover that in the lengthy interim, all my direct dependencies had already upgraded to nom v4. So I spent a while chasing compilers errors and making the necessary code adjustments. I almost gave up a few times, but eventually the thing compiled! I almost didn&#x27;t believe the terminal. But it compiles now and all the unit tests still pass and it seems to work, so I&#x27;m pleased with that.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Securing local development with containers</title>
          <pubDate>Fri, 13 Jul 2018 08:36:01 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2018/07/securing-local-development-with-containers/</link>
          <guid>https://peterlyons.com/problog/2018/07/securing-local-development-with-containers/</guid>
          <description xml:base="https://peterlyons.com/problog/2018/07/securing-local-development-with-containers/">&lt;p&gt;Starting this Spring when I changed OSes from mac to linux, I decided to experiment with using docker containers to isolate dangerous development tools from my local system. Given the npm malware attack yesterday, this seems like a good time to write up my results so far.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;motivation&quot;&gt;Motivation&lt;&#x2F;h2&gt;
&lt;p&gt;OK I&#x27;ll try not to get overly long-winded here, but let me just state broadly that I think the core fundamental idea in linux that you log in to your system with an effective userid, that userid has read&#x2F;write access to your entire home directory (typically), and when you execute a program, it runs as your userid and therefore generally has read&#x2F;write access to all of your files is utterly and fundamentally misguided and inappropriate. I have some designs that are basically the opposite of this, but I don&#x27;t want to digress into that. Pragmatically, I wanted to find some way to mitigate this geologically-huge security vulnerability using existing tools and not going full-on Stallman.&lt;&#x2F;p&gt;
&lt;p&gt;To just clarify the specific vulnerability here, I&#x27;m talking about running commands like &lt;code&gt;npm install&lt;&#x2F;code&gt; and having that download code from the Internet, some of which is malicious, then executing that malicious code and having it do any one of the following nasty things:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Delete a bunch of your files, either maliciously or due to a bug&lt;&#x2F;li&gt;
&lt;li&gt;Read a bunch of your private files such a ssh private keys and upload them to an attacker-controlled server&lt;&#x2F;li&gt;
&lt;li&gt;Make some subtle and hard-to-detect alteration to some key file&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;container-isolation-basic-approach&quot;&gt;Container Isolation Basic Approach&lt;&#x2F;h2&gt;
&lt;p&gt;So when I had a clean slate I decided to try to mitigate this risk with the following basic tactic:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Each project gets a docker container with a basic shell, node&#x2F;npm, and maybe a few other development tools as needed&lt;&#x2F;li&gt;
&lt;li&gt;npm and node never get executed directly on the host OS, only within the container&lt;&#x2F;li&gt;
&lt;li&gt;The container only gets a filesystem volume mounted with a specific project working directory. It has no access to my home directory, any dotfiles, or any sibling project directories&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;setup-script&quot;&gt;Setup script&lt;&#x2F;h2&gt;
&lt;p&gt;The pattern is similar for most projects, but varies a little bit depending on the tech stack I&#x27;m working with (these days mostly node.js or rust), and the specific needs of the project in terms of tools, environment variables, network ports, etc.&lt;&#x2F;p&gt;
&lt;p&gt;Here&#x27;s a representative sample for a node project. I typically check a file in as &lt;code&gt;bin&#x2F;docker-run.sh&lt;&#x2F;code&gt; to fire up that project&#x27;s docker container.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;#!&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;env bash

# Please Use Google Shell Style: https:&amp;#x2F;&amp;#x2F;google.github.io&amp;#x2F;styleguide&amp;#x2F;shell.xml

# ---- Start unofficial bash strict mode boilerplate
# http:&amp;#x2F;&amp;#x2F;redsymbol.net&amp;#x2F;articles&amp;#x2F;unofficial-bash-strict-mode&amp;#x2F;
set -o errexit    # always exit on error
set -o errtrace   # trap errors in functions as well
set -o pipefail   # don&amp;#x27;t ignore exit codes when piping output
set -o posix      # more strict failures in subshells
# set -x          # enable debugging

IFS=&amp;quot;$(printf &amp;quot;\n\t&amp;quot;)&amp;quot;
# ---- End unofficial bash strict mode boilerplate

cd &amp;quot;$(dirname &amp;quot;$0&amp;quot;)&amp;#x2F;..&amp;quot;
exec docker run --rm --interactive --tty \
  --volume &amp;quot;${PWD}:&amp;#x2F;opt&amp;quot; \
  --workdir &amp;#x2F;opt \
  --env USER=root \
  --env PATH=&amp;#x2F;usr&amp;#x2F;sbin:&amp;#x2F;usr&amp;#x2F;bin:&amp;#x2F;sbin:&amp;#x2F;bin:&amp;#x2F;opt&amp;#x2F;node_modules&amp;#x2F;.bin \
  &amp;quot;node:$(cat .nvmrc)&amp;quot; &amp;quot;${1-&amp;#x2F;bin&amp;#x2F;bash}&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Here&#x27;s some detail on what this does.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;exec docker run&lt;&#x2F;code&gt; runs the docker container. The &lt;code&gt;exec&lt;&#x2F;code&gt; just replaces the shell with the docker process instead of having the shell process stay around waiting.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;--rm&lt;&#x2F;code&gt; delete the container right away instead of leaving useless cruft around gradually filling your filesystem&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;--interactive --tty&lt;&#x2F;code&gt; set this up for an interactive terminal session&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;--volume&lt;&#x2F;code&gt; exposes the project&#x27;s files to the container&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;--workdir&lt;&#x2F;code&gt; puts your shell in the project root right away&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;--env&lt;&#x2F;code&gt; sets environment variables. You may need to set things like &lt;code&gt;HOME&lt;&#x2F;code&gt; or &lt;code&gt;USER&lt;&#x2F;code&gt;, maybe not. For node, adding &lt;code&gt;&#x2F;opt&#x2F;node_modules&#x2F;.bin&lt;&#x2F;code&gt; to your &lt;code&gt;PATH&lt;&#x2F;code&gt; can be handy so you can avoid the silly &lt;code&gt;npm install -g&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;For node, I get the desired node version from my &lt;code&gt;.nvmrc&lt;&#x2F;code&gt; file in the project root&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;--publish 9229:9229&lt;&#x2F;code&gt; is handy to enable devtools debugging to work&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;--publish 3000:3000&lt;&#x2F;code&gt; is what you need for a node server project that listens on port 3000&lt;&#x2F;li&gt;
&lt;li&gt;The &lt;code&gt;${1-&#x2F;bin&#x2F;bash}&lt;&#x2F;code&gt; means when no arguments are passed, run bash, but if an argument is passed, run that instead. Generally I don&#x27;t need that but I can do &lt;code&gt;.&#x2F;bin&#x2F;docker-run.sh node server.js&lt;&#x2F;code&gt; for example if I know I want to run the server.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;how-well-does-it-work&quot;&gt;How well does it work?&lt;&#x2F;h2&gt;
&lt;p&gt;So far all the basic stuff is working OK. Running npm works, running node works, debugging works OK, running an http server works. Terminal colors work. Arrow keys work. Bash history searching works (at least for a given shell session).&lt;&#x2F;p&gt;
&lt;p&gt;One gripe I have, which I could remedy I just haven&#x27;t gotten around to it yet is in the container I have a vanilla bash configuration without my normal toolbox of zsh and dozens of aliases, functions, settings, etc. Usually I&#x27;m only running 3 or 4 commands in the container in a tight loop, and arrow keys and history searching work fine, so it&#x27;s OK. However, bash history of commands in the container does not persist, so if I come up with a useful long command line, I need to take special action to capture it in a script or my notes.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;further-isolation&quot;&gt;Further isolation&lt;&#x2F;h2&gt;
&lt;p&gt;This is where I am at the moment, but of course as with all security efforts, there&#x27;s an endless list of additional measures that could be taken. Here&#x27;s the next few things I plan to look at.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Use a non-root user in the container&lt;&#x2F;li&gt;
&lt;li&gt;Get stricter with docker capability limitations&lt;&#x2F;li&gt;
&lt;li&gt;Maybe run git in the container&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Right now I&#x27;m only running npm and &lt;code&gt;node my-project.js&lt;&#x2F;code&gt; within the container (or cargo for a rust project). I trust git a lot more than I do npm, but I think with git hooks ultimately the same vulnerability exists with git, so I&#x27;d like to run that in the container. However, there&#x27;s a few kinks to work out in terms of filesystem userids, ssh agent access for pull&#x2F;push, etc.&lt;&#x2F;p&gt;
&lt;p&gt;I hope you found this interesting and useful. Stay safe out there!&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>The Art of the node.js Rescue</title>
          <pubDate>Wed, 11 Jul 2018 04:53:53 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2018/07/the-art-of-the-node-js-rescue/</link>
          <guid>https://peterlyons.com/problog/2018/07/the-art-of-the-node-js-rescue/</guid>
          <description xml:base="https://peterlyons.com/problog/2018/07/the-art-of-the-node-js-rescue/">&lt;p&gt;I&#x27;ve recently been helping a client get their node.js mobile back end API server ready to launch a new service. In this post I&#x27;ll outline the guidelines I use when I&#x27;m brought in to true-up a problematic node.js codebase.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;first-triage-and-repair-server-basics&quot;&gt;First, triage and repair server basics&lt;&#x2F;h2&gt;
&lt;p&gt;Before any real rescue efforts can happen, I need to work through the fundamental issues and get up to a bare minimum of a working project and service.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Is the code even in a git repository?&lt;&#x2F;strong&gt; I&#x27;ve only encountered the &quot;here&#x27;s the zip file the previous agency delivered&quot; once, but step one is get the existing codebase without any new modifications into an initial git commit and get it pushed to a git host.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Does it have documentation?&lt;&#x2F;strong&gt; Can a new developer get from &lt;code&gt;git clone&lt;&#x2F;code&gt; to a running server using just the README (no slack allowed!)? If not, I have to reverse engineer that and document it in the README as I figure it out. As I come to understand the basic software stack, third party service integrations, deployment setup, etc, all that gets documented.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Are the dependencies accurately tracked?&lt;&#x2F;strong&gt;
Usually they are but if not I fix &lt;code&gt;package.json&lt;&#x2F;code&gt; and generate a new &lt;code&gt;packge-lock.json&lt;&#x2F;code&gt; as necessary.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Does the server start and run?&lt;&#x2F;strong&gt; If not, I need to get to that milestone. Even though this sounds like a basic thing any project would have starting at minute 4 of its lifetime, in my experience many node projects get carried away with bullshit fancy code to handle configuration, clustering, process supervision, fancy logging setups, etc, and often that code is misguided or just flat out broken.&lt;&#x2F;p&gt;
&lt;p&gt;OK if the server will start and listen for HTTP requests, I can switch into my normal course of treatment to get it functioning well.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;automated-unit-tests-are-the-foundation&quot;&gt;Automated unit tests are the foundation&lt;&#x2F;h2&gt;
&lt;p&gt;The next phase is setting up a good automated unit test stack. These days I reach for &lt;code&gt;tap&lt;&#x2F;code&gt;, &lt;code&gt;supertest&lt;&#x2F;code&gt;, and &lt;code&gt;nock&lt;&#x2F;code&gt; as my first choice libraries. Here are the important points to achieve:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Unit tests should run locally&lt;&#x2F;li&gt;
&lt;li&gt;Tests should be fast and deterministic&lt;&#x2F;li&gt;
&lt;li&gt;Running partial sets of tests at any granularity should be straightforward&lt;&#x2F;li&gt;
&lt;li&gt;Tests should be runnable under the devtools debugger&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The main bit of work is finding the right set of libraries, helper functions, and setup&#x2F;teardown code that make sense for the service. I usually test the HTTP interface via &lt;code&gt;supertest&lt;&#x2F;code&gt; because the code to call an API endpoint via supertest is concise enough to be basically in the same category as just calling a function with arguments. Since the tests are coded against the same stable API interface that the web or mobile front ends use, this is a stable integration point and I can typically overhaul or rewrite an endpoint implementation without changing its HTTP interface. Usually API endpoints are not that much code, but if you do have a really complex endpoint, go ahead and write unit tests for the various internal implementation functions.&lt;&#x2F;p&gt;
&lt;p&gt;Once the tests are working locally, I&#x27;ll set up continuous integration so they work on pull requests and are integrated into github.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;why-automated-unit-tests&quot;&gt;Why automated unit tests?&lt;&#x2F;h3&gt;
&lt;p&gt;If I put on my cynical hat for a moment, I could sum up the bulk of my consultancy as &quot;I come in after non-unit-testing developers and get their code working by writing tests&quot;. Yes, that&#x27;s a cynical characterization but there&#x27;s a kernel of painful truth there.&lt;&#x2F;p&gt;
&lt;p&gt;JavaScript as a language has near-zero support for writing correct programs. It allows and encourages us to write code that does not get even the most basic analysis for correctness. On a typical low-quality node.js server codebase, about the only guarantee likely to actually be upheld is that every file in the require dependency graph is syntactically valid javascript, and absolutely nothing beyond that is guaranteed. ReferenceErrors and TypeErrors are almost guaranteed to exist in large quantity. There&#x27;s a plague of code out there crashing in production that was clearly never run: not on the developer&#x27;s laptop, not in CI, no one tested it in QA. First execution is on the production server crashing when triggered by an end user.&lt;&#x2F;p&gt;
&lt;p&gt;Putting on my less-cynical hat, I mostly still believe a well-tested node.js codebase is something you can reasonably deliver to a client, and you can point to some pragmatic realities it offers:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Large set of developers able to work with it&lt;&#x2F;li&gt;
&lt;li&gt;Enormous ecosystem of available libraries&lt;&#x2F;li&gt;
&lt;li&gt;Good to great speed of developing features&lt;&#x2F;li&gt;
&lt;li&gt;Good to great performance at runtime&lt;&#x2F;li&gt;
&lt;li&gt;Excellent tooling throughout the software development lifecycle&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;However, these &lt;strong&gt;only&lt;&#x2F;strong&gt; hold true if you have solid test coverage. Untested javascript is such a massive liability and a terrible-odds gamble that I think we as a community working with this technology stack need to take a hard and clear stance and make the following statement:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Untested javascript is not a viable deliverable for professional software development.&lt;&#x2F;strong&gt; Viable professional javascript &lt;strong&gt;must&lt;&#x2F;strong&gt; be delivered with extensive tests.&lt;&#x2F;p&gt;
&lt;p&gt;Untested javascript is just incredibly likely to be rife with bugs and comes with enormous cost and risk to any refactoring. As agencies, consultants, and employees we need to stop delivering it. Clients need to be educated to insist on a working automated test suite running in a continuous integration system as a baseline deliverable. I would say this is analogous to a plumber leaving a job without ever having put running water through the system.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;establishing-patterns-and-antipatterns&quot;&gt;Establishing Patterns and Antipatterns&lt;&#x2F;h2&gt;
&lt;p&gt;A node server codebase lends itself to boilerplatey patterns repeated across a lot of endpoints. I generally set up a dedicated set of example routes to establish the new, correct code patterns with good examples. These of course have full unit test coverage and the idea is to have clean patterns for input validation, control flow, error handling, logging, etc.&lt;&#x2F;p&gt;
&lt;p&gt;There are also usually repeated antipatterns. Of course, a well-applied middleware could potentially eliminate a whole class of boilerplate, so that&#x27;s the ideal target, but often times I find little micro-antipatterns in how the DB is queried or promises are used, etc. I code up examples that illustrate how these are broken and point to the corrected patterns that should be applied when making changes to particular endpoints.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;tracking-and-fixing-bugs&quot;&gt;Tracking and fixing bugs&lt;&#x2F;h2&gt;
&lt;p&gt;Once the unit testing stack is solid, I begin the main phase of the real work here which is going through the API endpoints and identifying where the bugs and issues are. The unit tests are the guide here and the work should be prioritized using whatever information is available. Focus on the high-importance or high-frequency API calls first and leave the ancillary and supporting calls until later.&lt;&#x2F;p&gt;
&lt;p&gt;The key tools for this include basic logging, a bug tracking tool, and optionally an error tracking service such as Sentry. The process loop I will repeat many times in this phase will look more or less as follows:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Identify a potential bug via an error in the log file, a server crash with stack trace, or a specific API response that is known to be incorrect&lt;&#x2F;li&gt;
&lt;li&gt;File a bug for the issue in the bug tracker with the relevant details so it can be understood and reproduced&lt;&#x2F;li&gt;
&lt;li&gt;Reproduce the failure in a unit test. Be sure to do this before making any changes to the relevant application code.&lt;&#x2F;li&gt;
&lt;li&gt;Once reproduced in a failing test, code a fix for the issue&lt;&#x2F;li&gt;
&lt;li&gt;Guide the fix through delivery and mark as resolved&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;resist-the-temptation-to-rewrite-and-overhaul&quot;&gt;Resist the temptation to rewrite and overhaul&lt;&#x2F;h2&gt;
&lt;p&gt;When faced with a nasty codebase, one may feel discomfort, frustration, and anxiety about the state of the code. These feelings can make the following things really tempting:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Start a new codebase entirely&lt;&#x2F;li&gt;
&lt;li&gt;Bulk update all the dependencies to latest&lt;&#x2F;li&gt;
&lt;li&gt;Update to the latest node.js and npm&lt;&#x2F;li&gt;
&lt;li&gt;Do some drastic modification across the entire codebase&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;My advice here is to resist this temptation. All of these activities I think serve the developer&#x27;s emotions at the expense of value to the client.&lt;&#x2F;p&gt;
&lt;p&gt;A rewrite discards any latent value in the codebase and forces the client to pay again from scratch for development of the functionality of the server. In extreme cases, this can be the only reasonable way forward, but 98% of the time the codebase can be salvaged. If you make a recommendation to your client to rewrite, make sure you have a compelling cost&#x2F;benefit analysis. And on the other side, be aware of how the sunk cost fallacy can factor in your decision to continue with an existing codebase. Of course, if you do need to make a recommendation to the client to embark on a rewrite, prepare a thorough case study of how specifically the first attempt failed and how each of those failures will be specifically avoided in the rewrite.&lt;&#x2F;p&gt;
&lt;p&gt;The goal of this type of rescue is to make the software stable and reliable. Just bulk updating all the dependencies is almost certainly going introduce novel bugs and work counter to that goal. It&#x27;s fine to update specific and particular dependencies when you have a concrete reason to do so, but just updating things in bulk for general &quot;hygiene&quot; is not appropriate in this situation, and without solid unit tests, you have no indication what has continued to work, continued to be broken, been fixed, or been broken in a novel way.&lt;&#x2F;p&gt;
&lt;p&gt;The same logic applies to updating the node.js version. Until you have a substantial set of unit tests, it&#x27;s totally in conflict with the project goals to do this.&lt;&#x2F;p&gt;
&lt;p&gt;As you add unit tests and increase code coverage, at some point it becomes safer to make broad updates and changes. Exactly where that point is at is a judgement call, but I recommend being conservative here, erring on the side of more tests.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;code-autoformatting-tools&quot;&gt;Code autoformatting tools&lt;&#x2F;h2&gt;
&lt;p&gt;I&#x27;m a huge fan of autoformatting tools (&lt;code&gt;prettier&lt;&#x2F;code&gt;, &lt;code&gt;eslint --fix&lt;&#x2F;code&gt;, etc) and they&#x27;ve become pretty core to how I work. I don&#x27;t worry about formatting issues when I&#x27;m actually typing code, I just hit &lt;code&gt;cmd+f&lt;&#x2F;code&gt; and trust it to make the code pretty.&lt;&#x2F;p&gt;
&lt;p&gt;However, on an existing codebase that has not been using an autoformatter, I recommend caution. I would wait until automated unit test coverage is fairly high before considering running autoformatting across an entire codebase. And when doing so, be aware that this will essentially discard the existing git history and lose track of who wrote which code when (git blame, etc). This is a pretty steep trade-off. I personally don&#x27;t value git history that much so I have my own point where I&#x27;m OK running an autoformatter on a whole codebase, just be sure to understand the trade-offs when making this decision.&lt;&#x2F;p&gt;
&lt;p&gt;If you do have valuable history in git that you don&#x27;t want to mess up, another strategy to consider is file-by-file extraction of code into a set that is autoformatted and a set that is left untouched.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;linting&quot;&gt;Linting&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;eslint&lt;&#x2F;code&gt; and particularly &lt;a href=&quot;https:&#x2F;&#x2F;www.npmjs.com&#x2F;package&#x2F;eslint-stats&quot;&gt;eslint-stats&lt;&#x2F;a&gt; can be helpful guides. However, due to concerns about adding bugs in untested code, I don&#x27;t recommend changing the code based on eslint warnings&#x2F;errors until after you have unit tests coverage. These can help identify troublesome areas in the code, but don&#x27;t go into the codebase and fix eslint issues throughout without unit test coverage.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;a-side-note-about-promises&quot;&gt;A side note about promises&lt;&#x2F;h2&gt;
&lt;p&gt;Async control flow in node.js is hard. It&#x27;s hard enough that many developers never really learn to write it correctly. This is true both in the callback paradigm and with promises as well. However, promises in particular seem to be an area of pervasive misuse and misunderstanding. Every node.js codebase I&#x27;ve found that uses promises has incorrect promise usage baked into core patterns and then repeatedly copied throughout the codebase. As a community, we really missed the mark with promises when it comes to education and tooling. Even with the eslint promise plugin, there&#x27;s a huge number of issues that are plain as day to me and no eslint plugin I&#x27;ve found even detects them as warnings. I&#x27;m optimistic that as &lt;code&gt;async&#x2F;await&lt;&#x2F;code&gt; takes over as the basic mechanism for async control flow, things will improve for the easy case of a series of sequential operations. However, I think we&#x27;ll still have a mess to deal with for any case with looping or complex control flow patterns.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;node-js-rescue-checklist&quot;&gt;node.js rescue checklist&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;Get the code into a git repository&lt;&#x2F;li&gt;
&lt;li&gt;Document key processes in the README
&lt;ul&gt;
&lt;li&gt;Initial developer setup&lt;&#x2F;li&gt;
&lt;li&gt;Standard development task flow&lt;&#x2F;li&gt;
&lt;li&gt;Overview of tech stack, deployment, integrations&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;Ensure dependencies are properly tracked and documented&lt;&#x2F;li&gt;
&lt;li&gt;Ensure the server starts and runs&lt;&#x2F;li&gt;
&lt;li&gt;Establish a core unit testing stack&lt;&#x2F;li&gt;
&lt;li&gt;Get tests running in CI&lt;&#x2F;li&gt;
&lt;li&gt;Set up logging and maybe an error tracking service&lt;&#x2F;li&gt;
&lt;li&gt;Set up code coverage reports&lt;&#x2F;li&gt;
&lt;li&gt;Set up linting&lt;&#x2F;li&gt;
&lt;li&gt;Track bugs in a bug tracker&lt;&#x2F;li&gt;
&lt;li&gt;Reproduce bugs in unit tests&lt;&#x2F;li&gt;
&lt;li&gt;Add unit tests to get substantial code coverage&lt;&#x2F;li&gt;
&lt;li&gt;Set up autoformatting&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</description>
      </item>
      <item>
          <title>Linux Mint 19 Cinnamon</title>
          <pubDate>Wed, 04 Jul 2018 19:48:31 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2018/07/linux-mint-19-cinnamon/</link>
          <guid>https://peterlyons.com/problog/2018/07/linux-mint-19-cinnamon/</guid>
          <description xml:base="https://peterlyons.com/problog/2018/07/linux-mint-19-cinnamon/">&lt;h2 id=&quot;xfce4-problems&quot;&gt;xfce4 problems&lt;&#x2F;h2&gt;
&lt;p&gt;I had some problems with xfce4 that eventually made it untenable.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;No way to make &lt;code&gt;ctrl-w&lt;&#x2F;code&gt; always close browser tab instead of being an emacs kill word keybinding&lt;&#x2F;li&gt;
&lt;li&gt;xfwm4 would occassionally lock up and prevent me from dragging windows around. I had to run &lt;code&gt;xfwm4 --replace&lt;&#x2F;code&gt; to fix it. This is kind of a deal breaker for a window manager.&lt;&#x2F;li&gt;
&lt;li&gt;Reordering window buttons in the window list by drag and drop didn&#x27;t work&lt;&#x2F;li&gt;
&lt;li&gt;I couldn&#x27;t find good keyboard shortcuts to switch tabs in xfce4-terminal&lt;&#x2F;li&gt;
&lt;li&gt;Sometimes on resume, wifi wouldn&#x27;t work&lt;&#x2F;li&gt;
&lt;li&gt;Laptop would not suspend properly if screensaver was active (so ridiculous)&lt;&#x2F;li&gt;
&lt;li&gt;Keyboard shortcuts and other keyboard settings scattered around like 4 different settings apps
&lt;ul&gt;
&lt;li&gt;There&#x27;s even 2 different apps for &quot;Window Manager Settings&quot; and &quot;Window Manager Tweaks&quot; FFS. Let&#x27;s just send users on scavenger hunts recreationally.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;linux-mint-19-cinnamon&quot;&gt;Linux Mint 19 Cinnamon&lt;&#x2F;h2&gt;
&lt;p&gt;After experimenting with the live USB image in beta, Linux Mint 19 Cinnamon went full release recently and I installed that same day. Even though I&#x27;ve run linux on and off since ~1999, this was the first time I kept my home directory on a separate partition and could reinstall the OS without wiping my home directory. This has turned out to be awesome because linux does a pretty good job of keeping most personal settings in your home directory, so even after the reinstall, a lot of things continued to have my customizations. For example the actions assigned to my extra mouse buttons are configured in &lt;code&gt;~&#x2F;.xbindkeysrc&lt;&#x2F;code&gt; and that survives the reinstall just fine.&lt;&#x2F;p&gt;
&lt;p&gt;So now I&#x27;ve got lots of nice things working in Cinnamon.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ctrl+a&lt;&#x2F;code&gt; does select all properly
&lt;ul&gt;
&lt;li&gt;I just use &lt;code&gt;home&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;end&lt;&#x2F;code&gt; instead of emacs keybindings as needed&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;ctrl+w&lt;&#x2F;code&gt; closes a chrome tab properly&lt;&#x2F;li&gt;
&lt;li&gt;I can close the laptop lid and know the OS will actually suspend&lt;&#x2F;li&gt;
&lt;li&gt;pomodoro applet in my panel&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;super+right&lt;&#x2F;code&gt;, &lt;code&gt;super+left&lt;&#x2F;code&gt; to switch tabs in gnome-terminal&lt;&#x2F;li&gt;
&lt;li&gt;can reorder window buttons in the window list&lt;&#x2F;li&gt;
&lt;li&gt;gpaste clipboard history and panel applet&lt;&#x2F;li&gt;
&lt;li&gt;System Settings GUI is more clearly organized and usable&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;I also tweaked my ergodox layout to give me F11 and F12 keys which I use to switch to adjacent workspaces, and I also needed a more accessiblyALT modifier, which generally I try to avoid but it&#x27;s useful for some things. I really wish I could make &lt;code&gt;super+right&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;supert+left&lt;&#x2F;code&gt; switch tabs in chrome.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Linux Setup Progress</title>
          <pubDate>Sat, 21 Apr 2018 22:42:02 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2018/04/linux-setup-progress/</link>
          <guid>https://peterlyons.com/problog/2018/04/linux-setup-progress/</guid>
          <description xml:base="https://peterlyons.com/problog/2018/04/linux-setup-progress/">&lt;p&gt;Here&#x27;s some miscellaneous notes on what I&#x27;ve gotten set up on my new linux laptop so far.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;distribution-and-desktop-environment&quot;&gt;Distribution and Desktop Environment&lt;&#x2F;h2&gt;
&lt;p&gt;I tried 3:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;PopOS (ubuntu tweaked for system76, gnome-shell desktop)&lt;&#x2F;li&gt;
&lt;li&gt;Xubuntu (ubuntu with Xfce4 desktop)&lt;&#x2F;li&gt;
&lt;li&gt;Linux Mint Xfce4 (ubuntu with xfce4 desktop)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;I like to think that I don&#x27;t care that much about my desktop environment but I couldn&#x27;t find a good clipboard manager for gnome shell and that was enough to make my try some others.&lt;&#x2F;p&gt;
&lt;p&gt;I don&#x27;t know what went on with my Xubuntu install but it was a disaster. No gui to set up wifi out of the box and weird crashiness. I ran away screaming.&lt;&#x2F;p&gt;
&lt;p&gt;Linux Mint Xfce4 edition is where I landed and so far is working OK.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;text-snippets&quot;&gt;Text Snippets&lt;&#x2F;h2&gt;
&lt;p&gt;On mac I had a keyboard maestro macro that I loved that handled text expansion really nicely. I triggered it with &lt;code&gt;,,&lt;&#x2F;code&gt; and it would regex match the previous abbrevation, so to type my email I would type &lt;code&gt;em,,&lt;&#x2F;code&gt; and it would replace that with my email, based on the contents of &lt;code&gt;~&#x2F;projects&#x2F;snippets&#x2F;em&lt;&#x2F;code&gt;. It was really easy to add new ones on the fly, replace them, etc.&lt;&#x2F;p&gt;
&lt;p&gt;I haven&#x27;t found a way to exactly match that trigger mechanism on linux (and honestly it took me a long time to arrive at that final solution on the mac). But I found something also great, just different. I have an xfce4 keyboard shortcut &lt;code&gt;ctrl-,&lt;&#x2F;code&gt; which runs a shell script I wrote which basically populates &lt;a href=&quot;https:&#x2F;&#x2F;git.suckless.org&#x2F;dmenu&#x2F;&quot;&gt;dmenu&lt;&#x2F;a&gt; with all of my abbreviations. This is nice because I get visual completion of the abbreviations. Once dmenu has handled selecting the abbreviation, my script reads the corresponding file and copies its content into the x11 clipboard via &lt;code&gt;xclip&lt;&#x2F;code&gt; then immediately pastes it as well via &lt;code&gt;xdotool&lt;&#x2F;code&gt; typing &lt;code&gt;ctrl-v&lt;&#x2F;code&gt; to complete the expansion. Also this dmenu approach means there&#x27;s no abbreviation in the target app to delete. The main trick I had to figure out was terminal needs &lt;code&gt;ctrl-shift-v&lt;&#x2F;code&gt; to paste which is annoying. Here&#x27;s a snippet of how I figured that out (get the PID of the current window and see if it&#x27;s command is my terminal emulator)&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sh&quot; class=&quot;language-sh &quot;&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;
#check for terminal or not
pid=$(xdotool getactivewindow getwindowpid)
exec=$(cat &amp;#x2F;proc&amp;#x2F;${pid}&amp;#x2F;cmdline)
if [[ &amp;quot;${exec}&amp;quot; == &amp;quot;xfce4-terminal&amp;quot; ]]; then
  xdotool key ctrl+shift+v
else
  # paste into the active window via keyboard shortcut
  xdotool key ctrl+v
fi
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;commander&quot;&gt;Commander&lt;&#x2F;h2&gt;
&lt;p&gt;Commander is my heads-up-display style app where I trigger fancy scripts by name. Think shell but powered by python. I found a really great fit for this in &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;lanoxx&#x2F;tilda&quot;&gt;tilda&lt;&#x2F;a&gt; which I have bound to &lt;code&gt;super-space&lt;&#x2F;code&gt; running commander it and works great.&lt;&#x2F;p&gt;
&lt;p&gt;Things are different enough that I might rethink whether I really need a long-running python process. I might port commander from python to node and the startup time might be fast enough to just run every command as a separate process either just shell or node.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;docker&quot;&gt;Docker&lt;&#x2F;h2&gt;
&lt;p&gt;This time around my plan is to do development with command line developer tools like node, npm, pip, etc always run in a docker container with a volume mount to just one directory containing a specific project, not my entire filesystem or entire home directory. I think it will mostly work but there might be some hassles and kinks.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;sticky-keys&quot;&gt;Sticky Keys&lt;&#x2F;h2&gt;
&lt;p&gt;Sticky keys setup is actually super easy on Xfce4: there&#x27;s a checkbox in the accessibility settings. Done. The only thing I had to tweak was to add an indicator widget to the xfce4 panel to show when modifier keys are stuck down. That package is called &lt;code&gt;indicator-xkbmod&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;function-key-app-hotkeys&quot;&gt;Function Key App Hotkeys&lt;&#x2F;h2&gt;
&lt;p&gt;I&#x27;m used to mapping my function keys to apps: F1 is browser, F2 is terminal, etc. Now that I have multiple desktops again, I &lt;em&gt;might&lt;&#x2F;em&gt; move to some different approach, but so far I&#x27;m trying to recreate that system. I have something that sort of works via &lt;code&gt;wmctrl&lt;&#x2F;code&gt; and &lt;code&gt;xprop&lt;&#x2F;code&gt; but there&#x27;s some quirks I still need to figure out.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fewer-modifiers&quot;&gt;Fewer modifiers&lt;&#x2F;h2&gt;
&lt;p&gt;I&#x27;m realizing how nice it is on mac to have 2 good modifier keys: command and control (which I have bound to my caps lock key). I guess I should think about swapping my super and alt keys so super gets the prime real estate on either side of the space bar and alt can be next to that which is in the unreachable palm of your hand area.&lt;&#x2F;p&gt;
&lt;p&gt;In any case, my atom keybindings are going through a chaotic reorganization but hopefully everything will settle down soon.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;emacs-key-bindings&quot;&gt;Emacs Key Bindings&lt;&#x2F;h2&gt;
&lt;p&gt;On mac, &lt;code&gt;ctrl-a&lt;&#x2F;code&gt; and &lt;code&gt;ctrl-e&lt;&#x2F;code&gt; will move the cursor the the start and end of a line (borrowing emacs default keybindings) and this works everywhere: terminal, GUI text editors, browsers, Apple Apps, etc. I&#x27;m realizing on linux neither firefox nor chrome do this by default. Haven&#x27;t looked into solving via extensions yet but either I&#x27;ll do that or I&#x27;ll become re-accustomed to using my hardware home&#x2F;end keys again.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fonts&quot;&gt;Fonts&lt;&#x2F;h2&gt;
&lt;p&gt;Oh my God the default resolution&#x2F;zoom&#x2F;font situation is letters for ants small. For chrome I set the default zoom way high. For atom I set my theme font to the largest allowed value of 20pt and it&#x27;s usable but still on the small side.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xfce4-settings-dialogs&quot;&gt;Xfce4 Settings Dialogs&lt;&#x2F;h2&gt;
&lt;p&gt;I&#x27;ll chalk this one up to the absence of designers from the linux world, but every settings app in Xfce4 is designed as if it were a dialog box (which it isn&#x27;t, it&#x27;s a standalone application), and by default comes up occupying like 20% of the screen width and requiring tons of horizontal scrolling. I&#x27;m in the habit of only working with maximized windows, so I find this to be a nuisance, but at least a quick &lt;code&gt;ctrl-m&lt;&#x2F;code&gt; maximizes them so they are usable.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Switching Back to Linux</title>
          <pubDate>Mon, 16 Apr 2018 03:31:48 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2018/04/switching-back-to-linux/</link>
          <guid>https://peterlyons.com/problog/2018/04/switching-back-to-linux/</guid>
          <description xml:base="https://peterlyons.com/problog/2018/04/switching-back-to-linux/">&lt;p&gt;So I bought a laptop from System76 with linux and I&#x27;m slowly getting my tools and environment going. I&#x27;ve been using macos on macbook for a while, maybe 8 years or so, not sure. I&#x27;m pretty dang adjusted to it, and so far the shift back to linux has been pretty jarring. If I wait too long to write down thoughts, I&#x27;ll skip them, so here&#x27;s some stream-of-consciousness notes so far.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-new-laptop-hardware&quot;&gt;The New Laptop Hardware&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;nice
&lt;ul&gt;
&lt;li&gt;light weight&lt;&#x2F;li&gt;
&lt;li&gt;small and light power adapter&lt;&#x2F;li&gt;
&lt;li&gt;normal plug on the power adapter like a lamp not a giant heavy brick that falls out of the socket&lt;&#x2F;li&gt;
&lt;li&gt;Keyboard has dedicated delete key, home, end, page up&#x2F;down all of which I like (macbooks don&#x27;t have these)&lt;&#x2F;li&gt;
&lt;li&gt;For the price, I get a lot more ram, SSD, disk, CPU, and ports&lt;&#x2F;li&gt;
&lt;li&gt;no dongles&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;not nice
&lt;ul&gt;
&lt;li&gt;space bar on the keyboard is glitchy&lt;&#x2F;li&gt;
&lt;li&gt;trackpad is really really terrible. I&#x27;m not sure I&#x27;ll be able to use it effectively. Probably need a dedicated mouse that I always bring with my laptop.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;software-stuff&quot;&gt;Software Stuff&lt;&#x2F;h2&gt;
&lt;p&gt;I have a pretty long list of stuff I need to find linux equivalents for.&lt;&#x2F;p&gt;
&lt;p&gt;The password manager situation is bleak. I use 1Password on my mac, but there&#x27;s no native linux app. They have a chrome extension that works on linux, but not with local files, only with a paid cloud hosted account. I searched a long time for any viable process to get from 1Password to KeyPass without developing a new import&#x2F;export tool and it looks like there&#x27;s nothing ready to go, so I ponied up for the cloud account in the name of expediency. I&#x27;ll probably try to write an import&#x2F;export tool later but I&#x27;m pretty much dead in the water without a working password manager so I wanted to get unblocked on that.&lt;&#x2F;p&gt;
&lt;p&gt;It took a while for me to find the right commands to adjust the keyboard repeat settings, which it turns out I need to be in a very precise configuration or I find the keyboard unusable (this is the same for me on any computer). If I&#x27;m working on a friend&#x27;s laptop with slow key repeat for more than 5 minutes I get frustrated and have to configure their setting. I did eventually find what I need and also found a way to get them to run when I log in so that&#x27;s square now. Oh and I found the equivalent of &quot;sticky keys&quot;. It&#x27;s not as nice as on macos because there&#x27;s no on-screen indication of stuck keys and AFAIK so far no easy way to turn it on&#x2F;off but it&#x27;s doing the main thing fine.&lt;&#x2F;p&gt;
&lt;p&gt;For launching and activating applications with hotkeys I&#x27;m using gnome keyboard shortcuts that run a script that uses &lt;code&gt;wmctrl&lt;&#x2F;code&gt; to find the intended window and activate it. This is so far a reasonably good substitute for my equivalent keyboard maestro macros.&lt;&#x2F;p&gt;
&lt;p&gt;For my heads-up commander terminal, I easily found &lt;code&gt;tilda&lt;&#x2F;code&gt; which is basically exactly what I was looking for. Luckily commander is python and largely cross platform.&lt;&#x2F;p&gt;
&lt;p&gt;WiFi &quot;just worked&quot; for real this time, I&#x27;m pleased to report, even the captive portal at the coffee shop from which I write this post. Well, that&#x27;s mostly true in that gnome detected the captive portal, but the actual portal consent page only worked in chrome not firefox.&lt;&#x2F;p&gt;
&lt;p&gt;The screen and fonts have been a challenge so far. In general everything is too small, which I can solve probably with some combination of zoom settings and maybe a lower resolution but I want to do some more research before implementing something. I&#x27;ve been hacking around it with a few font size increases and some browser zooming.&lt;&#x2F;p&gt;
&lt;p&gt;In general the fonts look bad compared to macos but that&#x27;s something that I adjust to quickly and stop noticing. Also that old familiar X windows pointer just looks pretty sorry. I&#x27;m not sure why but I have a mental association with that mouse pointer icon and craptastic software.&lt;&#x2F;p&gt;
&lt;p&gt;That&#x27;s where I am now. It&#x27;s more or less usable but I still have a pretty big laundry list of stuff to configure before I can start doing any actual project work on this laptop.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Denver Rust Meetup Reactivated</title>
          <pubDate>Thu, 01 Mar 2018 06:45:50 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2018/03/denver-rust-meetup-reactivated/</link>
          <guid>https://peterlyons.com/problog/2018/03/denver-rust-meetup-reactivated/</guid>
          <description xml:base="https://peterlyons.com/problog/2018/03/denver-rust-meetup-reactivated/">&lt;p&gt;So I just went to my first meetup of the Denver&#x2F;Boulder Rust meetup group. The group has been around a while but went inactive for over a year. I was and am really excited to get it going again. The meetup went really well! We had about 20 attendees and folks seemed pretty engaged and interested. One of our planned speakers canceled at the last minute due to a family emergency, but Joel Dice and I just did what software developers normally do: expand time spent to fill up time allocated! Just kidding (kind of)! Actually, I only went 4 minutes over my 20 minute slot which for me was a personal best as without multiple practices my default length for &quot;talk about tech topic X&quot; is 90 minutes before I start too realize I&#x27;m talking too long.&lt;&#x2F;p&gt;
&lt;p&gt;After the talks we grouped up to work on stuff and I paired with &lt;a href=&quot;https:&#x2F;&#x2F;twitter.com&#x2F;DebugSteven&quot;&gt;@DebugSteven&lt;&#x2F;a&gt; on trying to get the rustdoc HTML docs to stay collapsed if you click the collapser minus sign and reload the page (currently they forget and show up expanded again). We got a simple change coded and I&#x27;ll be submitting a pull request shortly. Well, maybe not so shortly because it seems the &lt;code&gt;main.js&lt;&#x2F;code&gt; file we changed is actually part of the main &quot;rust&quot; repo with the entire language and a bunch of other tools. So even for me to do a &lt;code&gt;python x.py doc&lt;&#x2F;code&gt; seems to required several CPU-hours of compilation.&lt;&#x2F;p&gt;
&lt;p&gt;I ordered too much food. I knew I was going to. On the phone I thought &quot;I&#x27;m about to order too much food&quot;. Then I hung up and thought &quot;I just ordered too much food&quot;. I don&#x27;t really know how this works, but luckily folks were willing to take all the leftovers home so I didn&#x27;t end up having to deal with that.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>StackOverflow 100K</title>
          <pubDate>Thu, 15 Feb 2018 13:16:57 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2018/02/stackoverflow-100k/</link>
          <guid>https://peterlyons.com/problog/2018/02/stackoverflow-100k/</guid>
          <description xml:base="https://peterlyons.com/problog/2018/02/stackoverflow-100k/">&lt;p&gt;I just crossed 100,000 reputation on stackoverflow. I&#x27;ve posted 1618 answers. Most of this activity was around node.js and javascript, especially from when I was really focused on learning a new ecosystem between 2011 and 2014. Analytics indicate my answers have reached 6.3 million people, which I&#x27;m very pleased with.&lt;&#x2F;p&gt;
&lt;p&gt;I earned gold badges for javascript, node.js, and express. My favorite badge is &quot;Necromancer&quot; (answer a question more than 60 days later with a score of 5 or more), which I earned 11 times.&lt;&#x2F;p&gt;
&lt;p&gt;The past two years or so have been mostly compound interest. A lot of it is from answering really, really basic questions early enough to become a repeat hit in search queries. The answer I put the most effort into is &lt;a href=&quot;https:&#x2F;&#x2F;stackoverflow.com&#x2F;a&#x2F;19623507&#x2F;266795&quot;&gt;how to structure an express.js application&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;stackoverflow.com&#x2F;users&#x2F;266795&#x2F;peter-lyons&quot;&gt;Here&#x27;s my stackoverflow profile&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Hourly Billing Is OK</title>
          <pubDate>Sat, 13 Jan 2018 18:01:42 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2018/01/hourly-billing-is-ok/</link>
          <guid>https://peterlyons.com/problog/2018/01/hourly-billing-is-ok/</guid>
          <description xml:base="https://peterlyons.com/problog/2018/01/hourly-billing-is-ok/">&lt;h2 id=&quot;tl-dr&quot;&gt;TL;DR&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;fixed bid, value-priced business consulting is OK&lt;&#x2F;li&gt;
&lt;li&gt;software consulting billed hourly is also OK&lt;&#x2F;li&gt;
&lt;li&gt;they are distinct lines of work and expertise&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;background&quot;&gt;Background&lt;&#x2F;h2&gt;
&lt;p&gt;There are three highly-visible people advocating strongly against hourly billing and in favor of value pricing. Jonathan Stark is so jazzed about it that he wrote an e-book on it. Patrick McKenzie and Thomas Ptacek have also written fairly extensively along the same lines.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;problems-with-hourly-billing&quot;&gt;Problems with Hourly Billing&lt;&#x2F;h2&gt;
&lt;p&gt;A lot of the writing I&#x27;ve read and podcasts I&#x27;ve heard on this topic is primarily focused on disparaging hourly billing for its many evils. Jonathan Stark is particularly vehement about this point going so far as to call it his mission to &quot;rid the world&quot; of hourly billing. He&#x27;s also written an e-book with an insulting and ableist title that I will not be stating here or linking to.&lt;&#x2F;p&gt;
&lt;p&gt;In my research for this article I found a bunch of specific bullet points written on an old HackerNews comment by Thomas Ptacek and responded to each one. That allowed me to get some feelings out and clarify my thoughts but I don&#x27;t think it&#x27;s valuable to post, so I&#x27;m going to heavily summarize my points&#x2F;responses here.&lt;&#x2F;p&gt;
&lt;p&gt;The basic takeaway I got from reading these laundry lists of problems with hourly billing is they find it causes a host of behaviors on both sides that are value-distracted. This includes bickering or negotiating small details like the dollar figure per hour, the hours billed on a given invoice, or the quantity of invoices. They claim that it poorly positions you and incentivizes you to book maximum hours.&lt;&#x2F;p&gt;
&lt;p&gt;None of these claims have manifested in my work. I don&#x27;t negotiate my rate. I never have and no client has ever initiated a negotiation. It goes one of 3 ways. 1. The client ghosts after I first state my rate. So long! 2. The client comments that the rate is high but agrees to pay it and does so happily. 3. The client happily pays it with no comment. That&#x27;s it.&lt;&#x2F;p&gt;
&lt;p&gt;No client of mine has ever contested an invoice. I send 1-liner invoices every 2 weeks that take the form &quot;I worked N hours at rate X so your total due is Y&quot;. There&#x27;s no breakdown of the hours into tasks.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-real-message&quot;&gt;The Real Message&lt;&#x2F;h2&gt;
&lt;p&gt;I think most of the anti-hourly writing has a valuable message but that message has nothing to do with billing whatsoever.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Do value-focused consulting, not pure software development&lt;&#x2F;strong&gt;. I think that makes perfect sense. Don&#x27;t focus exclusively on technical matters. Work with product owners, UX designers, marketing, and executives to discover and clarify clear business wins and laser focus on delivering that value. You can charge more doing this, and it&#x27;s what I call &quot;software consulting&quot; as opposed to &quot;contract software development&quot; where you are either doing straight staff augmentation or outsourced software development and are walled off from more strategic involvement.&lt;&#x2F;p&gt;
&lt;p&gt;I think some of the concerns about hourly billing are really concerns about software development vs software consulting but are misattributed to a billing&#x2F;pricing issue.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-about-those-20k-week-engagements&quot;&gt;What about those $20K&#x2F;week engagements&lt;&#x2F;h2&gt;
&lt;p&gt;I think one of the main hooks here is Patrick McKenzie posting highly transparent articles about his big-ticket value-priced consulting engagements. The whole thing smacks of &quot;You could be sipping cocktails on a Caribbean beach&quot; BS a bit but here&#x27;s what I think the underlying truth is.&lt;&#x2F;p&gt;
&lt;p&gt;There is a totally separate business service here that is what Patrick McKenzie and Jonathan Stark do. It&#x27;s business consulting. In particular they blend in a lot of technical know-how, but their service offering is fundamentally not in software development. They are basically MBAs who can code for hire. They deal in things like email marketing, copy writing, sales methodologies, A&#x2F;B testing, sales funnel optimization, SEO, pricing strategy, etc. These are all great and valuable skills and value pricing those engagements sounds great! No objection to that. But my reading of their writing is they claim your average software consultant should aspire to do this, and I disagree. It&#x27;s a totally different line of work requiring a totally different set of expertise. For me, it would be completely unsatisfying and I suspect many other developers would agree. I like coding and technical work. My consulting has a significant non-technical portion, but at the end of the day I&#x27;m still spending a lot of time coding.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;daily-rates&quot;&gt;Daily Rates&lt;&#x2F;h2&gt;
&lt;p&gt;Daily rates are fine. I don&#x27;t consider them fundamentally different from hourly rates and if you are going to work exclusively for 1 client for a period, I recommend them and they should be more lucrative than hourly rates. However, having done 6+ months of daily rate, I prefer hourly personally even though I take home less cash. Basically I prefer to bill 3-5 super-solid productive hours a day and then go about my non-work life without a second thought, and daily billing isn&#x27;t really compatible with the days when I only have 3 hours in me or I&#x27;m blocked waiting for someone else. I can also interleave multiple clients more easily with hourly billing. My ideal workload both from a productivity and satisfaction is two active hourly clients.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;when-value-pricing-works-for-me&quot;&gt;When Value Pricing Works for Me&lt;&#x2F;h2&gt;
&lt;p&gt;I have or would do value pricing for more well-defined &quot;productized consulting&quot; things I sometimes do. I charge flat value-based rates for training courses, for example. Because I work in software and don&#x27;t teach the same material that many times without significant content updates, it&#x27;s not as lucrative as it could be. But teaching the same course a dozen times is also not as rewarding or enjoyable to me personally. I would and have bid out code audits or retainers (slightly separate topic) fixed as well. But currently this is a small minority of my work.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;please-change-your-tone&quot;&gt;Please Change Your Tone&lt;&#x2F;h2&gt;
&lt;p&gt;This is a plea to these authors to change their message and tone. Hourly billing is fine and powers a huge portion of the economy. It&#x27;s simple, versatile, and resilient. The constant disparagement I read and hear hurts my feelings and makes me angry and frustrated and sad. I feel my lifestyle and that of many of my close family members and friends is being needlessly attacked and portrayed as evil and exploitive when it&#x27;s actually fine. The world is complex. Calculating the value of work is extremely complex if not impossible in many&#x2F;most situations. Value pricing is only even remotely possible in a particular subset of relatively straightforward situations.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>zsh lazy loading</title>
          <pubDate>Thu, 04 Jan 2018 14:54:23 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2018/01/zsh-lazy-loading/</link>
          <guid>https://peterlyons.com/problog/2018/01/zsh-lazy-loading/</guid>
          <description xml:base="https://peterlyons.com/problog/2018/01/zsh-lazy-loading/">&lt;p&gt;I was getting frustrated with how long it took for a new terminal tab to start with zsh and display my prompt. I chatted with zsh wizard &lt;a href=&quot;https:&#x2F;&#x2F;alok.github.io&#x2F;&quot;&gt;Alok Singh&lt;&#x2F;a&gt; and finally bothered to dig into it and removed the glaringly-slow subprocess spawns (things like &lt;code&gt;brew --prefix openssl&lt;&#x2F;code&gt;) and some nvm related stuff was slow. What I came upon in my web research was &lt;a href=&quot;https:&#x2F;&#x2F;kev.inburke.com&#x2F;kevin&#x2F;profiling-zsh-startup-time&#x2F;&quot;&gt;this post&lt;&#x2F;a&gt; which had pretty slick lazy loading pattern for integration with third party utilities like nvm, virtualenv, rbenv, and similar.&lt;&#x2F;p&gt;
&lt;p&gt;The trick is you define a shell function yourself matching the name of the third party utility shell function. Your function&#x27;s job is to load the real utility when it is run, which will replace your placeholder function with the real one.&lt;&#x2F;p&gt;
&lt;p&gt;It looks like this for &lt;code&gt;nvm&lt;&#x2F;code&gt;, for example:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sh&quot; class=&quot;language-sh &quot;&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;##### nvm (node version manager) #####
# placeholder nvm shell function
# On first use, it will set nvm up properly which will replace the `nvm`
# shell function with the real one
nvm() {
  if [[ -d &amp;#x27;&amp;#x2F;usr&amp;#x2F;local&amp;#x2F;opt&amp;#x2F;nvm&amp;#x27; ]]; then
    NVM_DIR=&amp;quot;&amp;#x2F;usr&amp;#x2F;local&amp;#x2F;opt&amp;#x2F;nvm&amp;quot;
    export NVM_DIR
    # shellcheck disable=SC1090
    source &amp;quot;${NVM_DIR}&amp;#x2F;nvm.sh&amp;quot;
    if [[ -e ~&amp;#x2F;.nvm&amp;#x2F;alias&amp;#x2F;default ]]; then
      PATH=&amp;quot;${PATH}:${HOME}.nvm&amp;#x2F;versions&amp;#x2F;node&amp;#x2F;$(cat ~&amp;#x2F;.nvm&amp;#x2F;alias&amp;#x2F;default)&amp;#x2F;bin&amp;quot;
    fi
    # invoke the real nvm function now
    nvm &amp;quot;$@&amp;quot;
  else
    echo &amp;quot;nvm is not installed&amp;quot; &amp;gt;&amp;amp;2
    return 1
  fi
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This should keep shell init time short but still not require any explicit initialization of these shell integrations.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Available for Consulting</title>
          <pubDate>Wed, 03 Jan 2018 17:32:11 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2018/01/available-for-consulting/</link>
          <guid>https://peterlyons.com/problog/2018/01/available-for-consulting/</guid>
          <description xml:base="https://peterlyons.com/problog/2018/01/available-for-consulting/">&lt;p&gt;I have availability for new consulting engagements this year.&lt;&#x2F;p&gt;
&lt;p&gt;Do you have an ambitious Node.js project you want to supercharge? I can get your team cruising with rock-solid practices and past all the pitfalls and stumbling blocks.&lt;&#x2F;p&gt;
&lt;p&gt;Adding Node.js and JavaScript to your tech stack? How about some expert-led training to be sure your team is fluent with the tools?&lt;&#x2F;p&gt;
&lt;p&gt;Ready to experience the game-changing effects of serverless applications? I spent most of last year immersed in AWS Lambda, Internet of Things (IoT), and event-driven architectures and would love to build a solution for you that is easy and affordable to operate.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a href=&quot;&#x2F;contact&quot;&gt;Get in touch!&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>rust dependency hell with features</title>
          <pubDate>Wed, 20 Dec 2017 00:00:37 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2017/12/rust-dependency-hell-with-features/</link>
          <guid>https://peterlyons.com/problog/2017/12/rust-dependency-hell-with-features/</guid>
          <description xml:base="https://peterlyons.com/problog/2017/12/rust-dependency-hell-with-features/">&lt;p&gt;I&#x27;ve encountered my first bit of rust dependency hell. I&#x27;m trying to use a combination of nom v3.2.1 (a parsing combinator framework) plus nom_pem v1.0.3 (which uses nom to parse the PEM file format) plus der-parser v0.4.4 (which uses nom to parse DER data). This would almost work EXCEPT der-parser has the following line in it&#x27;s &lt;code&gt;Cargo.toml&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;nom = {version = &quot;^3.1&quot;, features = [&quot;verbose-errors&quot;]}&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;I haven&#x27;t used feature toggling at all yet, and my initial reaction is &lt;em&gt;OMG whose ideas was this and how did they think this would ever work?&lt;&#x2F;em&gt;. In this case, the feature &lt;code&gt;verbose-errors&lt;&#x2F;code&gt; enables a totally different and incompatible Error API in nom, so it&#x27;s very likely this only works if all of your nom-based deps agree on whether to use or not use this feature. One out of line and it&#x27;s a hung jury: your code doesn&#x27;t compile.&lt;&#x2F;p&gt;
&lt;p&gt;So at the moment my thought is: only top-level application projects should use cargo feature toggles and library crates should never use them. But as I read more maybe it&#x27;ll get refined to something like &quot;feature toggles are OK only within the bounds defined by the semver version, but if a feature toggle changes the semver API conventions, it&#x27;s verboten.&quot;&lt;&#x2F;p&gt;
&lt;p&gt;Off to read a bit more on this topic.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>rust converting bytes chars and strings</title>
          <pubDate>Sat, 16 Dec 2017 18:57:12 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2017/12/rust-converting-bytes-chars-and-strings/</link>
          <guid>https://peterlyons.com/problog/2017/12/rust-converting-bytes-chars-and-strings/</guid>
          <description xml:base="https://peterlyons.com/problog/2017/12/rust-converting-bytes-chars-and-strings/">&lt;p&gt;I found converting from many representations of essentially the same data really inconsistent and hard to memorize in rust. I was doing a lot of work that frequently switched between text and binary and couldn&#x27;t find a good quick reference cheat sheet, so I sat down to pair with &lt;a href=&quot;https:&#x2F;&#x2F;jaredmcdonald.github.io&#x2F;&quot;&gt;Jared McDonald&lt;&#x2F;a&gt; at Recurse Center and code one up.&lt;&#x2F;p&gt;
&lt;p&gt;Whether you&#x27;ve got some bytes, chars, Strings, strs, arrays, or vecs, we&#x27;ll get you from A to B!&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;use std::str;

fn main() {
    &amp;#x2F;&amp;#x2F; This is &amp;quot;hello world&amp;quot; as an array of bytes.
    &amp;#x2F;&amp;#x2F; You can also start from a byte string b&amp;quot;hello world&amp;quot; and debug print that to get the
    &amp;#x2F;&amp;#x2F; utf8 encoded decimal values
    println!(&amp;quot;hello byte string: {:?}&amp;quot;, b&amp;quot;hello world&amp;quot;);

    &amp;#x2F;&amp;#x2F; OK so let&amp;#x27;s say you have an array of u8s
    let array_of_u8 = [104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100];

    &amp;#x2F;&amp;#x2F; [u8] to String (lossy)
    &amp;#x2F;&amp;#x2F; Any invalid bytes that are not utf8 will be replaced with
    &amp;#x2F;&amp;#x2F; the unicode replacement character &amp;#x27;\u{FFFD}&amp;#x27;
    &amp;#x2F;&amp;#x2F; You get a Cow (Clone on Write) not exactly a String
    let string_utf8_lossy = String::from_utf8_lossy(&amp;amp;array_of_u8);
    println!(&amp;quot;string_utf8_lossy: {}&amp;quot;, string_utf8_lossy);

    &amp;#x2F;&amp;#x2F; [u8] to String (result)
    &amp;#x2F;&amp;#x2F; The non-lossy version needs a vec not an array
    let mut vec_of_u8 = vec![];
    vec_of_u8.extend_from_slice(&amp;amp;array_of_u8);
    let string_utf8_result = String::from_utf8(vec_of_u8).unwrap();
    println!(&amp;quot;string_utf8_result: {}&amp;quot;, string_utf8_result);

    &amp;#x2F;&amp;#x2F; [u8] to str (result)
    let str_utf8_result = str::from_utf8(&amp;amp;array_of_u8).unwrap();
    println!(&amp;quot;str_utf8_result: {}&amp;quot;, str_utf8_result);

    &amp;#x2F;&amp;#x2F; [u8] to str (lossy)
    &amp;#x2F;&amp;#x2F; There is no str::from_utf8_lossy. Have to use String::from_utf8_lossy

    &amp;#x2F;&amp;#x2F; [u8] to Vec&amp;lt;char&amp;gt;
    let vec_of_chars: Vec&amp;lt;char&amp;gt; = array_of_u8.iter().map(|byte| *byte as char).collect();
    println!(&amp;quot;vec_of_chars: {:?}&amp;quot;, vec_of_chars);

    &amp;#x2F;&amp;#x2F; Vec&amp;lt;char&amp;gt; to Vec&amp;lt;u8&amp;gt;
    let vec_of_u8s: Vec&amp;lt;u8&amp;gt; = vec_of_chars.iter().map(|c| *c as u8).collect();
    println!(&amp;quot;vec_of_u8s: {:?}&amp;quot;, vec_of_u8s);

    &amp;#x2F;&amp;#x2F; Vec&amp;lt;char&amp;gt; to String
    let mut string_of_collected_chars: String = vec_of_chars.iter().collect();
    println!(&amp;quot;string_of_collected_chars: {}&amp;quot;, string_of_collected_chars);

    &amp;#x2F;&amp;#x2F; Now we have a mutable String. We can push chars
    string_of_collected_chars.push(&amp;#x27;!&amp;#x27;);

    &amp;#x2F;&amp;#x2F; and we can push a str
    string_of_collected_chars.push_str(&amp;quot;!!&amp;quot;);

    &amp;#x2F;&amp;#x2F; String to str
    let str_slice = &amp;amp;string_of_collected_chars[..5];
    println!(&amp;quot;str_slice: {}&amp;quot;, &amp;amp;str_slice);

    &amp;#x2F;&amp;#x2F; String to [u8]
    let array_of_u8_from_string = string_of_collected_chars.as_bytes();
    println!(&amp;quot;array_of_u8_from_string: {:?}&amp;quot;, array_of_u8_from_string);

    &amp;#x2F;&amp;#x2F; String to Vec&amp;lt;char&amp;gt;
    let vec_of_chars_to_string: Vec&amp;lt;char&amp;gt; = string_of_collected_chars.chars().collect();
    println!(&amp;quot;vec_of_chars: {:?}&amp;quot;, vec_of_chars_to_string);

    &amp;#x2F;&amp;#x2F; String from several Strings
    let concat_strings = vec![&amp;quot;abc&amp;quot;.to_string(), &amp;quot;def&amp;quot;.to_string()].concat();
    println!(&amp;quot;concat_strings: {}&amp;quot;, concat_strings);
    let joined_strings = vec![&amp;quot;abc&amp;quot;.to_string(), &amp;quot;def&amp;quot;.to_string()].join(&amp;quot;---&amp;quot;);
    println!(&amp;quot;joined_strings: {}&amp;quot;, joined_strings);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</description>
      </item>
      <item>
          <title>Recurse Center 23: SQL window functions</title>
          <pubDate>Tue, 12 Dec 2017 23:01:30 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2017/12/recurse-center-23-sql-window-functions/</link>
          <guid>https://peterlyons.com/problog/2017/12/recurse-center-23-sql-window-functions/</guid>
          <description xml:base="https://peterlyons.com/problog/2017/12/recurse-center-23-sql-window-functions/">&lt;p&gt;Today was the first day where I focused primarily on my SQL project, which was my second priority overall. I did most of the pgexercises on aggregation. There were quite a few of them and by the end they were really challenging. I learned bit about common table expressions which seem awesome and windowing functions which seem great for the business rollup type reports which are otherwise frustrating in regular SQL. I met with the study group today and we lamented the lack of good material that actually teaches this stuff clearly. We are looking at 3-4 books to potentially select.&lt;&#x2F;p&gt;
&lt;p&gt;I managed to get into code flow this afternoon on tealeaves and I expanded my arsenal of test key files to include the special &lt;code&gt;-o&lt;&#x2F;code&gt; openssh-key-v1 format for the rsa, dsa, and ecdsa algorithms. ed25519 keys always use this format, but the other algorithms can be saved in 2 formats, this one or ASN.1.&lt;&#x2F;p&gt;
&lt;p&gt;The good news is that some of my nom code to parse this format in the public key code can be re-used for private key code as well. I&#x27;m excited to refactor to that tomorrow.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Recurse Center 22: tealeaves refactored</title>
          <pubDate>Tue, 12 Dec 2017 01:17:07 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2017/12/recurse-center-22-tealeaves-refactored/</link>
          <guid>https://peterlyons.com/problog/2017/12/recurse-center-22-tealeaves-refactored/</guid>
          <description xml:base="https://peterlyons.com/problog/2017/12/recurse-center-22-tealeaves-refactored/">&lt;p&gt;Today I finished a big push of work on tealeaves and got the code fully cleaned up and merged to master and all the tests passing again. When I introduced &lt;code&gt;nom_pem&lt;&#x2F;code&gt; last week things destabilized and it took a while to shake all that out. I&#x27;m about ready to start adding some linter style rules like making sure the filesystem permissions on your private keys are restrictive.&lt;&#x2F;p&gt;
&lt;p&gt;I also worked a bit more on pgexercises this afternoon but they really are just exercises with nothing to actually teach you the SQL you need, so I switched back to some Khan Academy tutorials I had worked on about 2 years ago.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Recurse Center 21: nom and combinators</title>
          <pubDate>Sat, 09 Dec 2017 01:37:35 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2017/12/recurse-center-21-nom-and-combinators/</link>
          <guid>https://peterlyons.com/problog/2017/12/recurse-center-21-nom-and-combinators/</guid>
          <description xml:base="https://peterlyons.com/problog/2017/12/recurse-center-21-nom-and-combinators/">&lt;p&gt;Last night I asked RC about whether &lt;code&gt;nom&lt;&#x2F;code&gt; or &lt;code&gt;untrusted&lt;&#x2F;code&gt; seemed like better crate choices for parsing mixed text and binary file formats. This morning I woke up super early for no reason and was ready to go so I watched RC alumnus Stefanie Schirmer&#x27;s talk on parser combinators.&lt;&#x2F;p&gt;
&lt;div class=&quot;youtube-video-container&quot;&gt;
&lt;iframe src=&quot;https:&#x2F;&#x2F;www.youtube-nocookie.com&#x2F;embed&#x2F;oU2418-8_KI&quot; allowfullscreen&gt;&lt;&#x2F;iframe&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;Then I was excited to dig in so after I commuted in to RC early I searched on crates for &quot;nom pem&quot; since the PEM format was what I needed to parse next. I found an existing crate &lt;code&gt;nom_pem&lt;&#x2F;code&gt; that looked promising so I gave it a whirl. I was able to get it to parse a test file but I couldn&#x27;t get at the parsed data. I was getting a &quot;that field is private&quot; error which was odd. How was I supposed to do anything with this parser if the parser output is private? So I looked again at the crate and saw &quot;Last updated 2 hours ago&quot;. That was not the most recent update, that was the only update ever. Someone had published a nom PEM parsing crate to crates.io while I was watching the combinator video!&lt;&#x2F;p&gt;
&lt;p&gt;So I was download number 5 (probably a bunch of bots beat me to it) for that crate and the first github star. Within a few hours I had forked it and sent in 3 small PRs to fix trivial but showstopper issues, and by the afternoon they all had been merged. Go open source!&lt;&#x2F;p&gt;
&lt;p&gt;So I dove into learning &lt;code&gt;nom&lt;&#x2F;code&gt; which makes heavy use of macros which are hard. I spent basically all day starting with the most trivial example of like &quot;can I tell that &#x27;a&#x27; is the letter &#x27;a&#x27;?&quot; and eventually was at the point where I could parse the openssh public key format. Then I started to refactor that code into tealeaves but there&#x27;s still more to do.&lt;&#x2F;p&gt;
&lt;p&gt;Today I also heard one of the RC mini-batchers talk about some cool clojure spec libraries and had a nice dinner out instead of take-out which was fun.&lt;&#x2F;p&gt;
&lt;p&gt;Today&#x27;s unix command line tip is that there&#x27;s a golang project by google called &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;google&#x2F;der-ascii&quot;&gt;der-ascii&lt;&#x2F;a&gt; that can translate both ways between the binary DER format used in some crypto stuff and an editable ascii version. Works like this:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;grep -v - files&amp;#x2F;ssh-rsa-1024-a-private-key.pem| base64 -D | der2ascii
SEQUENCE {
  INTEGER { 0 }
  INTEGER { `00a3475d589021587893902869f524c6493856c3ad46f28eeee793a22da33dfd0c690f67a99b3acef3dcc877f4ad98fb91feb6c39d25a28871ad73a03bc7143a559de8eb22e3ca69a72ca6059a7ff5526878de96d29d82f2227be0308efb1e9f5fbf256c11f35e7ee8213961980b208666463f48c115a884aabc1ba7705f3293a7` }
  INTEGER { 65537 }
  INTEGER { `3bfa5915d14c0e7dac67061176159e2903630bda38f79cfdb15f8ff187c79b8ed580ed29667641d35ec4dd1baa314a282512e9e46e10b86259bee19b53d3e06140b6b2bfaaf9434b92569e5f67cd7d90deeeb0fb609fcdacb83518da6e7f39dc5c39bfb726362458fae49c03127338799e1104183cb4015a71b5f1cea91dd621` }
  INTEGER { `00d07dcd4433eebddf5ca318643c4b6066bc37ebd01e9b9468e8b97a0d9542b15ad236247a43f35b9a54acc7398ef9b0283658ca103e1b3e2eed4bf9f0b2abf4bf` }
  INTEGER { `00c87c1d8693dc1d624e30e6627fd79246da11aac0f95467193226a348cfb0ec2c605516cb73a472d558b54752666445046bb4e02057227260208f744832a29319` }
  INTEGER { `0b2b622ed0356f18a346dd8ca92a449cdc6286909cc32afd3fd287f66853ad5ab73e4d4ffb89f3135e8bef1467537b1b7c65df55656e62337365099bda8699d3` }
  INTEGER { `394f31697ceed8ff76f68784f4a27cd0001a38c37d02618e5445b33b67135e0fb961d9684320692b0f769272bd8e4024695d850bf99c8131755d4c922ed74651` }
  INTEGER { `54dfe6859a0ca3df5d85bbc031b6da4428af4328629157045495aedc9f3056b2538ebf513fbcc4bfe79d4a35a7cc3afedf79b7f856997cb3783dfad052a6ca06` }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Pretty handy! I couldn&#x27;t find it in homebrew so I had to compile it by hand and manually install it, but it works.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Recurse Center 20: Parsing is hard. Let&#x27;s go shopping.</title>
          <pubDate>Fri, 08 Dec 2017 03:13:00 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2017/12/recurse-center-20-parsing-is-hard-lets-go-shopping/</link>
          <guid>https://peterlyons.com/problog/2017/12/recurse-center-20-parsing-is-hard-lets-go-shopping/</guid>
          <description xml:base="https://peterlyons.com/problog/2017/12/recurse-center-20-parsing-is-hard-lets-go-shopping/">&lt;p&gt;So I&#x27;ve found that the &lt;code&gt;pem&lt;&#x2F;code&gt; crate I&#x27;ve been using doesn&#x27;t parse headers, which are present in encrypted dsa private key files, and the &lt;code&gt;mailparse&lt;&#x2F;code&gt; crate doesn&#x27;t parse properly with unix newlines, which seems to be what openssh generates even though in theory the RFCs involved require carriage return newline AFAIK.&lt;&#x2F;p&gt;
&lt;p&gt;So I spent most of the day writing my own PEM parser that handles headers and thinking about the things rust programmers think about:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Can this be done without allocating any extra memory?&lt;&#x2F;li&gt;
&lt;li&gt;Have I failed at life if I have to copy this 10-byte string before freeing it 4 nanoseconds later?&lt;&#x2F;li&gt;
&lt;li&gt;Can I represent all of my data as a giant enum so rust models it as a single &lt;code&gt;u64&lt;&#x2F;code&gt; bitfield or something?&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;You know, that kind of stuff.&lt;&#x2F;p&gt;
&lt;p&gt;But after another shopping spree on crates dot io I&#x27;m leaning heavily toward learning to use the &lt;code&gt;nom&lt;&#x2F;code&gt; crate to parse this. There&#x27;s even an open github issue asking for an example DER private key parser, which is what I&#x27;ve been working on. However, the &lt;code&gt;untrusted&lt;&#x2F;code&gt; crate also looks promising. Tomorrow I&#x27;ll see how quickly I can get something easy done with &lt;code&gt;nom&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;I also gave a talk on Serverless today and pointed out my &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;focusaurus&#x2F;hexagonal-lambda&quot;&gt;hexagonal-lambda&lt;&#x2F;a&gt; sample project.&lt;&#x2F;p&gt;
&lt;p&gt;Here&#x27;s the unix tip of the day. If you have some utf8 characters encoded as hexadecimal you can convert to regular utf8 like this:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;echo 65636473612d736861322d6e69737470333834 | xxd -r -p&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Which prints &lt;code&gt;ecdsa-sha2-nistp384&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Recurse Center 19: ASN.1 Tagged Context</title>
          <pubDate>Wed, 06 Dec 2017 23:22:37 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2017/12/recurse-center-19-asn-1-tagged-context/</link>
          <guid>https://peterlyons.com/problog/2017/12/recurse-center-19-asn-1-tagged-context/</guid>
          <description xml:base="https://peterlyons.com/problog/2017/12/recurse-center-19-asn-1-tagged-context/">&lt;p&gt;Coding on tealeaves today was mostly pulling interesting bits out of ssh keys. Misc things discovered&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;openssh will only generate 1024 bit dsa keys, which are not secure, but longer key sizes are supported by other software&lt;&#x2F;li&gt;
&lt;li&gt;ecdsa keys the &quot;bits&quot; field represents either 256, 384, or 521 bit size curves and those are the only valid sizes&lt;&#x2F;li&gt;
&lt;li&gt;The specific curves are encoded in ASN.1 as object identifiers that appear to be assigned well-known OIDs which tell you which curve it is&lt;&#x2F;li&gt;
&lt;li&gt;At least one &lt;a href=&quot;https:&#x2F;&#x2F;security.stackexchange.com&#x2F;questions&#x2F;5096&#x2F;rsa-vs-dsa-for-ssh-authentication-keys&#x2F;46781#46781&quot;&gt;stackoverflow answerer&lt;&#x2F;a&gt; thinks ecdsa is backdoored by the NSA&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;I also was struggling with tooling, which I guess is the name of the game in lower-level systems programming, and I ended up writing a little rust unix utility to hard wrap files at 32 characters after stripping whitespace so I can pipe hex dumps into it and get nice column widths. That was actually fairly fast and easy to do so for simple things it feels like once you&#x27;re over the learning curve rust might actually deliver on its promise of scripting language ease with no-compromise performance at runtime.&lt;&#x2F;p&gt;
&lt;p&gt;Later I paired with one of the RC mini-batchers who taught me about &lt;code&gt;fold -w 32&lt;&#x2F;code&gt; which is a standard unix tool that can do most of the same thing.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Recurse Center 18: SQL joins</title>
          <pubDate>Tue, 05 Dec 2017 23:37:08 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2017/12/recurse-center-18-sql-joins/</link>
          <guid>https://peterlyons.com/problog/2017/12/recurse-center-18-sql-joins/</guid>
          <description xml:base="https://peterlyons.com/problog/2017/12/recurse-center-18-sql-joins/">&lt;p&gt;Today I mostly worked on some &lt;a href=&quot;https:&#x2F;&#x2F;pgexercises.com&quot;&gt;pgexercises&lt;&#x2F;a&gt;. I learned some good things and did some fairly complex joins but I still feel like I need a lot more practice with joins before I&#x27;ll actually get that clear light bulb of understanding. Our study group met and reviewed some stuff and made plans. I was hoping to also do some web security capture the flag exercises AND make progress on tealeaves but didn&#x27;t even come close to getting to either of those today.&lt;&#x2F;p&gt;
&lt;p&gt;My general feeling is as per my focusaurus username, I do much better working on just 1 thing at a time and the past few days I&#x27;ve tried to sprinkle in some web security and SQL stuff and my main rust&#x2F;tealeaves project basically came to a halt. I think mostly this is OK as I can work heads down alone on tealeaves when I get home but the other topics benefit a lot from working with my peers here. We&#x27;ll see how I feel in another 2 days or so. I was casually hoping to have tealeaves be mostly working for modern ssh key formats by Thursday but it might not quite get there including CLI usability stuff.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>How to use docker to run local databases</title>
          <pubDate>Tue, 05 Dec 2017 23:25:41 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2017/12/how-to-use-docker-to-run-local-databases/</link>
          <guid>https://peterlyons.com/problog/2017/12/how-to-use-docker-to-run-local-databases/</guid>
          <description xml:base="https://peterlyons.com/problog/2017/12/how-to-use-docker-to-run-local-databases/">&lt;p&gt;Here&#x27;s how I run use docker to run databases for local development. I think my approach is the simplest I&#x27;ve seen and if something is unnecessary I skip it. You don&#x27;t need data containers or port mappings, so I don&#x27;t use them. The goal here is to make it very easy to run databases while developing applications or doing DB development work. It&#x27;s basically the same for macOS and linux but I&#x27;m currently on macOS so that&#x27;s where my instructions come from.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;goals&quot;&gt;Goals&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;Same process works mostly the same for many databases (postgresql, mysql, mongodb, redis, etc)&lt;&#x2F;li&gt;
&lt;li&gt;Data persists as expected&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;ul&gt;
&lt;li&gt;Can start&#x2F;stop containers, reboot host without losing data&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;ol&gt;
&lt;li&gt;When we want to wipe data and start fresh, that&#x27;s easy too&lt;&#x2F;li&gt;
&lt;li&gt;Easy to transfer files into the container and out of the container&lt;&#x2F;li&gt;
&lt;li&gt;Easy to get a shell inside  the container to run tools there (psql etc)&lt;&#x2F;li&gt;
&lt;li&gt;Easy to purge when done&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;my-approach&quot;&gt;My Approach&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;Use a single docker-compose yaml file because 1 file is the easiest number of files to have&lt;&#x2F;li&gt;
&lt;li&gt;Each database gets 2 volume mounts
&lt;ul&gt;
&lt;li&gt;1 for ad hoc file transfer
&lt;ul&gt;
&lt;li&gt;This is always &lt;code&gt;&#x27;~&#x2F;docker-volumes&#x2F;host:&#x2F;host&#x27;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;1 for the database data itself
&lt;ul&gt;
&lt;li&gt;The left&#x2F;host path follows the pattern &lt;code&gt;~&#x2F;docker-volumes&#x2F;container-name&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;the right&#x2F;container path needs to match the path the container stores data by default. You&#x27;ll need to look at the container&#x27;s docs to find the correct path to use. I&#x27;ve provided correct values for some popular databases below.&lt;&#x2F;li&gt;
&lt;li&gt;Look at &lt;a href=&quot;https:&#x2F;&#x2F;hub.docker.com&quot;&gt;hub.docker.com&lt;&#x2F;a&gt;, search for your DB container by name, then search in the page for &lt;code&gt;-v&lt;&#x2F;code&gt; and you&#x27;ll usually find it&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;Databases run on their default port bound to loopback
&lt;ul&gt;
&lt;li&gt;Simplest thing. Least surprise.&lt;&#x2F;li&gt;
&lt;li&gt;Also allows DB management GUIs to work easily.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;If DB users are required, set their password to &quot;password&quot;
&lt;ul&gt;
&lt;li&gt;This is local stuff that does not need much security&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;the-compose-file&quot;&gt;The compose file&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;yaml&quot; class=&quot;language-yaml &quot;&gt;&lt;code class=&quot;language-yaml&quot; data-lang=&quot;yaml&quot;&gt;# Run with:
# docker-compose -f ~&amp;#x2F;projects&amp;#x2F;dotfiles&amp;#x2F;docker-compose-local-dev.yml up -d
version: &amp;#x27;2&amp;#x27;
services:
  # https:&amp;#x2F;&amp;#x2F;hub.docker.com&amp;#x2F;_&amp;#x2F;postgres&amp;#x2F;
  postgres:
    container_name: &amp;#x27;postgres&amp;#x27;
    ports: [&amp;#x27;127.0.0.1:5432:5432&amp;#x27;]
    volumes: [
      # for ad-hoc transfers in&amp;#x2F;out of container
      &amp;#x27;~&amp;#x2F;docker-volumes&amp;#x2F;host:&amp;#x2F;host&amp;#x27;,
      # for DB data itself
      &amp;#x27;~&amp;#x2F;docker-volumes&amp;#x2F;postgres:&amp;#x2F;var&amp;#x2F;lib&amp;#x2F;postgresql&amp;#x2F;data&amp;#x27;
      ]
    image: &amp;#x27;postgres&amp;#x27;
    environment:
      POSTGRES_PASSWORD: &amp;#x27;password&amp;#x27;
      PGDATA: &amp;#x27;&amp;#x2F;var&amp;#x2F;lib&amp;#x2F;postgresql&amp;#x2F;data&amp;#x2F;pgdata&amp;#x27;
  mysql:
    container_name: &amp;#x27;mysql&amp;#x27;
    ports: [&amp;#x27;127.0.0.1:3306:3306&amp;#x27;]
    volumes: [
      # for ad-hoc transfers in&amp;#x2F;out of container
      &amp;#x27;~&amp;#x2F;docker-volumes&amp;#x2F;host:&amp;#x2F;host&amp;#x27;,
      # for DB data itself
      &amp;#x27;~&amp;#x2F;docker-volumes&amp;#x2F;mysql:&amp;#x2F;var&amp;#x2F;lib&amp;#x2F;mysql&amp;#x27;
      ]
    image: &amp;#x27;mysql&amp;#x27;
    environment:
      MYSQL_ROOT_PASSWORD: &amp;#x27;password&amp;#x27;
  mongo:
    container_name: &amp;#x27;mongo&amp;#x27;
    ports: [&amp;#x27;127.0.0.1:27017:27017&amp;#x27;]
    volumes: [
      # for ad-hoc transfers in&amp;#x2F;out of container
      &amp;#x27;~&amp;#x2F;docker-volumes&amp;#x2F;host:&amp;#x2F;host&amp;#x27;,
      # for DB data itself
      &amp;#x27;~&amp;#x2F;docker-volumes&amp;#x2F;mongo:&amp;#x2F;data&amp;#x2F;db&amp;#x27;
      ]
    image: &amp;#x27;mongo&amp;#x27;
  dynamodb:
   container_name: &amp;#x27;dynamodb&amp;#x27;
   ports: [&amp;#x27;127.0.0.1:8000:8000&amp;#x27;]
   volumes: [
        # for ad-hoc transfers in&amp;#x2F;out of container
        &amp;#x27;~&amp;#x2F;docker-volumes&amp;#x2F;host:&amp;#x2F;host&amp;#x27;,
        # for DB data itself
        &amp;#x27;~&amp;#x2F;docker-volumes&amp;#x2F;dynamodb:&amp;#x2F;var&amp;#x2F;dynamodb_local&amp;#x27;
        ]
   image: &amp;#x27;peopleperhour&amp;#x2F;dynamodb&amp;#x27;
  redis:
    container_name: &amp;#x27;redis&amp;#x27;
    ports: [&amp;#x27;127.0.0.1:6379:6379&amp;#x27;]
    volumes: [
      # for ad-hoc transfers in&amp;#x2F;out of container
      &amp;#x27;~&amp;#x2F;docker-volumes&amp;#x2F;host:&amp;#x2F;host&amp;#x27;,
      # for DB data itself
      &amp;#x27;~&amp;#x2F;docker-volumes&amp;#x2F;redis:&amp;#x2F;data&amp;#x27;
      ]
    image: &amp;#x27;redis&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Check the &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;focusaurus&#x2F;dotfiles&#x2F;blob&#x2F;master&#x2F;docker-compose-local-dev.yml&quot;&gt;docker compose file in my github dotfiles repo&lt;&#x2F;a&gt; for latest version.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;how-to-do-basic-operations&quot;&gt;How to do basic operations&lt;&#x2F;h2&gt;
&lt;p&gt;Commands are run from the host (macOS) terminal unless explicitly marked as run from within a container.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Start everything: &lt;code&gt;docker-compose -f local-dev.yml up -d&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Stop everything: &lt;code&gt;docker-compose -f local-dev.yml down&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Start some DBs but not all: &lt;code&gt;docker-compose -f local-dev.yml up -d postgres mongo&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Get a shell in the DB container: &lt;code&gt;docker exec -i -t postgres bash&lt;&#x2F;code&gt;
&lt;ul&gt;
&lt;li&gt;From here you can run the DB repl in the container like &lt;code&gt;psql&lt;&#x2F;code&gt;, &lt;code&gt;mongo&lt;&#x2F;code&gt;, etc&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;Copy a file into the container: &lt;code&gt;cp some-file.sql ~&#x2F;docker-volumes&#x2F;host&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Read a file from within the DB container:
&lt;ul&gt;
&lt;li&gt;Get a shell: &lt;code&gt;docker exec -i -t postgres bash&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Read the file (container shell): &lt;code&gt;more &#x2F;host&#x2F;some-file.sql&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;Copy a file from the container out to the host
&lt;ul&gt;
&lt;li&gt;Get a shell: &lt;code&gt;docker exec -i -t postgres bash&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Copy (container shell): &lt;code&gt;cp &#x2F;path&#x2F;to&#x2F;some&#x2F;file &#x2F;host&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;Fully wipe the data and start over for one database
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;docker-compose -f local-dev.yml stop&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;rm -rf ~&#x2F;docker-volumes&#x2F;postgres &amp;amp;&amp;amp; mkdir ~&#x2F;docker-volumes&#x2F;postgres&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;docker-compose -f local-dev.yml up -d postgres&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;bonus-docker-shell-aliases&quot;&gt;Bonus docker shell aliases&lt;&#x2F;h2&gt;
&lt;p&gt;These may come in handy:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sh&quot; class=&quot;language-sh &quot;&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;alias dcf=&amp;quot;docker-compose -f ~&amp;#x2F;projects&amp;#x2F;dotfiles&amp;#x2F;docker-compose-local-dev.yml&amp;quot;
alias dcup=&amp;quot;docker-compose -f ~&amp;#x2F;projects&amp;#x2F;dotfiles&amp;#x2F;docker-compose-local-dev.yml up -d&amp;quot;
alias deit=&amp;quot;docker exec --interactive --tty&amp;quot;
alias doco=&amp;quot;docker-compose&amp;quot;
alias dpa=&amp;quot;docker ps -a&amp;quot;
alias drit=&amp;#x27;docker run --rm --interactive --tty&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</description>
      </item>
      <item>
          <title>Recurse Center 17: ssh public keys</title>
          <pubDate>Tue, 05 Dec 2017 01:48:47 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2017/12/recurse-center-17-ssh-public-keys/</link>
          <guid>https://peterlyons.com/problog/2017/12/recurse-center-17-ssh-public-keys/</guid>
          <description xml:base="https://peterlyons.com/problog/2017/12/recurse-center-17-ssh-public-keys/">&lt;p&gt;This morning I paired on figuring out the structure of rsa private keys which was much easier since someone already wrote &lt;a href=&quot;https:&#x2F;&#x2F;etherhack.co.uk&#x2F;asymmetric&#x2F;docs&#x2F;rsa_key_breakdown.html&quot;&gt;detailed documentation&lt;&#x2F;a&gt; with fabulous color-coded binary structures. So mostly we just needed to figure out specifically what is meant when someone says &quot;this is a 1024-bit rsa key&quot;. The answer is it&#x27;s the size of the &lt;em&gt;modulus&lt;&#x2F;em&gt; field.&lt;&#x2F;p&gt;
&lt;p&gt;Then we needed the rust code to parse it out, which we used the &lt;code&gt;yasna&lt;&#x2F;code&gt; crate for. My pair had an excellent catch of the fact that we were getting an error due to &quot;extra data at end of file&quot; because we were only parsing the first few fields and ignore the rest which are uninteresting, but yasna expects you to fully parse the ASN.1 structure it seems.&lt;&#x2F;p&gt;
&lt;p&gt;After that we dug into the public key formats which are generally smaller and simpler. I think I have ed25519 and rsa figured out and can confirm you can match corresponding private&#x2F;public keys by a common value in the payload. Also you can detect trivial tampering of the algorithm label field as that data is duplicated in the base64 payload (which you could of course also tamper with).&lt;&#x2F;p&gt;
&lt;p&gt;This afternoon there was a great web security presentation and a bunch of us will be doing a web security capture the flag exercise&#x2F;contest this week. Looks fun but I think I&#x27;ll have to time-box my effort as there are at least 48 vulnerabilities to be found and it&#x27;s mostly unaided discovery of them.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Recurse Center 16: tealeaves is useful</title>
          <pubDate>Fri, 01 Dec 2017 22:53:15 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2017/12/recurse-center-16-tealeaves-is-useful/</link>
          <guid>https://peterlyons.com/problog/2017/12/recurse-center-16-tealeaves-is-useful/</guid>
          <description xml:base="https://peterlyons.com/problog/2017/12/recurse-center-16-tealeaves-is-useful/">&lt;p&gt;Today I refactored all my various scratch iterations of code to dig into the details of an ed25519 openssh private key file and integrated that into the main &lt;code&gt;tealeaves&lt;&#x2F;code&gt; executable file. I added some unit tests for edge cases and I&#x27;ve so far been pleased with how unit testing is in rust. The cargo command line is weird for testing: I often have to do something like &lt;code&gt;RUST_BACKTRACE=1 cargo test --jobs=1 long_field -- --nocapture&lt;&#x2F;code&gt; to try to debug which seems silly.&lt;&#x2F;p&gt;
&lt;p&gt;But basically I can run tealeaves now on my &lt;code&gt;~&#x2F;.ssh&lt;&#x2F;code&gt; and it prints and accurate and useful description of what each file is.&lt;&#x2F;p&gt;
&lt;p&gt;There&#x27;s also now a SQL study group happening so I&#x27;ll be spending some time on that which was my minor project for RC which I have yet to actually spend any time on.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>OpenSSH ed25519 private key file format</title>
          <pubDate>Fri, 01 Dec 2017 16:46:17 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2017/12/openssh-ed25519-private-key-file-format/</link>
          <guid>https://peterlyons.com/problog/2017/12/openssh-ed25519-private-key-file-format/</guid>
          <description xml:base="https://peterlyons.com/problog/2017/12/openssh-ed25519-private-key-file-format/">&lt;p&gt;Today I finished understanding the openssh private key format for ed25519 keys. Yesterday&#x27;s analysis had a few remaining mysteries that a fellow RCer helped me solve plus a pair of mistakes that threw off some fields. So here for the record is a &quot;complete&quot; byte-by-byte analysis of what&#x27;s inside an openssh ed25519 private key (both with and without a passphrase). This was done with OpenSSH_7.5p1, LibreSSL 2.5.4 on macOS 10.13. I say &quot;complete&quot; in quotes because you could go one layer deeper into the actual key payloads but for our purposes those details don&#x27;t have specific meaning.&lt;&#x2F;p&gt;
&lt;p&gt;First, let&#x27;s generate a test keypair (hit ENTER for no passphrase when prompted):&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sh&quot; class=&quot;language-sh &quot;&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;mkdir &amp;#x2F;tmp&amp;#x2F;test-keys
cd &amp;#x2F;tmp&amp;#x2F;test-keys
ssh-keygen -t ed25519 -f ssh-ed25519-private-key.pem
Generating public&amp;#x2F;private ed25519 key pair.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in ssh-ed25519-private-key.pem.
Your public key has been saved in ssh-ed25519-private-key.pem.pub.
The key fingerprint is:
SHA256:qXcj+K4QZHxJ712c&amp;#x2F;M1PPutkyxT&amp;#x2F;Hn+1RKBIHLlypY4 plyons@avi
The key&amp;#x27;s randomart image is:
+--[ED25519256]--+
|      . ..o      |
|   . . o + + o   |
|    + o o = * .  |
|   o . o B o . + |
|    .   S .   o.+|
|     . E .     +=|
|    . o o o   .**|
|     . o o .  =.O|
|      .oo     .*=|
+----[SHA256]-----+
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Now let&#x27;s get at those tasty bytes of payload as a hex dump:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;grep -v - ssh-ed25519-private-key.pem | base64 -D | xxd -p                  
6f70656e7373682d6b65792d763100000000046e6f6e65000000046e6f6e
650000000000000001000000330000000b7373682d656432353531390000
00203cfe2afb025f46582e502b97f7dfa5a08dea09f87abfa8d5bfcaabf2
9fbb369500000090a2224bbaa2224bba0000000b7373682d656432353531
39000000203cfe2afb025f46582e502b97f7dfa5a08dea09f87abfa8d5bf
caabf29fbb36950000004002a1965d1a2684d50d29f2be0efd8e2fae3c5b
b013d06f7818416333955271a53cfe2afb025f46582e502b97f7dfa5a08d
ea09f87abfa8d5bfcaabf29fbb36950000000a706c796f6e734061766901
0203
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;OK here&#x27;s how it breaks down.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# ASCII magic &amp;quot;openssh-key-v1&amp;quot; plus null byte
6f70656e7373682d6b65792d7631 00
00000004 int length = 4
6e6f6e65 string cipher = none
00000004 int length = 4
6e6f6e65 string kdfname = none
00000000 int length = 0
# zero-length kdfoptions placeholder here
00000001 int number of public keys = 1
00000033 int length first public key = 51 (4 + 11 + 4 + 32)
0000000b int length = 11
7373682d65643235353139 string key type = ssh-ed25519
00000020 int length = 32
# public key payload 32 bytes
# probably encoding a point on the ed25519 curve
3cfe2afb025f46582e502b97f7dfa5a0
8dea09f87abfa8d5bfcaabf29fbb3695

00000090 int length = 144 size of remaining payload
# 8 + 4 + 11 + 4 + 32 + 4 + 64 + 4 + 10 + 3
a2224bbaa2224bba iv&amp;#x2F;salt? (Not sure about these 8 bytes)

# Here&amp;#x27;s a repeat of the public key (part of the private key pair)
0000000b int length = 11
7373682d656432353531 39 string key type = ssh-ed25519
00000020  int length = 32
# public key payload 32 bytes
# probably encoding a point on the ed25519 curve
3cfe2afb025f46582e502b97f7dfa5a0
8dea09f87abfa8d5bfcaabf29fbb3695

00000040 int length = 64
# 32 bytes private key payload 1
02a1965d1a2684d50d29f2be0efd8e2f
ae3c5bb013d06f7818416333955271a5
# 32 bytes is the public&amp;#x2F;point again
3cfe2afb025f46582e502b97f7dfa5a0
8dea09f87abfa8d5bfcaabf29fbb3695

0000000a int length = 10
706c796f6e7340617669 private key payload 2
010203 padding 3 bytes incrementing integers
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Now let&#x27;s generate one and encrypt with a passphrase&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;ssh-keygen -t ed25519 -f ssh-ed25519-passphrase-private-key.pem
Generating public&amp;#x2F;private ed25519 key pair.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in ssh-ed25519-passphrase-private-key.pem.
Your public key has been saved in ssh-ed25519-passphrase-private-key.pem.pub.
The key fingerprint is:
SHA256:QfPM5uIVVM6yT5sXeCvSjETCcP5zmTH+Rt7wY&amp;#x2F;91Ik8 plyons@avi
The key&amp;#x27;s randomart image is:
+--[ED25519256]--+
|      . + ...    |
|       * * o     |
|        = O =    |
|         B = *   |
|        S B B =  |
|       . + O B * |
|        . o O Eo=|
|           . B..=|
|              . +|
+----[SHA256]-----+
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Here&#x27;s the dump&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;grep -v - ssh-ed25519-passphrase-private-key.pem | base64 -D | xxd -p
6f70656e7373682d6b65792d7631000000000a6165733235362d63626300
0000066263727970740000001800000010d08f6b8fd17593f246db4ac6c4
5a11930000001000000001000000330000000b7373682d65643235353139
0000002062837be86c63712896b8e0e7543e367c3abd0c0b5ad3e764ea0e
4f8ddd7d00ef000000901e60c56ef30d0ff02e07b57bf14645076c32c86c
88ecad545ca28424e4739aff5895bebd6778e70b6c54b309b9fdb0c94102
bf8cef5b97d3d75636967e67e4b9c1ee72ae81074b0ce0f7e540e051d569
05da263af3e383342cc75b3145242abb75257586a119c9d3673dfb7eabe4
696350904e7c7af3cd77f28bea10374e15bc6536c2e1029438fdd3930bee
bbc5ac30
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Here&#x27;s the annotated structure:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#ASCII magic &amp;quot;openssh-key-v1&amp;quot; plus null byte
6f70656e7373682d6b65792d7631 00
0000000a int length = 10
6165733235362d636263 string cipher = aes256-cbc
00000006 int length = 6
626372797074 string kdfname = bcrypt
00000018 int length = 24 (kdfoptions)
00000010 int length = 16
d08f6b8fd17593f246db4ac6c45a1193 salt&amp;#x2F;iv for bcrypt
00000010 int work factor = 16
00000001 int number of public keys = 1
00000033 int length = 51 public key 1 size (4 + 11 + 4 + 32)

# Public key 1
0000000b int length = 11
7373682d65643235353139 string key type = ssh-ed25519
00000020 int length = 32
# public key payload 32 bytes
# probably encoding a point on the ed25519 curve
62837be86c63712896b8e0e7543e367c
3abd0c0b5ad3e764ea0e4f8ddd7d00ef

00000090 int length = 144 encrypted aes256-cbc output (16x9)
1e60c56ef30d0ff02e07b57bf1464507
6c32c86c88ecad545ca28424e4739aff
5895bebd6778e70b6c54b309b9fdb0c9
4102bf8cef5b97d3d75636967e67e4b9
c1ee72ae81074b0ce0f7e540e051d569
05da263af3e383342cc75b3145242abb
75257586a119c9d3673dfb7eabe46963
50904e7c7af3cd77f28bea10374e15bc
6536c2e1029438fdd3930beebbc5ac30
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;So there you have it. You could dig even deeper into the keys themselves to extract the various specific numbers (usually they have 1-letter names from the math equations they belong to like e,n,p,q etc) but I didn&#x27;t bother for this since for this key format all I want to know is that it&#x27;s an ed25519 private key and whether or not it has a passphrase.&lt;&#x2F;p&gt;
&lt;p&gt;Other bits I learned while pairing are that ed25519 is the name of a specific curve in elliptic curve cryptography and the numbers represent &lt;code&gt;((2^255) - 19)&lt;&#x2F;code&gt; as a Mersenne prime (out of my depth here, could be wrong). Part of it is the x coordinate and then you can compute the y bit if you know the sign or something like that.&lt;&#x2F;p&gt;
&lt;p&gt;Also in the openssh source code they explicitly error out if the number of keys field isn&#x27;t 1 so only 1 key is supported.&lt;&#x2F;p&gt;
&lt;p&gt;For further reading check out &lt;a href=&quot;https:&#x2F;&#x2F;blog.mozilla.org&#x2F;warner&#x2F;2011&#x2F;11&#x2F;29&#x2F;ed25519-keys&#x2F;&quot;&gt;How do Ed5519 keys work?&lt;&#x2F;a&gt; by Brian Warner.&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;This post is technically my Recurse Center 15 post but it was suggested that this post would make a handy reference so I didn&#x27;t want to muck up the clarity&#x2F;SEO of the title.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Recurse Center 14: OpenSSH key files</title>
          <pubDate>Thu, 30 Nov 2017 01:05:34 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2017/11/recurse-center-14-openssh-key-files/</link>
          <guid>https://peterlyons.com/problog/2017/11/recurse-center-14-openssh-key-files/</guid>
          <description xml:base="https://peterlyons.com/problog/2017/11/recurse-center-14-openssh-key-files/">&lt;p&gt;So today (my 15th day of RC sabbatical) was where the rubber was supposed to hit the road. I was working on actually digging into the meaty bits of the OpenSSH private key PEM files and starting to peek into the base64 encoded data.&lt;&#x2F;p&gt;
&lt;p&gt;I did some research this morning and futzed around with both bash pipelines and rust code to start to understand how these files work.&lt;&#x2F;p&gt;
&lt;p&gt;First fun thing I found was looking at the raw data inside a PEM-encoded private key with this pipeline:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sh&quot; class=&quot;language-sh &quot;&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;# strip out the header&amp;#x2F;footer lines (they have dashes)
grep -v - .&amp;#x2F;local&amp;#x2F;test_files&amp;#x2F;test-ssh-ed25519-2-passphrase.privatekey |
  # Decode the base64, this handles newlines no problem
  base64 -D |
  # dump that as hex
  hexdump
0000000 6f 70 65 6e 73 73 68 2d 6b 65 79 2d 76 31 00 00
0000010 00 00 0a 61 65 73 32 35 36 2d 63 62 63 00 00 00
0000020 06 62 63 72 79 70 74 00 00 00 18 00 00 00 10 d9
0000030 3b fe b4 54 93 c8 05 a7 77 21 7d 1c a5 69 1d 00
0000040 00 00 10 00 00 00 01 00 00 00 33 00 00 00 0b 73
0000050 73 68 2d 65 64 32 35 35 31 39 00 00 00 20 2e 40
0000060 78 7a 53 e2 89 d8 8f 54 9d 4b a3 3e 56 58 85 c7
0000070 1d 52 2e 7d 78 4e 9b e3 a5 89 1b 61 79 2a 00 00
0000080 00 90 68 c8 c3 da e6 6b cd d2 2f 4a 74 ad e9 7b
0000090 75 6a a6 8f d1 6d 24 86 fa 1e b1 77 25 0c e3 fa
00000a0 30 6d da 7c 72 11 be ac 98 0e b0 7b 7d 85 87 aa
00000b0 d1 49 c7 97 0a b4 5f 6e 61 5d 10 7c cd 55 c5 38
00000c0 45 e8 4c b8 2d 23 67 7b 23 18 97 06 7a 9d 0d bc
00000d0 69 d1 88 73 ba 34 8e 39 4e 50 01 cc 36 a7 c8 8b
00000e0 58 57 e9 c5 f4 b0 eb 79 ab 1b e6 64 a0 78 59 a6
00000f0 2b 05 03 00 57 bb 7b d4 19 ed 62 98 b1 db 4e 51
0000100 27 8e b7 7e f8 e2 42 d0 18 01 99 b6 c2 30 ad 9c
0000110 33 9e                                          
0000112
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;After some futzing I was sure I had the right data and extracted some telltale ASCII strings indicating I was definitely on the right track.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sh&quot; class=&quot;language-sh &quot;&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;# remove the PEM header&amp;#x2F;footer dash lines
grep -v - .&amp;#x2F;local&amp;#x2F;test_files&amp;#x2F;test-ssh-ed25519-2-passphrase.privatekey |
  # Decode the base64
  base64 -D |
  # find stuff that looks like ASCII words
  strings

openssh-key-v1
aes256-cbc
bcrypt
ssh-ed25519
(some gobbledygook omitted)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Bingo! Now that I know some key words to look for, let me make a handy reference from ASCII to hex for these words.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sh&quot; class=&quot;language-sh &quot;&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;for word in openssh-key-v1 aes256-cbc bcrypt ssh-ed25519 none
do
  echo -n &amp;quot;${word}: &amp;quot;
  echo -n &amp;quot;${word}&amp;quot; |
  hexdump |
  head -1 |
  cut -d &amp;quot; &amp;quot; -f 2-
done

openssh-key-v1: 6f 70 65 6e 73 73 68 2d 6b 65 79 2d 76 31
aes256-cbc: 61 65 73 32 35 36 2d 63 62 63
bcrypt: 62 63 72 79 70 74
ssh-ed25519: 73 73 68 2d 65 64 32 35 35 31 39
none: 6e 6f 6e 65
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Nice! OK so I&#x27;ve been studying that and got some RC help with the openssh C source that generates it. Here&#x27;s what I discovered about the byte-by-byte details of this openssh private key file.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;14 bytes magic ASCII &quot;openssh-key-v1&quot;
&lt;ul&gt;
&lt;li&gt;We can see our hex: &lt;code&gt;6f 70 65 6e 73 73 68 2d 6b 65 79 2d 76 31&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;1 NULL (zero) byte terminating that string
&lt;ul&gt;
&lt;li&gt;This is a bit weird because there&#x27;s a mix of null-terminated C strings and length-prefixed string structs, but that&#x27;s what it is&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;4 byte length for next string&lt;&#x2F;li&gt;
&lt;li&gt;N bytes string cipher name
&lt;ul&gt;
&lt;li&gt;&quot;none&quot; for no passphrase (cleartext), otherwise &quot;aes256-cbc&quot;&lt;&#x2F;li&gt;
&lt;li&gt;We can see in our hex aes256-cbc &lt;code&gt;61 65 73 32 35 36 2d 63 62 63&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;4 byte length for next string&lt;&#x2F;li&gt;
&lt;li&gt;N bytes string kdfname (kdf means Key Derivation Function)
&lt;ul&gt;
&lt;li&gt;We can see in our hex bcrypt &lt;code&gt;62 63 72 79 70 74&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;4 byte length for next string&lt;&#x2F;li&gt;
&lt;li&gt;N bytes kdfoptions&lt;&#x2F;li&gt;
&lt;li&gt;4 byte integer number of public keys in this file&lt;&#x2F;li&gt;
&lt;li&gt;one string for each public key&lt;&#x2F;li&gt;
&lt;li&gt;then an encrypted and padded blob for the private keys&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Part of the struggle researching this is AFAICT there&#x27;s no clear, googlable name for this file format. It&#x27;s just the bespoke openssh-key-v1 format but nobody calls it that and there&#x27;s no file extension.&lt;&#x2F;p&gt;
&lt;p&gt;So mostly I think I&#x27;m actually coding a command line tool in rust that will be useful to me now. I&#x27;m excited to keep pushing forward tomorrow.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Recurse Center 13: ssh private key PEM files</title>
          <pubDate>Tue, 28 Nov 2017 22:48:55 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2017/11/recurse-center-13-ssh-private-key-pem-files/</link>
          <guid>https://peterlyons.com/problog/2017/11/recurse-center-13-ssh-private-key-pem-files/</guid>
          <description xml:base="https://peterlyons.com/problog/2017/11/recurse-center-13-ssh-private-key-pem-files/">&lt;p&gt;Today I used the rust &lt;code&gt;pem&lt;&#x2F;code&gt; crate to check whether a given file was or was not parseable as PEM encoded. PEM stands for Privacy Enhanced Mail. I learned that most ssh private keys are stored as PEM files and there are ED25519, ECDSA, RSA, and DSA types (maybe more but those are the ones I&#x27;m familiar with and read about a bit in the openssh source code). Still not clear on whether the public keys are in SSLEAY format or not. Also did some pairing and showed another RCer the rust &lt;code&gt;?&lt;&#x2F;code&gt; operator.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>rust custom Display and sorting</title>
          <pubDate>Tue, 28 Nov 2017 21:16:34 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2017/11/rust-custom-display-and-sorting/</link>
          <guid>https://peterlyons.com/problog/2017/11/rust-custom-display-and-sorting/</guid>
          <description xml:base="https://peterlyons.com/problog/2017/11/rust-custom-display-and-sorting/">&lt;p&gt;So as an exercise I wanted to make a rust struct with custom order and blog the process. I did this in my tealeaves project to get some data to group&#x2F;sort by severity (errors then warnings then infos) and it worked nicely but it was mostly the derived implementations and I wanted to review. So let&#x27;s make a basic Person struct (very similar to the example in the official rust docs, but we&#x27;ll get fancier).&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;struct Person {
    name: String,
}

fn main() {
    println!(&amp;quot;{}&amp;quot;, Person { name: &amp;quot;Sheena&amp;quot;.to_string() });
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&quot;No can do!&quot; says the compiler:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;error[E0277]: the trait bound `Person: std::fmt::Display` is not satisfied
 --&amp;gt; src&amp;#x2F;main.rs:6:20
  |
6 |     println!(&amp;quot;{}&amp;quot;, Person { name: &amp;quot;Sheena&amp;quot;.to_string() });
  |                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `Person` cannot be formatted with the default formatter; try using `:?` instead if you are using a format string
  |
  = help: the trait `std::fmt::Display` is not implemented for `Person`
  = note: required by `std::fmt::Display::fmt`

error: aborting due to previous error
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;OK, let&#x27;s make Person displayable.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;use std::fmt;

struct Person {
    name: String,
}

impl fmt::Display for Person {
    fn fmt(&amp;amp;self, f: &amp;amp;mut fmt::Formatter) -&amp;gt; fmt::Result {
        write!(f, &amp;quot;{}&amp;quot;, self.name)
    }
}

fn main() {
    println!(&amp;quot;{}&amp;quot;, Person { name: &amp;quot;Sheena&amp;quot;.to_string() });
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;We run that and it prints &quot;Sheena&quot;. Pretty neat!&lt;&#x2F;p&gt;
&lt;p&gt;OK but now I wanted to make my Person struct sortable by age. As is often the case in rust, one part of this was way harder than in most languages and another part was also way harder than in other languages :-p.&lt;&#x2F;p&gt;
&lt;p&gt;First, we need a way to compute a person&#x27;s age based on their birthday, so I extended my Person struct to also store a &lt;code&gt;born&lt;&#x2F;code&gt; property, but in order to model that as a calendar date, we need the &lt;code&gt;chrono&lt;&#x2F;code&gt; crate and a while reading the docs to figure out what type we should use. After my research, I decided &lt;code&gt;chrono::NaiveDate&lt;&#x2F;code&gt; would be viable.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;extern crate chrono;
use chrono::NaiveDate;
use std::fmt;

struct Person {
    name: String,
    born: NaiveDate,

}

impl fmt::Display for Person {
    fn fmt(&amp;amp;self, f: &amp;amp;mut fmt::Formatter) -&amp;gt; fmt::Result {
        write!(f, &amp;quot;{}&amp;quot;, self.name)
    }
}

fn main() {
    println!(&amp;quot;{}&amp;quot;, Person { name: &amp;quot;Sheena&amp;quot;.to_string(), born: NaiveDate::from_ymd(1970, 1, 17) });
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;OK now I wanted a more concise way to define instances of Person, so I added a constructor function.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;extern crate chrono;
use chrono::NaiveDate;
use std::fmt;

struct Person {
    name: String,
    born: NaiveDate,
}

impl Person {
    pub fn new(name: String, year: i32, month: u32, day: u32) -&amp;gt; Self {
        Person {
            name,
            born: NaiveDate::from_ymd(year, month, day),
        }
    }
}

impl fmt::Display for Person {
    fn fmt(&amp;amp;self, f: &amp;amp;mut fmt::Formatter) -&amp;gt; fmt::Result {
        write!(f, &amp;quot;{}&amp;quot;, self.name)
    }
}

fn main() {
    println!(&amp;quot;{}&amp;quot;, Person::new(&amp;quot;Sheena&amp;quot;.to_string(), 1970, 1, 17));
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Next let&#x27;s write an &lt;code&gt;age&lt;&#x2F;code&gt; function so we can see how old a person is at this moment. Again I had to read the docs for a long time, and there may be a better way to do this, but my basic approach ended up being get a Duration between now and their birthday, convert that to a number of days, and divide by 365 to get something approximately yearish. While we&#x27;re at it we&#x27;ll update our Display trait to print their age.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;extern crate chrono;
use chrono::NaiveDate;
use chrono::Local;
use std::fmt;

struct Person {
    name: String,
    born: NaiveDate,
}

impl Person {
    pub fn new(name: String, year: i32, month: u32, day: u32) -&amp;gt; Self {
        Person {
            name,
            born: NaiveDate::from_ymd(year, month, day),
        }
    }

    pub fn age(&amp;amp;self) -&amp;gt; i64 {
        NaiveDate::signed_duration_since(Local::today().naive_local(), self.born).num_days() &amp;#x2F; 365
    }
}

impl fmt::Display for Person {
    fn fmt(&amp;amp;self, f: &amp;amp;mut fmt::Formatter) -&amp;gt; fmt::Result {
        write!(f, &amp;quot;{} (age: {})&amp;quot;, self.name, self.age())
    }
}

fn main() {
    println!(&amp;quot;{}&amp;quot;, Person::new(&amp;quot;Sheena&amp;quot;.to_string(), 1970, 1, 17));
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;OK now to make them sortable by age we need 4 total traits: &lt;code&gt;Eq, PartialEq, Ord, and PartialOrd&lt;&#x2F;code&gt;. &lt;code&gt;Eq&lt;&#x2F;code&gt; we can derive satisfactorily. The other three are mostly  boilerplate we copy from the rust documentation and adjust to sort people based on their &lt;code&gt;born&lt;&#x2F;code&gt; attribute with youngest first.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;extern crate chrono;
use chrono::{NaiveDate, Local};
use std::{cmp, fmt};

#[derive(Eq)]
struct Person {
    name: String,
    born: NaiveDate,
}

impl Person {
    pub fn new(name: String, year: i32, month: u32, day: u32) -&amp;gt; Self {
        Person {
            name,
            born: NaiveDate::from_ymd(year, month, day),
        }
    }

    pub fn age(&amp;amp;self) -&amp;gt; i64 {
        NaiveDate::signed_duration_since(Local::today().naive_local(), self.born).num_days() &amp;#x2F; 365
    }
}

impl PartialOrd for Person {
    fn partial_cmp(&amp;amp;self, other: &amp;amp;Person) -&amp;gt; Option&amp;lt;cmp::Ordering&amp;gt; {
        Some(other.cmp(self))
    }
}

impl Ord for Person {
    fn cmp(&amp;amp;self, other: &amp;amp;Person) -&amp;gt; cmp::Ordering {
        self.born.cmp(&amp;amp;other.born)
    }
}

impl PartialEq for Person {
    fn eq(&amp;amp;self, other: &amp;amp;Person) -&amp;gt; bool {
        self.born == other.born
    }
}

impl fmt::Display for Person {
    fn fmt(&amp;amp;self, f: &amp;amp;mut fmt::Formatter) -&amp;gt; fmt::Result {
        write!(f, &amp;quot;{} (age: {})&amp;quot;, self.name, self.age())
    }
}

fn main() {
    let mut people = vec![];
    people.push(Person::new(&amp;quot;Imogen Heap&amp;quot;.to_string(), 1977, 12, 9));
    people.push(Person::new(&amp;quot;Fatboy Slim&amp;quot;.to_string(), 1963, 7, 31));
    people.push(Person::new(&amp;quot;Weird Al&amp;quot;.to_string(), 1959, 10, 23));
    people.push(Person::new(&amp;quot;Zoë Keating&amp;quot;.to_string(), 1972, 2, 2));
    people.sort();
    for person in people.iter() {
        println!(&amp;quot;{}&amp;quot;, person);
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;So now we run this and we see a nice display format for people and they are sorted youngest first:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Imogen Heap (age: 39)
Zoë Keating (age: 45)
Fatboy Slim (age: 54)
Weird Al (age: 58)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</description>
      </item>
      <item>
          <title>Recurse Center 12: AsRef</title>
          <pubDate>Mon, 27 Nov 2017 23:10:25 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2017/11/recurse-center-12-asref/</link>
          <guid>https://peterlyons.com/problog/2017/11/recurse-center-12-asref/</guid>
          <description xml:base="https://peterlyons.com/problog/2017/11/recurse-center-12-asref/">&lt;p&gt;I spent a long time today trying to understand how the &lt;code&gt;AsRef&lt;&#x2F;code&gt; trait works. I failed.&lt;&#x2F;p&gt;
&lt;p&gt;I was able to split my tealeaves project code out into a handful of files and more strongly model the filesystem checks as Enums in the type system. If you are printing filesystem permission octals, don&#x27;t forget the &lt;code&gt;{:o}&lt;&#x2F;code&gt; format string.&lt;&#x2F;p&gt;
&lt;p&gt;I find if I battle the compiler too long or just in general struggle to understand something, it drains me really quickly. No dopamine hits from little victories. This is one of the reasons I like unit testing - lots of little frequent victories and most of the time no time debugging weird production issues whilst praying for the sweet kiss of death.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Recurse Center 11: rsfs rust associated types</title>
          <pubDate>Tue, 21 Nov 2017 23:01:17 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2017/11/recurse-center-11-rsfs-rust-associated-types/</link>
          <guid>https://peterlyons.com/problog/2017/11/recurse-center-11-rsfs-rust-associated-types/</guid>
          <description xml:base="https://peterlyons.com/problog/2017/11/recurse-center-11-rsfs-rust-associated-types/">&lt;p&gt;So I&#x27;m working on a tool that will interact heavily with the filesystem. This has always presented a unit testing problem both from a test speed perspective but as well as the perspective of simulating unusual errors.&lt;&#x2F;p&gt;
&lt;p&gt;Yesterday I shopped on crates.io and found the &lt;code&gt;filesystem&lt;&#x2F;code&gt; crate which looked to be built exactly to what I needed. However, I quickly discovered it was missing some key things like exposing a file&#x27;s size without having to read it and getting detailed permissions.&lt;&#x2F;p&gt;
&lt;p&gt;Today I shopped again and found &lt;code&gt;rsfs&lt;&#x2F;code&gt; which had the detailed unix stuff I needed. In theory I want to be able to use the real filesystem when my tool is run for real and a throw-away in-memory filesystem for unit tests (including being able to simulate rare error cases). In my mind this meant passing in some abstract filesystem interface to the guts of my module as a function argument. So I set out to learn the basics of the &lt;code&gt;rsfs&lt;&#x2F;code&gt; crate.&lt;&#x2F;p&gt;
&lt;p&gt;Here&#x27;s the first thing that prints out the permissions mode in octal of a real file that exists on my filesystem.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;extern crate rsfs;
use rsfs::GenFS; &amp;#x2F;&amp;#x2F; trait gives us .metadata()
use rsfs::Metadata; &amp;#x2F;&amp;#x2F; trait gives us .permissions()
use rsfs::unix_ext::PermissionsExt; &amp;#x2F;&amp;#x2F; trait gives us .mode()

fn main() {
    let fs  = rsfs::disk::FS;
    let meta = fs.metadata(&amp;quot;&amp;#x2F;tmp&amp;#x2F;foo.txt&amp;quot;).unwrap();
    let perms = meta.permissions();
    let mode = perms.mode();
    println!(&amp;quot;{:o}&amp;quot;, mode);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;hr &#x2F;&gt;
&lt;pre data-lang=&quot;sh&quot; class=&quot;language-sh &quot;&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
     Running `target&amp;#x2F;debug&amp;#x2F;fstool`
644

$ chmod 755 &amp;#x2F;tmp&amp;#x2F;foo.txt

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
     Running `target&amp;#x2F;debug&amp;#x2F;fstool`
755
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;OK, so far so good. Now, can we simulate that with an in-memory filesystem?&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;extern crate rsfs;
use rsfs::GenFS; &amp;#x2F;&amp;#x2F; trait gives us .metadata()
use rsfs::Metadata; &amp;#x2F;&amp;#x2F; trait gives us .permissions()
use rsfs::unix_ext::PermissionsExt; &amp;#x2F;&amp;#x2F; trait gives us .mode()

fn main() {
    let fs  = rsfs::mem::unix::FS::new();
    fs.create_dir_all(&amp;quot;&amp;#x2F;tmp&amp;quot;).unwrap();
    fs.create_file(&amp;quot;&amp;#x2F;tmp&amp;#x2F;foo.txt&amp;quot;).unwrap();
    let meta = fs.metadata(&amp;quot;&amp;#x2F;tmp&amp;#x2F;foo.txt&amp;quot;).unwrap();
    let perms = meta.permissions();
    let mode = perms.mode();
    println!(&amp;quot;{:o}&amp;quot;, mode);
}

&amp;#x2F;&amp;#x2F; fn main_disk() {
&amp;#x2F;&amp;#x2F;     let fs  = rsfs::disk::FS;
&amp;#x2F;&amp;#x2F;     let meta = fs.metadata(&amp;quot;&amp;#x2F;tmp&amp;#x2F;foo.txt&amp;quot;).unwrap();
&amp;#x2F;&amp;#x2F;     let perms = meta.permissions();
&amp;#x2F;&amp;#x2F;     let mode = perms.mode();
&amp;#x2F;&amp;#x2F;     println!(&amp;quot;{:o}&amp;quot;, mode);
&amp;#x2F;&amp;#x2F; }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;hr &#x2F;&gt;
&lt;pre data-lang=&quot;sh&quot; class=&quot;language-sh &quot;&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;$ cargo run
   Compiling fstool v0.1.0 (file:&amp;#x2F;&amp;#x2F;&amp;#x2F;private&amp;#x2F;tmp&amp;#x2F;x)
    Finished dev [unoptimized + debuginfo] target(s) in 1.11 secs
     Running `target&amp;#x2F;debug&amp;#x2F;fstool`
666
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Yup, looks acceptable.&lt;&#x2F;p&gt;
&lt;p&gt;OK let&#x27;s try to extract a helper function that works on either. My first thought would be the filesystem argument would be of type &lt;code&gt;rsfs::GenFS&lt;&#x2F;code&gt; which is the &quot;generic filesystem&quot; trait whose whole purpose is to be agnostic about which specific filesystem implementation is used.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;extern crate rsfs;
use rsfs::GenFS; &amp;#x2F;&amp;#x2F; trait gives us .metadata()
use rsfs::Metadata; &amp;#x2F;&amp;#x2F; trait gives us .permissions()
use rsfs::unix_ext::PermissionsExt; &amp;#x2F;&amp;#x2F; trait gives us .mode()

fn get_mode(fs: &amp;amp;GenFS, path: &amp;amp;str) -&amp;gt; u32 {
    let meta = fs.metadata(path).unwrap();
    let perms = meta.permissions();
    let mode = perms.mode();
    mode
}

fn main() {
    let mem = rsfs::mem::unix::FS::new();
    mem.create_dir_all(&amp;quot;&amp;#x2F;tmp&amp;quot;).unwrap();
    mem.create_file(&amp;quot;&amp;#x2F;tmp&amp;#x2F;foo.txt&amp;quot;).unwrap();

    let disk = rsfs::disk::FS;

    println!(&amp;quot;{:o}&amp;quot;, get_mode(mem, &amp;quot;&amp;#x2F;tmp&amp;#x2F;foo.txt&amp;quot;));
    println!(&amp;quot;{:o}&amp;quot;, get_mode(disk, &amp;quot;&amp;#x2F;tmp&amp;#x2F;foo.txt&amp;quot;));
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;hr &#x2F;&gt;
&lt;pre data-lang=&quot;sh&quot; class=&quot;language-sh &quot;&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;$ cargo run
   Compiling fstool v0.1.0 (file:&amp;#x2F;&amp;#x2F;&amp;#x2F;private&amp;#x2F;tmp&amp;#x2F;x)
error[E0191]: the value of the associated type `DirBuilder` (from the trait `rsfs::GenFS`) must be specified
 --&amp;gt; src&amp;#x2F;main.rs:6:18
  |
6 | fn get_mode(fs: &amp;amp;GenFS, path: &amp;amp;str) -&amp;gt; u32 {
  |                  ^^^^^ missing associated type `DirBuilder` value

error[E0191]: the value of the associated type `Permissions` (from the trait `rsfs::GenFS`) must be specified
 --&amp;gt; src&amp;#x2F;main.rs:6:18
  |
6 | fn get_mode(fs: &amp;amp;GenFS, path: &amp;amp;str) -&amp;gt; u32 {
  |                  ^^^^^ missing associated type `Permissions` value

error[E0191]: the value of the associated type `ReadDir` (from the trait `rsfs::GenFS`) must be specified
 --&amp;gt; src&amp;#x2F;main.rs:6:18
  |
6 | fn get_mode(fs: &amp;amp;GenFS, path: &amp;amp;str) -&amp;gt; u32 {
  |                  ^^^^^ missing associated type `ReadDir` value

error[E0191]: the value of the associated type `Metadata` (from the trait `rsfs::GenFS`) must be specified
 --&amp;gt; src&amp;#x2F;main.rs:6:18
  |
6 | fn get_mode(fs: &amp;amp;GenFS, path: &amp;amp;str) -&amp;gt; u32 {
  |                  ^^^^^ missing associated type `Metadata` value

error[E0191]: the value of the associated type `OpenOptions` (from the trait `rsfs::GenFS`) must be specified
 --&amp;gt; src&amp;#x2F;main.rs:6:18
  |
6 | fn get_mode(fs: &amp;amp;GenFS, path: &amp;amp;str) -&amp;gt; u32 {
  |                  ^^^^^ missing associated type `OpenOptions` value

error[E0191]: the value of the associated type `DirEntry` (from the trait `rsfs::GenFS`) must be specified
 --&amp;gt; src&amp;#x2F;main.rs:6:18
  |
6 | fn get_mode(fs: &amp;amp;GenFS, path: &amp;amp;str) -&amp;gt; u32 {
  |                  ^^^^^ missing associated type `DirEntry` value

error[E0191]: the value of the associated type `File` (from the trait `rsfs::GenFS`) must be specified
 --&amp;gt; src&amp;#x2F;main.rs:6:18
  |
6 | fn get_mode(fs: &amp;amp;GenFS, path: &amp;amp;str) -&amp;gt; u32 {
  |                  ^^^^^ missing associated type `File` value

error: aborting due to 7 previous errors

error: Could not compile `fstool`.

To learn more, run the command again with --verbose.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Damn, the compiler sure is mad about a bunch of associated types. OK let&#x27;s try to specify the associated types, at least for the bits we think we really need.&lt;&#x2F;p&gt;
&lt;p&gt;We tried many variants and sadly I can&#x27;t really walk you through the whole sequence of compiler error, code change, rinse repeat, but it was probably 19 iterations. We knew that basically when we called &lt;code&gt;.metadata().permissions()&lt;&#x2F;code&gt; we needed to tell the type system &quot;Hey, use PermissionsExt there because that has the .mode() function we need and don&#x27;t just give me back Permissions because there&#x27;s no .mode() function there&quot;. But because that&#x27;s nested underneath the Metadata associated type, we had to first say, OK for Metadata, we want a Permissions associated type that has all the combined functionality of both Permissions and PermissionsExt, which looks like &lt;code&gt;Permissions + PermissionExt&lt;&#x2F;code&gt; in trait sublanguage.&lt;&#x2F;p&gt;
&lt;p&gt;However, the syntax here doesn&#x27;t let us write it exactly that way. We get an error &lt;code&gt;the trait rsfs::Permissions cannot be made into an object&lt;&#x2F;code&gt;. At this point I&#x27;m basically 2 levels below my depth here but a fellower RCer was pairing with me and knew that we basically needed to define that combination of associated types as &lt;code&gt;P&lt;&#x2F;code&gt; and then explain to the typesystem to use it both for our &lt;code&gt;Metadata&lt;&#x2F;code&gt; associated type and our &lt;code&gt;GenFS&lt;&#x2F;code&gt; type. That ultimately ends up looking like the next snippet. Even though the compiler initially complained about needing ALL the associated types specified, I think because we are only actually using APIs related to permissions, we got away with only specifying the associated types necessary for the API calls we actually use in our code.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;extern crate rsfs;
use rsfs::GenFS; &amp;#x2F;&amp;#x2F; trait gives us .metadata()
use rsfs::Metadata; &amp;#x2F;&amp;#x2F; trait gives us .permissions()
use rsfs::unix_ext::PermissionsExt; &amp;#x2F;&amp;#x2F; trait gives us .mode()
use rsfs::Permissions;

fn get_mode&amp;lt;P: Permissions + PermissionsExt,
            M: Metadata&amp;lt;Permissions = P&amp;gt;,
            F: GenFS&amp;lt;Permissions = P, Metadata = M&amp;gt;&amp;gt;
    (fs: &amp;amp;F,
     path: &amp;amp;str)
     -&amp;gt; u32 {
    let meta = fs.metadata(path).unwrap();
    let perms = meta.permissions();
    let mode = perms.mode();
    mode
}

fn main() {
    let mem = rsfs::mem::unix::FS::new();
    mem.create_dir_all(&amp;quot;&amp;#x2F;tmp&amp;quot;).unwrap();
    mem.create_file(&amp;quot;&amp;#x2F;tmp&amp;#x2F;foo.txt&amp;quot;).unwrap();

    let disk = rsfs::disk::FS;

    println!(&amp;quot;{:o}&amp;quot;, get_mode(&amp;amp;mem, &amp;quot;&amp;#x2F;tmp&amp;#x2F;foo.txt&amp;quot;));
    println!(&amp;quot;{:o}&amp;quot;, get_mode(&amp;amp;disk, &amp;quot;&amp;#x2F;tmp&amp;#x2F;foo.txt&amp;quot;));
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;hr &#x2F;&gt;
&lt;pre data-lang=&quot;sh&quot; class=&quot;language-sh &quot;&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;$ cargo run
   Compiling fstool v0.1.0 (file:&amp;#x2F;&amp;#x2F;&amp;#x2F;private&amp;#x2F;tmp&amp;#x2F;x)
    Finished dev [unoptimized + debuginfo] target(s) in 1.5 secs
     Running `target&amp;#x2F;debug&amp;#x2F;fstool`
666
755
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Success! We now have a business logic function that works identically on both an in-memory filesystem and the real filesystem. This is pretty exciting for me especially since the rsfs crate docs indicate you can synthesize error cases, which lights up my &quot;100% code coverage&quot; light bulb.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Recurse Center 10: filesystem palindrome</title>
          <pubDate>Tue, 21 Nov 2017 00:05:33 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2017/11/recurse-center-10-filesystem-palindrome/</link>
          <guid>https://peterlyons.com/problog/2017/11/recurse-center-10-filesystem-palindrome/</guid>
          <description xml:base="https://peterlyons.com/problog/2017/11/recurse-center-10-filesystem-palindrome/">&lt;p&gt;This morning I worked on my &quot;tealeaves&quot; crypto diagnostic library. Just the basics of statting files but some interesting type system modeling and grokking what &lt;code&gt;#[derive(PartialOrd)]&lt;&#x2F;code&gt; does for enums and structs. I basically got rust to handle printing errors then warnings then OK messages via the sorting mechanisms.&lt;&#x2F;p&gt;
&lt;p&gt;I found a could-be-awesome crate called &lt;code&gt;filesystem&lt;&#x2F;code&gt; which gives a consistent interface for both an in-memory filesystem for fast unit tests as well as the real filesystem for production code. It&#x27;s something I&#x27;ve always wanted. However, at the moment the API is not quite rich enough for what I want to do so I&#x27;m not sure it will work unless I enhance it.&lt;&#x2F;p&gt;
&lt;p&gt;Late in the day a group of us did a polyglot pairing exercise where we paired up to do some basic exercises in a programming language neither of us knew. I paired with JB in clojure and we implemented a program to find the longest palindrome in a sentence. I learned a bunch of stuff about clojure and it was a fun exercise.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Recurse Center 9: rayon par iter</title>
          <pubDate>Fri, 17 Nov 2017 22:05:44 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2017/11/recurse-center-9-rayon-par-iter/</link>
          <guid>https://peterlyons.com/problog/2017/11/recurse-center-9-rayon-par-iter/</guid>
          <description xml:base="https://peterlyons.com/problog/2017/11/recurse-center-9-rayon-par-iter/">&lt;p&gt;Today I refactored my little rust blockchain miner to divide the mining work across multiple CPUs. The rayon crate&#x27;s &lt;code&gt;par_iter()&lt;&#x2F;code&gt; provides the magic for this. Before coding that, I was imagining how I&#x27;d handle that given I&#x27;m searching a 32-bit integer space where each number is equally likely to be a &quot;golden nonce&quot; (meaning a successful block mine). The most straightforward approach I could think of would be to break the single range into equal-sized partial ranges and ask the OS to distribute the load across cores. So instead of trying 0 then 1 then 2 in serial, you&#x27;d have 4 workers starting at (using 100 for obviousness) 0, 25, 50, 75 for the first round then each would advance so you&#x27;d have 1, 26, 51, 76 and so on.&lt;&#x2F;p&gt;
&lt;p&gt;I coded a simpler scratch version and printed debug output and was delighted to see at least part of that holding true with rayon. I could definitely see several sequences of incrementing nonces being tested in ascending order, they just weren&#x27;t obviously mapped to the range in a way I could grok.&lt;&#x2F;p&gt;
&lt;p&gt;I also found out the testing I did yesterday with the &lt;code&gt;shaman&lt;&#x2F;code&gt; crate had a bug and you can indeed both give it read-only access to an existing vector of the payload data plus you can call &lt;code&gt;.insert()&lt;&#x2F;code&gt; more than once, so I refactored to just call &lt;code&gt;.insert()&lt;&#x2F;code&gt; with a reference to the payload then again with a reference to the 4-byte nonce slice and that avoids any memory copies and mutation, which I believe is nearly as good as you can do in rust.&lt;&#x2F;p&gt;
&lt;p&gt;So now my macbook can mine 8-16x faster than yesterday. However, the concurrent version no longer has deterministic behavior, so finding the same nonce on the same payload doesn&#x27;t always take the same number of hashes, and the lower difficulty setting the larger the number of possible golden nonces so on separate runs you can mine different nonces. But however with multiple runs you do see it is just a small set of nonces that repeat.&lt;&#x2F;p&gt;
&lt;p&gt;How to print progress in a unit-test-friendly way is still unclear, at least in terms of what would be the idiom in rust. Maybe since output is captured by default you just don&#x27;t worry about it? Not sure but I tried to use a channel such that each worker could send a message to the parent thread whenever it wanted to report a batch of a million non-golden nonces tested, allowing the parent thread to handling printing that as a dot to stdout, but I couldn&#x27;t get that to compile.&lt;&#x2F;p&gt;
&lt;p&gt;One helpful development pattern I&#x27;m using a lot is focusing on the core function signatures and building a mock-up in a separate &quot;scratch&quot; project I have where the guts of most functions are stubbed out to just return a hard coded value or some dummy data but all the type signatures are exactly what I need for the real project. Then I iterate on that and do my compiler battling, and when that&#x27;s over only then do I start adding implementation logic to the function bodies. That has been helpful this week for sure.&lt;&#x2F;p&gt;
&lt;p&gt;In other news I made Firefox Quantum my default browser today, partially due to rust fanboyism. So far so good. The &quot;Vertical Tabs Reloaded&quot; add-on is nice.&lt;&#x2F;p&gt;
&lt;p&gt;Oh random shell tip when debugging checksums. I wasn&#x27;t sure if I was using the rust &lt;code&gt;shaman&lt;&#x2F;code&gt; crate correctly so I wrote a scratch rust program to feed it data from the first command line argument, then simulate a nonce as &lt;code&gt;&quot;aaaa&quot;&lt;&#x2F;code&gt; and print the hex of that hash. Then I verified with an independent implementation via the command line: &lt;code&gt;echo -n abcdefghijkaaaa| shasum -a 256&lt;&#x2F;code&gt; and when both hashes were identical I knew I had the right code. (Don&#x27;t forget the &lt;code&gt;-n&lt;&#x2F;code&gt; or you&#x27;ll accidentally include the newline as your hash payload).&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Never Graduate</title>
          <pubDate>Fri, 17 Nov 2017 21:11:59 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2017/11/never-graduate/</link>
          <guid>https://peterlyons.com/problog/2017/11/never-graduate/</guid>
          <description xml:base="https://peterlyons.com/problog/2017/11/never-graduate/">&lt;p&gt;&lt;strong&gt;Neo&lt;&#x2F;strong&gt;: Whoa. Déjà vu. Déjà vu.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Trinity&lt;&#x2F;strong&gt;: What did you see, Neo?&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Neo&lt;&#x2F;strong&gt;: It felt like I just did three Code and Coffees back to back.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Morpheus&lt;&#x2F;strong&gt;: Code and Coffee is a system. A system with rules. Rules like &quot;Code and Coffee is once a week&quot;. Rules like &quot;Code and Coffee is nine to noon&quot;.
Some of these rules can be bent. Others can be broken.
What if I told you that you could do sixty Code and Coffees in a row?&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Neo&lt;&#x2F;strong&gt;: That&#x27;s impossible.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Morpheus&lt;&#x2F;strong&gt;: Is that what you believe? What if I told you some people do one hundred and eighty Code and Coffees back to back? Impossible? Free your mind. Never graduate.&lt;&#x2F;p&gt;
&lt;p&gt;(&lt;em&gt;Morpheus slow-mo backflips over a desk, reaches  overhead to start typing on a laptop while still upside down before landing into a swivel chair&lt;&#x2F;em&gt;)&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Recurse Center 8: binary literals</title>
          <pubDate>Fri, 17 Nov 2017 02:18:51 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2017/11/recurse-center-8-binary-literals/</link>
          <guid>https://peterlyons.com/problog/2017/11/recurse-center-8-binary-literals/</guid>
          <description xml:base="https://peterlyons.com/problog/2017/11/recurse-center-8-binary-literals/">&lt;p&gt;Not that much to report today mostly some tweaks to my rust blockchain miner exercise. I remembered rust has binary literals and bitwise operations are one place they actually are useful. I&#x27;ve watched all the &lt;a href=&quot;http:&#x2F;&#x2F;intorust.com&quot;&gt;Into Rust&lt;&#x2F;a&gt; tutorials&#x2F;exercises and they were great but there&#x27;s only a handful. I did an exercism on isograms and demoed my rustblock program during Thursday evening demos. As tends to be the case, I learned a lot from the RCers while talking about the miner and apparently the bitcoin difficulty factor is so high that you need datacenter scale computing power to even hope to mine one block in a small number of minutes. My rust miner starts to take more than a few seconds searching for a golden nonce with about 27 or 28 leading zeros so presumably the bitcoin work factor is quite a bit larger than that (with a maximum of 256 since that&#x27;s the sha size but practically has to be quite a bit lower than that to allow space for enough blocks to get unique hashes for all of bitcoin&#x27;s lifetime I guess).&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Recurse Center 7: blockchain mining in rust</title>
          <pubDate>Thu, 16 Nov 2017 00:01:27 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2017/11/recurse-center-7-blockchain-mining-in-rust/</link>
          <guid>https://peterlyons.com/problog/2017/11/recurse-center-7-blockchain-mining-in-rust/</guid>
          <description xml:base="https://peterlyons.com/problog/2017/11/recurse-center-7-blockchain-mining-in-rust/">&lt;p&gt;This morning I tweaked my hexagonal lambda tooling and tests. No big deal. I also started watching the &quot;Into Rust&quot; screencasts. The first one is great for answering the &quot;Why Rust?&quot; question which I got asked twice today already.&lt;&#x2F;p&gt;
&lt;p&gt;After lunch I paired on the inner guts of a blockchain miner in rust. Just enough to combine a nonce with an array of block payload bytes and hunt for a sha256 &quot;golden nonce&quot; with a given number of leading zero bits.&lt;&#x2F;p&gt;
&lt;p&gt;We initially arbitrarily picked a suboptimal crate for the sha256 algorithm that needed a mutable reference to the block+nonce data so we had to clone it every time we had to hash a different nonce. I eventually did a second shopping trip to crates.io and found shaman which just needed an immutable reference to our data, which was perfect. There&#x27;s a little bit of bitwise logic and bit shifting required which is normally &quot;scary low level&quot; land from which I run away but with a pair to check me on things like &quot;If I want the second bit of a u8 to be a 1, that&#x27;s 64, right?&quot; it was OK.&lt;&#x2F;p&gt;
&lt;p&gt;We increased the difficulty factor up to 24 leading zero bits and were able to mine blocks quickly on my 2014 mackbook, but at 25 bits it started to take a minute or so. I haven&#x27;t tried 26 but maybe I&#x27;ll try tonight and leave it running with a timer.&lt;&#x2F;p&gt;
&lt;p&gt;Some of the RCers played &quot;Coding Bee&quot; this evening which was a very amusing spectacle. It&#x27;s done in 2-coder teams. You are read a small programming exercise of self-selected difficulty (easy&#x2F;medium&#x2F;hard). You then code a solution by each taking turns speaking a single character to a dedicated typist who types it up onto the projector, but the players are not allowed to look at the screen. There&#x27;s also no backspace, just &quot;clear line&quot; and you have to speak all indentation and punctuation. Amazingly, some teams actually wrote correct programs.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Recurse Center 6: nth prime</title>
          <pubDate>Tue, 14 Nov 2017 22:33:52 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2017/11/recurse-center-6-nth-prime/</link>
          <guid>https://peterlyons.com/problog/2017/11/recurse-center-6-nth-prime/</guid>
          <description xml:base="https://peterlyons.com/problog/2017/11/recurse-center-6-nth-prime/">&lt;p&gt;This morning I wrote a script to manage AWS session tokens and asked for a code review. Not optimistic that I&#x27;ll ultimately use it and I&#x27;ll probably go back to aws-vault, but at least it works now and I can test drive it.&lt;&#x2F;p&gt;
&lt;p&gt;I did the 3 next exercism rust exercises. There are 70+ of them and I&#x27;m worried they are too much about basic programming problems and not enough about how to write idiomatic rust, so I think starting tomorrow I&#x27;m going to just cherry pick a few that look the most interesting.&lt;&#x2F;p&gt;
&lt;p&gt;Heading to Etsy to hear Suz Hinton talk about Code as Craft this evening.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Recurse Center 5: command line arg parsing</title>
          <pubDate>Mon, 13 Nov 2017 22:52:32 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2017/11/recurse-center-5-command-line-arg-parsing/</link>
          <guid>https://peterlyons.com/problog/2017/11/recurse-center-5-command-line-arg-parsing/</guid>
          <description xml:base="https://peterlyons.com/problog/2017/11/recurse-center-5-command-line-arg-parsing/">&lt;p&gt;This morning I integrated the rust clap crate into some of my little CLI exercise utilities and learned a bit about dealing with a mix of &lt;code&gt;Option&lt;&#x2F;code&gt; and &lt;code&gt;Result&lt;&#x2F;code&gt; types, which is annoying. clap is really nice. I found a problem with the docs and filed a github issue and sent them a PR. It&#x27;s actually ultimately an issue with the crates.io website code so I&#x27;m hoping one of those maintainers makes a fix that will handle the situation for all crates.&lt;&#x2F;p&gt;
&lt;p&gt;I&#x27;ve been basically trying to make a customized &lt;code&gt;aws-vault&lt;&#x2F;code&gt; and hitting some frustrations. Not sure if this approach makes sense but we&#x27;ll see.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Quick Thoughts: Machine Learning</title>
          <pubDate>Sun, 12 Nov 2017 14:33:55 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2017/11/quick-thoughts-machine-learning/</link>
          <guid>https://peterlyons.com/problog/2017/11/quick-thoughts-machine-learning/</guid>
          <description xml:base="https://peterlyons.com/problog/2017/11/quick-thoughts-machine-learning/">&lt;p&gt;Things machine learning plus UX teams seemingly can&#x27;t get right yet:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Is this non-friend facebook message important or not?&lt;&#x2F;li&gt;
&lt;li&gt;Do I care if this person liked some random thing on facebook?&lt;&#x2F;li&gt;
&lt;li&gt;Do I need notifications or directions to an event I&#x27;m already at?&lt;&#x2F;li&gt;
&lt;li&gt;Am I in a vehicle right now?&lt;&#x2F;li&gt;
&lt;li&gt;What zoom level would be useful for this map?&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</description>
      </item>
      <item>
          <title>Recurse Center 4: Reservoir Sampling</title>
          <pubDate>Fri, 10 Nov 2017 23:08:36 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2017/11/recurse-center-4-reservoir-sampling/</link>
          <guid>https://peterlyons.com/problog/2017/11/recurse-center-4-reservoir-sampling/</guid>
          <description xml:base="https://peterlyons.com/problog/2017/11/recurse-center-4-reservoir-sampling/">&lt;p&gt;This morning I battled the borrow checker for a while trying to properly pass a variable through a closure to a child thread. I eventually figured out calling &lt;code&gt;.to_owned()&lt;&#x2F;code&gt; in the right place was needed, but it took a long time to get to that point. My normal urge is to try to understand what&#x27;s happening by studying closely the compiler error message and the standard library docs, but for pragmatic reasons I need to let go of that and not wait to just google &quot;how to X in rust&quot; sooner rather than later.&lt;&#x2F;p&gt;
&lt;p&gt;In the afternoon I paired with Casey on refining my rust basics for my little toy CLI apps and learned some good idioms there with the question operator. Then we refactored my tirefox program to use the reservoir sampling algorithm from The Art of Computer Programming so we could sample our words file in a single pass instead of needing a separate pass to count the words.&lt;&#x2F;p&gt;
&lt;p&gt;I managed to get in 2 sets of physical exercise which some of the RCers do periodically and two halfway decent naps which made a huge difference (it&#x27;s after 6PM and I&#x27;m still functioning). Another takeaway is that I learn a lot while pairing so I intend to request to pair very frequently and accept all offers of pairing.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Recurse Center 3: Box</title>
          <pubDate>Fri, 10 Nov 2017 04:58:32 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2017/11/recurse-center-3-box/</link>
          <guid>https://peterlyons.com/problog/2017/11/recurse-center-3-box/</guid>
          <description xml:base="https://peterlyons.com/problog/2017/11/recurse-center-3-box/">&lt;p&gt;Today I wrote a little unix utility to ensure a given line is present in a file. It&#x27;s not that much longer than I would expect in a scripting language (60 lines vs probably ~10 for python), but the number of machinations I had to go through was vastly larger. To get basename of &lt;code&gt;argv[0]&lt;&#x2F;code&gt; I had to make a &lt;code&gt;Path&lt;&#x2F;code&gt; struct which brings lots of &lt;code&gt;Option&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;Result&lt;&#x2F;code&gt; handling into things. I&#x27;m getting less terrible at reading the standard library reference documentation. I continue to be frustrated that the expandable sections for each method are all expanded by default. Maybe I&#x27;ll work on a PR for that later assuming it&#x27;s not baked into the tool they use to generate the docs.&lt;&#x2F;p&gt;
&lt;p&gt;The timing in NYC is indeed proving to be tricky as I predicted. I work best first thing in the morning, but that&#x27;s when the subways are most crowded. So RC mostly starts late around 10:30ish and goes late. I also start to fade fast into late afternoon exhaustion around 4PM. Today I just came in early. The commute wasn&#x27;t that bad, but I had to let 2 trains pass because they were totally full. I think I&#x27;ll end up working at home or from a coffee shop in Park Slope or from Brooklyn Boulders in the mornings for about 2 hours, then commute into RC, and either head home on the early side or find exhaustion-compatible things to fill the last 2 or so hours before commuting back home.&lt;&#x2F;p&gt;
&lt;p&gt;Oh and I learned 3 new ways to make coffee so far: chemex, aeropress, and keurig (not as easy as you might assume). Only tried 2 myself but tomorrow I&#x27;ll try making a mug via aeropress.&lt;&#x2F;p&gt;
&lt;p&gt;I also paired with Casey at the end of the day and got a good verbal explanation of the differences between Refs and Boxes in rust.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Recurse Center 2: Generics</title>
          <pubDate>Wed, 08 Nov 2017 22:27:21 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2017/11/recurse-center-2-generics/</link>
          <guid>https://peterlyons.com/problog/2017/11/recurse-center-2-generics/</guid>
          <description xml:base="https://peterlyons.com/problog/2017/11/recurse-center-2-generics/">&lt;p&gt;Worked through 3 of the really tricky chapters of Rust By Example today. Still not a ton of lightbulbs illuminating but it will come with more coding. I feel hesitant to code more exercises until I get through the next 3 chapters on error handling etc and then I&#x27;ll have a bit more knowledge whereas this morning hacking through stuff it was clear I didn&#x27;t have the basics down when I had working code and just wanted to extract it unchanged into a function, which should in theory be straightforward, but I was unable to express the type signatures correctly.&lt;&#x2F;p&gt;
&lt;p&gt;Also paired a little bit on passport.js and nginx with my batchmates.&lt;&#x2F;p&gt;
&lt;p&gt;Already had a bit of panic about what if I need 2 weeks just to get basic fluency in rust then I have only 4 weeks to code on a project.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Recurse Center 1: Shuffle Iterator</title>
          <pubDate>Tue, 07 Nov 2017 23:38:20 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2017/11/recurse-center-1-shuffle-iterator/</link>
          <guid>https://peterlyons.com/problog/2017/11/recurse-center-1-shuffle-iterator/</guid>
          <description xml:base="https://peterlyons.com/problog/2017/11/recurse-center-1-shuffle-iterator/">&lt;p&gt;This post is pretty stream of consciousness.&lt;&#x2F;p&gt;
&lt;p&gt;Thoughts on rust language and syntax so far (about ~45 hours spent studying, trickled in over the last 6 months or so).&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-hard&quot;&gt;The Hard&lt;&#x2F;h2&gt;
&lt;p&gt;rust stuff that are hard for me, unfamiliar, or I haven&#x27;t studied yet&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;ref&#x2F;stack&#x2F;heap&lt;&#x2F;li&gt;
&lt;li&gt;move&#x2F;borrow&lt;&#x2F;li&gt;
&lt;li&gt;lifetimes&lt;&#x2F;li&gt;
&lt;li&gt;traits&lt;&#x2F;li&gt;
&lt;li&gt;generics&lt;&#x2F;li&gt;
&lt;li&gt;ampersand ref syntax, tick lifetime syntax&lt;&#x2F;li&gt;
&lt;li&gt;keeping track of intermediate type conversions when chaining methods like &lt;code&gt;.iter().map().collect()&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;super&#x2F;self unclear when&#x2F;how these are OK to use, especially self&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;the-easy&quot;&gt;The Easy&lt;&#x2F;h2&gt;
&lt;p&gt;rust stuff that is easy for me or familiar&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;pub, access&lt;&#x2F;li&gt;
&lt;li&gt;most syntax
&lt;ul&gt;
&lt;li&gt;function&lt;&#x2F;li&gt;
&lt;li&gt;let assignment&lt;&#x2F;li&gt;
&lt;li&gt;match&lt;&#x2F;li&gt;
&lt;li&gt;loop&lt;&#x2F;li&gt;
&lt;li&gt;if&lt;&#x2F;li&gt;
&lt;li&gt;most keywords&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;Cargo.toml&lt;&#x2F;li&gt;
&lt;li&gt;most cargo subcommands are clear enough for the basics&lt;&#x2F;li&gt;
&lt;li&gt;mod keyword (mostly I think)&lt;&#x2F;li&gt;
&lt;li&gt;Option&#x2F;Some&#x2F;None, Result&#x2F;OK&#x2F;Err (similar to elm)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;the-bad&quot;&gt;The Bad&lt;&#x2F;h2&gt;
&lt;p&gt;rust stuff I dislike&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;double colon namespacing, one should suffice&lt;&#x2F;li&gt;
&lt;li&gt;C-style abbreviations&lt;&#x2F;li&gt;
&lt;li&gt;turbofish type hinter seems weird both syntax and position
&lt;ul&gt;
&lt;li&gt;unclear when I need an annotation before the assignment, a turbofish, or both&#x2F;neither&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;println macro is so verbose&lt;&#x2F;li&gt;
&lt;li&gt;something feels weird about the &lt;code&gt;use&lt;&#x2F;code&gt; statement when bringing a trait into scope but not getting a name for that. Why is this necessary?&lt;&#x2F;li&gt;
&lt;li&gt;I mostly don&#x27;t like aliasing you can do with &lt;code&gt;use&lt;&#x2F;code&gt;. I acknowledge it&#x27;s necessary but linters should check it&#x27;s only used when necessary. If you alias something from the std lib just because you don&#x27;t like the name, you discard a bunch of easy understanding and readability by Un-Ronsealing (https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Does_exactly_what_it_says_on_the_tin) stuff&lt;&#x2F;li&gt;
&lt;li&gt;attribute syntax is overly verbose&lt;&#x2F;li&gt;
&lt;li&gt;Needing doc comments on every line. I&#x27;d so much rather just have a start and end delimiter so hard wrapping via editor tooling was easy
&lt;ul&gt;
&lt;li&gt;maybe make an atom plugin for this?&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;attribute config language. Why is there a &lt;code&gt;not(fool = &quot;bar&quot;)&lt;&#x2F;code&gt; syntax instead of &lt;code&gt;foo != bar&lt;&#x2F;code&gt; like actual rust&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;the-good&quot;&gt;The Good&lt;&#x2F;h2&gt;
&lt;p&gt;rust stuff I like&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;lower_snake_case for most things&lt;&#x2F;li&gt;
&lt;li&gt;both separating external API tests but keeping internal unit tests in the same file as the code feel good to me&lt;&#x2F;li&gt;
&lt;li&gt;benchmarking support baked in, not that I&#x27;ve ever benchmarked anything, but it&#x27;s cool&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;notes-and-questions-from-today&quot;&gt;Notes and Questions from Today&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;don&#x27;t skim the rust by example snippets, read them line by line or you&#x27;ll miss key stuff&lt;&#x2F;li&gt;
&lt;li&gt;are super and self always referring to module scopes?&lt;&#x2F;li&gt;
&lt;li&gt;I&#x27;m losing&#x2F;lost interest in the web because I feel like it&#x27;s actively and rapidly and irrecoverably getting worse (mostly due to advertising and corporate interests) vs unix&#x2F;command line which is at least a stable level of brokenness and not getting worse.&lt;&#x2F;li&gt;
&lt;li&gt;Need a totally new-to-me coding process to deal with rust
&lt;ul&gt;
&lt;li&gt;First just get the type signatures right and return a hard coded basic value&lt;&#x2F;li&gt;
&lt;li&gt;one line at a time try to get something working, anything&lt;&#x2F;li&gt;
&lt;li&gt;separate &lt;code&gt;src&#x2F;scratch.rs&lt;&#x2F;code&gt; file I can periodically wipe clean to have a low-context area to attempt things in isolation&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;social screw ups: I referred to people as &quot;guys&quot; once. I&#x27;m sorry.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</description>
      </item>
      <item>
          <title>Recurse Center 0: Rust Anagrams</title>
          <pubDate>Tue, 07 Nov 2017 03:05:19 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2017/11/recurse-center-0-rust-anagrams/</link>
          <guid>https://peterlyons.com/problog/2017/11/recurse-center-0-rust-anagrams/</guid>
          <description xml:base="https://peterlyons.com/problog/2017/11/recurse-center-0-rust-anagrams/">&lt;p&gt;Today was my first day at Recurse Center! I was excited. Like Christmas excited. And in fact, I woke up at 5AM due to said excitement. We weren&#x27;t supposed to show up earlier than 10AM so I had a while to wait around in the morning.&lt;&#x2F;p&gt;
&lt;p&gt;All day was orientation type stuff about the logistics of the space, time to meet people and chat over food, panels about how best to use our time here, and some lightning talks.&lt;&#x2F;p&gt;
&lt;p&gt;We did a pair programming exercise which I did in rust. We managed to get a no-op version compiling and then spent the remaining 45 minutes with various flavors of non-compiling code and battling lifetimes. After dinner I took another crack at it and had something passing all the tests after another 2 hours most of which was reading online trying to find examples that did the type conversions I needed to appease the compiler.&lt;&#x2F;p&gt;
&lt;p&gt;https:&#x2F;&#x2F;github.com&#x2F;focusaurus&#x2F;rust-basics&#x2F;blob&#x2F;master&#x2F;anagram&#x2F;src&#x2F;lib.rs&lt;&#x2F;p&gt;
&lt;p&gt;I was pleased that RC encourages naps and plan to nap at least once a day. There are also 2 people from the F2 batch doing some rust and a bunch of folks knowledgeable about rust on the RC chat server which I think will be great because getting in-person help can save tons of time with hard-to-google rust type conversion errors such as I had tonight which amounts to &quot;I have &lt;code&gt;Vec&amp;lt;&amp;amp;&amp;amp;str&amp;gt;&lt;&#x2F;code&gt; but I need &lt;code&gt;Vec&amp;lt;&amp;amp;str&amp;gt;&lt;&#x2F;code&gt;&quot; which as you might predict is tough as a web search term.&lt;&#x2F;p&gt;
&lt;p&gt;I found these two good blog posts in my search:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;http:&#x2F;&#x2F;hermanradtke.com&#x2F;2015&#x2F;06&#x2F;22&#x2F;effectively-using-iterators-in-rust.html&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;http:&#x2F;&#x2F;hermanradtke.com&#x2F;2015&#x2F;05&#x2F;06&#x2F;creating-a-rust-function-that-accepts-string-or-str.html&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;I also learned what a Functor is in one of the lightning talks and no it&#x27;s not as hard as it sounds but oh man it is nothing like I what I thought it might be. In fact, after the talk I asked a question that amounts to &quot;Wait so which of your examples is actually the functor itself: A or B&quot; and the answer was &quot;neither, it&#x27;s C&quot; and that was very surprising.&lt;&#x2F;p&gt;
&lt;p&gt;Today was I believe the one and only structured day at RC. Starting tomorrow it&#x27;s entirely self-directed.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>How I Plan to Learn Rust</title>
          <pubDate>Wed, 25 Oct 2017 16:04:24 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2017/10/how-i-plan-to-learn-rust/</link>
          <guid>https://peterlyons.com/problog/2017/10/how-i-plan-to-learn-rust/</guid>
          <description xml:base="https://peterlyons.com/problog/2017/10/how-i-plan-to-learn-rust/">&lt;p&gt;So the week after next I start the Winter 1 2017 half-batch of &lt;a href=&quot;https:&#x2F;&#x2F;www.recurse.com&#x2F;&quot;&gt;the Recurse Center&lt;&#x2F;a&gt;. My primary project will be learning rust and systems programming. My secondary project will be some SQL modeling and reporting stuff.&lt;&#x2F;p&gt;
&lt;p&gt;Here&#x27;s my plan to learn rust.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;✓ Read &lt;a href=&quot;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;stable&#x2F;book&#x2F;second-edition&quot;&gt;The Rust Programming Language&lt;&#x2F;a&gt; Book, 2nd edition
&lt;ul&gt;
&lt;li&gt;✓ Do all &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;focusaurus&#x2F;rust-basics&quot;&gt;exercises&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;✓ Watch any interesting conference videos from the most recent Rust Conf&lt;&#x2F;li&gt;
&lt;li&gt;✓ Implement &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;focusaurus&#x2F;rust-basics&#x2F;tree&#x2F;master&#x2F;hex_dump&quot;&gt;the unix hexdump utility in rust&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;✓Listen to &lt;a href=&quot;http:&#x2F;&#x2F;www.newrustacean.com&#x2F;&quot;&gt;The New Rustacean Podcast&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;✓ Listen to &lt;a href=&quot;https:&#x2F;&#x2F;request-for-explanation.github.io&#x2F;podcast&#x2F;&quot;&gt;The Request for Explanation Podcast&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Read and do all activities in &lt;a href=&quot;https:&#x2F;&#x2F;rustbyexample.com&#x2F;custom_types.html&quot;&gt;Rust by Example&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Do the &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;carols10cents&#x2F;rustlings&quot;&gt;rustlings&lt;&#x2F;a&gt; exercises&lt;&#x2F;li&gt;
&lt;li&gt;Maybe read &lt;a href=&quot;https:&#x2F;&#x2F;www.safaribooksonline.com&#x2F;library&#x2F;view&#x2F;programming-rust&#x2F;9781491927274&#x2F;titlepage01.html&quot;&gt;Programming Rust&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Maybe &lt;a href=&quot;https:&#x2F;&#x2F;hackernoon.com&#x2F;learn-blockchains-by-building-one-117428612f46&quot;&gt;build a simple blockchain in rust&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Read &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;ayasin&#x2F;rust-talk&quot;&gt;High Performance JavaScript with Rust&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Do some embedded work via &lt;a href=&quot;https:&#x2F;&#x2F;japaric.github.io&#x2F;discovery&#x2F;&quot;&gt;Discovery&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Watch the &lt;a href=&quot;http:&#x2F;&#x2F;intorust.com&#x2F;&quot;&gt;Into Rust&lt;&#x2F;a&gt; screencasts&lt;&#x2F;li&gt;
&lt;li&gt;Do &lt;a href=&quot;http:&#x2F;&#x2F;cglab.ca&#x2F;~abeinges&#x2F;blah&#x2F;too-many-lists&#x2F;book&#x2F;&quot;&gt;Learning Rust With Entirely Too Many Linked Lists&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Ultimately my final project type thing might be a crypto helper program in the spirit of &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;elastic&#x2F;tealess&quot;&gt;tealess&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Got any suggestions for me? Leave a disqus comment below or tweet me or whatever.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Quick Thoughts</title>
          <pubDate>Sun, 08 Oct 2017 03:09:20 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2017/10/quick-thoughts/</link>
          <guid>https://peterlyons.com/problog/2017/10/quick-thoughts/</guid>
          <description xml:base="https://peterlyons.com/problog/2017/10/quick-thoughts/">&lt;h3 id=&quot;code-coverage&quot;&gt;Code Coverage&lt;&#x2F;h3&gt;
&lt;p&gt;Organizing code so my 100% tested code is kept separate from other dev&#x27;s 70% tested code so at least part of my coverage report is green.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;flat-ui&quot;&gt;Flat UI&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.nngroup.com&#x2F;articles&#x2F;flat-ui-less-attention-cause-uncertainty&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;dynamodb-tables&quot;&gt;DynamoDB Tables&lt;&#x2F;h3&gt;
&lt;p&gt;WTF is with dynamodb not having the concept of a database. This is terrible. Now we need to configure every table name for each deployment?&lt;&#x2F;p&gt;
&lt;h3 id=&quot;terraform-drop&quot;&gt;Terraform Drop&lt;&#x2F;h3&gt;
&lt;p&gt;Heads up, if you use terraform and change your dynamodb table key schema, it will drop and recreate your table without any extra warning.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;deterministic-builds&quot;&gt;Deterministic Builds&lt;&#x2F;h3&gt;
&lt;p&gt;TIL debian has a strip-nondetermisism perl script you can feed archives (zip, tar, etc) and it will make them deterministic&lt;&#x2F;p&gt;
&lt;h3 id=&quot;almost-swiped&quot;&gt;Almost Swiped&lt;&#x2F;h3&gt;
&lt;p&gt;Mobile interaction annoyance: If I swipe to dismiss a notification, but not far enough so it stays there, then I swipe it again immediately, I&#x27;m fucking trying to dismiss it, cut me some slack and dismiss it already.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;hidden-passwords&quot;&gt;Hidden Passwords&lt;&#x2F;h3&gt;
&lt;p&gt;I want mobile password inputs to take up the entire screen with keyboard letters and FFS allow me to show the password. 95% of the time shoulder surfing is not a threat to me.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;slack-interviews&quot;&gt;Slack Interviews&lt;&#x2F;h3&gt;
&lt;p&gt;I don&#x27;t do whiteboard interviews or coding algorithms. I just ask the candidate to set up some reasonable slack notification preferences.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;slack-accounts&quot;&gt;Slack Accounts&lt;&#x2F;h3&gt;
&lt;p&gt;If only we could rewind time and have slack use a single account model&lt;&#x2F;p&gt;
&lt;h3 id=&quot;joi-vs-json-schema&quot;&gt;joi vs JSON schema&lt;&#x2F;h3&gt;
&lt;p&gt;I switched from joi to basic json schema objects for broadest tool support and integration and because manipulating POJOs with JS is really easy. But now the verbosity is getting to me and I might switch back to joi.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;xkcd-compiling&quot;&gt;XKCD Compiling&lt;&#x2F;h3&gt;
&lt;p&gt;Instead of &quot;compiling&quot; my excuse for slacking off is &quot;terraforming&quot;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;api-gateway-error-messages&quot;&gt;API Gateway error messages&lt;&#x2F;h3&gt;
&lt;p&gt;For AWS API Gateway errors, use JSON with {&quot;message&quot;: errorMessage} so your application errors and the default APIG ones have the same schema&lt;&#x2F;p&gt;
&lt;h3 id=&quot;terraform-apply-errors&quot;&gt;Terraform apply errors&lt;&#x2F;h3&gt;
&lt;p&gt;terraform doesn&#x27;t really deliver on its promise&#x2F;potential. So many cases where &quot;plan&quot; looks good and &quot;apply&quot; fails at runtime. I wonder if we could build a stronger type system and move errors from apply time to plan time like elm did for javascript errors from run time to compile time.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ignite-bullshit&quot;&gt;Ignite Bullshit&lt;&#x2F;h3&gt;
&lt;p&gt;Ignite presentation idea: Bullshit. 5 topics, 3 slides per topic: the pitch, why it&#x27;s bullshit, the better option.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;shift&quot;&gt;Shift&lt;&#x2F;h3&gt;
&lt;p&gt;Maybe shift key should be last resort as a modifier key. Because you need it for normal typing, seems more ripe for mistakes vs ctrl&#x2F;opt&#x2F;cmd.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;dark-patterns&quot;&gt;Dark Patterns&lt;&#x2F;h3&gt;
&lt;p&gt;Showing a notification widget to a non-logged-in user is a dark pattern.&lt;&#x2F;p&gt;
&lt;p&gt;TransUnion dark pattern. Fill out a form. Opt out of email. Submit form. Get an error about password field. All fields retain their proper values except opt-in to email is checked again.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;devops-tooling-woes&quot;&gt;DevOps Tooling Woes&lt;&#x2F;h3&gt;
&lt;p&gt;I really wish there was 1 fucking tool in the entire devops ecosystem that understood what a real multi-stage deployment flow should look like.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;terraform-variable-scope&quot;&gt;Terraform variable scope&lt;&#x2F;h3&gt;
&lt;p&gt;Terraform what the actual fuck went on with your &quot;declarative&quot; design? &lt;code&gt;* function_name: duplicate local. local value names must be unique&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;scrumish&quot;&gt;Scrumish&lt;&#x2F;h3&gt;
&lt;p&gt;What methodolgy do you follow? Arbitrary but we say &quot;sprint&quot; a bunch.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;awsmess&quot;&gt;AWSmess&lt;&#x2F;h3&gt;
&lt;p&gt;An open source AWS linter called awsmess&lt;&#x2F;p&gt;
&lt;h3 id=&quot;code-search&quot;&gt;Code Search&lt;&#x2F;h3&gt;
&lt;p&gt;OMG If I ask to search logs&#x2F;code&#x2F;tech stuff for XYZ-12345 I DO NOT MEAN (&quot;XYZ&quot; OR &quot;12345&quot;)&lt;&#x2F;p&gt;
&lt;h3 id=&quot;object-storage&quot;&gt;Object Storage&lt;&#x2F;h3&gt;
&lt;p&gt;Nerd rage at tech companies that call S3&#x2F;filesystems &quot;Object Storage&quot;. You don&#x27;t get it. Files and Objects are 2 different things to developers.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;hclfmt-crap&quot;&gt;hclfmt crap&lt;&#x2F;h3&gt;
&lt;p&gt;Vertically aligning equal assignment operators is terrible. Creates SCM noise. Makes things less readable.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;duolingo-spaced-repetition&quot;&gt;Duolingo Spaced Repetition&lt;&#x2F;h3&gt;
&lt;p&gt;Not buying their spaced repetition algorithm. Keeps asking me to re-prove that I know words like &quot;gracias&quot;.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;old-man-yells-at-url&quot;&gt;Old Man Yells at URL&lt;&#x2F;h3&gt;
&lt;p&gt;The URL query string. Intended for search. Now just used for tracking and when you do a search they don&#x27;t even give you a bookmarkable URL.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Quick Thoughts: Less Twitter</title>
          <pubDate>Fri, 01 Sep 2017 14:43:35 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2017/09/quick-thoughts-less-twitter/</link>
          <guid>https://peterlyons.com/problog/2017/09/quick-thoughts-less-twitter/</guid>
          <description xml:base="https://peterlyons.com/problog/2017/09/quick-thoughts-less-twitter/">&lt;h3 id=&quot;less-twitter&quot;&gt;Less Twitter&lt;&#x2F;h3&gt;
&lt;p&gt;So I started journaling my quick thoughts basically at the beginning of 2016 and only managed to post them to my blog once. So it goes, I guess. I have a backlog of them, most of which I&#x27;ll just ignore and I&#x27;ll post the highlights below.&lt;&#x2F;p&gt;
&lt;p&gt;With my twitter feed becoming unpleasant due to so much Trump noise, I&#x27;ve reduced my use a twitter a lot. I&#x27;ve somewhat switched to reddit which is better organized into narrow topics. Instead of tweeting I&#x27;m just making little notes in my quick thought journal which I&#x27;ll just post on my blog.&lt;&#x2F;p&gt;
&lt;p&gt;On to the 2016 backlog...&lt;&#x2F;p&gt;
&lt;h3 id=&quot;chesterton-s-fence&quot;&gt;Chesterton&#x27;s Fence&lt;&#x2F;h3&gt;
&lt;p&gt;I often think this when looking at my own code.
&lt;a href=&quot;http:&#x2F;&#x2F;mcntyr.com&#x2F;52-concepts-cognitive-toolkit&#x2F;&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;why-babel-with-node&quot;&gt;Why babel with node&lt;&#x2F;h3&gt;
&lt;p&gt;At the beginning of 2016 I was frustrated and baffled as to why with node.js 6.3 supporting almost all of ES2015 natively, why would someone introduce the babel transpiler into a node-only server side project. So much ouch for so little benefit.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;promise-edge-cases&quot;&gt;Promise Edge Cases&lt;&#x2F;h3&gt;
&lt;p&gt;Promise library docs have special sections for delays&#x2F;setTimeout, browsers, databases, etc. async.js has no such special cases.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;postgresql-toast&quot;&gt;PostgreSQL TOAST&lt;&#x2F;h3&gt;
&lt;p&gt;TOAST stands for The Oversized-Attribute Storage Technique, probably the best acronym in the history of Computer Science.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a href=&quot;http:&#x2F;&#x2F;rachbelaid.com&#x2F;introduction-to-postgres-physical-storage&#x2F;&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;git-and-wip-commits&quot;&gt;Git and WIP commits&lt;&#x2F;h3&gt;
&lt;p&gt;How about a git that deals with both tiny WIP changes and larger more meaningful units?&lt;&#x2F;p&gt;
&lt;h3 id=&quot;easier-to-reason&quot;&gt;Easier to Reason&lt;&#x2F;h3&gt;
&lt;p&gt;For every X units of complexity removed by &quot;Easier to Reason About&quot; techniques, programmers will take the opportunity to introduce &amp;gt;X other techniques that make things more complicated.&lt;&#x2F;p&gt;
&lt;p&gt;I&#x27;ve also more recently experienced this phenomenon with serverless patterns. For every X units of complexity managed services and serverless patterns eliminate, programmers will add at least X complexity units in bizarre hacks, purported cost optimizations, etc.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;long-term-low-supervision&quot;&gt;Long Term Low Supervision&lt;&#x2F;h3&gt;
&lt;p&gt;Thinking back on the fact that I&#x27;ve at least twice had long-term (&amp;gt; 1 year) projects with shockingly low supervision. One was leading a small team, one was mostly solo with light collaboration&#x2F;devops stuff. Both went really well from my perspective.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;writing-less-damn-code&quot;&gt;Writing Less Damn Code&lt;&#x2F;h3&gt;
&lt;p&gt;[http:&#x2F;&#x2F;www.heydonworks.com&#x2F;article&#x2F;on-writing-less-damn-code](Writing Less Damn Code)&lt;&#x2F;p&gt;
&lt;h3 id=&quot;software-is-about-people&quot;&gt;Software Is About People&lt;&#x2F;h3&gt;
&lt;p&gt;For all this talk on podcasts about &quot;software is about people&quot; there are a lot of architecture articles that make no mention of team size or team quantity.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;aws&quot;&gt;AWS&lt;&#x2F;h3&gt;
&lt;p&gt;Ha ha https:&#x2F;&#x2F;www.aws.org is the American Welding Society&lt;&#x2F;p&gt;
&lt;h3 id=&quot;consulting-feast-or-famine&quot;&gt;Consulting Feast or Famine&lt;&#x2F;h3&gt;
&lt;p&gt;My consulting work had been in famine mode for 6+ weeks. Full stop on work pending project sponsorship. Suddenly client calls a meeting and is like &quot;How quickly can we get a demo working?&quot;. So I guess it&#x27;s feast mode for the next 2 months or so. This is good because I could use some extra savings to cover my time at the Recurse Center this fall when I won&#x27;t have any income.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;relative-dates&quot;&gt;Relative Dates&lt;&#x2F;h3&gt;
&lt;p&gt;meetup.com&#x27;s message app says a message is from 1092 days ago. Wow. Such friendly UX. Such superior to an actual date.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;unusual-call-volume&quot;&gt;Unusual Call Volume&lt;&#x2F;h3&gt;
&lt;p&gt;Stop trying to explain long customer support queues as &quot;unexpected load&quot;. Bullshit. It&#x27;s because you just don&#x27;t give a fuck. Own it.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>I hate electron apps</title>
          <pubDate>Wed, 17 May 2017 04:15:02 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2017/05/i-hate-electron-apps/</link>
          <guid>https://peterlyons.com/problog/2017/05/i-hate-electron-apps/</guid>
          <description xml:base="https://peterlyons.com/problog/2017/05/i-hate-electron-apps/">&lt;p&gt;The following applications I use regularly on my mac on built on electron:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Atom editor&lt;&#x2F;li&gt;
&lt;li&gt;Slack&lt;&#x2F;li&gt;
&lt;li&gt;Google Play Music &quot;Desktop&quot;&lt;&#x2F;li&gt;
&lt;li&gt;Insomnia HTTP client&lt;&#x2F;li&gt;
&lt;li&gt;Part of my PIA VPN client&lt;&#x2F;li&gt;
&lt;li&gt;Keybase&lt;&#x2F;li&gt;
&lt;li&gt;Github Desktop (soon)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;I hate them for the following reasons in loosely descending order of importance.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;They have a &quot;Helper&quot; process that periodically goes berzerk on my CPU for minutes on end doing God knows what&lt;&#x2F;li&gt;
&lt;li&gt;The have an unending stream of &quot;evergreen updates&quot; and need to be restarted multiple times a week&lt;&#x2F;li&gt;
&lt;li&gt;They often have some aspect of the web browser back&#x2F;forward navigation which is terrible and busted and I don&#x27;t want in native desktop apps&lt;&#x2F;li&gt;
&lt;li&gt;They are large in terms of download&#x2F;install size and contain tons of duplicated code for all kinds of shit chrome needs but a single-language, single-purpose native application should not&lt;&#x2F;li&gt;
&lt;li&gt;They are slow to start up. I can remember the days when high-end Apple laptops first started getting SSDs and you could launch applications and have them fully loaded and usable in a fraction of a second. That&#x27;s my standard and none of these apps come close anymore. Instead every damn time I open Google Play Music I&#x27;m treated to a 30-second spinner&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;But the thing that breaks my heart is just how poorly they compare to golden-age native OS X applications like Fantastical and other apps made by for-profit companies that directly charge a price to the end users; apps made by developers that understand why people choose OS X despite its numerous challenges compared to a PC (less hardware choice, less customization, fewer applications available, 2nd-tier support, etc). We bought Macs because the overall experience was a class above.&lt;&#x2F;p&gt;
&lt;p&gt;In contrast, Fantastical costs some money (about 2 restaurant meals worth), is 33MB on disk. Is fully loaded with my entire multicalendar data set displayed on the screen in under 2s regardless of network status, is idle when no activity is necessary, and has a nice update every several months which never contains regressions.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>VPN annoyances</title>
          <pubDate>Wed, 10 May 2017 19:47:39 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2017/05/vpn-annoyances/</link>
          <guid>https://peterlyons.com/problog/2017/05/vpn-annoyances/</guid>
          <description xml:base="https://peterlyons.com/problog/2017/05/vpn-annoyances/">&lt;p&gt;I&#x27;ve recently started using a VPN all the time (at least as much as possible). My more tinfoily nerd friends all recommended &lt;a href=&quot;https:&#x2F;&#x2F;www.privateinternetaccess.com&#x2F;&quot;&gt;PIA&lt;&#x2F;a&gt; so I signed up for that. There are some issues and frustrations I wasn&#x27;t expecting so here&#x27;s what I&#x27;ve seen so far.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Extra-annoying captchas from ReCaptcha. It seems being on a VPN exit IP automatically classifies you for the most-frustrating setting of ReCaptcha. I&#x27;ve seen this even on PIA&#x27;s own tech support form submission.&lt;&#x2F;li&gt;
&lt;li&gt;ReCaptcha on browsing CloudFlare HTTPS sites. This one really shocked me. Just to browse (HTTPS GET) a site on cloudflare, I&#x27;m prompted to complete a captcha. I can barely stand captchas when I need to submit an important form, but just to read a site? No thanks. ⌘-w.&lt;&#x2F;li&gt;
&lt;li&gt;NameCheap wouldn&#x27;t let me log in. Site loaded but gave me error messages AS IF my password was wrong, which it wasn&#x27;t, which caused me to change it several times, which didn&#x27;t work, which caused me to open a support case wherein they asked if I was using a VPN told me to disconnect. What. The. Actual. Fuck? They seem to have since made this less-terrible but still WTF?&lt;&#x2F;li&gt;
&lt;li&gt;Craigslist just straight up blocks all PIA exit IPs. Total dead end.&lt;&#x2F;li&gt;
&lt;li&gt;PIA seems to not be that good about reconnecting when you close your laptop and open it in a different wifi network. The icon still shows it is connected but it is in fact not and you have to manually disconnect and reconnect to get back online.&lt;&#x2F;li&gt;
&lt;li&gt;Google Drive does not seem to sync&#x2F;connect when the VPN is active&lt;&#x2F;li&gt;
&lt;li&gt;NetFlix geolocates your IP and restricts your access to content based on that country. So you see different titles available when VPNed verses not.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Any pro tips to help out with this?&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Meaning in Technical Communication</title>
          <pubDate>Tue, 25 Apr 2017 15:59:59 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2017/04/meaning-in-technical-communication/</link>
          <guid>https://peterlyons.com/problog/2017/04/meaning-in-technical-communication/</guid>
          <description xml:base="https://peterlyons.com/problog/2017/04/meaning-in-technical-communication/">&lt;p&gt;Just a quick tip to be on the lookout for commonly-used words in technical discussions that are problematic due to being low on meaning and high on emotional weight. Watch out for:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;secure&lt;&#x2F;li&gt;
&lt;li&gt;easy&lt;&#x2F;li&gt;
&lt;li&gt;flexible&lt;&#x2F;li&gt;
&lt;li&gt;lightweight, heavyweight&lt;&#x2F;li&gt;
&lt;li&gt;virtual, physical, logical&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;If someone is using these and you are confused or trying to have a productive discussion, call them out and request for more specific, clearer words. Some better examples might be&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;lower-latency&lt;&#x2F;li&gt;
&lt;li&gt;longer startup time&lt;&#x2F;li&gt;
&lt;li&gt;smaller network transfer size&lt;&#x2F;li&gt;
&lt;li&gt;straightforward to add new endpoints in the future&lt;&#x2F;li&gt;
&lt;li&gt;requires fewer processes&lt;&#x2F;li&gt;
&lt;li&gt;lower annual cost&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</description>
      </item>
      <item>
          <title>AWS Lambda Object Mother</title>
          <pubDate>Mon, 24 Apr 2017 20:41:40 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2017/04/aws-lambda-object-mother/</link>
          <guid>https://peterlyons.com/problog/2017/04/aws-lambda-object-mother/</guid>
          <description xml:base="https://peterlyons.com/problog/2017/04/aws-lambda-object-mother/">&lt;p&gt;I&#x27;ve been coding a project build on AWS Lambda and I wanted to post a unit testing pattern that has been really helpful especially for input validation. The lambda function input starts with an &quot;event&quot; object which I&#x27;m sure in the first version of lambda was reasonably small and simple, but now it can get quite complex with many deeply nested properties when API Gateway authorizers and path parameters are involved. Thus I&#x27;ve been applying the &quot;Object Mother&quot; test pattern to create event objects I can pass to my lambda handler functions during unit testing.&lt;&#x2F;p&gt;
&lt;p&gt;Here&#x27;s a few key points before the code excerpt.&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;My helper function by default will return a complete and valid event object. This is convenient, but it&#x27;s also important to prove that if I alter only 1 property and the event is considered invalid, I know the property I altered did in fact cause the invalid state, and not some other missing or invalid property.&lt;&#x2F;li&gt;
&lt;li&gt;I use the &lt;code&gt;dot2val&lt;&#x2F;code&gt; module to allow me to consisely express a deeply nested value I want to override. I can also easily null out a nested value in the middle of the object structure with this technique.&lt;&#x2F;li&gt;
&lt;li&gt;When integrated with API Gateway the &lt;code&gt;event.body&lt;&#x2F;code&gt; property will unfortunately be an unparsed JSON string literal. Once this is stringified, it&#x27;s a pain to modify. The pattern I have here allows tweaking of the body object before it gets stringified, which is nice.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;Here&#x27;s what it ends up looking like.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;const dot2val = require(&amp;quot;dot2val&amp;quot;);
const myLambdaHandler = require(&amp;quot;.&amp;#x2F;my-lambda&amp;quot;).handler;
const schemas = require(&amp;quot;..&amp;#x2F;schemas&amp;quot;);
const tap = require(&amp;quot;tap&amp;quot;);

function mockEvent(path, value) {
  &amp;#x2F;&amp;#x2F; First, construct a fully valid mock object
  const event = {
    pathParameters: {
      sort: &amp;quot;asc&amp;quot;
    },
    body: {
      email: schemas.email.example,
      firstName: schemas.name.example,
      lastName: schemas.name.example
    },
    requestContext: {
      authorizer: {
        id: &amp;quot;42&amp;quot;
      }
    }
  };
  &amp;#x2F;&amp;#x2F; If any values need to change, override them here
  if (path) {
    dot2val.set(event, path, value);
  }
  &amp;#x2F;&amp;#x2F; Make body into a JSON string to match lambda environment
  event.body = JSON.stringify(event.body);
  return event;
}

const invalids = [
  [&amp;quot;body.email&amp;quot;, &amp;quot;@jdoe&amp;quot;],
  [&amp;quot;body.email&amp;quot;, &amp;quot;no at here&amp;quot;],
  [&amp;quot;body.email&amp;quot;, false],
  [&amp;quot;body.email&amp;quot;, null],
  [&amp;quot;body.email&amp;quot;, undefined],
  [&amp;quot;body.firstName&amp;quot;, &amp;quot;a&amp;quot;.repeat(51)],
  [&amp;quot;body.firstName&amp;quot;, 0],
  [&amp;quot;body.firstName&amp;quot;, false],
  [&amp;quot;body.firstName&amp;quot;, null],
  [&amp;quot;body.firstName&amp;quot;, undefined],
  [&amp;quot;body.lastName&amp;quot;, &amp;quot;a&amp;quot;.repeat(51)],
  [&amp;quot;body.lastName&amp;quot;, 0],
  [&amp;quot;body.lastName&amp;quot;, false],
  [&amp;quot;body.lastName&amp;quot;, null],
  [&amp;quot;body.lastName&amp;quot;, undefined],
  [&amp;quot;body&amp;quot;, undefined],
  [&amp;quot;pathParameters.sort&amp;quot;, 0],
  [&amp;quot;pathParameters.sort&amp;quot;, false],
  [&amp;quot;pathParameters.sort&amp;quot;, null],
  [&amp;quot;pathParameters&amp;quot;, undefined]
];

invalids.forEach(pair =&amp;gt; {
  tap.test(`createUser validates ${pair[0]}`, {skip: false}, test =&amp;gt; {
    const event = mockEvent(pair[0], pair[1]);
    myLambdaHandler(event, {}, (error, res) =&amp;gt; {
      tap.error(error, &amp;quot;should succeed without error&amp;quot;);
      tap.same(res.statusCode, 400, &amp;quot;should send 400 status&amp;quot;);
      test.end();
    });
  });
});
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;So the pattern I use is:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Set up my object mother function, which here I name &lt;code&gt;mockEvent&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Create a list of invalid properties which pairs their object dot notation path string and the value. This is a pretty consice way to test a lot of cases.&lt;&#x2F;li&gt;
&lt;li&gt;A tiny bit of metaprogramming if you will creates a tap unit test for each invalid property without repetition of the test code itself.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
</description>
      </item>
      <item>
          <title>secrets in zsh history</title>
          <pubDate>Mon, 17 Apr 2017 00:25:00 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2017/04/secrets-in-zsh-history/</link>
          <guid>https://peterlyons.com/problog/2017/04/secrets-in-zsh-history/</guid>
          <description xml:base="https://peterlyons.com/problog/2017/04/secrets-in-zsh-history/">&lt;p&gt;I&#x27;ve been trying to get a manageable system for dealing with projects that need a lot of settings, some of which are secrets such as credentials. In general, environment variables are the most widely-supported mechanism. They have many shortcomings: they are exposed by the OS to other processes running as your user or root via the &lt;code&gt;&#x2F;proc&lt;&#x2F;code&gt; filesystem (at least on linux), don&#x27;t have data types other than strings, can&#x27;t express nesting or arrays easily, etc. However, they can get the job done if you keep things simple.&lt;&#x2F;p&gt;
&lt;p&gt;Anyway one of my main concerns was keeping these secrets relatively secret but also not massively inconvenient for actual development work. My approach so far has been to keep them encrypted in 1password as a multiline notes field that is just some shell syntax to export them as environment variables. It looks something like this:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sh&quot; class=&quot;language-sh &quot;&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;export AWS_ACCESS_KEY_ID=&amp;#x27;blah blah blah&amp;#x27;
# Use this for dev
export AWS_REGION=&amp;#x27;us-west-2&amp;#x27;
# Use this to test the standby region
export AWS_REGION=&amp;#x27;us-east-1&amp;#x27;
# etc etc
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;So I feel pretty good that this is securely off disk. I would copy and paste this into my shell and go about my work. However, these commands then get put into my shell history which are clear text files. I wanted something this convenient but keeping secrets out of my shell history.&lt;&#x2F;p&gt;
&lt;p&gt;What I came up with is this:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;eval $(pbpaste;echo)&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Which I set up as a keyboard macro so I type &lt;code&gt;epb,,&lt;&#x2F;code&gt;, keyboard maestro detects the double comma and looks up the proper snippet to expand the text. The eval tells the shell to execute the code and the pbpaste bit outputs the text I just copied from 1password.&lt;&#x2F;p&gt;
&lt;p&gt;So this is better. Some shells allow omitting a command from history by typing a leading space in front of it, just FYI but I felt that was too error prone to use consistently.&lt;&#x2F;p&gt;
&lt;p&gt;It&#x27;s still not ideal as stuff lives in memory probably longer than it should, and the mechanism to clear things is to exit the shell, which I don&#x27;t do regularly enough because there&#x27;s usually state in the shell I don&#x27;t want to have to re-setup like activating python virtualenvs, etc. It&#x27;s also possible to unintentionally copy some other text before you paste it into the terminal, thus shooting yourself with your&lt;code&gt; eval&lt;&#x2F;code&gt; footgun.&lt;&#x2F;p&gt;
&lt;p&gt;Got something better to suggest?&lt;&#x2F;p&gt;
&lt;p&gt;Side note for zsh: &lt;code&gt;setopt interactivecomments&lt;&#x2F;code&gt; will avoid annoying errors when you paste snippets with comments into an interactive shell.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;UPDATE (2017-09-12)&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;It looks like &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;99designs&#x2F;aws-vault&quot;&gt;aws-vault&lt;&#x2F;a&gt; is a handy tool for this as well. I&#x27;ve started using it on 2 projects and so far so good, but it only handles the AWS creds, not randow other project vars.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Python Last Month</title>
          <pubDate>Sat, 01 Apr 2017 17:09:16 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2017/04/python-last-month/</link>
          <guid>https://peterlyons.com/problog/2017/04/python-last-month/</guid>
          <description xml:base="https://peterlyons.com/problog/2017/04/python-last-month/">&lt;p&gt;I needed to compute timestamps representing the start and end of &quot;last month&quot; so I could build a URL needed to generate an invoicing report. Because the URL uses actual dates, I can&#x27;t just bookmark it since it changes every month. Python has a lot of date and time management capabilities, but finding the one that is going to get the job done correctly and succinctly is actually harder than the code itself.&lt;&#x2F;p&gt;
&lt;p&gt;With that in mind, here&#x27;s essentially a 3-liner that does it&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;import time
import datetime

now = time.localtime()

# Get the last day of last month by taking the first day of this month
# and subtracting 1 day.
last = datetime.date(now.tm_year, now.tm_mon, 1) - datetime.timedelta(1)

# Set the day to 1 gives us the start of last month
first = last.replace(day=1)

# The default string representation of these datetime instances is 
# YYYY-mm-dd format (which is what I usually need),
# so we can just print them out
print first, last
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Here&#x27;s what I see running it on the command line.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sh&quot; class=&quot;language-sh &quot;&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;python &amp;#x2F;tmp&amp;#x2F;range.py
2017-03-01 2017-03-31
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</description>
      </item>
      <item>
          <title>Transforming Gaia</title>
          <pubDate>Mon, 27 Mar 2017 15:43:01 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2017/03/transforming-gaia/</link>
          <guid>https://peterlyons.com/problog/2017/03/transforming-gaia/</guid>
          <description xml:base="https://peterlyons.com/problog/2017/03/transforming-gaia/">&lt;p&gt;I interviewed Robert Jordan, who is Director of Engineering at &lt;a href=&quot;https:&#x2F;&#x2F;www.gaia.com&quot;&gt;Gaia&lt;&#x2F;a&gt;, where in 2016 they completed a major update to their subscription video service.&lt;&#x2F;p&gt;
&lt;div class=&quot;youtube-video-container&quot;&gt;
&lt;iframe src=&quot;https:&#x2F;&#x2F;www.youtube-nocookie.com&#x2F;embed&#x2F;XYxQw-YujEw&quot; allowfullscreen&gt;&lt;&#x2F;iframe&gt;
&lt;&#x2F;div&gt;
</description>
      </item>
      <item>
          <title>Fun with Inodes</title>
          <pubDate>Fri, 17 Mar 2017 19:03:32 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2017/03/fun-with-inodes/</link>
          <guid>https://peterlyons.com/problog/2017/03/fun-with-inodes/</guid>
          <description xml:base="https://peterlyons.com/problog/2017/03/fun-with-inodes/">&lt;p&gt;I had an inherited client project running on an EC2 Ubuntu instance like a champ. The system had racked up nearly 900 days of uptime! Not a lot of traffic but still it was running nginx with TLS, a node&#x2F;express app, and mysql with pretty much 100% uptime for over 2 years.&lt;&#x2F;p&gt;
&lt;p&gt;As I went to do some routine TLS certificate maintenance, the system started throwing &quot;No Space Left On Device&quot; errors. I was confused because the root filesystem still had ample free space according to &lt;code&gt;df -h&lt;&#x2F;code&gt;. A round of googling eventually clued me in to having exhausted all of the ext4 filesystem inodes, which again I eventually through research understood to be mostly caused by continually accumulating unused debian linux kernel .deb packages through normal automated security patching. We had at least 15 different versions of the kernel available and each requires many files under &lt;code&gt;&#x2F;usr&#x2F;src&#x2F;linux&lt;&#x2F;code&gt;. I eventually found a solution consisting of:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Free up some inodes by &lt;code&gt;rm -rf &#x2F;usr&#x2F;src&#x2F;linux-headers-X&lt;&#x2F;code&gt; where X were older versions no longer needed. You can confirm the version you are running with &lt;code&gt;uname -r&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;Identifying a longish list of important packages with broken dependencies and reinstalling them with &lt;code&gt;apt-get remove&lt;&#x2F;code&gt; followed by &lt;code&gt;apt-get install&lt;&#x2F;code&gt;. The list ended up being &lt;code&gt;linux-headers-virtual linux-virtual linux-image-virtual libc6-dev libstdc++6-4.6-dev g++-4.6 g++&lt;&#x2F;code&gt; which is rather frightening to uninstall (not sure the system would be usable if you rebooted between the remove and the reinstall).&lt;&#x2F;li&gt;
&lt;li&gt;Cleaning up using the commands in &lt;a href=&quot;http:&#x2F;&#x2F;askubuntu.com&#x2F;a&#x2F;564558&#x2F;43030&quot;&gt;this askubuntu answer&lt;&#x2F;a&gt;: &lt;code&gt;apt-get install -f &amp;amp;&amp;amp; apt-get autoremove&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;I ended up freeing up nearly 400K inodes and over 2.5 GB of disk space during this activity. The &lt;code&gt;apt-get autoremove&lt;&#x2F;code&gt; for all those linux kernel image packages took a long time (several hours) as there&#x27;s a bunch of processing associated with each one.&lt;&#x2F;p&gt;
&lt;p&gt;My appreciation for dpkg&#x2F;apt has always been strong, but here&#x27;s another anecdote to pile on. Prior to this, I&#x27;ve done many many successful upgrades of debian and ubuntu and even when things start to go a bit sideways, always managed to recover a working system without wiping and reinstalling. Now I know that you can run out of inodes over and over on a server for months at a stretch, foolishly reboot before attempting any repair and still boot and run your production processes fine while you handle the repairs.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Right tool for the job</title>
          <pubDate>Sat, 18 Feb 2017 20:28:04 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2017/02/right-tool-for-the-job/</link>
          <guid>https://peterlyons.com/problog/2017/02/right-tool-for-the-job/</guid>
          <description xml:base="https://peterlyons.com/problog/2017/02/right-tool-for-the-job/">&lt;p&gt;&quot;Use the right tool for the job&quot; is an oft-repeated aphorism in the tech world. In my experience, I have found this aphorism to be said a lot but actually implemented rarely and to also omit the reality of how technology choices are made. So this post will be calling bullshit on this phrase. I hear &quot;Use the right tool for the job&quot;, but what is see is more like:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;We are unable to learn new tools, so everything is built in the tools we already know
&lt;ul&gt;
&lt;li&gt;My point here is not &quot;and this is bad and we are bad because of this&quot;, it&#x27;s just the actual reality on the ground, and it&#x27;s fine.&lt;&#x2F;li&gt;
&lt;li&gt;Learning new programming languages is hard and takes time, money, effort, and determination. Even then many developers never achieve a level of capability and confidence that matches their primary language.&lt;&#x2F;li&gt;
&lt;li&gt;New databases almost by definition are harder to develop against and operate than one you are already competent with, so you end up with slow development speed, slow queries, or operational issues and the project is deemed a failure and you abandon it and move back to the old database&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;Any new tool is inherently &quot;right&quot; because ignorance biases positive
&lt;ul&gt;
&lt;li&gt;We have a thorough understanding of the shortcomings of our current tools due to our operational experience with them. The marketing materials for the new tool clearly indicate all of these problems are solved in the new tool. We have no operational experience with the new tool so we don&#x27;t know that it comes with its own different set of shortcomings, so we are constantly switching to new tools assuming they are better. Sometimes they are, often they are worse, often they are essentially on par with the old tools.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;We have an 8x8 tool shed
&lt;ul&gt;
&lt;li&gt;There&#x27;s only so much staff and time we have, and we do a lot of &quot;jobs&quot;. If we actually used the right tool for each job, we&#x27;d have an unmanageable number of tools and expertise in none of them.&lt;&#x2F;li&gt;
&lt;li&gt;So what happens is you use a core set of tools that does most of what you need well and if something can be done with this core set of tools adequately but not &quot;the right way&quot;, you just do it adequately because just like physical tools, software tools take space to store, maintain, repair, etc.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;In my world, the specific tool being considered when this phrase is repeated is usually a programming language, framework, library, database, or SaaS product. My TL;DR for each of these would probably be:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Programming language&lt;&#x2F;strong&gt;: There are teams that can add or switch programming languages and there are projects that can only accomplish this with new staff. Safe to assume your team is not the kind that can switch programming languages unless you have solid evidence indicating you can.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;library&#x2F;framework&lt;&#x2F;strong&gt;: In my experience these projects often do &quot;succeed&quot;. I usually feel the end state is not significantly better than the start state and a lot of libraries&#x2F;frameworks are at the end of the day basically the same tool. However, if your developers enjoy the switch and have more fun in the new framework, it might be worth the business cost, but I&#x27;d urge you to be clear with yourself that you are allowing the team X months to port to a framework basically as a job perk for retention purposes.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;databases&lt;&#x2F;strong&gt;: The switching cost tends to be very high. This can work out if the problems with the current database are crystal clear and the new database&#x27;s ability to relieve them well-known. But most of the time when I see a project running 8 or 9 different databases, they&#x27;d probably be better off overall sticking to 2 or 3 and having real expertise in them.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;SaaS Products&lt;&#x2F;strong&gt;: I think developers don&#x27;t advocate &quot;right tool&quot; to promote switching between competing SaaS products that often because there&#x27;s credit cards involved in comparing&#x2F;contrasting. Thus I don&#x27;t see as much spurious swapping of these. I think if you stick to products with well-defined functions you&#x27;ll be OK and won&#x27;t end up using them for the wrong job. If you use more &quot;platform&quot; type things (think Salesforce, CMSes, CRM) there&#x27;s more potential to get into wrong tool for the job territory.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Overall I think this in an interesting topic for me. Please share you experiences and thoughts using the comments below (this blog does have disqus comments but they are rarely used) or on twitter, etc.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>localstorage Analogy</title>
          <pubDate>Sat, 18 Feb 2017 19:43:01 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2017/02/localstorage-analogy/</link>
          <guid>https://peterlyons.com/problog/2017/02/localstorage-analogy/</guid>
          <description xml:base="https://peterlyons.com/problog/2017/02/localstorage-analogy/">&lt;p&gt;I was working with a friend on her dev bootcamp project writing a little wish list management single page web application just backed by localstorage in the browser. This was her first time working with localstorage. As we gradually built features into the app, when it came time to interact with localstorage, I could see she was unclear on what needed to be done, in what order, and why. So I made this analogy and I think that gave her a good frame of reference.&lt;&#x2F;p&gt;
&lt;p&gt;In the browser, localstorage is (essentially) a javascript object. Our application was modeling a wish list as a javascript array (the list&#x2F;array correspondence should be fairly clear). In that array each wish was initially just a string (whatever you typed in the &quot;add wish&quot; input text box). Later we would switch these to being objects so we could have proper IDs and timestamps, but for v1 we were shooting for the absolute simplest thing.&lt;&#x2F;p&gt;
&lt;p&gt;So as we were dealing with cold start, adding wishes, reloading the page with data already saved, etc, I made this analogy. Think of localstorage like a desk drawer. Inside that desk drawer is piece of paper (our wish list, which in code maps to the array) and on that paper we are writing our wishes (the javascript strings are the wishes).&lt;&#x2F;p&gt;
&lt;p&gt;So I said imagine you show up a new house (browser), go over to the desk, open the drawer and find there&#x27;s no paper in there. It&#x27;s empty. In real life, the presence&#x2F;absence of paper is determined just by looking. In code, we need an &lt;code&gt;if&lt;&#x2F;code&gt; conditional statement to see if our &lt;code&gt;localstorage.wishes&lt;&#x2F;code&gt; property is there or not. When our condition evaluates to false, we need to get a new blank piece of paper to be our wish list, which we do in code with &lt;code&gt;var wishList = [];&lt;&#x2F;code&gt;. Then to add a wish, we write it on the paper with &lt;code&gt;wishList.push(newWish)&lt;&#x2F;code&gt;. Then we put the paper in the drawer for storage by &lt;code&gt;localstorage.wishes = wishList;&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Similarly, once we have some wishes stored, if we reload the page, it&#x27;s like walking into a house where the wishlist in the desk is already there and has some wishes, so we just render those wishes to the DOM instead of starting from a blank slate.&lt;&#x2F;p&gt;
&lt;p&gt;It felt like this analogy made it clearer how the wishes array and localstorage needed to be manipulated at each point in the app&#x2F;data lifecycle.&lt;&#x2F;p&gt;
&lt;p&gt;That&#x27;s all, just wanted to share this in case others find it helpful.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>AWS API Gateway Misleading Error</title>
          <pubDate>Thu, 02 Feb 2017 15:47:44 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2017/02/aws-api-gateway-misleading-error/</link>
          <guid>https://peterlyons.com/problog/2017/02/aws-api-gateway-misleading-error/</guid>
          <description xml:base="https://peterlyons.com/problog/2017/02/aws-api-gateway-misleading-error/">&lt;p&gt;Just a quick note from the trenches of AWS Lambda and API Gateway. API Gateway will throw a very misleading HTTP 403 status code error with the JSON body &lt;code&gt;{&quot;message&quot;: &quot;Missing Authentication Token&quot;}&lt;&#x2F;code&gt;. This would naturally lead us to think there&#x27;s something wrong with authentication. But in my case, my endpoints are public, and this error simply means 404 Not Found that I&#x27;ve fat-fingered the path portion of the URL. Not sure why API Gateway responds this way, but rest assured there&#x27;s no nasty IAM or API Token issue to debug, it&#x27;s just an incorrect URL.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Update:&lt;&#x2F;strong&gt; Here&#x27;s a link to the &lt;a href=&quot;https:&#x2F;&#x2F;forums.aws.amazon.com&#x2F;thread.jspa?threadID=216684&amp;amp;tstart=0&quot;&gt;AWS Forum Thread regarding this issue&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Level 1 Web Development</title>
          <pubDate>Thu, 26 Jan 2017 20:19:46 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2017/01/level-1-web-development/</link>
          <guid>https://peterlyons.com/problog/2017/01/level-1-web-development/</guid>
          <description xml:base="https://peterlyons.com/problog/2017/01/level-1-web-development/">&lt;p&gt;Recently I&#x27;ve been thinking about the scope of web development and the lack of clear boundaries. So much of being a &quot;web developer&quot; is a giant unbounded field of deep topics. JavaScript has become a very large language in terms of syntax, semantics, and features in addition to the 20 years of legacy quirks and inconsistencies we never remove. HTML again gets complex fast once you start building realistic applications and trying to balance semantics, accessibility, SEO, etc. CSS3 also has a tremendous scope including transforms, animations, etc.&lt;&#x2F;p&gt;
&lt;p&gt;What I&#x27;ve been thinking about is would it be empowering to beginners to clearly define and name small subsets of these that can be realistically taught and learned in a fixed time frame (say 6 or 12 months just to throw out some numbers)?&lt;&#x2F;p&gt;
&lt;p&gt;So just to flesh this out, let&#x27;s say we took a subset of HTML elements and attributes that are the key building blocks and focused on those and clearly put all the more obscure tags, complex attributes and interactions off in a different set. We label this something like HTML5 Level 1. Forget about &lt;code&gt;object&lt;&#x2F;code&gt;, &lt;code&gt;map&lt;&#x2F;code&gt;, &lt;code&gt;audio&lt;&#x2F;code&gt;, &lt;code&gt;dd&lt;&#x2F;code&gt;, &lt;code&gt;cite&lt;&#x2F;code&gt;, etc - for now.&lt;&#x2F;p&gt;
&lt;p&gt;Same thing for CSS. Focus on the basics of simple layouts, colors, sizing. Enough to get some basic text and a few images on the page looking OK in all viewports. Basic class and nested selectors but no fancy attribute matching. No transforms or animations. No tweaking word wrapping or overflow, etc.&lt;&#x2F;p&gt;
&lt;p&gt;For JavaScript it&#x27;s trickier and I&#x27;d actually like to see an entirely new language (could compile to JS perhaps) but the main idea is that the scope is bounded. You can do basic programming but maybe not top-tier million-user webapp optimizations or complexity. You don&#x27;t have to get explainations about how IE8 did something weird and then the spec adopted it. Forget metaprogramming, prototypes, generators, multiple syntaxes for similar concepts, regular expressions etc.&lt;&#x2F;p&gt;
&lt;p&gt;I&#x27;d like to see self-taught folks and bootcamp grads be able to say &quot;I&#x27;m proficient in Front End Web Development 2017.1&quot; with confidence. I&#x27;d like them to know they can go on a job interview and have worked with everything in that defined scope. No surprises. No anxiety. No &quot;I&#x27;ll probably do OK as long as they don&#x27;t ask me to build a complex table&quot;. I&#x27;d like to see companies be able to put these people to productive work and say &quot;On day 1 we hope to see you submit a pull request using Front End Web Development 2017.1 tools. On day 180, we&#x27;ll have trained you up to 2017.2&quot;.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Learning elm</title>
          <pubDate>Sat, 10 Dec 2016 22:19:10 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2016/12/learning-elm/</link>
          <guid>https://peterlyons.com/problog/2016/12/learning-elm/</guid>
          <description xml:base="https://peterlyons.com/problog/2016/12/learning-elm/">&lt;h2 id=&quot;a-report-from-the-weeds&quot;&gt;A Report from the weeds&lt;&#x2F;h2&gt;
&lt;p&gt;So I&#x27;ve been trying to learn &lt;a href=&quot;http:&#x2F;&#x2F;elm-lang.org&#x2F;&quot;&gt;The Elm Programming Language&lt;&#x2F;a&gt;, which is a language&#x2F;architecture for building browser applications. The core ideas have been adopted into react and redux and I consider Elm to be taking the ideas deeper than you can do while still in JavaScript. So elm actually gives you a totally new programming language supporting static types, pure functional programming, and immutability in a high-integrity way.&lt;&#x2F;p&gt;
&lt;p&gt;Why am I bothering? Well, work-wise I&#x27;ve been floating around with abandoning the label of &quot;full-stack developer&quot; and &quot;niching down&quot; as we say in the consulting game to a purely back-end focused consultant. I think the state of browser development now is quite terrible and rapidly getting worse. So learning elm isn&#x27;t necessarily about doing real apps in it. It&#x27;s mostly for the experience of learning a program language with static types, immutability, and pure functions.&lt;&#x2F;p&gt;
&lt;p&gt;This post is not a hindsight, well-thought-out summary. I&#x27;m actively in the weeds porting an app from Angular 1.x to Elm. This is a snapshot of what I&#x27;ve seen so far. I don&#x27;t know all of Elm yet, not even half way there. I wanted to write while things are still confusing and chaotic to create an honest snapshot.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;compiler messages&lt;&#x2F;strong&gt;: Elm has a reputation about friendly&#x2F;good compiler error messages. This plays out to be mostly true. However, I mostly still can&#x27;t read them because the available docs so far that I&#x27;ve seen don&#x27;t really teach the language as a language. It&#x27;s all examples and &quot;type this here&quot; without giving me words and concepts for the syntax. So while the error messages are helpful, I still have to basically keyword scan them and guess what the problem might be.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;examples&lt;&#x2F;strong&gt;: I also read that the elm examples are great. I found them to be almost laughably basic and non-comprehensive. In elm, it turns out that doing an HTTP GET and doing an HTTP POST require significantly different code, but the example only shows you a basic GET and that&#x27;s not enough to cover the gaps between GET and POST in terms of encoding bodies, interpretting responses, decoding, etc. This was really frustrating several times. Need to decode a simple flat JSON object? Here&#x27;s a clear example right in the docs. Got an JSON object containing a child object or a list, as you would in any real-world application? Not mentioned at all in the docs, have to ask in slack&#x2F;FAQ and it turns out to be much more complicated. When dealing with HTML events, the docs mention there are &lt;code&gt;stopPropagation&lt;&#x2F;code&gt; and &lt;code&gt;preventDefault&lt;&#x2F;code&gt; things, but nothing about how to use them.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;if it compiles, it works&lt;&#x2F;strong&gt;: This is a claim you often see about elm as well as Haskell. I want to state for the record that in my small number of hours programming elm, I have already managed to make at least 2 programs that compiled and ran with incorrect behavior. This did not manifest as runtime exceptions, but the program was wrong nonetheless. In one case I had done a refactoring to split my app up into modules and I ended up with 2 things both named &lt;code&gt;model&lt;&#x2F;code&gt;, one of which was just returning static initial data. Both worked as used, but one was totally wrong. Another time I was trying to decode a string enum (think names of playing card suits like &quot;diamonds&quot;, &quot;hearts&quot;, &quot;spades&quot;, &quot;clubs&quot;) to elm types. I had a version that compiled and ran but because I was doing decoding wrong, it was always hitting my default &quot;unrecognized value&quot; pattern match and using the default. My success pattern matches would never have matched. Only found it when the app was misbehaving.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;syntax&lt;&#x2F;strong&gt;: Elm&#x27;s syntax is I think mostly OK. I&#x27;m not used to the comma-first thing &lt;code&gt;elm-format&lt;&#x2F;code&gt; does and at the moment it fails to provide clean git diffs and defeats sorting properties, which I like to do. So if I had to make a call now, I&#x27;d say it&#x27;s terrible but I&#x27;m keeping an open mind until I have more hours in the trenches. I&#x27;m also not sure why the function name must be repeated above the type annotation. Seems totally unnecessary. I don&#x27;t like the ambiguity between calling a function and referencing a variable in scope. This has annoyed me in ruby as well. I prefer these 2 things to have different syntax. It&#x27;s also weird that if you declare a function with no arguments, it&#x27;s not a function, it&#x27;s just a value. Same syntax, but declare at least 1 argument and you get a function. I understand why this is possible and that a function without arguments does not make sense in a side-effect-free language, but still I find the lack of syntax distinction unclear and confusing to read.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;docs&lt;&#x2F;strong&gt;: The core language&#x2F;library docs are coded in way that all of the current module&#x27;s functions are directly in scope so they are bare references like &lt;code&gt;decodeString&lt;&#x2F;code&gt; or even just &lt;code&gt;int&lt;&#x2F;code&gt; which is actually &lt;code&gt;Json.Decode.int&lt;&#x2F;code&gt;. This is completely baffling to me as a beginner. I can&#x27;t go into the &lt;code&gt;elm-repl&lt;&#x2F;code&gt; and type what I see. They are not written the way you would write them in an application using the module. You can&#x27;t copy&#x2F;paste them and most of the time you are confused about which module each name belongs to. I also now realize that almost without exception the core docs examples use ONLY functions from the module at hand. They aren&#x27;t real-world use cases combining modules to do something useful. They are super-isolated 1-liners. The JSON docs make no mention of how to decode dates. I had to go find a community module.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;order of constructor params&lt;&#x2F;strong&gt;: After banging my head against a compiler error message, I eventually figured out that when you define a type, the order that you declare the named fields matters. When you construct that type, you must pass things in that order, even though the fields have names. This came as a total shock to me. So for example:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;elm&quot; class=&quot;language-elm &quot;&gt;&lt;code class=&quot;language-elm&quot; data-lang=&quot;elm&quot;&gt;-- given this type alias
type alias Entry =
    { id : Int
    , body : String
    , tags : List String
    , created : Date
    }

-- If I want to decode from JSON,
-- which I do in a different file

entriesDecoder : JD.Decoder (List Entry)
entriesDecoder =
    JD.list
        (JD.map4 Entry
            (JD.field &amp;quot;id&amp;quot; JD.int)
            (JD.field &amp;quot;body&amp;quot; JD.string)
            (JD.field &amp;quot;tags&amp;quot; (JD.list JD.string))
            (JD.field &amp;quot;created&amp;quot; Json.Decode.Extra.date)
        )

-- The order of the fields: id, body, tags, created must exactly match!
-- I was staring at compiler error messages for a long time
-- It is actually mentioned in the docs, but it didn&amp;#x27;t click with me
-- exactly what the docs meant
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;BDFL issues?&lt;&#x2F;strong&gt;: I&#x27;m not deep enough in it to really comment here, but talking with friends it seems like the community enthusiasm is outpacing evancz&#x27;s ability to review and accept PRs, even just people expanding the documentation. At the moment there&#x27;s a &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;elm-lang&#x2F;elm-lang.org&#x2F;pull&#x2F;597&quot;&gt;1-line doc add PR&lt;&#x2F;a&gt; that&#x27;s been open for 6 months. I&#x27;m still keeping hope alive that time will sort this out, but at the moment I&#x27;m worried things are not in a good state in terms of community in the actual codebases. The slack channel though is very active and helpful.&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;Anyway I&#x27;m still charging forward with both my Angular to Elm side project port and reading the early access &quot;Elm in Action&quot; book as it comes out. More posts on this topic as things progress.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>JSON comment tricks</title>
          <pubDate>Sat, 10 Dec 2016 21:18:24 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2016/12/json-comment-tricks/</link>
          <guid>https://peterlyons.com/problog/2016/12/json-comment-tricks/</guid>
          <description xml:base="https://peterlyons.com/problog/2016/12/json-comment-tricks/">&lt;p&gt;So as you have probably learnt and been frustrated by, JSON does not officially support comments. This, as I understand it, was done intentionally by JSON&#x27;s creator Douglas Crockford to prevent abuse of comments for non-standard metadata. Be that as it may, and it may make sense for a data interchange format, JSON is used for configuration files commonly and the lack of comments can be frustrating. Here&#x27;s some hacky tricks I sometimes use.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;disable-keys-with-a-prefix&quot;&gt;Disable keys with a prefix&lt;&#x2F;h2&gt;
&lt;p&gt;Let&#x27;s say I need to quickly switch a config file between a version with and without a certain value:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;{&amp;quot;proxy&amp;quot;: &amp;quot;http:&amp;#x2F;&amp;#x2F;proxy.example.com:8765&amp;quot;}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;To disable it, I &quot;comment it out&quot; by changing the name of  the property.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;{&amp;quot;OFFproxy&amp;quot;: &amp;quot;http:&amp;#x2F;&amp;#x2F;proxy.example.com:8765&amp;quot;}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Usually this has the desired effect and no ill side effects. I can imagine there are strict cases where it won&#x27;t work, but mostly it&#x27;s effective.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;add-comments-above&quot;&gt;Add comments above&lt;&#x2F;h2&gt;
&lt;p&gt;Here&#x27;s how I add &quot;comments&quot;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;{
  &amp;quot;&amp;#x2F;&amp;#x2F; Use this when in the Narnia office&amp;quot;: 0,
  &amp;quot;OFFproxy&amp;quot;: &amp;quot;http:&amp;#x2F;&amp;#x2F;proxy-na.example.com:8765&amp;quot;,
  &amp;quot;&amp;#x2F;&amp;#x2F; Use this when in the Whoville office&amp;quot;: 0,
  &amp;quot;proxy&amp;quot;: &amp;quot;http:&amp;#x2F;&amp;#x2F;proxy-wv.example.com:8765&amp;quot;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;I just start a key with &quot;&#x2F;&#x2F;&quot; and set the value to zero. Usually works OK.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Podcast Roster</title>
          <pubDate>Tue, 08 Nov 2016 19:53:02 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2016/11/podcast-roster/</link>
          <guid>https://peterlyons.com/problog/2016/11/podcast-roster/</guid>
          <description xml:base="https://peterlyons.com/problog/2016/11/podcast-roster/">&lt;p&gt;Here&#x27;s a snapshot of podcasts I&#x27;m checking out these days.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;http:&#x2F;&#x2F;nodeup.com&#x2F;&quot;&gt;Node Up&lt;&#x2F;a&gt; for all things node.js&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;changelog.com&#x2F;&quot;&gt;The Changelog&lt;&#x2F;a&gt; Open source in general. Great podcast.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;http:&#x2F;&#x2F;blog.cognitect.com&#x2F;cognicast&quot;&gt;The Cognicast&lt;&#x2F;a&gt; From Cognitect, the company behind Clojure and Datomic. I don&#x27;t do any clojure work currently but this interview-style show is always full of enlightened discussion and highly interesting.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;javascriptair.com&#x2F;&quot;&gt;JavaScript Air&lt;&#x2F;a&gt; no longer active but lots of good episodes to review. I was a guest recently.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;devchat.tv&#x2F;js-jabber&quot;&gt;JS Jabber&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;devchat.tv&#x2F;freelancers&quot;&gt;The Freelancer&#x27;s Show&lt;&#x2F;a&gt; Panel discussions on freelancing. I liked some of the earlier episodes more than the more recent stuff, but it&#x27;s a good source of exposure to others&#x27; experiences freelancing&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;softskills.audio&#x2F;&quot;&gt;Soft Skills Engineering&lt;&#x2F;a&gt; only recently started listening to this but I&#x27;m hoping to learn from it&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;http:&#x2F;&#x2F;thewebplatform.libsyn.com&#x2F;&quot;&gt;The Web Platform Podcast&lt;&#x2F;a&gt; A bit more front-end focused so I cherry pick episodes that seem interesting&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;http:&#x2F;&#x2F;www.se-radio.net&#x2F;&quot;&gt;Software Engineering Radio&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</description>
      </item>
      <item>
          <title>Words to Avoid</title>
          <pubDate>Wed, 02 Nov 2016 15:19:12 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2016/11/words-to-avoid/</link>
          <guid>https://peterlyons.com/problog/2016/11/words-to-avoid/</guid>
          <description xml:base="https://peterlyons.com/problog/2016/11/words-to-avoid/">&lt;p&gt;I commonly groan as a new product, service, protocol, or format is launched with one of my trigger words in the name. To save you the effort of truly considering whether this word is justified in the name of your thing, here&#x27;s a ready-made list of words that do not belong in the name of your thing:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;simple&lt;&#x2F;li&gt;
&lt;li&gt;lightweight&lt;&#x2F;li&gt;
&lt;li&gt;easy&lt;&#x2F;li&gt;
&lt;li&gt;standard&lt;&#x2F;li&gt;
&lt;li&gt;universal&lt;&#x2F;li&gt;
&lt;li&gt;permanent&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;As a bonus, it is very likely that these words do not belong in the name of your thing, so only include them if they truly describe your thing:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;automatic&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</description>
      </item>
      <item>
          <title>How to Ally</title>
          <pubDate>Sun, 23 Oct 2016 21:23:30 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2016/10/how-to-ally/</link>
          <guid>https://peterlyons.com/problog/2016/10/how-to-ally/</guid>
          <description xml:base="https://peterlyons.com/problog/2016/10/how-to-ally/">&lt;p&gt;Article of the week right here: &lt;a href=&quot;https:&#x2F;&#x2F;codeascraft.com&#x2F;2016&#x2F;10&#x2F;19&#x2F;being-an-effective-ally-to-women-and-non-binary-people&#x2F;&quot;&gt;Being an effective ally to woman and non-binary  people&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>ripgrep is the new hotness</title>
          <pubDate>Fri, 21 Oct 2016 21:11:04 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2016/10/ripgrep-is-the-new-hotness/</link>
          <guid>https://peterlyons.com/problog/2016/10/ripgrep-is-the-new-hotness/</guid>
          <description xml:base="https://peterlyons.com/problog/2016/10/ripgrep-is-the-new-hotness/">&lt;p&gt;For any of you command line code searching nerds out there, just wanted to point out the newest addition to a long line of command line filesystem search utilities.&lt;&#x2F;p&gt;
&lt;p&gt;A brief, probably wildly inaccurate history goes something like this:&lt;&#x2F;p&gt;
&lt;p&gt;In the before time, the long long ago, there was &lt;code&gt;grep&lt;&#x2F;code&gt;. It did recursive regex searches and was pretty powerful and effective. Sometimes you would need to pair it with &lt;code&gt;find&lt;&#x2F;code&gt; and &lt;code&gt;xargs&lt;&#x2F;code&gt; to get more precision.&lt;&#x2F;p&gt;
&lt;p&gt;Sometime around 2006 Andy &quot;petdance&quot; Lester got sick of grep searching irrelevant metadata like SCM directories and wrote an updated tool called &lt;code&gt;ack&lt;&#x2F;code&gt; which was smart enough to ignore those by default. It was written in perl and faster than grep. This was touted as &quot;25% fewer characters to type than grep&quot;. Read more at &lt;a href=&quot;http:&#x2F;&#x2F;beyondgrep.com&quot;&gt;beyondgrep.com&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Not satisfied with a 3-character search command, Geoff Greer created &lt;code&gt;the_silver_searcher&lt;&#x2F;code&gt; in 2011. It is written in C and the command is &lt;code&gt;ag&lt;&#x2F;code&gt; (periodic table for silver), thus yet another 33% shorter to type than &lt;code&gt;ack&lt;&#x2F;code&gt;. read more at &lt;a href=&quot;http:&#x2F;&#x2F;geoff.greer.fm&#x2F;ag&#x2F;&quot;&gt;The Silver Searcher&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Recently we&#x27;ve been blessed with the newest candidate for king: ripgrep. ripgrep is written in rust and the command is &lt;code&gt;rg&lt;&#x2F;code&gt;. Read more at &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;BurntSushi&#x2F;ripgrep&quot;&gt;BurntSushi&#x2F;ripgrep&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Give it a whirl: &lt;code&gt;brew install ripgrep&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Interesting that ripgrep was not bold enough to go for the 1-character command. I wonder when someone will release a program called &lt;code&gt;s&lt;&#x2F;code&gt; and attempt to take the throne.&lt;&#x2F;p&gt;
&lt;p&gt;All these tools are useful, but most I think the usernames petdance and burntsushi and project name the silver searcher and ripgrep are fantastic. Great job on naming things!&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>A Tale of Two Nows</title>
          <pubDate>Fri, 14 Oct 2016 20:55:35 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2016/10/a-tale-of-two-nows/</link>
          <guid>https://peterlyons.com/problog/2016/10/a-tale-of-two-nows/</guid>
          <description xml:base="https://peterlyons.com/problog/2016/10/a-tale-of-two-nows/">&lt;p&gt;I recently endeavored to use an analytics database called &lt;a href=&quot;https:&#x2F;&#x2F;www.pipelinedb.com&#x2F;&quot;&gt;pipelindb&lt;&#x2F;a&gt; to build a leaderboard type feature for a client. Pipelinedb is a fork-and-enhance project built on postgresql and most the of magic is presented to the developer in the form of a &lt;code&gt;CONTINUOUS VIEW&lt;&#x2F;code&gt; construct, which is similar to a regular relational table&#x2F;view but has the ability to efficiently do sliding window queries on big-data data sets.&lt;&#x2F;p&gt;
&lt;p&gt;The sliding window query I needed was along the lines of &quot;show me the players on team X with the highest score in the last day&quot;. I&#x27;m changing the subject matter here to make it generic, but my requirements were basically that. I had to keep track of daily, weekly, and all-time player scores and be able to find the top N players in combination with some &lt;code&gt;WHERE&lt;&#x2F;code&gt; clauses like &lt;code&gt;WHERE team_id = 42&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;This was my first exposure to pipelinedb and working with my team and the docs, I was able to build the feature based on the following structures in pipelinedb:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;a stream where rows are inserted as players score&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;sql&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;INSERT INTO score_stream
  (player_id, team_id, timestamp)
  VALUES
  (23, 47, &amp;#x27;2016-10-13 20:08:17.505233+00&amp;#x27;);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;a continuous view built upon that stream:&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;sql&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;CREATE CONTINUOUS VIEW score_by_team_1d AS
  SELECT player.player_id, team.team_id, count(*) AS score
  FROM score_stream ss
  JOIN players ON ss.player_id::integer = players.player_id
  JOIN players_teams pt ON players.player_id = pt.player_id
  WHERE ss.timestamp &amp;gt; current_timestamp - interval &amp;#x27;1 day&amp;#x27;
  GROUP BY pt.term_id, pt.player_id;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;We got it built and deployed and all seemed to be well. But after a while, we started to have some doubts about the resulting data we were getting. The daily high score players weren&#x27;t changing as much as we would have intuitively expected.&lt;&#x2F;p&gt;
&lt;p&gt;I dug into this a bit using some ad-hoc tooling, dumping results into spreadsheets, and comparing and I also started to see patterns that looked like instead of our sliding windows tracking scores over specific periods like daily and weekly, it looked like they were just accumulating total scores. But it was particularly weird because it wasn&#x27;t all-time total scores, just totals since some particular date.&lt;&#x2F;p&gt;
&lt;p&gt;With this hypothesis in mind, I went back to the pipelinedb docs and did the classic slow, long facepalm when I found this clause:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;PipelineDB exposes the current_date, current_time, and current_timestamp values to use within queries, but by design these don’t work with sliding-window queries because they remain constant within a transaction and thus don’t necessarily represent the current moment in time.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;When we had prototyped our queries, they were based upon a function called &lt;code&gt;clock_timestamp()&lt;&#x2F;code&gt; which works properly with pipelindb continuous views. As we finalized the code and switched from proof-of-concept structures to our real structures, I changed that to &lt;code&gt;current_timestamp&lt;&#x2F;code&gt; based on my reading of the postgresql documentation. I thought that was the more appropriate function.&lt;&#x2F;p&gt;
&lt;p&gt;But it turns out I had this wrong. The &lt;code&gt;current_timestamp&lt;&#x2F;code&gt; had only ever evaluated to a single moment in time, specifically when we first launched the feature and ran the &lt;code&gt;CREATE CONTINUOUS VIEW&lt;&#x2F;code&gt; statement. Thus it wasn&#x27;t computing daily scores, it was computing scores since that specific, fixed moment in the past. Our sliding windows weren&#x27;t sliding. The query&#x27;s notion of &quot;now&quot; was not always being re-evaluated to the current moment in time.&lt;&#x2F;p&gt;
&lt;p&gt;The proper fix involved &quot;just&quot; dropping the errant &lt;code&gt;CONTINUOUS VIEW&lt;&#x2F;code&gt; definition and recreating it with the only change being to use &lt;code&gt;clock_timestamp()&lt;&#x2F;code&gt; instead of &lt;code&gt;current_timestamp&lt;&#x2F;code&gt;. This also discarded our current data for 1 day and 1 week intervals, meaning we&#x27;d have to wait a week before we could rely on that data again.&lt;&#x2F;p&gt;
&lt;p&gt;However, the realities of production data meant this fix out would take the better part of 2 weeks. Compounding the problem was the fact that a feature such as this that requires careful testing of data results over relatively long periods of time (days, weeks) meant that a severe but easy-to-miss bug such as this could get through dev, QA, and product management and escape detection. It&#x27;s also a challenge to write automated tests for this. It&#x27;s not impossible, it&#x27;s just a lot more involved that testing basic CRUD functionality.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>JavaScript Air Async Patterns</title>
          <pubDate>Wed, 12 Oct 2016 18:47:16 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2016/10/javascript-air-async-patterns/</link>
          <guid>https://peterlyons.com/problog/2016/10/javascript-air-async-patterns/</guid>
          <description xml:base="https://peterlyons.com/problog/2016/10/javascript-air-async-patterns/">&lt;p&gt;I just had the pleasure of being a guest on the &lt;a href=&quot;https:&#x2F;&#x2F;javascriptair.com&#x2F;episodes&#x2F;2016-10-12&#x2F;&quot;&gt;JavaScript Air&lt;&#x2F;a&gt; podcast&#x2F;screencast. It&#x27;s episode 044 &quot;Async Patterns in JavaScript&quot;.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;javascriptair.com&#x2F;episodes&#x2F;2016-10-12&#x2F;&quot;&gt;Watch the episode&lt;&#x2F;a&gt; and I hope you enjoy it! We had a great discussion and covered many aspects of asynchronous programming in JavaScript including Events, Streams, Callbacks, Promises, Generators, Coroutines, Observables&#x2F;RxJS, Async&#x2F;Await and more.&lt;&#x2F;p&gt;
&lt;blockquote class=&quot;twitter-tweet&quot; data-lang=&quot;en&quot;&gt;&lt;p lang=&quot;en&quot; dir=&quot;ltr&quot;&gt;We&amp;#39;re getting started! &lt;a href=&quot;https:&#x2F;&#x2F;t.co&#x2F;gfKCxXzavh&quot;&gt;https:&#x2F;&#x2F;t.co&#x2F;gfKCxXzavh&lt;&#x2F;a&gt; 🚀 Big announcment in this one, you wont want to miss. &lt;a href=&quot;https:&#x2F;&#x2F;twitter.com&#x2F;hashtag&#x2F;JavaScriptAir?src=hash&quot;&gt;#JavaScriptAir&lt;&#x2F;a&gt; &lt;a href=&quot;https:&#x2F;&#x2F;twitter.com&#x2F;hashtag&#x2F;jsAirQuestion?src=hash&quot;&gt;#jsAirQuestion&lt;&#x2F;a&gt; &lt;a href=&quot;https:&#x2F;&#x2F;t.co&#x2F;4iTsiIiZEe&quot;&gt;pic.twitter.com&#x2F;4iTsiIiZEe&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;&amp;mdash; JavaScript Air (@JavaScriptAir) &lt;a href=&quot;https:&#x2F;&#x2F;twitter.com&#x2F;JavaScriptAir&#x2F;status&#x2F;786251933178695680&quot;&gt;October 12, 2016&lt;&#x2F;a&gt;&lt;&#x2F;blockquote&gt;
&lt;script async src=&quot;&#x2F;&#x2F;platform.twitter.com&#x2F;widgets.js&quot; charset=&quot;utf-8&quot;&gt;&lt;&#x2F;script&gt;
</description>
      </item>
      <item>
          <title>Best Regular Expression Dev Tools</title>
          <pubDate>Mon, 05 Sep 2016 16:23:13 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2016/09/best-regular-expression-dev-tools/</link>
          <guid>https://peterlyons.com/problog/2016/09/best-regular-expression-dev-tools/</guid>
          <description xml:base="https://peterlyons.com/problog/2016/09/best-regular-expression-dev-tools/">&lt;p&gt;Just a quick post to point out some fantastic web applications to help you craft regular expressions in your text processing adventures. These tools make it vastly easier to develop a solid understanding of regular expressions compared to when I learned them by studying the classic O&#x27;Reilly book &lt;a href=&quot;https:&#x2F;&#x2F;www.amazon.com&#x2F;gp&#x2F;product&#x2F;0596528124&#x2F;ref=as_li_tl?ie=UTF8&amp;amp;tag=peterlyons-20&amp;amp;camp=1789&amp;amp;creative=9325&amp;amp;linkCode=as2&amp;amp;creativeASIN=0596528124&amp;amp;linkId=9a1eb23f1973f15070a0e7e176ea4f2f&quot;&gt;Mastering Regular Expressions&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;first-place-regex-101&quot;&gt;First Place: RegEx 101&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;regex101.com&#x2F;#javascript&quot;&gt;RegEx 101&lt;&#x2F;a&gt; is the overall best tool. The explanation block clearly explains how each part of your regular expression is interpretted. The syntax highlighting is great, and the UI doesn&#x27;t have any confusing parts about whether you need the surrounding slashes and so forth.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;honorable-mention&quot;&gt;Honorable Mention&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;regex-generator.olafneumann.org&quot;&gt;Olaf Neumann Regex-Generator&lt;&#x2F;a&gt; is a brilliant tool that starts with your input text, autosuggests common patterns, and guides you through building your regex.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a href=&quot;http:&#x2F;&#x2F;www.regexplained.co.uk&#x2F;&quot;&gt;RegExplained&lt;&#x2F;a&gt; has a great railroad diagram visualization of your regular expression.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a href=&quot;http:&#x2F;&#x2F;www.regexr.com&#x2F;&quot;&gt;regexr&lt;&#x2F;a&gt; is also handy although not as polished as RegEx 101.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;extendsclass.com&#x2F;regex-tester.html&quot;&gt;Regex Tester&lt;&#x2F;a&gt; has a nice railroad diagram and color coding.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.debuggex.com&quot;&gt;debuggex&lt;&#x2F;a&gt; Neat but no one wants to log into a utility site like this (needed to add unit tests)&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;simple-regex.com&#x2F;&quot;&gt;Simple Regex&lt;&#x2F;a&gt; is kind of like literate programming for regex. It&#x27;s a different language you write with clearer expressions like &lt;code&gt;any of (digit, letter, one of &quot;.-&quot;) once or more&lt;&#x2F;code&gt; and it compiles to regex is several mainstream programming languages.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;happy-matching&quot;&gt;Happy Matching!&lt;&#x2F;h2&gt;
&lt;p&gt;Get out there and be glad you may be able to get away without staring at the pages of this book for hours on end.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;images-na.ssl-images-amazon.com&#x2F;images&#x2F;I&#x2F;51s3zpVhkYL._SY445_QL70_.jpg&quot; alt=&quot;Mastering Regular Expressions&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Atom Self-Update Script</title>
          <pubDate>Mon, 08 Aug 2016 03:06:34 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2016/08/atom-self-update-script/</link>
          <guid>https://peterlyons.com/problog/2016/08/atom-self-update-script/</guid>
          <description xml:base="https://peterlyons.com/problog/2016/08/atom-self-update-script/">&lt;p&gt;My work setup requires a non-administrator account for normal login. I have sudo access set up for my normal user. For some reason, Atom&#x27;s autoupdate feature can&#x27;t seem to deal with this and gets stuck in a loop trying to install updates. So I just scripted it as below:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sh&quot; class=&quot;language-sh &quot;&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;update_atom() {
  local atom=&amp;quot;${HOME}Downloads&amp;#x2F;atom.zip&amp;quot;
  echo -n &amp;quot;downloading…&amp;quot;
  curl --silent --fail --location &amp;#x27;https:&amp;#x2F;&amp;#x2F;atom.io&amp;#x2F;download&amp;#x2F;mac&amp;#x27; &amp;gt; &amp;quot;${atom}&amp;quot;
  echo &amp;quot;✓&amp;quot;
  local version
  version=$(unzip -p &amp;quot;${atom}&amp;quot; Atom.app&amp;#x2F;Contents&amp;#x2F;Info.plist \
    | grep -A 1 -i CFBundleVersion \
    | grep string \
    | tr -d -c 0-9.)
  echo -n &amp;quot;install Atom ${version}? (y&amp;#x2F;n): &amp;quot;
  if [[ -n &amp;quot;${ZSH_VERSION}&amp;quot; ]]; then
    read -q confirm
  else
    read -n 1 confirm
  fi
  if [[ &amp;quot;${confirm}&amp;quot; != &amp;quot;y&amp;quot; ]]; then
    return 1
  fi
  echo -n &amp;quot;installing…&amp;quot;
  sudo rm -rf &amp;#x2F;Applications&amp;#x2F;Atom.app
  sudo unzip -q -d &amp;#x2F;Applications &amp;quot;${atom}&amp;quot;
  sudo chown -R &amp;quot;${USER}&amp;quot; &amp;#x2F;Applications&amp;#x2F;Atom.app
  echo -n &amp;quot;✓&amp;quot;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</description>
      </item>
      <item>
          <title>settimeout and nanoseconds</title>
          <pubDate>Sat, 30 Jul 2016 14:37:28 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2016/07/settimeout-and-nanoseconds/</link>
          <guid>https://peterlyons.com/problog/2016/07/settimeout-and-nanoseconds/</guid>
          <description xml:base="https://peterlyons.com/problog/2016/07/settimeout-and-nanoseconds/">&lt;p&gt;Just a quick &quot;today I learned&quot; that if you pass a number &amp;lt; 1 as the delay argument to &lt;code&gt;setTimeout&lt;&#x2F;code&gt; node.js will attempt to run your callback at that time even if it&#x27;s less than 1ms in the future.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;const pretty = require(&amp;#x27;pretty-hrtime&amp;#x27;)

setTimeout(() =&amp;gt; {
  const delta = process.hrtime(start)
  console.log(pretty(delta))
}, 0.5)

const start = process.hrtime()
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;If you run this in a shell loop, you&#x27;ll notice sometimes it is able to complete in 500 nanoseconds or so, but sometimes it can only manage 2.5ms.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;for i in $(seq 1 10); do node time.js; done
1.92 ms
562 μs
568 μs
1.87 ms
565 μs
2.62 ms
1.88 ms
1.87 ms
553 μs
566 μs
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</description>
      </item>
      <item>
          <title>Using lambdas with Bluebird.map</title>
          <pubDate>Tue, 26 Jul 2016 01:52:48 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2016/07/using-lambdas-with-bluebird-map/</link>
          <guid>https://peterlyons.com/problog/2016/07/using-lambdas-with-bluebird-map/</guid>
          <description xml:base="https://peterlyons.com/problog/2016/07/using-lambdas-with-bluebird-map/">&lt;h1 id=&quot;using-lambdas-with-bluebird-map&quot;&gt;Using lambdas with Bluebird.map&lt;&#x2F;h1&gt;
&lt;p&gt;A situation came up this week where a coworker had stumbled upon some really terse code. They weren&#x27;t exactly clear on what was going on, so we went through an exercise together of rewriting the code in the most verbose format, then gradually shrinking it one piece at a time to arrive at the very terse but completely equivalent format.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;our-task-put-users-into-groups&quot;&gt;Our Task: Put Users into Groups&lt;&#x2F;h2&gt;
&lt;p&gt;Here&#x27;s the situation. We need to do a series of database inserts to add a user to multiple groups. This is done with an SQL INSERT statement for each groupId linking that group to the given user.&lt;&#x2F;p&gt;
&lt;p&gt;Here&#x27;s the initial setup code that will be unchanged for all the examples in this post:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;const Bluebird = require(&amp;#x27;bluebird&amp;#x27;)

const db = {
  insert: (table, row, callback) =&amp;gt; {
    setTimeout(() =&amp;gt; {
      console.log(&amp;#x27;done adding row&amp;#x27;, row)
      callback(null, [])
    }, 1000)
  }
}
Bluebird.promisifyAll(db)

function addToGroup(userId, groupId) {
  console.log(&amp;#x27;adding user&amp;#x27;, userId, &amp;#x27;to group&amp;#x27;, groupId)
  return db.insertAsync(&amp;#x27;users_groups&amp;#x27;, {user_id: userId, group_id: groupId})
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;wtf-is-this&quot;&gt;WTF is this?&lt;&#x2F;h2&gt;
&lt;p&gt;The initial code encountered, which was unclear, was&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;const groupIds = [42, 27, 33, 99]
const userId = 12

Bluebird.map(groupIds, addToGroup.bind(null, userId))
  .then(() =&amp;gt; {
    console.log(&amp;#x27;done&amp;#x27;)
  })
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This makes use of &lt;code&gt;Function.prototype.bind&lt;&#x2F;code&gt; which is available on all functions. To show what&#x27;s going in in this very terse bit of code, we coded up the fully verbose format of equivalent code as follows.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;Bluebird.map(groupIds, function perGroup (groupId) {
  return addToGroup(userId, groupId)
})
  .then(() =&amp;gt; {
    console.log(&amp;#x27;done&amp;#x27;)
  })
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;We&#x27;re using &lt;code&gt;Bluebird.map&lt;&#x2F;code&gt; to iterate over each &lt;code&gt;groupId&lt;&#x2F;code&gt; in the &lt;code&gt;groupIds&lt;&#x2F;code&gt; array so the &lt;code&gt;perGroup&lt;&#x2F;code&gt; function will be invoked 4 times since there are 4 values in the &lt;code&gt;groupIds&lt;&#x2F;code&gt; array. Each invocation calls &lt;code&gt;addToGroup&lt;&#x2F;code&gt; with the arguments in the proper order, and returns the promise that &lt;code&gt;addToGroup&lt;&#x2F;code&gt; returns.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;addToGroup&lt;&#x2F;code&gt; does the DB insert against our mocked-up database library which has been promisified with the &lt;code&gt;Bluebird.promisifyAll()&lt;&#x2F;code&gt; utility function so &lt;code&gt;insertAsync&lt;&#x2F;code&gt; will return a bluebird promise and not need a callback even though &lt;code&gt;insert&lt;&#x2F;code&gt; is coded as a callback API.&lt;&#x2F;p&gt;
&lt;p&gt;When coded like that, it was clearer what was going on. So we then started to trim out syntax that is technically unnecessary step by step as a learning exercise.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shrinking-it-down&quot;&gt;Shrinking it down&lt;&#x2F;h2&gt;
&lt;p&gt;First to go was just the name of the &lt;code&gt;perGroup&lt;&#x2F;code&gt; function, making it a lambda (anonymous function).&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;Bluebird.map(groupIds, function (groupId) {
  return addToGroup(userId, groupId)
})
  .then(() =&amp;gt; {
    console.log(&amp;#x27;done&amp;#x27;)
  })
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;sprinkle-some-es2015-arrow-function-on-it&quot;&gt;Sprinkle Some ES2015 Arrow Function On It&lt;&#x2F;h2&gt;
&lt;p&gt;Next we opted for the terser ES2015 arrow function syntax.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;Bluebird.map(groupIds, (groupId) =&amp;gt; {
  return addToGroup(userId, groupId)
})
  .then(() =&amp;gt; {
    console.log(&amp;#x27;done&amp;#x27;)
  })
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;lose-the-curly-braces&quot;&gt;Lose the Curly Braces&lt;&#x2F;h2&gt;
&lt;p&gt;Now what we notice is we have an arrow function that just returns the value of a single expression. This means it&#x27;s eligible for the ultra-terse no-curly form. So we axe the curlies and the &lt;code&gt;return&lt;&#x2F;code&gt; keyword and keep it on one line.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;Bluebird.map(groupIds, (groupId) =&amp;gt; addToGroup(userId, groupId))
  .then(() =&amp;gt; {
    console.log(&amp;#x27;done&amp;#x27;)
  })
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;OK now that&#x27;s pretty terse. However, look closely at the anonymous mapping function itself &lt;code&gt;(groupId) =&amp;gt; addToGroup(userId, groupId)&lt;&#x2F;code&gt;. How different is it from &lt;code&gt;addToGroup&lt;&#x2F;code&gt;? Well, instead of taking 2 arguments, it only takes one. Then it calls &lt;code&gt;addToGroup&lt;&#x2F;code&gt; with 2 arguments, the first being the &lt;code&gt;userId&lt;&#x2F;code&gt; which is directly in the parent scope and the second being the &lt;code&gt;groupId&lt;&#x2F;code&gt; that comes from &lt;code&gt;Bluebird.map&lt;&#x2F;code&gt;. So you could think of it like a variant of &lt;code&gt;addToGroup&lt;&#x2F;code&gt; with the &lt;code&gt;userId&lt;&#x2F;code&gt; argument hard coded or &quot;baked in&quot; to be a particular value.&lt;&#x2F;p&gt;
&lt;p&gt;It turns out that this is exactly what &lt;code&gt;Function.prototype.bind&lt;&#x2F;code&gt; does. Given a function, bind creates a new function that has some of the arguments pre-specified (&quot;bound&quot;) to particular values, leaving only the remaining arguments to be varied on each function call.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;back-to-bind&quot;&gt;Back to .bind&lt;&#x2F;h2&gt;
&lt;p&gt;So that gets us back to the &lt;code&gt;.bind&lt;&#x2F;code&gt; format.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;Bluebird.map(groupIds, addToGroup.bind(null, userId))
  .then(() =&amp;gt; {
    console.log(&amp;#x27;done&amp;#x27;)
  })
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;It&#x27;s a few characters shorter than even the tersest arrow function.&lt;&#x2F;p&gt;
&lt;p&gt;What we are doing here is creating a new, unnamed function with pre-specified &lt;code&gt;this&lt;&#x2F;code&gt; value and first argument value. When we call &lt;code&gt;addToGroup.bind(null, userId)&lt;&#x2F;code&gt; passing &lt;code&gt;null&lt;&#x2F;code&gt; as the first argument means the function &lt;code&gt;bind&lt;&#x2F;code&gt; returns will not have access to a &lt;code&gt;this&lt;&#x2F;code&gt; variable when called. &lt;code&gt;addToGroup&lt;&#x2F;code&gt; is a regular function (not an object-oriented method) and makes no reference to &lt;code&gt;this&lt;&#x2F;code&gt;, so it&#x27;s perfectly OK to have it be &lt;code&gt;null&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;The second argument to &lt;code&gt;bind&lt;&#x2F;code&gt; will be &quot;bound&quot; as the first argument of our new &lt;code&gt;addToGroup&lt;&#x2F;code&gt; variant. That leaves the 2nd argument unspecified, which lines up with the &lt;code&gt;Bluebird.map&lt;&#x2F;code&gt; expecting a function that takes 1 argument which is a &lt;code&gt;groupId&lt;&#x2F;code&gt;. Remember that &lt;code&gt;bind&lt;&#x2F;code&gt; &lt;strong&gt;returns&lt;&#x2F;strong&gt; the new function without executing it. So you could express this in English as &quot;Hey addToGroup function, build me a variant of yourself with this userId fixed as the first argument and the other argument unspecified until later when I call the function&quot;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;lodash-partial-version&quot;&gt;lodash partial version&lt;&#x2F;h2&gt;
&lt;p&gt;Another option if that ugly &lt;code&gt;null&lt;&#x2F;code&gt; in the &lt;code&gt;.bind&lt;&#x2F;code&gt; call rubs you the wrong way would be lodash&#x27;s &lt;code&gt;_.partial&lt;&#x2F;code&gt; which looks like this:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;Bluebird.map(groupIds, _.partial(addToGroup, [userId]))
  .then(() =&amp;gt; {
    console.log(&amp;#x27;done&amp;#x27;)
  })
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;lessons-learned&quot;&gt;Lessons Learned&lt;&#x2F;h2&gt;
&lt;p&gt;My take is that the implicit-return arrow function flavor: &lt;code&gt;Bluebird.map(groupIds, (groupId) =&amp;gt; addToGroup(userId, groupId))&lt;&#x2F;code&gt; is the tersest you can go and still be readable to developers without much exposure to &lt;code&gt;.bind&lt;&#x2F;code&gt;, which is a fairly advanced feature in regular JS codebases, although in functional programming it implements the idea of partial application, which is very commonplace and not advanced for developers accustomed to the functional programming paradigm.&lt;&#x2F;p&gt;
&lt;p&gt;I think explicitly seeing that you have a function that takes a single &lt;code&gt;groupId&lt;&#x2F;code&gt; argument and gets &lt;code&gt;userId&lt;&#x2F;code&gt; from the parent scope is important for this to be clear, and with &lt;code&gt;.bind&lt;&#x2F;code&gt; you lose that and it becomes magic.&lt;&#x2F;p&gt;
&lt;p&gt;The main takeaway is that all formats of this code are exactly equivalent. It&#x27;s good to be able to read and understand every variation so you are prepared if you encounter it in your coding.&lt;&#x2F;p&gt;
&lt;p&gt;Thanks to &lt;a href=&quot;https:&#x2F;&#x2F;scottnonnenberg.com&#x2F;&quot;&gt;Scott Nonnenberg&lt;&#x2F;a&gt; for reviewing a draft of this post!&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Debugging built into node</title>
          <pubDate>Sun, 17 Jul 2016 16:38:01 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2016/07/debugging-built-into-node/</link>
          <guid>https://peterlyons.com/problog/2016/07/debugging-built-into-node/</guid>
          <description xml:base="https://peterlyons.com/problog/2016/07/debugging-built-into-node/">&lt;p&gt;Big news in node.js land! node.js v6.3.0 now ships with experimental support for built-in debugging via the chrome devtools. I&#x27;ve updated my &lt;a href=&quot;&#x2F;js-debug&quot;&gt;JS Debugging&lt;&#x2F;a&gt; article&#x2F;talk with all the details you need to use this awesome new capability to give yourself debugging superpowers.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Check for Pull Requests in All Your Repos</title>
          <pubDate>Thu, 09 Jun 2016 15:11:35 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2016/06/check-for-pull-requests-in-all-your-repos/</link>
          <guid>https://peterlyons.com/problog/2016/06/check-for-pull-requests-in-all-your-repos/</guid>
          <description xml:base="https://peterlyons.com/problog/2016/06/check-for-pull-requests-in-all-your-repos/">&lt;p&gt;My current work involves many microservice (ish) node.js projects. Each has its own git repository hosted on github. My team follows a pull request workflow requiring all code to hit our main branch via pull request. We have slack chat integration so we can see when a new pull request is created, but even with that I found myself confused about whether or not there were any pending pull requests that I needed to review.&lt;&#x2F;p&gt;
&lt;p&gt;Scripting to the rescue! Here&#x27;s what I did to quickly see a list of each repository and the title of each pending pull request. I hacked this together with cURL, an npm command line module named &lt;a href=&quot;https:&#x2F;&#x2F;www.npmjs.com&#x2F;package&#x2F;json&quot;&gt;json&lt;&#x2F;a&gt;,  and a few lines of shell scripts. Here&#x27;s the breakdown:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Log in to your github account and visit the &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;settings&#x2F;tokens&quot;&gt;personal access tokens&lt;&#x2F;a&gt; settings page&lt;&#x2F;li&gt;
&lt;li&gt;Click the button to generate a new access token. Give it a clear name like &quot;Pull Request Status Script&quot;. Check the &quot;repo&quot; scope checkbox
&lt;ul&gt;
&lt;li&gt;I believe this is the most specific&#x2F;limited&#x2F;correct permission, but if you know a way to grant read-only access to pull requests in a private repo, please let me know via disqus comment below.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;Click &quot;Generate Token&quot;, copy it, and save it to a file. You can do this on OS X with the command line &lt;code&gt;pbpaste&lt;&#x2F;code&gt; utility. For example: &lt;code&gt;pbpaste &amp;gt; ~&#x2F;.github-pr-status-token.txt&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Tighten permissions on that: &lt;code&gt;chmod 400 ~&#x2F;.github-pr-status-token.txt&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Install the npm module json: &lt;code&gt;npm install -g json&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Now you&#x27;re ready to add this shell script to your shell profile:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sh&quot; class=&quot;language-sh &quot;&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;check_pull_requests() {
  local organization=&amp;quot;YourGithubOrg&amp;quot; # Edit this
  for repo in repo-1 repo-2 repo-3 repo-4; do # Edit this repo list
    echo &amp;quot;* ${repo}&amp;quot;
    curl \
      --silent \
      --header &amp;quot;Authorization: token $(cat ~&amp;#x2F;.github-pr-status-token.txt)&amp;quot; \
      &amp;quot;https:&amp;#x2F;&amp;#x2F;api.github.com&amp;#x2F;repos&amp;#x2F;${organization}&amp;#x2F;${repo}&amp;#x2F;pulls&amp;quot; \
      | json -a title html_url
  done
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The output will look something like this:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;* repo-1
Update config for Heroku https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;YourGithubOrg&amp;#x2F;repo-1&amp;#x2F;pull&amp;#x2F;13
* repo-2
* repo-3
Fix Bug #42 https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;YourGithubOrg&amp;#x2F;repo-3&amp;#x2F;pull&amp;#x2F;42
* repo-4
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;I hope you find this useful. Get those PRs reviewed quickly!&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>SQLite on The Changelog</title>
          <pubDate>Fri, 03 Jun 2016 14:55:13 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2016/06/sqlite-on-the-changelog/</link>
          <guid>https://peterlyons.com/problog/2016/06/sqlite-on-the-changelog/</guid>
          <description xml:base="https://peterlyons.com/problog/2016/06/sqlite-on-the-changelog/">&lt;p&gt;I just listened to &lt;a href=&quot;https:&#x2F;&#x2F;changelog.com&#x2F;201&#x2F;&quot;&gt;The Changelog Episode 201&lt;&#x2F;a&gt; on &lt;a href=&quot;https:&#x2F;&#x2F;sqlite.org&#x2F;&quot;&gt;SQLite&lt;&#x2F;a&gt; with Richard Hipp. Completely fascinating. This project is completely on outlier on so many different facets.&lt;&#x2F;p&gt;
&lt;p&gt;It is one of the most widely-used software projects of all time. It is the work of a very small team (3 engineers I believe have done the vast majority of the coding). Richard codes it in a text editor he wrote (I believe Linus Torvalds also uses a custom editor he wrote). It is tracked in a version control system called &lt;a href=&quot;http:&#x2F;&#x2F;www.fossil-scm.org&quot;&gt;fossil&lt;&#x2F;a&gt; that Richard also wrote. SQLite has zero dependencies. Everything they need has been hand coded from scratch.&lt;&#x2F;p&gt;
&lt;p&gt;I found this interview completely spellbinding and would love to hear a follow-up. My mind just races with what Richard might say to me if he rode shotgun next to me coding for a few hours. What would he say about my tools? My methods? My domain knowledge?&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Why is Node Running</title>
          <pubDate>Fri, 08 Apr 2016 03:47:03 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2016/04/why-is-node-running/</link>
          <guid>https://peterlyons.com/problog/2016/04/why-is-node-running/</guid>
          <description xml:base="https://peterlyons.com/problog/2016/04/why-is-node-running/">&lt;p&gt;Ever have a node process you expect to complete and exit just hang there? This happens when the code has active listeners or timers that &lt;strong&gt;could&lt;&#x2F;strong&gt; potentially queue up more work to be done. For example, an open network connection that might still receive some data or a pending &lt;code&gt;setTimeout&lt;&#x2F;code&gt; timer scheduled for the future. When everything is neat and tidy and all network connections and timers are properly canceled and closed, node sees that the event loop callback queue is empty and exits the process entirely. However, as you graduate from small scripts to small and medium applications where several databases, upstream backing API endpoints, connection pooling, etc are in play, it can be a first-class mystery tracking down a process you expect to exit that&#x27;s just hanging there.&lt;&#x2F;p&gt;
&lt;p&gt;Enter &lt;a href=&quot;https:&#x2F;&#x2F;www.npmjs.com&#x2F;package&#x2F;why-is-node-running&quot;&gt;why-is-node-running&lt;&#x2F;a&gt; to save the day. To use it is pretty straightforward.&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;npm install why-is-node-running&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Run your app using the CLI wrapper so instead of &lt;code&gt;node server.js&lt;&#x2F;code&gt; you would run &lt;code&gt;.&#x2F;node_modules&#x2F;.bin&#x2F;why-is-node-running server.js&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;why-is-node-running will print out the command and PID you need which will be something like &lt;code&gt;kill -SIGUSR1 67641 for logging&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;In a separate terminal, run that command. The output stack traces usually should give you enough clues to track down the culprit.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;There were definitely times in the early days of node where I could have really used this. Glad it&#x27;s here now, though!&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>javascript try catch is fail</title>
          <pubDate>Wed, 23 Mar 2016 14:32:56 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2016/03/javascript-try-catch-is-fail/</link>
          <guid>https://peterlyons.com/problog/2016/03/javascript-try-catch-is-fail/</guid>
          <description xml:base="https://peterlyons.com/problog/2016/03/javascript-try-catch-is-fail/">&lt;p&gt;It is possible to write solid javascript code almost entirely without using try&#x2F;catch. The only common standard library function that requires it is &lt;code&gt;JSON.parse&lt;&#x2F;code&gt;. Almost everything else will expose environmental&#x2F;input errors with some other mechanism (usually a special value or surprising behavior).&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;parseInt(&#x27;turkey sandwich&#x27;, 10)&lt;&#x2F;code&gt; returns &lt;code&gt;NaN&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;parseFloat(&quot;I&#x27;m on a boat&quot;)&lt;&#x2F;code&gt; returns &lt;code&gt;NaN&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;new Date(&quot;St Swiven&#x27;s Day&quot;)&lt;&#x2F;code&gt; returns &lt;code&gt;Invalid Date&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;Math.max(&#x27;tomato&#x27;, &#x27;potato&#x27;)&lt;&#x2F;code&gt; returns &lt;code&gt;NaN&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;15&#x2F;0&lt;&#x2F;code&gt; evaluates to &lt;code&gt;Infinity&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&#x27;home&#x27;.slice(&#x27;biscuit&#x27;)&lt;&#x2F;code&gt; returns &lt;code&gt;&#x27;home&#x27;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Errors of any kind when dealing with &lt;code&gt;XMLHttpRequest&lt;&#x2F;code&gt; will not produce exceptions&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;error-first-callbacks-and-es5-are-ok&quot;&gt;Error-first callbacks and ES5 Are OK&lt;&#x2F;h3&gt;
&lt;p&gt;This, while contributing to JavaScript&#x27;s &quot;WAT&quot; factor, is actually a good situation, all things considered. It is good because it leaves exceptions to represent ONLY programmer errors and never operational errors. Operational errors are problems with the environment such as invalid user input, failed network communication, no disk space left, etc. They can be resolved without changing the program code itself. Programmer errors on the other hand indicate a flaw in the program code that can only be fixed by correcting the source code. For further information on this distinction, I highly recommend this archived version of a Joyent blog post on &lt;a href=&quot;https:&#x2F;&#x2F;web.archive.org&#x2F;web&#x2F;20140401155055&#x2F;https:&#x2F;&#x2F;www.joyent.com&#x2F;developers&#x2F;node&#x2F;design&#x2F;errors&quot;&gt;Error Handling in Node.js&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;In the vast majority of node code I&#x27;ve written, which uses the node error-first callback convention, I end up with the following properties&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Any exception thrown represents a programmer error (excluding JSON.parse as mentioned)&lt;&#x2F;li&gt;
&lt;li&gt;All operational errors are represented as either error-first callbacks or &lt;code&gt;error&lt;&#x2F;code&gt; events emitted
&lt;ul&gt;
&lt;li&gt;This is largely due to the async event loop constantly unwinding the stack, making try&#x2F;catch&#x2F;throw effectively useless&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Thus I can set up a &lt;code&gt;process.on(&#x27;uncaughtException&#x27;)&lt;&#x2F;code&gt; handler and confidently exit my program knowing if that event ever fires, it&#x27;s a programmer error, and as discussed in the &quot;Error Handling in Node.js&quot; article, exiting with an error and restarting is the correct thing to do.&lt;&#x2F;p&gt;
&lt;p&gt;Now, this does require discipline to properly handle all error-first callbacks and some boilerplate comes with that. Failing to do so risks operational errors going undetected, but usually within a few lines of code, those operational errors are inadvertently escalated to programmer errors when you try to access the first record in a database query result array, which is undefined because the query failed and you ignored the error. &lt;strong&gt;Operational errors eventually manifesting as impostor programmer errors are pretty bad (but...)&lt;&#x2F;strong&gt;. I do see YOLO-ignore-the-error code occasionally in my consulting practice, but I&#x27;m usually able to make a convincing argument that all errors must be at the very least logged if you don&#x27;t want to spend days debugging odd program behavior.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;es2017-async-await-ruins-this&quot;&gt;ES2017 async&#x2F;await ruins this&lt;&#x2F;h3&gt;
&lt;p&gt;Part of my motivation to write this post was exasperation encountering my first bleeding-edge babel&#x2F;ES2015&#x2F;ES20XX project that combines &lt;a href=&quot;https:&#x2F;&#x2F;tc39.github.io&#x2F;ecmascript-asyncawait&#x2F;&quot;&gt;async functions&lt;&#x2F;a&gt; (Slated for ES2017), the await keyword, promises, and try&#x2F;catch. You get code looking like this:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;try {
  let user = await db.users.getOrThrow({email})
  res.sedn(user.toJSON())
} catch (nouser) {
  res.status(404).send(&amp;#x27;user not found&amp;#x27;)
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The problem is this masks a programmer error as an operational error. What happens in the scenario I&#x27;m so concerned about is:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;DB query runs and finds the user, returns it&lt;&#x2F;li&gt;
&lt;li&gt;await&#x2F;promise do their magic and let this look like synchronous code&lt;&#x2F;li&gt;
&lt;li&gt;we try to send success, but there&#x27;s a typo: &lt;code&gt;res.sedn&lt;&#x2F;code&gt; instead of &lt;code&gt;res.send&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;this throws an exception, our good friend &quot;is not a function&quot;&lt;&#x2F;li&gt;
&lt;li&gt;the API is designed to indicate record not found with an exception
&lt;ul&gt;
&lt;li&gt;I consider this a poor design, but it exists&lt;&#x2F;li&gt;
&lt;li&gt;specifically the bookshelf.js ORM has a &lt;code&gt;.fetch({require: true})&lt;&#x2F;code&gt; API that does precisely this&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;the catch block catches it, miscategorises it as an operational error, and sends a 404, when the correct behavior would be to send a 500 and exit the process nonzero&lt;&#x2F;li&gt;
&lt;li&gt;Nobody notices this&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;(...)Programmer errors hiding as operational errors is worse&lt;&#x2F;strong&gt;.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;freaking-out&quot;&gt;Freaking out&lt;&#x2F;h3&gt;
&lt;p&gt;So with ES2017 the combination of await, implicit promises, and try&#x2F;catch for error handling seems to me like this scenario is going to become increasingly common, and I&#x27;m worried about how I can actually still detect programmer errors and exit. The duck typing of exception instances in javascript is sufficiently undefined, unimplemented, and unreliable that I doubt you can reliably distinguish unless you have a complete and well-understood catalog of all possible operational exceptions a block of code might generate, which I don&#x27;t think is feasible without slimming your &lt;code&gt;try&lt;&#x2F;code&gt; blocks down to a single operation, which loses a primary proposed benefit of try&#x2F;catch in reduction of error handling boilerplate.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;what-s-to-be-done&quot;&gt;What&#x27;s to be done?&lt;&#x2F;h3&gt;
&lt;p&gt;At the moment, I&#x27;m not sure how this is going to play out. I&#x27;m sticking to ES5 and callbacks for now. I think exceptions should represent programmer errors and never be used for control flow nor for operational errors. That&#x27;s why they have stack traces, and that should be their sole purpose. But I&#x27;d love to hear your thoughts and suggestions, so post a comment here via disqus (click &quot;Show Comments&quot; below) if that&#x27;s your thing or take it to twitter or hackernews etc.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Update 2017-11-04&lt;&#x2F;strong&gt;: Eran Hammer is proposing &lt;a href=&quot;https:&#x2F;&#x2F;hapi.dev&#x2F;module&#x2F;bounce&#x2F;&quot;&gt;Bounce&lt;&#x2F;a&gt; as a solution to this problem, and it looks promising.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;see-also&quot;&gt;See Also&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;staltz.com&#x2F;promises-are-not-neutral-enough.html&quot;&gt;Promises are not neutral enough&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>keeping npm dependencies up to date</title>
          <pubDate>Tue, 15 Mar 2016 02:16:37 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2016/03/keeping-npm-dependencies-up-to-date/</link>
          <guid>https://peterlyons.com/problog/2016/03/keeping-npm-dependencies-up-to-date/</guid>
          <description xml:base="https://peterlyons.com/problog/2016/03/keeping-npm-dependencies-up-to-date/">&lt;p&gt;I&#x27;ve recently tried a few tools to help me keep the dependencies of my node&#x2F;npm projects up to date. Here&#x27;s a quick report on my experience, but first a few notes.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;why-bother&quot;&gt;Why bother?&lt;&#x2F;h3&gt;
&lt;p&gt;Keeping up with the barrage of updates can be a tedious chore. Is it even worth the effort? For silly side projects and things you&#x27;ve generally not committed to maintaining, no, it&#x27;s not worth the effort. But for an open source library that is actively used or a side project you want to be perceived favorably, it&#x27;s worthwhile.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;It shows recent maintenance activity, which is a strong signal of a non-abandoned project&lt;&#x2F;li&gt;
&lt;li&gt;it shows ability to add to a project after the initial release and &quot;new project energy&quot; has dissipated
&lt;ul&gt;
&lt;li&gt;You might be surprised how few projects can pass this test&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;It can help to avoid security vulnerabilities&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;greenkeeper&quot;&gt;greenkeeper&lt;&#x2F;h3&gt;
&lt;p&gt;The first tool&#x2F;service I tried was &lt;a href=&quot;http:&#x2F;&#x2F;greenkeeper.io&quot;&gt;greenkeeper&lt;&#x2F;a&gt;. It integrates with both npm and github and works by detecting when new packages are published to npm and sending your project a github pull request to update your &lt;code&gt;package.json&lt;&#x2F;code&gt; file to the new version. The intention is for you to have continuous integration set up so automated tests can confirm the new version still works properly in your library, and if you, you can just click &quot;Merge&quot; and get on with things.&lt;&#x2F;p&gt;
&lt;p&gt;Overall the service works as advertised. I eventually disabled it, though because it is too tedious to manage given multiple projects. I ended up with several emails&#x2F;PRs per day and increasing complexity to manage them, avoid conflicts, click around the github web UI for ages, etc.&lt;&#x2F;p&gt;
&lt;p&gt;So I went back to the command line with the next tool.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;npm-check-updates&quot;&gt;npm-check-updates&lt;&#x2F;h3&gt;
&lt;p&gt;I found &lt;a href=&quot;https:&#x2F;&#x2F;www.npmjs.com&#x2F;package&#x2F;npm-check-updates&quot;&gt;npm-check-updates&lt;&#x2F;a&gt; to work really well. It&#x27;s a command line tool (abbreviation &lt;code&gt;ncu&lt;&#x2F;code&gt; also available) you can run in your project&#x27;s root directory and it will print out your stale dependencies, optionally updating your &lt;code&gt;package.json&lt;&#x2F;code&gt; file. Then you should run &lt;code&gt;npm install&lt;&#x2F;code&gt; to actually install the new deps, make sure your tests pass, and then commit and potentially release a patch update to your project.&lt;&#x2F;p&gt;
&lt;p&gt;I ended up adding a little shell function to loop over my node projects and run &lt;code&gt;ncu -u&lt;&#x2F;code&gt; on all of them so I can quickly update any stale dependencies.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;packages-to-avoid&quot;&gt;packages to avoid&lt;&#x2F;h3&gt;
&lt;p&gt;I looked at the following packages and they seem to be unmaintained, so don&#x27;t bother:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.npmjs.com&#x2F;package&#x2F;npm-update&quot;&gt;npm-update&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.npmjs.com&#x2F;package&#x2F;npm-modernize&quot;&gt;npm-modernize&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;general-tips&quot;&gt;general tips&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;Make sure your test and release process (the git part, CI part, and npm part) are automated well enough that the process is not tedious nor error-prone&lt;&#x2F;li&gt;
&lt;li&gt;consider scheduling a specific time to handle non-urgent updates to avoid needless distractions. I&#x27;ve made this the first part of my &quot;Open Source Wednesday Morning&quot; routine I do while at the Code and Coffee get together.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</description>
      </item>
      <item>
          <title>expressjs gratitude</title>
          <pubDate>Sun, 28 Feb 2016 19:51:45 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2016/02/expressjs-gratitude/</link>
          <guid>https://peterlyons.com/problog/2016/02/expressjs-gratitude/</guid>
          <description xml:base="https://peterlyons.com/problog/2016/02/expressjs-gratitude/">&lt;p&gt;There&#x27;s some drama in the &lt;a href=&quot;http:&#x2F;&#x2F;expressjs.com&#x2F;&quot;&gt;expressjs&lt;&#x2F;a&gt; world today and I just wanted to express my thanks to all the contributors and supporters. ExpressJS has been involved in some capacity in probably 100% of my independent consulting projects since 2014 and the 2 full-time gigs prior to that as well. ExpressJS pays my bills!&lt;&#x2F;p&gt;
&lt;p&gt;Specifically I want to thank &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;dougwilson&quot;&gt;Doug Wilson&lt;&#x2F;a&gt;, &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;jonathanong&quot;&gt;Jonathan Ong&lt;&#x2F;a&gt; and &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;tj&quot;&gt;TJ Holowaychuk&lt;&#x2F;a&gt; for their great work on this project and the ecosystem around it.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Quick Thoughts: Universe Lifetime</title>
          <pubDate>Sat, 02 Jan 2016 18:30:14 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2016/01/quick-thoughts-universe-lifetime/</link>
          <guid>https://peterlyons.com/problog/2016/01/quick-thoughts-universe-lifetime/</guid>
          <description xml:base="https://peterlyons.com/problog/2016/01/quick-thoughts-universe-lifetime/">&lt;h2 id=&quot;introducing-quick-thoughts&quot;&gt;Introducing Quick Thoughts&lt;&#x2F;h2&gt;
&lt;p&gt;Thinking about ways to share my tech thoughts and engage with the Internet, I&#x27;ve been feeling frustrated by twitter&#x27;s tweet length limit and ensuing conversation and clarification struggles. But also the relatively high friction of a blog post has been limiting as well. A new idea I&#x27;m going to try is to quickly log &quot;Thoughts of the Week&quot; then edit and curate them into a weekly blog post. I&#x27;ll be doing this using the tagging feature in my &lt;a href=&quot;https:&#x2F;&#x2F;mjournal.peterlyons.com&quot;&gt;mjourna&lt;&#x2F;a&gt;l open source journaling software.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;tyranny-of-noise&quot;&gt;Tyranny of Noise&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;a href=&quot;http:&#x2F;&#x2F;www.nytimes.com&#x2F;interactive&#x2F;2015&#x2F;12&#x2F;29&#x2F;arts&#x2F;design&#x2F;sound-architecture.html&quot;&gt;NYT: Sound Architecture&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;This one is close to home for me. I&#x27;m sensitive to noise and am baffled that common American single family homes and apartments do great at creating visual privacy but have abysmal acoustic privacy. Lately I&#x27;ve been getting more and more annoyed with the helicopter-level sounds my gas furnace makes and the sounds of the ductwork snapping as it expands and contracts given the very cold temperatures we have had the past two weeks. I&#x27;d love to live in a house with radiant heating at some point to enjoy the quiet.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;fast-charging-nexus-5x&quot;&gt;Fast Charging Nexus 5X&lt;&#x2F;h3&gt;
&lt;p&gt;Sigh. Yet another mess. If you have a Google Nexus 5X or 6P smartphone, &lt;a href=&quot;http:&#x2F;&#x2F;www.droid-life.com&#x2F;2015&#x2F;10&#x2F;19&#x2F;nexus-6p-nexus-5x-quick-charge&#x2F;&quot;&gt;here&#x27;s the article&lt;&#x2F;a&gt; that explains the goats you need to sacrifice to get the &quot;charging rapidly&quot; to work.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;djb-and-cosmological-theories&quot;&gt;DJB and Cosmological Theories&lt;&#x2F;h3&gt;
&lt;p&gt;Leave it to &lt;a href=&quot;http:&#x2F;&#x2F;cr.yp.to&#x2F;djb.html&quot;&gt;DJB&lt;&#x2F;a&gt; to include this beauty in the &lt;a href=&quot;https:&#x2F;&#x2F;cr.yp.to&#x2F;libtai&#x2F;tai64.html#tai64n&quot;&gt;docs for the TAI64 timestamp format&lt;&#x2F;a&gt; used by multilog:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;Integers 2^63 and larger are reserved for future extensions. Under many
cosmological theories, the integers under 2^63 are adequate to cover the entire
expected lifetime of the universe; in this case no extensions will be necessary.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
</description>
      </item>
      <item>
          <title>Command Line: Newbie to Ninja</title>
          <pubDate>Sun, 06 Dec 2015 17:52:52 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2015/12/command-line:-newbie-to-ninja/</link>
          <guid>https://peterlyons.com/problog/2015/12/command-line:-newbie-to-ninja/</guid>
          <description xml:base="https://peterlyons.com/problog/2015/12/command-line:-newbie-to-ninja/">&lt;h2 id=&quot;part-one-bow-to-your-sensei&quot;&gt;Part One: Bow to Your Sensei&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;static1.1.sqspcdn.com&#x2F;static&#x2F;f&#x2F;1209144&#x2F;20265241&#x2F;1347563118103&#x2F;RexKwonDo2.jpg?token=mU%2Beslcb3UKPj3JU%2FUTKrbccAxI%3D&quot; alt=&quot;Bow to your Sensei&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;These tips are the dojo ground rules you must learn before even the most basic command line work can get done. If you haven&#x27;t got these down, everything is going to take much longer than it needs to. These are also definitely a prerequisite to any command line activity while pair programming if you don&#x27;t want your pair to pull their hair out during the session.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;tab-completion&quot;&gt;tab completion&lt;&#x2F;h3&gt;
&lt;p&gt;The shell can help you out with typing long filesystem paths and avoiding typos in command names. It does this with a feature called &quot;tab completion&quot;. Tab completion allows you to start entering just the first few characters of something then type the &lt;code&gt;&amp;lt;TAB&amp;gt;&lt;&#x2F;code&gt; key and the shell will complete it for you. So for example, instead of typing &lt;code&gt;cd .&#x2F;external&#x2F;plugins&lt;&#x2F;code&gt;, you could type &lt;code&gt;cd .&#x2F;ex&amp;lt;TAB&amp;gt;&lt;&#x2F;code&gt; and the shell would expand the rest of the directory name &quot;external&quot;, allowing you to keep typing &lt;code&gt;pl&amp;lt;TAB&amp;gt;&lt;&#x2F;code&gt; to expand plugins. This saves a lot of typing and prevents typos from ruining your day. Pretty much every experienced command line user I&#x27;ve paired with uses tab completion on nearly every command they run.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;navigating-command-history&quot;&gt;navigating command history&lt;&#x2F;h3&gt;
&lt;p&gt;Another glaring sign that it&#x27;s your first day at the command line is re-typing a command you recently ran. The shell keeps a history of commands you ran and makes it easy to re-run a previous command because this is something that happens all the time in typical interactive shell usage. Here are some shell history basics that will keep you moving fast.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Use the up and down arrow keys to scroll back&#x2F;forward through your history
&lt;ul&gt;
&lt;li&gt;CTRL-p (previous) and CTRL-n (next) do the same thing and touch typists may prefer these as your hands don&#x27;t need to leave the home row&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;And for something you remember running but it was far enough back that you&#x27;d need to hit the up arrow many times to find it, there is &lt;code&gt;CTRL-r&lt;&#x2F;code&gt; for reverse interactive searching your history. Type &lt;code&gt;CTRL-r&lt;&#x2F;code&gt; then any portion of the command you remember and the shell will search backward through your history for a match. Keep typing to find the exact match and hit &lt;code&gt;ENTER&lt;&#x2F;code&gt; when you see it to re-run that command. If you need to edit it before running again to modify it slightly, use the arrow keys to move your cursor and the line will be loaded as your current command but not yet executed.&lt;&#x2F;p&gt;
&lt;p&gt;For example:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;cd &amp;#x2F;tmp
sudo restart nginx
ls &amp;#x2F;var&amp;#x2F;log
vi &amp;#x2F;tmp&amp;#x2F;some&amp;#x2F;file.txt
&amp;lt;CTRL-r&amp;gt;restart&amp;lt;ENTER&amp;gt; #&amp;lt;- quickly re-run the sudo restart nginx command
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;line-editing-keyboard-shortcuts&quot;&gt;line editing keyboard shortcuts&lt;&#x2F;h3&gt;
&lt;p&gt;Second only to re-typing previous commands in terms of obvious beginner indicators would be backspacing over most of a command to make an edit to the beginning of the command, then re-typing everything you deleted.&lt;&#x2F;p&gt;
&lt;p&gt;Instead, use the shell&#x27;s interactive line editing features. These are keyboard shortcuts taken from the 2 most popular command-line text editors: vi and emacs. Most shells support both of these. By default the bash shell loads the emacs keybindings. You can switch to vi bindings with &lt;code&gt;set -o vi&lt;&#x2F;code&gt; and back to emacs with &lt;code&gt;set -o emacs&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;The most important ones for emacs are &lt;code&gt;CTRL-a&lt;&#x2F;code&gt; to move the cursor to the beginning of the line and &lt;code&gt;CTRL-e&lt;&#x2F;code&gt; to go to the end.&lt;&#x2F;p&gt;
&lt;p&gt;If you use &lt;code&gt;vi&lt;&#x2F;code&gt; for text editing, you&#x27;ll love having all your favorite move&#x2F;edit commands available.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;don-t-cd-for-one-off-commands&quot;&gt;don&#x27;t cd for one-off commands&lt;&#x2F;h3&gt;
&lt;p&gt;Another common pattern I see with beginners at the command line is a tendency to &lt;code&gt;cd&lt;&#x2F;code&gt; to a new directory for every command. Think of &lt;code&gt;cd&lt;&#x2F;code&gt; like taking off your coat and staying a while. If you are just running a single command dealing with a different directory, there&#x27;s no need to &lt;code&gt;cd&lt;&#x2F;code&gt; to that directory first. &lt;code&gt;cd&lt;&#x2F;code&gt; when you are going to stay in a single directory and run a bunch of commands for a while. This will keep your paths short since you can use relative paths and tab completion. But if you just want to create a directory in &lt;code&gt;&#x2F;tmp&lt;&#x2F;code&gt;, just run &lt;code&gt;mkdir &#x2F;tmp&#x2F;foo&lt;&#x2F;code&gt; without &lt;code&gt;cd &#x2F;tmp&lt;&#x2F;code&gt; first.&lt;&#x2F;p&gt;
&lt;p&gt;In particular, avoid the tendency to &lt;code&gt;cd&lt;&#x2F;code&gt; every time you use the &lt;code&gt;ls&lt;&#x2F;code&gt; command. It usually ends up causing you extra work to &lt;code&gt;cd&lt;&#x2F;code&gt; back to your project directory. &lt;code&gt;ls&lt;&#x2F;code&gt; can take the names of directories&#x2F;files to list as command line arguments so instead of&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;cd &amp;#x2F;var&amp;#x2F;log
ls
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;just run&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;ls &amp;#x2F;var&amp;#x2F;log
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;understand-space-delimited-args&quot;&gt;understand space-delimited args&lt;&#x2F;h3&gt;
&lt;p&gt;&quot;Do I need to quote it?&quot; is something beginners often ask. You may have heard that all the complex edge cases of bash quoting rules are complex and tricky, and that is indeed true. Here be dragons. However, for the basic cases, it&#x27;s really simple. By default the command you enter is separated into distinct values on spaces. So if you enter &lt;code&gt;ls &#x2F;tmp &#x2F;home&#x2F;me &#x2F;var&#x2F;log&lt;&#x2F;code&gt; the shell is going to parse that as 4 distinct tokens with &lt;code&gt;ls&lt;&#x2F;code&gt; being the program to run and each of the 3 directory paths as the arguments. So what that means is &lt;strong&gt;when a value you want to pass to a program contains a space, you need to quote it so the shell knows it is one value not several&lt;&#x2F;strong&gt;. So if I have a file path with a space in it like &lt;code&gt;&#x2F;tmp&#x2F;uprade notes.txt&lt;&#x2F;code&gt; and I want to pass that to the &lt;code&gt;wc&lt;&#x2F;code&gt; program, I need to type &lt;code&gt;wc &quot;&#x2F;tmp&#x2F;upgrade notes.txt&quot;&lt;&#x2F;code&gt; so the &lt;code&gt;wc&lt;&#x2F;code&gt; program gets just 1 argument that is a valid filesystem path instead of 2 arguments: &lt;code&gt;&#x2F;tmp&#x2F;upgrade&lt;&#x2F;code&gt; and &lt;code&gt;notes.txt&lt;&#x2F;code&gt;, neither of which are valid. This rule applies to the shell entirely, and it doesn&#x27;t matter which program is at the start of your command line command, the shell does the parsing before passing the values as arguments to the program being executed.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;part-two-breaking-boards&quot;&gt;Part Two: Breaking Boards&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;l7.alamy.com&#x2F;zooms&#x2F;94bc81f5d797471ebc5790c8ee4d90e7&#x2F;female-breaking-boards-with-bare-fist-in-a-karate-demonstration-an0nnm.jpg&quot; alt=&quot;Breaking Boards&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Now that you&#x27;ve got the basics, let&#x27;s add some intermediate skills and understanding that will make you more effective.&lt;&#x2F;p&gt;
&lt;p&gt;Let&#x27;s start with 2 issues that are a constant nuisance to beginners but once properly understood, immediately solvable every time for the intermediate developer.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;no-such-file-or-directory&quot;&gt;no such file or directory&lt;&#x2F;h3&gt;
&lt;p&gt;One of the core subsystem of every computer is a hierarchical data structure called the filesystem. Overall, it&#x27;s pretty great. It&#x27;s extremely general purpose and versatile while being pretty straightforward. However, it is pretty easy to get confused about filesystem paths and inadvertently ask the computer to read a file path that does not actually exist. Sadly, the common pattern for this is the bad path gets passed in from a high level application and the actual error doesn&#x27;t occur until deep in the core of the OS where no context exists about who or what wants to read this file and for what purpose. So at the command line, the error message can often be entirely unhelpful &quot;No such file or directory&quot; with an unfamiliar path you&#x27;ve never heard of. This can be quite confusing and frustrating.&lt;&#x2F;p&gt;
&lt;p&gt;However, once you understand the basics of the filesystem including importantly absolute paths, relative paths, &lt;code&gt;.&#x2F;&lt;&#x2F;code&gt;, &lt;code&gt;..&#x2F;&lt;&#x2F;code&gt;, symlinks, and the per-process current working directory, these errors suddenly all become trivial to solve.&lt;&#x2F;p&gt;
&lt;p&gt;It&#x27;s worth spending a few hours mastering this stuff so you can handle it properly once and for all.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;learn-how-command-lines-are-verbalized&quot;&gt;learn how command lines are verbalized&lt;&#x2F;h3&gt;
&lt;p&gt;When discussing things in person, on the phone, or over video chat, it helps to have a common understanding of how command line commands are verbalized so your coworker doesn&#x27;t have to give you keystroke-by-keystroke instructions like your keyboard is the control panel of a Boeing 787.&lt;&#x2F;p&gt;
&lt;p&gt;Pair up with someone and practice speaking command lines to each other and learn to parse what is a command name, which is a command line option, and how some of these odd unix&#x2F;C abbreviations are pronounced.&lt;&#x2F;p&gt;
&lt;p&gt;Here&#x27;s a few examples to get you started:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&quot;make dir dash P temp foo&quot; = &lt;code&gt;mkdir -p &#x2F;tmp&#x2F;foo&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&quot;L S  temp foo&quot; = &lt;code&gt;ls &#x2F;tmp&#x2F;foo&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&quot;vee eye etsy profile&quot; = &lt;code&gt;vi &#x2F;etc&#x2F;profile&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&quot;find dot dash name star dot text pipe to W C dash L&quot; = &lt;code&gt;find . -name &#x27;*.txt&#x27; | wc -l&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;part-three-enter-the-dragon&quot;&gt;Part Three: Enter the Dragon&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;latimesblogs.latimes.com&#x2F;.a&#x2F;6a00d8341c630a53ef0168e87fb4c5970c-600wi&quot; alt=&quot;Enter the Dragon&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Here are a few more advanced techniques.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;learn-when-you-don-t-need-sudo&quot;&gt;learn when you don&#x27;t need sudo&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;sudo&lt;&#x2F;code&gt; is a command that comes with some legacy baggage about what it is, why it is needed, and when to use it. I commonly see beginners try it at random when things don&#x27;t work. Don&#x27;t do this. &lt;code&gt;sudo&lt;&#x2F;code&gt; let&#x27;s you run an individual command with full root permissions. You need it when doing sysadmin type things like installing software system-wide, applying security patches, editing core OS configuration files, starting and stopping system services etc. You should NOT need it for doing normal user things like running read-only commands, managing files within your home directory, running programs that are already installed, etc. Read through an &lt;a href=&quot;https:&#x2F;&#x2F;www.linux.com&#x2F;learn&#x2F;tutorials&#x2F;306766:linux-101-introduction-to-sudo&quot;&gt;introduction to sudo&lt;&#x2F;a&gt; and get clear on excatly when it is or is not necessary.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;osx-pbpaste-pbcopy&quot;&gt;OSX pbpaste&#x2F;pbcopy&lt;&#x2F;h3&gt;
&lt;p&gt;If you work on OSX for development, the &lt;code&gt;pbpaste&lt;&#x2F;code&gt; and &lt;code&gt;pbcopy&lt;&#x2F;code&gt; commands are convenient bridges from the main OS clipboard (&quot;pasteboard&quot; is what the &quot;pb&quot; stands for) and the command line. For example, if you copy a bunch of text in your editor and want to see how many characters there are, you can run &lt;code&gt;pbpaste | wc -c&lt;&#x2F;code&gt; on the command line. Similarly, if you want to copy a program&#x27;s output you can pipe data into &lt;code&gt;pbcopy&lt;&#x2F;code&gt;. For example, I might run something like &lt;code&gt;npm run lint | pbcopy&lt;&#x2F;code&gt; if I know that is going to print out a bunch of errors that I want to work through one at a time. I&#x27;ll then paste it into an empty buffer in my text editor so I can fix each item then delete it&#x27;s line from my editor buffer until I&#x27;m done.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;use-short-lived-functions-and-aliases&quot;&gt;use short-lived functions and aliases&lt;&#x2F;h3&gt;
&lt;p&gt;Many advanced command line users learn to effectively define aliases and shell functions in their dotfiles to improve their productivity. This is a great idea and a programmer&#x27;s dotfiles can serve as an essential tool as well as a source of pride and the occasional geek contest about whose dotfiles are more sophisticated&#x2F;ridiculous.&lt;&#x2F;p&gt;
&lt;p&gt;However, one practice I see less often but is also really useful is defining quick aliases or functions just for the current session. I often do this if I know I&#x27;ll need to run a series of commands over and over in a sequence like edit some code, stop a service, copy new code into place, restart a service. I might quickly define an alias for all those steps like &lt;code&gt;alias r=&quot;cp .&#x2F;src &#x2F;deploy &amp;amp;&amp;amp; stop myapp &amp;amp;&amp;amp; start myapp&quot;&lt;&#x2F;code&gt;. This lets me focus on a rapid turnaround in my edit&#x2F;test cycle and makes me more effective.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;share-your-dotfiles-on-github&quot;&gt;share your dotfiles on github&lt;&#x2F;h3&gt;
&lt;p&gt;The conventional name for the repo where your dotfiles live on github is just &quot;dotfiles&quot; and by searching around you can find and study how others use the shell. It feels a little bit like getting a tour of someone&#x27;s workshop and you can readily see people&#x27;s styles and preferences. Perhaps the most famous repo is &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;mathiasbynens&#x2F;dotfiles&quot;&gt;Mathias Bynens dotfiles&lt;&#x2F;a&gt; with over 11K stars on github.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;use-long-options-in-shell-scripts-for-readability&quot;&gt;use long options in shell scripts for readability&lt;&#x2F;h3&gt;
&lt;p&gt;When typing interactive commands, brevity is good for speed and convenience. However, when you are writing a script that is getting checked into source code management as part of a project, consider using the long options. This will make it easier to read the script and understand what it does for the maintainers, who are unlikely to be as intimately familiar with every command your script runs as you were when you wrote it.&lt;&#x2F;p&gt;
&lt;p&gt;So instead of:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;curl -b &amp;#x27;active=1&amp;#x27; -D &amp;#x2F;tmp&amp;#x2F;foo \
  -e http:&amp;#x2F;&amp;#x2F;example.com -O http:&amp;#x2F;&amp;#x2F;example.com
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Code it as:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;curl \
  --cookie &amp;#x27;active=1&amp;#x27; \
  --dump-header &amp;#x2F;tmp&amp;#x2F;headers.txt \
  --referer http:&amp;#x2F;&amp;#x2F;example.com \
  --remote-name \
  http:&amp;#x2F;&amp;#x2F;example.com
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The longer names will tend to be easier to understand without referring to the documentation. The backslash-continued lines also help with readability.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;references-and-further-reading&quot;&gt;references and further reading&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;My &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;focusaurus&#x2F;dotfiles&quot;&gt;dotfiles on github&lt;&#x2F;a&gt;. Feel free to study and mine for snippets.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;tpope&#x2F;tpope&quot;&gt;Tim Pope&#x27;s dotfiles&lt;&#x2F;a&gt;. Maybe yours would look like this if you had written a hundred vim plugins&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;search?o=desc&amp;amp;q=dotfiles&amp;amp;s=stars&amp;amp;type=Repositories&amp;amp;utf8=%E2%9C%93&quot;&gt;Github dotfiles repos sorted by most stars&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;http:&#x2F;&#x2F;www.catonmat.net&#x2F;download&#x2F;readline-emacs-editing-mode-cheat-sheet.pdf&quot;&gt;Bash Emacs Editing Mode Cheat Sheet&lt;&#x2F;a&gt; by Peteris Krumins who writes &lt;a href=&quot;http:&#x2F;&#x2F;www.catonmat.net&#x2F;&quot;&gt;an outstanding blog with incredible unix expertise&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;http:&#x2F;&#x2F;redsymbol.net&#x2F;articles&#x2F;unofficial-bash-strict-mode&#x2F;&quot;&gt;The Unofficial Bash Strict Mode&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;quickleft.com&#x2F;blog&#x2F;tag&#x2F;command-line&#x2F;&quot;&gt;Jessica Dillon&#x27;s great command line tutorial series&lt;&#x2F;a&gt; on the Quick Left Blog&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;http:&#x2F;&#x2F;www.shellcheck.net&#x2F;&quot;&gt;Shell Check&lt;&#x2F;a&gt; shell script linter&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;sstephenson&#x2F;bats&quot;&gt;Bash Automated Testing System&lt;&#x2F;a&gt;
by Sam Stephenson&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;robbyrussell&#x2F;oh-my-zsh&quot;&gt;Oh My Zsh&lt;&#x2F;a&gt; Popular plugin system for customizing zsh&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</description>
      </item>
      <item>
          <title>Terrible Customer Support from Chase</title>
          <pubDate>Wed, 25 Nov 2015 23:49:10 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2015/11/terrible-customer-support-from-chase/</link>
          <guid>https://peterlyons.com/problog/2015/11/terrible-customer-support-from-chase/</guid>
          <description xml:base="https://peterlyons.com/problog/2015/11/terrible-customer-support-from-chase/">&lt;p&gt;So the Chase mobile banking app does not work properly on my new Google Nexus 5X phone. When taking a photo to deposit a check, the on-screen preview is rotated 180 degrees. This makes it really awkward to try to center the check because when you move the phone intuitively to fix alignment, it gets worse not better.&lt;&#x2F;p&gt;
&lt;p&gt;I googled this and found it&#x27;s a known problem due to some technical details about this particular phone and it requires the chase mobile app developers to code a fix and release an update via the google play store.&lt;&#x2F;p&gt;
&lt;p&gt;Here&#x27;s a &lt;a href=&quot;https:&#x2F;&#x2F;www.reddit.com&#x2F;r&#x2F;Android&#x2F;comments&#x2F;3rjbo8&#x2F;nexus5x_marshmallow_camera_problem&#x2F;&quot;&gt;reddit thread&lt;&#x2F;a&gt; where the Tech Lead for the Android Camera Framework confirms it.&lt;&#x2F;p&gt;
&lt;p&gt;Here&#x27;s &lt;a href=&quot;https:&#x2F;&#x2F;www.reddit.com&#x2F;r&#x2F;nexus5x&#x2F;comments&#x2F;3q891a&#x2F;camera_apps_that_arent_inverted&#x2F;&quot;&gt;another reddit thread&lt;&#x2F;a&gt; where 2 other users confirm the problem in the Chase app specifically.&lt;&#x2F;p&gt;
&lt;p&gt;Here&#x27;s &lt;a href=&quot;http:&#x2F;&#x2F;forums.androidcentral.com&#x2F;nexus-5x&#x2F;598233-bug-nexus-5x-camera-upside-down-apps.html&quot;&gt;an android central thread&lt;&#x2F;a&gt; on this topic.&lt;&#x2F;p&gt;
&lt;p&gt;So I tried to tell Chase about it. It went like this:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Sent them a message via their &quot;Secure Messaging Center&quot; within their banking web site
&lt;ul&gt;
&lt;li&gt;They came back saying &quot;we checked, it&#x27;s not an actual problem. Fuck off.&quot;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;I sent them feedback via the google play store which amounts basically to an email
&lt;ul&gt;
&lt;li&gt;They responded &quot;psych! Yeah we can&#x27;t actually communicate over these messages, sent us a message via our online banking web site&quot;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;When I pressed again in their online banking message system that yes this is a real issue, here are the links they said &quot;Oh fuck off we can&#x27;t actually do tech support here, call this telephone number, I&#x27;m sure it will be fun to speak web URLs to them letter-by-letter&quot;&lt;&#x2F;li&gt;
&lt;li&gt;So I called them and spoke with tech support who thought I was saying a particular photograph was upside down for a while. When I finally explained this is a bug in their app to his satisfaction, he started to tell me to call some other number...&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Then I got profane, which I almost never do, but I said something along the lines of &quot;No. Fuck that. I&#x27;ve already sent you emails in 2 different systems and called one telephone number. I&#x27;m not sending smoke signals. I&#x27;m not telegraphing anyone. You fucking record this bug in your system and communicate it internally to the right people on my behalf&quot;. The profanity seemed to actually prompt helpfulness, which is a sad truth, but eventually I found a reddit shortlink and spoke it to him over the phone. Hopefully they&#x27;ll fix their app.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Announcing White Glove</title>
          <pubDate>Mon, 16 Nov 2015 15:20:59 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2015/11/announcing-white-glove/</link>
          <guid>https://peterlyons.com/problog/2015/11/announcing-white-glove/</guid>
          <description xml:base="https://peterlyons.com/problog/2015/11/announcing-white-glove/">&lt;p&gt;I&#x27;ve open-sourced my mongodb&#x2F;couchdb data integrity analysis tool called white glove. You can find it at &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;focusaurus&#x2F;white-glove&quot;&gt;focusaurus&#x2F;white-glove&lt;&#x2F;a&gt; on github and &lt;a href=&quot;https:&#x2F;&#x2F;npmjs.com&#x2F;package&#x2F;white-glove&quot;&gt;white-glove on npm&lt;&#x2F;a&gt;. It helps with both schema discovery of a new or unfamiliar database as well as scanning your data for inconsistencies that may help you prevent problems before they cause any impact to end users.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Project Fi and Nexus 5X report</title>
          <pubDate>Wed, 11 Nov 2015 00:30:08 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2015/11/project-fi-and-nexus-5x-report/</link>
          <guid>https://peterlyons.com/problog/2015/11/project-fi-and-nexus-5x-report/</guid>
          <description xml:base="https://peterlyons.com/problog/2015/11/project-fi-and-nexus-5x-report/">&lt;p&gt;So after having a Google Nexus 5 smartphone for the last 2+ years, when I saw that &lt;a href=&quot;https:&#x2F;&#x2F;fi.google.com&#x2F;about&#x2F;&quot;&gt;Project Fi&lt;&#x2F;a&gt; had the potential to give me better coverage and a higher data limit for lower cost than T-Mobile, I decided to go for it.&lt;&#x2F;p&gt;
&lt;p&gt;Here&#x27;s my notes on both the Project Fi service and the &lt;a href=&quot;https:&#x2F;&#x2F;www.google.com&#x2F;nexus&#x2F;5x&#x2F;&quot;&gt;Google Nexus 5X&lt;&#x2F;a&gt; smartphone.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Project Fi
&lt;ul&gt;
&lt;li&gt;Value: great
&lt;ul&gt;
&lt;li&gt;cheap data rates and my modest usage means I have basically all the data, TXT, and voice I need for $40&#x2F;month&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;Coverage: better
&lt;ul&gt;
&lt;li&gt;Coming from T-Mobile I have the coverage I used to have plus access to Sprint and Wi-Fi calling so it&#x27;s a no-brainer improvement on this front. Weirdly the heart of the downtown business district in my home town is a T-Mobile dead zone.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;Transition: flawless&lt;&#x2F;li&gt;
&lt;li&gt;All the cell number porting stuff and account stuff went smooth as butter&lt;&#x2F;li&gt;
&lt;li&gt;Overall it&#x27;s really good service with no long-term contract, an unlocked phone, and a low monthly bill. Really pleased with this.&lt;&#x2F;li&gt;
&lt;li&gt;There are some annoying limitations of Google Apps accounts vs regular gmail accounts. I use my main Google Apps accounts for most things, but they don&#x27;t work with Google Play Store or Google Voice, so I need to use an old gmail account for that, which is annoying and confusing.&lt;&#x2F;li&gt;
&lt;li&gt;Having voicemail just be there without having to dial a number and use an IVR touchpad is really nice. Amazing it took so long to reach this point.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;Nexus 5X
&lt;ul&gt;
&lt;li&gt;battery charges much faster and lasts much longer than Nexus 5. It will probably take me a while to adjust the chronic battery paranoia I learned using my Nexus 5 which could do ~6 hours at most.&lt;&#x2F;li&gt;
&lt;li&gt;Camera is indeed better&lt;&#x2F;li&gt;
&lt;li&gt;Fingerprint reader is nice&lt;&#x2F;li&gt;
&lt;li&gt;Larger storage capacity is welcome
&lt;ul&gt;
&lt;li&gt;Still kind of mind-blowing that my Creative Nomad Xen mp3 player circa 2006 had double the storage of my current smartphone a decade later.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;Performance is not noticeable better compared to my Nexus 5. Most things are still a bit laggy when they shouldn&#x27;t be, which the exception of the Calendar app which responds instantly and caches tons of data locally, which I wish every other app would do. It&#x27;s disgusting that Google Inbox does not automatically sync my most recent 1GB of email at all times. I can use Here Maps and download a detailed map of the entire state of Colorado for offline use but getting the email with my flight details from last much while offline - not so much.&lt;&#x2F;li&gt;
&lt;li&gt;The USB type C charger connection is a bit of a hassle as you are extremely likely to buy the wrong thing when shopping for chargers and cables on amazon at the moment.
&lt;ul&gt;
&lt;li&gt;Let &lt;a href=&quot;http:&#x2F;&#x2F;www.amazon.com&#x2F;gp&#x2F;pdp&#x2F;profile&#x2F;A25GROL6KJV3QG&#x2F;ref=cm_cr_rdp_pdp&quot;&gt;Google Engineer Benson Leung&lt;&#x2F;a&gt;&#x27;s amazon reviews be your guide.&lt;&#x2F;li&gt;
&lt;li&gt;However, if you do indeed find the right thing, it&#x27;s nice to see on the screen &quot;Charging rapidly&quot; as a confirmation.&lt;&#x2F;li&gt;
&lt;li&gt;I caught the dramatic price drop of the Nexus 5 after the initial batch last time around, but this time with the 5X I didn&#x27;t wait. Less than a week after my order, google dropped the price significantly.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;Android Marshmallow
&lt;ul&gt;
&lt;li&gt;Overall a lot better&lt;&#x2F;li&gt;
&lt;li&gt;Transition was a bit annoying as google does not transfer any app settings or OS settings other than your google accounts themselves. Had to go through app by app and adjust notifications and preferences&lt;&#x2F;li&gt;
&lt;li&gt;Some fixes when connecting via bluetooth to my car stereo&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;So without too much time&#x2F;effort configuring stuff, my smartphone is now in good working order and I have everything set up how I like and don&#x27;t have to futz with it. Overall within the apps themselves I&#x27;m still feeling constantly frustrated by current UI trends of stacking stuff on top of each other, animations, and moving&#x2F;disappearing tap targets.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Tools for Cleaning Up Messy JavaScript</title>
          <pubDate>Thu, 22 Oct 2015 16:38:52 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2015/10/tools-for-cleaning-up-messy-javascript/</link>
          <guid>https://peterlyons.com/problog/2015/10/tools-for-cleaning-up-messy-javascript/</guid>
          <description xml:base="https://peterlyons.com/problog/2015/10/tools-for-cleaning-up-messy-javascript/">&lt;p&gt;In my consulting work, I encounter a pretty high quantity and diversity of codebases as client projects flow in and out. For many reasons, it is very common for a project to get increasingly messy with time, and often by the time it drops in my lap, it&#x27;s a certifiable mess.&lt;&#x2F;p&gt;
&lt;p&gt;Dealing with a mess can be a huge drag and productivity drain as even the simplest debugging investigation is rife with confusion, duplication, misdirection, etc. Here&#x27;s a few quick tools tips to help get things organized and clean again.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;esformatter&quot;&gt;esformatter&lt;&#x2F;h2&gt;
&lt;p&gt;If the overall code style is highly inconsistent or hard to read, it might make sense to hit it with a giant &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;millermedeiros&#x2F;esformatter&quot;&gt;esformatter&lt;&#x2F;a&gt; hammer and just force everything into a single style. The go programming language does this language-wide via &lt;a href=&quot;https:&#x2F;&#x2F;golang.org&#x2F;cmd&#x2F;gofmt&#x2F;&quot;&gt;gofmt&lt;&#x2F;a&gt;. Keep these tips in mind:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;javascript is interpreted not compiled so if auto-formatting goes wrong and you don&#x27;t have enough code coverage in unit tests, you might not discover problems until production
&lt;ul&gt;
&lt;li&gt;minimize this risk by autoformatting in small batches and making sure any tests you have continue to pass&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;esformatter and similar tools are still pretty young and could have bugs that change your code in bad ways
&lt;ul&gt;
&lt;li&gt;always carefully inspect the diffs before committing automatic changes&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;Do the formatting in small batches and group changes into a large number of small git commits
&lt;ul&gt;
&lt;li&gt;after every change, confirm via unit tests if you have them and eslint that the code is still not obviously broken&lt;&#x2F;li&gt;
&lt;li&gt;keeping the git commits granular will enable you to more effectively bisect the code to track down a specific problem introduced by one of these changes&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;Do all the work on a branch, and do no manual code changes of any type on this branch
&lt;ul&gt;
&lt;li&gt;For example, don&#x27;t mix some manual variable renames with automated esformatter changes&lt;&#x2F;li&gt;
&lt;li&gt;You want to be able to discard the branch entirely without losing and human-authored changes&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;Get the project done quickly
&lt;ul&gt;
&lt;li&gt;This is a complete recipe for merge conflicts and you&#x27;ll never get it done doing small bits at a time while active development is happening concurrently. If you need to, just declare a small moratorium on development for a weekend or whatever and get everything formatted and merged before new feature development continues&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;eslint&quot;&gt;eslint&lt;&#x2F;h2&gt;
&lt;p&gt;I &lt;a href=&quot;&#x2F;problog&#x2F;2015&#x2F;10&#x2F;eslint:-toward-javascript-lint-nirvana&quot;&gt;covered eslint in some detail in my previous post&lt;&#x2F;a&gt;. Once you&#x27;ve got the code reasonably formatted and consistent, throw eslint at it to get a sense of where bugs and issues may lie. Often times you may be getting hundreds or thousands of errors and warnings. I think focusing on either the most frequent errors (codebase improves the most by fixing these) or least frequent (fixing these can get you to OK on a specific eslint rule quickly) are reasonable approaches. The key thing here psychologically for me is to not get overwhelmed and frustrated and hopeless.&lt;&#x2F;p&gt;
&lt;p&gt;One nice plugin that can help with initial analysis, triaging, and scheduling is &lt;a href=&quot;https:&#x2F;&#x2F;www.npmjs.com&#x2F;package&#x2F;eslint-stats&quot;&gt;eslint-stats&lt;&#x2F;a&gt; which can make it easy to see which problems are most common.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;beautify-with-words&quot;&gt;beautify-with-words&lt;&#x2F;h2&gt;
&lt;p&gt;True story. A client&#x27;s codebase was uglified then autoformatted by a previous developer before delivery to the client as &quot;source code&quot;. Thus at a glance it looked like source code as it had newlines and indenting, but all the variable names had been minified to single letters. This made it extremely difficult to read. One tool that helped us gradually get back to sanity was &lt;a href=&quot;https:&#x2F;&#x2F;www.npmjs.com&#x2F;package&#x2F;beautify-with-words&quot;&gt;beautify with words&lt;&#x2F;a&gt; which finds all those 1-letter variable names and generates a longer, unique, pronounceable (but otherwise gibberish) variable name for them. After that you can easily find and replace all once you understand what an appropriate semantic name for the variable is.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;grasp&quot;&gt;grasp&lt;&#x2F;h2&gt;
&lt;p&gt;There&#x27;s a really cool utility called &lt;a href=&quot;http:&#x2F;&#x2F;www.graspjs.com&#x2F;&quot;&gt;grasp&lt;&#x2F;a&gt; that parses javascript into an abstract syntax tree and allows you to programmatically alter that tree, then generate new source code. Lots of potential for interesting uses here, but so far I&#x27;ve only really used it for the use case of syntax-aware search and replace. The problem with generic text editor search and replace for variables is it will also change that name within a string or a comment or embedded within another word, etc. The bottom line is careless find&#x2F;replace in a generic text editor can break your code. With grasp, you can tell it to replace just an identifier and it really knows what that means. For example, to rename a variable from &lt;code&gt;user&lt;&#x2F;code&gt; to &lt;code&gt;account&lt;&#x2F;code&gt;, we could do:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;grasp &amp;#x27;#user&amp;#x27; --replace account
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;My workflow with grasp is as follows.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;highlight a small but coherent and valid javascript snippet in my text editor
&lt;ul&gt;
&lt;li&gt;has to be valid JS to work with grasp. I usually grab an entire function declaration or conditional block&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;copy it into the clipboard&lt;&#x2F;li&gt;
&lt;li&gt;run grasp in the terminal:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pbpaste | grasp &#x27;#user&#x27; --replace account | pbcopy&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;this pastes the text into grasp&#x27;s stdin, and copies grasp&#x27;s stdout back into the clipboard&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;back in my editor just paste the results in, replacing the still-selected original snippet&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;keep-it-clean&quot;&gt;Keep It Clean&lt;&#x2F;h2&gt;
&lt;p&gt;Hopefully these tools will help you out in the wild cleaning up messy codebases!&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>eslint: toward javascript lint nirvana</title>
          <pubDate>Thu, 22 Oct 2015 15:42:56 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2015/10/eslint:-toward-javascript-lint-nirvana/</link>
          <guid>https://peterlyons.com/problog/2015/10/eslint:-toward-javascript-lint-nirvana/</guid>
          <description xml:base="https://peterlyons.com/problog/2015/10/eslint:-toward-javascript-lint-nirvana/">&lt;p&gt;The world of javascript code linters is a fertile breeding ground for vociferous debate over unimportant details. It&#x27;s easy to stray into the bad patterns of arguing with colleagues, playing &quot;curly brace police&quot;, and other variants. However, used well with the right mindset and team communication, good code linters and autoformatters can be truly valuable additions to your development workflow and tool chain.&lt;&#x2F;p&gt;
&lt;p&gt;If you haven&#x27;t yet discovered eslint, it&#x27;s my pleasure to refer you to &lt;a href=&quot;http:&#x2F;&#x2F;eslint.org&#x2F;docs&#x2F;about&#x2F;&quot;&gt;the eslint about page&lt;&#x2F;a&gt;. Go read the eslint philosophy. It&#x27;s the strictly correct thing. eslint is &quot;agenda free&quot; and 100% configurable. Earlier tools starting from the grandparent of Douglas Crockford&#x27;s jslint, then jshint veered off the rails there and eslint is here to take over.&lt;&#x2F;p&gt;
&lt;p&gt;So getting started with eslint can be as easy as:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;npm install --save-dev eslint
echo node_modules &amp;gt;&amp;gt; .eslintignore
PATH=$(pwd)&amp;#x2F;node_modules&amp;#x2F;.bin:$PATH
eslint .
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The eslint rules have pretty good, clear names. The docs are solid. I wrote my own custom formatter called &lt;a href=&quot;https:&#x2F;&#x2F;www.npmjs.com&#x2F;package&#x2F;eslint-formatter-comment&quot;&gt;eslint-formatter-comment&lt;&#x2F;a&gt; which I use for text editor friendliness and rule-disabling convenience.&lt;&#x2F;p&gt;
&lt;p&gt;If you want to role with some of the more prolific npm authors, you can decide to start with the &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;feross&#x2F;standard&quot;&gt;@feross&#x2F;standard&lt;&#x2F;a&gt; configuration by doing &lt;code&gt;npm install --save-dev eslint-config-standard&lt;&#x2F;code&gt; then adding this to your &lt;code&gt;.eslintrc&lt;&#x2F;code&gt; JSON file:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;{
  &amp;quot;extends&amp;quot;: [
    &amp;quot;.&amp;#x2F;node_modules&amp;#x2F;eslint-config-standard&amp;#x2F;eslintrc.json&amp;quot;
  ]
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;formatting-with-esformatter&quot;&gt;Formatting With esformatter&lt;&#x2F;h2&gt;
&lt;p&gt;OK great so eslint has found hundreds of issues with your code. What do you do? Manually fixing them can be very time consuming, tedious, and error prone. eslint now has the &lt;code&gt;--fix&lt;&#x2F;code&gt; command and can automatically fix up your source code for some (but not all) rules. I haven&#x27;t tried it out yet as when I first started working with eslint that feature didn&#x27;t exist. But you should give it a try and hopefully its capabilities will continue to expand with time. However, there&#x27;s another project &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;millermedeiros&#x2F;esformatter&quot;&gt;esformatter&lt;&#x2F;a&gt; that can do most code style fixes (indentation, braces, spacing, semicolons, etc) automatically.&lt;&#x2F;p&gt;
&lt;p&gt;When I found esformatter, tried it, and found it to be by-and-large correct and reliable, it was a godsend. I work on so many different client projects as well as some nonprofit codebases where most developers only contribute on a handful of days. This leads to highly inconsistent codebases in terms of format&#x2F;style. To be able to just define a &lt;code&gt;.esformatter&lt;&#x2F;code&gt; config and fix up the entire codebase in one fell swoop is really awesome and can be a big morale boost as well. esformatter does have some bugs, but so far they are minor and easy to work around in my experience.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;how-i-roll-with-eslint&quot;&gt;How I Roll With eslint&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;install the stack
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;npm install --save-dev eslint eslint-config-standard eslint-plugin-standard eslint-formatter-comment&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;set up my &lt;code&gt;.eslintrc&lt;&#x2F;code&gt; with the &lt;code&gt;extends&lt;&#x2F;code&gt; as above&lt;&#x2F;li&gt;
&lt;li&gt;set up my &lt;code&gt;.eslintignore&lt;&#x2F;code&gt; with at least &lt;code&gt;node_modules&lt;&#x2F;code&gt; and whatever other project-specific paths I need to ignore&lt;&#x2F;li&gt;
&lt;li&gt;start running it via just &lt;code&gt;eslint .&lt;&#x2F;code&gt; or a small wrapper script that uses my custom output format&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;atom-text-editor-integration&quot;&gt;Atom Text Editor Integration&lt;&#x2F;h2&gt;
&lt;p&gt;I have the &lt;code&gt;linter&lt;&#x2F;code&gt; and &lt;code&gt;linter-eslint&lt;&#x2F;code&gt; atom plugins installed for good integration with Atom. I can directly spot errors in my code without being visually distracting or overwhelming.&lt;&#x2F;p&gt;
&lt;p&gt;For esformatter I have the Atom &lt;code&gt;esformatter&lt;&#x2F;code&gt; plugin and a ctrl-f keystroke defined:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x27;.editor&amp;#x27;:
  &amp;#x27;ctrl-. e r c&amp;#x27;: &amp;#x27;eslint:reload-config&amp;#x27;
  &amp;#x27;ctrl-. l i&amp;#x27;: &amp;#x27;eslint:lint&amp;#x27;
  &amp;#x27;ctrl-f&amp;#x27;: &amp;#x27;esformatter&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;thoughts-on-feross-standard&quot;&gt;Thoughts on feross&#x2F;standard&lt;&#x2F;h2&gt;
&lt;p&gt;@feross&#x2F;standard is a bit controversial, but the specific choices it puts forward seem good enough to me, and given there are good tools to just automatically format code to that style, I&#x27;m OK with just embracing it. I&#x27;ve got 3 medium-sized projects all using it as the baseline and it&#x27;s OK. I make it even stricter by adding a few more rules, most notably I&#x27;m still strongly in the camp of 80-char max line length, so I enable that rule as well. I certainly would have made some different choices, in particular I think double quotes are much better because JSON, but now that I can just hit &lt;code&gt;ctrl-f&lt;&#x2F;code&gt; and have the correct quotes, it&#x27;s OK.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;why-do-you-even-care&quot;&gt;Why Do You Even Care?&lt;&#x2F;h2&gt;
&lt;p&gt;I have had colleagues chastise me for being picky about code format, saying it&#x27;s a matter of personal choice and not worthwhile to point out or fix. For me, I find consistent style to be a marker of being detail-oriented and paying close attention. If a developer is sloppy or inconsistent with their code formatting, I&#x27;m immediately concerned that maybe they aren&#x27;t paying careful attention to the actual behavior and semantics of their code either. It&#x27;s so easy to let unused variables, shadowing, and other common errors slip into your code over time, and automated linting provides an easy fix.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;toward-nirvana&quot;&gt;Toward Nirvana&lt;&#x2F;h2&gt;
&lt;p&gt;Ideally, I&#x27;d want to see a single tool like eslint be able to handle all these tasks:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;show errors on the command line&lt;&#x2F;li&gt;
&lt;li&gt;show errors in the editor&lt;&#x2F;li&gt;
&lt;li&gt;automatically fix files in place on the command line&lt;&#x2F;li&gt;
&lt;li&gt;automatically fix files in place in the editor&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;eslint seems headed clearly in this direction, but it&#x27;s not there yet. I would love to see eslint completely subsume esformatter and make it obsolete.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Eliminate Useless Callback Wrappers in JavaScript</title>
          <pubDate>Thu, 03 Sep 2015 04:21:37 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2015/09/eliminate-useless-callback-wrappers-in-javascript/</link>
          <guid>https://peterlyons.com/problog/2015/09/eliminate-useless-callback-wrappers-in-javascript/</guid>
          <description xml:base="https://peterlyons.com/problog/2015/09/eliminate-useless-callback-wrappers-in-javascript/">&lt;p&gt;Just a quick tip from some code I encountered today. Instead of this:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;function outerFunc (callback) {
  doSomething(42, function (err) {
    callback(err)
  })
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Eliminate the useless callback wrapper function:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;function outerFunc (callback) {
  doSomething(42, callback)
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;It&#x27;s more concise and more efficient and entirely equivalent.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>The Virtuous Traitor</title>
          <pubDate>Mon, 29 Jun 2015 02:38:19 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2015/06/the-virtuous-traitor/</link>
          <guid>https://peterlyons.com/problog/2015/06/the-virtuous-traitor/</guid>
          <description xml:base="https://peterlyons.com/problog/2015/06/the-virtuous-traitor/">&lt;p&gt;I was just reading some articles and comment threads debating the issue of &lt;code&gt;git merge&lt;&#x2F;code&gt; verses &lt;code&gt;git rebase&lt;&#x2F;code&gt;. I think it&#x27;s a good case study for a decision-making methodology of actually trying both sides. I think outside of software development, the cost of trying both sides of a debate can be high in terms of time, effort, and money. Let&#x27;s say hypothetically there are several competing theories of long-term crop management for farmers. It would take many years, lots of technical know-how, and lots of dedication to split a large farm into portions, each serving as a guinea pig for a particular crop management technique. However, in software development, many of these techniques can easily be tried on some small&#x2F;fast basis like a side project, a particular feature, etc.&lt;&#x2F;p&gt;
&lt;p&gt;For example, my web site has a mix of content and features that make it debatable as to whether a real dynamic application is warranted or would a static site generator approach suffice. I bypassed a lot of debate by just running it both ways for a meaningful amount of time, and at the end it was clear to me which way I preferred.&lt;&#x2F;p&gt;
&lt;p&gt;When you see some of these debates, for example:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;git vs mercurial&lt;&#x2F;li&gt;
&lt;li&gt;merge vs rebase&lt;&#x2F;li&gt;
&lt;li&gt;functional vs OO&lt;&#x2F;li&gt;
&lt;li&gt;shell scripts vs configuration mgmt&lt;&#x2F;li&gt;
&lt;li&gt;use DB constraints heavily or lightly&lt;&#x2F;li&gt;
&lt;li&gt;language X vs language Y&lt;&#x2F;li&gt;
&lt;li&gt;tool X vs tool Y&lt;&#x2F;li&gt;
&lt;li&gt;Would I prefer consulting to full-time employment?&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;It&#x27;s good to think about how much time and effort would it take to actually give each option an honest, non-trivial try. Often I find it&#x27;s worth it and after the fact I have real first-hand knowledge to show for it. I&#x27;m particularly fond of first-hard knowledge&#x2F;experience and skeptical of opinions held based on reading&#x2F;hearing third-party opinions.&lt;&#x2F;p&gt;
&lt;p&gt;Here&#x27;s a few recent things I put through this methodology:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Is coffeescript worth using?&lt;&#x2F;li&gt;
&lt;li&gt;Are docker containers interesting and useful?&lt;&#x2F;li&gt;
&lt;li&gt;Is ansible better than my existing shell scripts?&lt;&#x2F;li&gt;
&lt;li&gt;Would Atom be any better than Sublime?&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Based on the cases made in the articles I was reading today, I plan to try a rebase-driven methodology on my next git project given I&#x27;ve mostly been doing merges and see how it goes.&lt;&#x2F;p&gt;
&lt;p&gt;Play for both teams. It&#x27;s often the best way to guide a decision. And don&#x27;t forget that after a while things change and sometimes we need to re-visit old preferences.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>12-Factor Apps in node</title>
          <pubDate>Wed, 27 May 2015 14:33:52 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2015/05/12-factor-apps-in-node/</link>
          <guid>https://peterlyons.com/problog/2015/05/12-factor-apps-in-node/</guid>
          <description xml:base="https://peterlyons.com/problog/2015/05/12-factor-apps-in-node/">&lt;p&gt;Here&#x27;s my talk for the &lt;a href=&quot;http:&#x2F;&#x2F;www.meetup.com&#x2F;Node-js-Denver-Boulder&#x2F;&quot;&gt;node.js Boulder meetup group&lt;&#x2F;a&gt; about 12-Factor apps in node.js. &lt;a href=&quot;&#x2F;twelve_factor_nodejs&quot;&gt;Slides are here&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;div class=&quot;youtube-video-container&quot;&gt;
&lt;iframe src=&quot;https:&#x2F;&#x2F;www.youtube-nocookie.com&#x2F;embed&#x2F;te5dA3xpgK0&quot; allowfullscreen&gt;&lt;&#x2F;iframe&gt;
&lt;&#x2F;div&gt;
</description>
      </item>
      <item>
          <title>OSX Development Setup</title>
          <pubDate>Fri, 06 Feb 2015 20:42:03 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2015/02/osx-development-setup/</link>
          <guid>https://peterlyons.com/problog/2015/02/osx-development-setup/</guid>
          <description xml:base="https://peterlyons.com/problog/2015/02/osx-development-setup/">&lt;p&gt;This post will describe my current setup for development on OS X.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;requirements&quot;&gt;Requirements&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;easy to maintain&lt;&#x2F;li&gt;
&lt;li&gt;easy to recreate&lt;&#x2F;li&gt;
&lt;li&gt;easy to work with during development&lt;&#x2F;li&gt;
&lt;li&gt;friendly to many projects&#x2F;clients with highly varied software stacks&lt;&#x2F;li&gt;
&lt;li&gt;don&#x27;t pollute my main OSX install too much&lt;&#x2F;li&gt;
&lt;li&gt;Don&#x27;t interfere with my core tools: text editor, local filesystem, git, command line terminal&lt;&#x2F;li&gt;
&lt;li&gt;allow offline work&lt;&#x2F;li&gt;
&lt;li&gt;don&#x27;t make me crazy with too many DHCP IPs, port mappings, virtual networks, etc&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;tools&quot;&gt;Tools&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.virtualbox.org&#x2F;&quot;&gt;VirtualBox&lt;&#x2F;a&gt; for running other OSes in VMs on top of OS X&lt;&#x2F;li&gt;
&lt;li&gt;Ubuntu as my preferred OS for linux based servers&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.docker.com&#x2F;&quot;&gt;Docker&lt;&#x2F;a&gt; for containerized apps&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.vagrantup.com&#x2F;&quot;&gt;Vagrant&lt;&#x2F;a&gt; to script creation of virtualbox VMs&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;http:&#x2F;&#x2F;boot2docker.io&#x2F;&quot;&gt;boot2docker&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;problog&#x2F;images&#x2F;2015&#x2F;osx-dev-setup.png&quot; alt=&quot;OSX Dev Setup&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;my-app-stacks&quot;&gt;My App Stacks&lt;&#x2F;h3&gt;
&lt;p&gt;I generally build web applications that involve a node.js based application server, one or more database servers, and an optional front-end web server (used in stage and production but not in development).&lt;&#x2F;p&gt;
&lt;h3 id=&quot;just-go-to-town-with-homebrew&quot;&gt;Just go to town with homebrew?&lt;&#x2F;h3&gt;
&lt;p&gt;The simplest thing would probably be to install all my tools and databases with homebrew straight into OS X. And I think actually that&#x27;s probably fine for a lot of people. I don&#x27;t do it for reasons that are admittedly a bit emotional&#x2F;OCD.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;I feel like getting too much stuff installed in my OS gradually pollutes it, destabilizes it, slows it down, and makes me feel doubt about it&lt;&#x2F;li&gt;
&lt;li&gt;Managing background daemons on OS X is awkward&lt;&#x2F;li&gt;
&lt;li&gt;Since I deploy to linux, I&#x27;d like to run and manage my DBs  on linux&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;virtualbox-for-dbs&quot;&gt;VirtualBox for DBs&lt;&#x2F;h3&gt;
&lt;p&gt;So for the above reasons, I use Vagrant to script a base Ubuntu VM I call &quot;dbs&quot;. I give this 2 network interfaces:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Adapter 1 is a NAT setup with a DHCP-assigned IP that gives the VM access to the Internet via my Mac&#x27;s Internet connection&lt;&#x2F;li&gt;
&lt;li&gt;Adapter 2 is a host-only network with a static IP so I can always access this host&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;I put the host name &quot;dbs&quot; with the host-only IP in my &lt;code&gt;&#x2F;etc&#x2F;hosts&lt;&#x2F;code&gt; file on my mac and I can access it via &lt;code&gt;ssh dbs&lt;&#x2F;code&gt;. I directly install via &lt;code&gt;apt-get install&lt;&#x2F;code&gt; all the DBs I want to use for local development and client work: postgresql, mysql, couchdb, elasticsearch, etc. I run these on the default port for simplicity and try not to do too much configuration. I do have to configure them to bind to the host-only IP address though, which also makes sure they are not reachable from anywhere other than my mac.&lt;&#x2F;p&gt;
&lt;p&gt;That gets me what I need for developing multiple client projects. After a reboot I have a shell alias to start this VM: &lt;code&gt;VBoxManage startvm dbs&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;docker-for-staging-containers&quot;&gt;Docker for staging containers&lt;&#x2F;h3&gt;
&lt;p&gt;I&#x27;ve started using docker a bit both for client work and one personal side project and so far I&#x27;m liking it. To allow me to do docker builds and deploy containers, I run docker inside the &quot;dbs&quot; VM as well.&lt;&#x2F;p&gt;
&lt;p&gt;When I created this setup &lt;a href=&quot;http:&#x2F;&#x2F;boot2docker.io&#x2F;&quot;&gt;boot2docker&lt;&#x2F;a&gt; was not quite ready for prime time, but if I had to start over, I might just use boot2docker and run everything as containers. But what I have is every bit as easy to manage for the moment, so I haven&#x27;t bothered to rebuild it.&lt;&#x2F;p&gt;
&lt;p&gt;So for docker work I just &lt;code&gt;export DOCKER_HOST=tcp:&#x2F;&#x2F;dbs:2375&lt;&#x2F;code&gt; and use the docker command from homebrew on my mac to do my docker work.&lt;&#x2F;p&gt;
&lt;p&gt;For my dockerized side project, I can run the full stack of docker containers exactly like I do in production all locally on my laptop, which provides an adequate staging environment such that I don&#x27;t feel a dedicated staging system in the cloud is necessary. I use the same automation scripts to automate docker stuff on my staging VM and for production I just point them at the production host which is a digital ocean droplet. So far it&#x27;s worked pretty well for me.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>setTimeout and Friends</title>
          <pubDate>Wed, 12 Mar 2014 21:12:31 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2014/03/settimeout-and-friends/</link>
          <guid>https://peterlyons.com/problog/2014/03/settimeout-and-friends/</guid>
          <description xml:base="https://peterlyons.com/problog/2014/03/settimeout-and-friends/">&lt;p&gt;So I was recently scheduled to do an &lt;a href=&quot;http:&#x2F;&#x2F;airpair.com&quot;&gt;AirPair&lt;&#x2F;a&gt; pair programming session to help someone understand the various javascript functions available for asynchronous programming.&lt;&#x2F;p&gt;
&lt;p&gt;This topic is key to writing correct code both in node.js and the browser, but it is indeed easy to see how things can be quite confusing when first encountering a myriad of functions that all seem to basically do the same thing.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;setTimeout&lt;&#x2F;code&gt;, &lt;code&gt;setImmediate&lt;&#x2F;code&gt;, &lt;code&gt;process.nextTick&lt;&#x2F;code&gt;? Which one is best? What are the differences?&lt;&#x2F;p&gt;
&lt;p&gt;The basic concept at work here is normally in javascript your code executes &quot;now&quot; meaning line by line, one thing after another, until there&#x27;s no more code. However, with a user interacting with the browser or with node.js code responding to HTTP requests, we need a way to ask the runtime to run some code &quot;later&quot;.  And of course using event handlers we already have a mechanism to ask the runtime to execute our code &quot;whenever X happens&quot; where X is a mouse click, keystroke, etc in the browser or a database call returning in node.js.&lt;&#x2F;p&gt;
&lt;p&gt;So I went and researched this and put together what I hope to be a mostly comprehensive overview of all of the relevant functions and some of the tricky points to be aware of. The good news is mostly this stuff is actually pretty straightforward once you get the basic handle of it, and there are just a few key subtle gotchas that take a little more practice to wrangle.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;settimeout-functiontocall-mstowait&quot;&gt;&lt;code&gt;setTimeout(functionToCall, msToWait)&lt;&#x2F;code&gt;&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;Read the &lt;a href=&quot;https:&#x2F;&#x2F;developer.mozilla.org&#x2F;en-US&#x2F;docs&#x2F;Web&#x2F;API&#x2F;Window.setTimeout&quot;&gt;window.setTimeout MDN docs&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;works in browsers and node.js&lt;&#x2F;li&gt;
&lt;li&gt;invoke the given function some time later, on a different tick of the event loop&lt;&#x2F;li&gt;
&lt;li&gt;returns an opaque value (numeric ID) that can be later passed to &lt;code&gt;clearTimeout&lt;&#x2F;code&gt; to cancel the scheduled call&lt;&#x2F;li&gt;
&lt;li&gt;subject to 4ms &quot;clamping&quot; in HTML5 spec (see MDN docs above) and this is even worse in old-IE. What this means if you write &lt;code&gt;setTimeout(makeCookies, 0)&lt;&#x2F;code&gt;, the browser will actually wait at least 4ms before invoking &lt;code&gt;makeCookies&lt;&#x2F;code&gt;. This use of &lt;code&gt;setTimeout&lt;&#x2F;code&gt; with zero delay is basically a hack since there is&#x2F;was not &lt;code&gt;setImmediate&lt;&#x2F;code&gt; and can be easily abused, so browsers (and the actual specs) have deemed to throttle things by a few ms as a pragmatic compromise.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;cleartimeout-timeoutid&quot;&gt;&lt;code&gt;clearTimeout(timeoutID)&lt;&#x2F;code&gt;&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;Read the &lt;a href=&quot;https:&#x2F;&#x2F;developer.mozilla.org&#x2F;en-US&#x2F;docs&#x2F;Web&#x2F;API&#x2F;window.clearTimeout&quot;&gt;window.clearTimeout MDN docs&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;works in browsers and node.js&lt;&#x2F;li&gt;
&lt;li&gt;cancel a previous call to &lt;code&gt;setTimout&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;straightforward&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;setinterval-functiontocall-intervalms&quot;&gt;&lt;code&gt;setInterval(functionToCall, intervalMs)&lt;&#x2F;code&gt;&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;Read the &lt;a href=&quot;https:&#x2F;&#x2F;developer.mozilla.org&#x2F;en-US&#x2F;docs&#x2F;Web&#x2F;API&#x2F;window.setInterval&quot;&gt;window.setInterval MDN docs&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;works in browsers and node.js&lt;&#x2F;li&gt;
&lt;li&gt;similar to &lt;code&gt;setTimeout&lt;&#x2F;code&gt;, just calls function repeatedly&lt;&#x2F;li&gt;
&lt;li&gt;inactive browser tabs may be throttled compared to active tabs&lt;&#x2F;li&gt;
&lt;li&gt;Watch out for overlapping invocations. Use recursive &lt;code&gt;setTimeout&lt;&#x2F;code&gt; instead if work could take longer to complete than the delay interval (don&#x27;t start the work again if you&#x27;re not finished with the last batch of work yet). This is discussed in more detail in the MDN docs linked above.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;clearinterval-intervalid&quot;&gt;&lt;code&gt;clearInterval(intervalID)&lt;&#x2F;code&gt;&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;same as &lt;code&gt;clearTimeout&lt;&#x2F;code&gt; just pairs with &lt;code&gt;setInterval&lt;&#x2F;code&gt; instead&lt;&#x2F;li&gt;
&lt;li&gt;straightforward&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;process-nexttick-functiontocall&quot;&gt;&lt;code&gt;process.nextTick(functionToCall)&lt;&#x2F;code&gt;&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;works in node.js only, not standard for browsers&lt;&#x2F;li&gt;
&lt;li&gt;Read the &lt;a href=&quot;http:&#x2F;&#x2F;nodejs.org&#x2F;docs&#x2F;latest&#x2F;api&#x2F;all.html#all_process_nexttick_callback&quot;&gt;process.nextTick docs&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Up to 1000 of these (configurable as &lt;code&gt;process.maxTickDepth&lt;&#x2F;code&gt;) will all happen in a row before any I&#x2F;O is allowed to interject. Thus misusing &lt;code&gt;nextTick&lt;&#x2F;code&gt; can starve the system of I&#x2F;O and cause performance degradation&lt;&#x2F;li&gt;
&lt;li&gt;OK to use in code that has to work on node v0.8 or older, just be cautious to not do an onslaught of these that will cause your program to become I&#x2F;O starved.&lt;&#x2F;li&gt;
&lt;li&gt;But generally in node.js &lt;code&gt;setImmediate&lt;&#x2F;code&gt; should be preferred&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;setimmediate&quot;&gt;&lt;code&gt;setImmediate&lt;&#x2F;code&gt;&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;works in node.js v0.10 or newer only, not standard for browsers (except IE10 and IE11)&lt;&#x2F;li&gt;
&lt;li&gt;no &lt;code&gt;window&lt;&#x2F;code&gt; global object in node, so &lt;code&gt;setImmediate&lt;&#x2F;code&gt; can be directly called&lt;&#x2F;li&gt;
&lt;li&gt;Read the &lt;a href=&quot;http:&#x2F;&#x2F;nodejs.org&#x2F;api&#x2F;all.html#all_setimmediate_callback_arg&quot;&gt;node.js setImmediate docs&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Read the &lt;a href=&quot;https:&#x2F;&#x2F;developer.mozilla.org&#x2F;en-US&#x2F;docs&#x2F;Web&#x2F;API&#x2F;Window.setImmediate&quot;&gt;MDN window.setImmediate docs here&lt;&#x2F;a&gt;, which basically say &quot;yeah this doesn&#x27;t exist. Don&#x27;t use it.&quot;&lt;&#x2F;li&gt;
&lt;li&gt;schedules a function to be invoked after the rest of the javascript in the current tick runs, but before doing more I&#x2F;O&lt;&#x2F;li&gt;
&lt;li&gt;the main difference in node.js compared with &lt;code&gt;process.nextTick&lt;&#x2F;code&gt; is &lt;code&gt;setImmediate&lt;&#x2F;code&gt; will run your callback and then allow I&#x2F;O (and the associated callbacks) to get a chance to run, and then move on to the next queued &lt;code&gt;setImmediate&lt;&#x2F;code&gt; callback&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;http:&#x2F;&#x2F;ie.microsoft.com&#x2F;testdrive&#x2F;Performance&#x2F;setImmediateSorting&#x2F;Default.html&quot;&gt;This demo&lt;&#x2F;a&gt; explains the difference between &lt;code&gt;setTimeout&lt;&#x2F;code&gt; and &lt;code&gt;setImmediate&lt;&#x2F;code&gt; in Internet Explorer best&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;clearimmediate&quot;&gt;&lt;code&gt;clearImmediate&lt;&#x2F;code&gt;&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;works in node.js and IO only, not standard for browsers&lt;&#x2F;li&gt;
&lt;li&gt;otherwise exactly analogous to &lt;code&gt;clearTimeout&lt;&#x2F;code&gt; and &lt;code&gt;clearInterval&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;straightforward&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;http:&#x2F;&#x2F;nodejs.org&#x2F;docs&#x2F;latest&#x2F;api&#x2F;all.html#all_clearimmediate_immediateobject&quot;&gt;node.js docs&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;developer.mozilla.org&#x2F;en-US&#x2F;docs&#x2F;Web&#x2F;API&#x2F;Window.clearImmediate&quot;&gt;MDN docs&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;naming-stuff-is-hard-izs&quot;&gt;&quot;naming stuff is hard&quot; --@izs&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;setImmediate&lt;&#x2F;code&gt; happens on the NEXT tick.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;process.nextTick&lt;&#x2F;code&gt; happens on the SAME tick (usually, with caveat about &lt;code&gt;maxTickDepth&lt;&#x2F;code&gt;).&lt;&#x2F;p&gt;
&lt;h2 id=&quot;which-one-to-use-in-node-js&quot;&gt;Which one to use in node.js&lt;&#x2F;h2&gt;
&lt;p&gt;Generally &lt;code&gt;setImmediate&lt;&#x2F;code&gt; is the best practice starting with node.js v0.10, so that&#x27;s what you should use. However, if you need to support node v0.8, using &lt;code&gt;process.nextTick&lt;&#x2F;code&gt; is still fine. The whole problem of I&#x2F;O starvation doesn&#x27;t occur in most projects. It&#x27;s only in certain types of code that are either misguided and poorly coded or truly have a weird edge case. For your run-of-the-mill node.js callback, either is just fine but &lt;code&gt;setImmediate&lt;&#x2F;code&gt; is better.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;reference-articles&quot;&gt;Reference Articles&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;http:&#x2F;&#x2F;nodejs.org&#x2F;api&#x2F;timers.html&quot;&gt;The node.js Timers documentation&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;http:&#x2F;&#x2F;www.nczonline.net&#x2F;blog&#x2F;2013&#x2F;07&#x2F;09&#x2F;the-case-for-setimmediate&#x2F;&quot;&gt;The Case for setImmediate&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;http:&#x2F;&#x2F;stackoverflow.com&#x2F;questions&#x2F;15349733&#x2F;setimmediate-vs-nexttick&quot;&gt;setImmediate vs nextTick&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;NobleJS&#x2F;setImmediate&quot;&gt;A cross-browser implementation of the new setImmediate API&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;http:&#x2F;&#x2F;blog.izs.me&#x2F;post&#x2F;59142742143&#x2F;designing-apis-for-asynchrony&quot;&gt;Designing APIs for Asynchrony&lt;&#x2F;a&gt; - Isaac&#x27;s blog post featuring the key phrase &quot;&lt;strong&gt;Do Not Release Zalgo&lt;&#x2F;strong&gt;&quot;
&lt;ul&gt;
&lt;li&gt;In this post Isaac refers strongly to &lt;a href=&quot;http:&#x2F;&#x2F;blog.ometer.com&#x2F;2011&#x2F;07&#x2F;24&#x2F;callbacks-synchronous-and-asynchronous&#x2F;&quot;&gt;callbacks, synchronous and asynchronous&lt;&#x2F;a&gt; by havoc&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Isaac Schlueter&#x27;s comment on &lt;strong&gt;The Case for setImmediate&lt;&#x2F;strong&gt;:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;I agree the point you’re making here, 100%. However, a slight correction about Node’s APIs.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;First of all, process.nextTick is actually first in, first out. Proof:&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;$ node -e &#x27;process.nextTick(console.log.bind(console, 1)); process.nextTick(console.log.bind(console, 2))&#x27;
1
2&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;Second, process.nextTick isn’t quite the same as setImmediate, at least as of 0.10. Node has a setImmediate function which does happen on the next turn of the event loop, and no sooner. process.nextTick happens before the next turn of the event loop, so it is not suitable for deferring for I&#x2F;O. It IS suitable for deferring a callback until after the current stack unwinds, but making sure to call the function &lt;em&gt;before&lt;&#x2F;em&gt; any additional I&#x2F;O happens. In other words, every entrance to JS from the event loop goes like:&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;Event loop wakes up (epoll, etc.)Do the thing that you said to do when that event happens (call
handle.onread or whatever)Process the nextTick queue completelyReturn to the loop&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;setImmediate is something like a timer that schedules an immediate wake-up, but on the next turn of the event loop. (It’s been pointed out that they’re named incorrectly, since setImmediate is on the next “tick”, and process.nextTick is “immediate”, but whatever, naming stuff is hard, and we’re borrowing the bad name from the browser spec.)&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;The difference may seem like a minor nit-pick, but it’s actually quite relevant. If you are recursively calling process.nextTick, then you’re never actually yielding to the event loop, so you’re not accepting new connections, reading files and sockets, sending outgoing data, etc.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;But that aside, yes. It is completely baffling to me that there is any resistance to this API in browsers, where it is so clearly an obvious win.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;Thanks to &lt;a href=&quot;https:&#x2F;&#x2F;twitter.com&#x2F;derickbailey&quot;&gt;@derickbailey&lt;&#x2F;a&gt; and &lt;a href=&quot;https:&#x2F;&#x2F;twitter.com&#x2F;sambreed&quot;&gt;@sambreed&lt;&#x2F;a&gt; for reviewing and editing a draft this post.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Article on using the JavaScript Debugger</title>
          <pubDate>Mon, 24 Feb 2014 23:04:39 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2014/02/article-on-using-the-javascript-debugger/</link>
          <guid>https://peterlyons.com/problog/2014/02/article-on-using-the-javascript-debugger/</guid>
          <description xml:base="https://peterlyons.com/problog/2014/02/article-on-using-the-javascript-debugger/">&lt;p&gt;So I wrote an article on using the Chrome Developer tools debugger in the browser and node.js. It&#x27;s got diagrams and screencasts and the whole nine yards. Check it out over at &lt;a href=&quot;&#x2F;js-debug&quot;&gt;Lighting Up Your JavaScript With the Debugger&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Announcing Plus Party</title>
          <pubDate>Sat, 22 Feb 2014 15:07:02 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2014/02/announcing-plus-party/</link>
          <guid>https://peterlyons.com/problog/2014/02/announcing-plus-party/</guid>
          <description xml:base="https://peterlyons.com/problog/2014/02/announcing-plus-party/">&lt;p&gt;So when I started dealing with 1099-MISCs for tax season this year in January, I found myself struggling to get my data in a single place and compute a few simple sums for expenses, income, etc. My finances in 2013 weren&#x27;t really complex enough to need a real small business finance software package, but I did find myself struggling with having to collect numbers from several different web sites to get things in order. At the end of the day I just needed to copy some numbers from web sites and total them up. However, I couldn&#x27;t find any calculator out there that worked well for my use case of wanting a few key things:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;ignore extraneous formatting that may have been copy&#x2F;pasted along with the numbers&lt;&#x2F;li&gt;
&lt;li&gt;keep all the numbers on screen so I can easily see what has already been input and what has not&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;So, I ended up building one. Well, first I paired with &lt;a href=&quot;https:&#x2F;&#x2F;twitter.com&#x2F;debrazapata&quot;&gt;@debrazapata&lt;&#x2F;a&gt; prototyping it at a coffee shop here in Louisville. We built the whole thing in one short session and she typed everything out having no experience with AngularJS. &lt;a href=&quot;http:&#x2F;&#x2F;jsfiddle.net&#x2F;5QcL5&#x2F;7&#x2F;&quot;&gt;Here&#x27;s the jsfiddle she built&lt;&#x2F;a&gt;. My version is up here at &lt;a href=&quot;&#x2F;plusParty&quot;&gt;&#x2F;plusParty&lt;&#x2F;a&gt; including clips from Clue the movie, which is a well-deserved cult classic.&lt;&#x2F;p&gt;
&lt;p&gt;So go check it out!&lt;&#x2F;p&gt;
&lt;h1&gt;&lt;a href=&quot;&#x2F;plus-party&#x2F;&quot;&gt;Plus Party: Get Your Addition On!&lt;&#x2F;a&gt;&lt;&#x2F;h1&gt;
</description>
      </item>
      <item>
          <title>Announcing Wallah</title>
          <pubDate>Fri, 21 Feb 2014 22:55:34 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2014/02/announcing-wallah/</link>
          <guid>https://peterlyons.com/problog/2014/02/announcing-wallah/</guid>
          <description xml:base="https://peterlyons.com/problog/2014/02/announcing-wallah/">&lt;p&gt;I released an open source node.js package called &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;focusaurus&#x2F;wallah&quot;&gt;wallah&lt;&#x2F;a&gt;. I use it as a submodule in my projects and it allows my build scripts to automatically pre-install my application&#x27;s dependencies (node, npm packages, python, pip packages) lazily as needed and implicitly when one of my build script commands runs.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;focusaurus&#x2F;wallah&quot;&gt;&lt;img src=&quot;&#x2F;images&#x2F;wallah.jpg&quot; alt=&quot;wallah&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a href=&quot;http:&#x2F;&#x2F;www.flickr.com&#x2F;photos&#x2F;meanestindian&#x2F;4127563975&#x2F;&quot;&gt;Photo by Meena Kadri&lt;&#x2F;a&gt;. Copyright. Licensed &lt;a href=&quot;http:&#x2F;&#x2F;creativecommons.org&#x2F;licenses&#x2F;by-nc-nd&#x2F;2.0&#x2F;&quot;&gt;Create Commons Attribution-NonCommercial-NoDerivs 2.0 Generic&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Web Site Overhaul</title>
          <pubDate>Thu, 20 Feb 2014 15:24:48 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2014/02/web-site-overhaul/</link>
          <guid>https://peterlyons.com/problog/2014/02/web-site-overhaul/</guid>
          <description xml:base="https://peterlyons.com/problog/2014/02/web-site-overhaul/">&lt;p&gt;I&#x27;m back in consulting mode these days, so I went and spruced up the web site a bit. You&#x27;ll find mostly the same content, but there&#x27;s a new design featuring improved typography and a slicker adaptive design for those small screens we tote around everywhere.&lt;&#x2F;p&gt;
&lt;p&gt;I went and did a technology refresh across the whole stack as well, so the code has all been coverted &lt;a href=&quot;&#x2F;problog&#x2F;2014&#x2F;01&#x2F;from-coffeescript-back-to-javascript&quot;&gt;from CoffeeScript back to JavaScript&lt;&#x2F;a&gt;. All jQuery has been abandoned in favor of plain modern JavaScript (sometimes jokingly called vanilla.js), and the more app-like pages have been rewritten as &lt;a href=&quot;http:&#x2F;&#x2F;angularjs.org&quot;&gt;AngularJS&lt;&#x2F;a&gt; apps.&lt;&#x2F;p&gt;
&lt;p&gt;The test suite was already pretty comprehensive, but I&#x27;ve made it even slicker with &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;visionmedia&#x2F;supertest&quot;&gt;supertest&lt;&#x2F;a&gt;. I&#x27;ve also written a handful of tests in &lt;a href=&quot;http:&#x2F;&#x2F;karma-runner.github.io&#x2F;0.10&#x2F;index.html&quot;&gt;karma&lt;&#x2F;a&gt; for the angular stuff. Pretty slick, but I still have more I could build there.&lt;&#x2F;p&gt;
&lt;p&gt;I&#x27;ve also been quite delighted with using &lt;a href=&quot;http:&#x2F;&#x2F;browserify.org&#x2F;&quot;&gt;browserify&lt;&#x2F;a&gt; as my browser-side module system. The contrast of the insanity of requirejs vs. my up-and-running-in-twenty-seconds experience with browserify cannot be overstated. @substack FTW. Losers, go home.&lt;&#x2F;p&gt;
&lt;p&gt;A few years ago I abandoned maintaining my own photo management software in favor of just using flickr, but I used my old photo gallery application as a nice easy sandbox to learn AngularJS, so that has been updated and has better keyboard shortcuts and performance as it&#x27;s a single page application now.&lt;&#x2F;p&gt;
&lt;p&gt;My folder structure has also evolved significantly, moving away from a Ruby on Rails type layout to a group-by-coupling approach which I find immensely superior. Of course this whole site is &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;focusaurus&#x2F;peterlyons.com&quot;&gt;open source on github&lt;&#x2F;a&gt;, so check out the overhaul everything got in the v5.0.0 tag.&lt;&#x2F;p&gt;
&lt;p&gt;Oh yes and the deployment system has been totally revamped in light of my new and deep love for &lt;a href=&quot;http:&#x2F;&#x2F;ansibleworks.org&quot;&gt;ansible&lt;&#x2F;a&gt; and &lt;a href=&quot;http:&#x2F;&#x2F;vagrantup.com&quot;&gt;Vagrant&lt;&#x2F;a&gt;, so I can bootstrap a staging system from base OS image to fully deployed with a single command now and the whole issue of OSX local development not matching Ubuntu deployment is completely solved now.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Open Office Hours Tuesday at Scrib</title>
          <pubDate>Sun, 26 Jan 2014 18:57:27 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2014/01/open-office-hours-tuesday-at-scrib/</link>
          <guid>https://peterlyons.com/problog/2014/01/open-office-hours-tuesday-at-scrib/</guid>
          <description xml:base="https://peterlyons.com/problog/2014/01/open-office-hours-tuesday-at-scrib/">&lt;p&gt;I&#x27;ll be holding open office hours all day this Tuesday January 28th at the Scrib coworking space on Broadway near Spruce (basement level in office building where Unseen Bean is). I&#x27;m a full-stack web developer and veteran software developer, these days specializing in node.js and JavaScript. Come by for a code review, Q&amp;amp;A on node.js as a technology platform choice, pair programming, a plain-English explanation of what the heck a session cookie is, or however I can help you out. Non-technical folks - don&#x27;t be shy; I think I&#x27;m pretty good at explaining technology in plain English terms and analogies.&lt;&#x2F;p&gt;
&lt;p&gt;Email me to reserve a time slot of just swing by first come, first served.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a href=&quot;mailto:pete@peterlyons.com&quot;&gt;pete@peterlyons.com&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>From CoffeeScript Back to JavaScript</title>
          <pubDate>Wed, 22 Jan 2014 17:50:43 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2014/01/from-coffeescript-back-to-javascript/</link>
          <guid>https://peterlyons.com/problog/2014/01/from-coffeescript-back-to-javascript/</guid>
          <description xml:base="https://peterlyons.com/problog/2014/01/from-coffeescript-back-to-javascript/">&lt;p&gt;So after about two years of preferring CoffeeScript for my application code, I&#x27;m switching back to JavaScript. Here are some thoughts on my experience.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;why-i-am-switching-back&quot;&gt;Why I Am Switching Back&lt;&#x2F;h2&gt;
&lt;p&gt;So in winter 2011, given my personal situation and skillset, the cost&#x2F;benefit equation for CoffeeScript was an overall positive. What I have found is that in the intervening two years, the equation has reversed to be an overall negative for CoffeeScript. So that&#x27;s why I&#x27;m switching, Here are some of the specifics.&lt;&#x2F;p&gt;
&lt;p&gt;When I first started writing CoffeeScript, it made things easy for me. Some of this had to do with my background in python and my disfluency at the time with idiomatic JavaScript. Since then my JavaScript has gotten much better and I&#x27;ve learned how to write it idiomatically. Also the general availability of ECMAScript 5 as well as underscore&#x2F;lodash helped a lot there.&lt;&#x2F;p&gt;
&lt;p&gt;These days I make heavy use of node-inspector and the v8&#x2F;chrome debugger. Before, I was mostly a stack trace and &lt;code&gt;console.log&lt;&#x2F;code&gt; debugger. But now I find tremendous power and clarity stepping through my code in node-inspector, and this works much better when the code I see in node-inspector is exactly the same as the code I see in my text editor.&lt;&#x2F;p&gt;
&lt;p&gt;I&#x27;ve grown weary of the compiler overhead, extra build step, extra dependencies, extra tooling, and helping n00bs when they mix tabs and spaces and get compile errors.&lt;&#x2F;p&gt;
&lt;p&gt;Also, the community overall has had more time to settle. Many hip things that had potential to catch on have been pretty soundly spurned, at least by the node.js thought leadership. For example, fibers, coffeescript, iced coffeescript. I think the Ruby on Rails use of CoffeeScript by default is probably the single biggest vote of support and will probably keep CoffeeScript alive and prominent for at least a few years, but within the node.js community it is past peak and on the decline I think.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;coffeescript-features-i-valued-most&quot;&gt;CoffeeScript Features I Valued Most&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;No explicit punctuation to close a block&lt;&#x2F;strong&gt;. I do indeed strongly prefer significant white space and required indentation. CoffeeScript gives me one less thing to obsess over and OCD about, and when I write JavaScript, especially when I refactor heavily-nested node.js code with callbacks, managing all those &lt;code&gt;});&lt;&#x2F;code&gt; marks is a real nuisance. I hope to get reliable auto-formatting working in my sublime text setup soon. At the moment I have good linters, but no 1-click &quot;just fix all the formatting&quot; button yet.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;multiline string literals&lt;&#x2F;strong&gt;. Oh my God, as a web developer everything I do all day is manipulating strings. I can&#x27;t believe we don&#x27;t have first-class string support in our language. This is such a no-downside complete touchdown feature.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;string interpolation&lt;&#x2F;strong&gt;. Again, this is like what we do, y&#x27;know, all the time.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;for loops and list comprehensions&lt;&#x2F;strong&gt;. Python got these right in the 90s. Why can&#x27;t we have nice things?&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;existential operator (user?.name)&lt;&#x2F;strong&gt;. This really does come in handy and really does generate JavaScript that is more correct than what you would code by hand.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;coffeescript-features-that-are-nice-but-not-ultimately-compelling&quot;&gt;CoffeeScript Features That Are Nice But Not Ultimately Compelling&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;skinny arrow functions&lt;&#x2F;strong&gt;. Good use of punctuation but I have editor macros anyway so I don&#x27;t actually type the word &quot;function&quot; ever.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;default function arguments&lt;&#x2F;strong&gt;. Also very handy. However, my code tends to use fewer and fewer function arguments and making them optional is rarer and rarer. What I really want is language support for an &lt;code&gt;options&lt;&#x2F;code&gt; object with a set of default values.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;array slicing and splicing&lt;&#x2F;strong&gt;. I think if you are doing this a lot, you are probably misusing arrays and might be better served with other data structures.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;nested object literals without so much punctuation and syntax&lt;&#x2F;strong&gt;. Yes, this is nice, especially for options objects and configuration objects. But I can live without it.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;splat arguments&lt;&#x2F;strong&gt;. Also quite nice and saves error-prone boilerplate, but I also don&#x27;t code varargs functions often enough to feel this pain sharply.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;coffeescript-features-i-just-plain-dislike-now&quot;&gt;CoffeeScript Features I Just Plain Dislike Now&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;optional parentheses&lt;&#x2F;strong&gt;. These just don&#x27;t ultimately help consistently or by a large enough amount. Another thing python got correct.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;classes and all Object Oriented features&lt;&#x2F;strong&gt;. I think these features are ultimately out of harmony with JavaScript at a deep level. Embrace prototypes, mixins, and the truth about JavaScript. This is an uphill battle that you just can&#x27;t win. Also if you are using inheritence very often there are more idiomatic ways to get comparable code reuse.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;fat arrow functions&lt;&#x2F;strong&gt;. I actually now find &lt;code&gt;var self = this;&lt;&#x2F;code&gt; to be clearer and easier to explain and understand. I&#x27;ve also grokked &lt;code&gt;function.bind&lt;&#x2F;code&gt; and make pretty frequent use of that without missing fat arrows that much.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;@ symbol for this&lt;&#x2F;strong&gt;. I generally don&#x27;t like punctuation, thus my complete resentment of perl and all its progeny. I&#x27;d rather type four lowercase letters &lt;code&gt;this&lt;&#x2F;code&gt; which convey some meaning than rely on assigning meaning to punctuation symbols arbitrarily.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;using words instead of punctuation for logical operators&lt;&#x2F;strong&gt;. This is basically inconsistent with my point above about the @ symbol, but I read something by TJ Holowaychuk that convinced me that reading logical expressions with the punctuation standing out is actually very easy, but when they are words instead it&#x27;s actually harder to scan them at a glance.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;aliases for true and false&lt;&#x2F;strong&gt;. Complete useless bloat. true and false. Done.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;practical-considerations&quot;&gt;Practical Considerations&lt;&#x2F;h2&gt;
&lt;p&gt;When I started with CoffeeScript, I wasn&#x27;t that concerned with the wider node.js and JavaScript community. I enjoy eschewing social norms and being independent. However, now I am more interested in writing open source npm packages and contributing to others&#x27; modules. Thus I think it makes sense for me to focus on JavaScript.&lt;&#x2F;p&gt;
&lt;p&gt;One point I want to be clear on is that I think it has always been a poor choice to code a reusable open source node.js module in CoffeeScript. Those should always be in JavaScript, unless of course what they do is directly about CoffeeScript (like connect-coffee-script). However, for applications themselves are are generally not reusable, it&#x27;s fine to use CoffeeScript if the cost&#x2F;benefit equation is positive for you and your team.&lt;&#x2F;p&gt;
&lt;p&gt;But for me I&#x27;m more and more extracting reusable modules from my applications and once that happens it&#x27;s a pain to switch between CoffeeScript for the application code and JavaScript for the reusable modules the application uses, which are often worked on in parallel. So that&#x27;s another practical consideration pushing me to all JavaScript.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;was-coding-coffeescript-a-mistake&quot;&gt;Was Coding CoffeeScript A Mistake?&lt;&#x2F;h2&gt;
&lt;p&gt;Absolutely not. I&#x27;m a better programmer overall, a better CoffeeScript programmer, and a better JavaScript programmer because I made a significant investment and effort to learn and use CoffeeScript on several small applications. Especially if you don&#x27;t know python or ruby, learning CoffeeScript and using it in a few small applications will teach you valuable things.&lt;&#x2F;p&gt;
&lt;p&gt;So if you have never coded CoffeeScript and are thinking about it, here&#x27;s my take:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;reusable open source modules: JavaScript&lt;&#x2F;li&gt;
&lt;li&gt;Small application(s) with few developers: CoffeeScript, sure. Try it out. If, like me, you change your mind a year or two later, the cost to switch back to JavaScript is quite low.&lt;&#x2F;li&gt;
&lt;li&gt;A huge application you are building a company around or have a large team working on: JavaScript. In this case the chances of ever successfully switching back to JavaScript are pretty slim.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</description>
      </item>
      <item>
          <title>New Geek Badge: Soldering</title>
          <pubDate>Tue, 19 Nov 2013 06:08:35 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2013/11/new-geek-badge:-soldering/</link>
          <guid>https://peterlyons.com/problog/2013/11/new-geek-badge:-soldering/</guid>
          <description xml:base="https://peterlyons.com/problog/2013/11/new-geek-badge:-soldering/">&lt;p&gt;So after a few months of Chinese holiday delays, my &lt;a href=&quot;https:&#x2F;&#x2F;www.massdrop.com&#x2F;buy&#x2F;ergodox&quot;&gt;ErgoDox&lt;&#x2F;a&gt; DIY keyboard kit has finally arrived. There are amazing videos of how to assemble the thing on youtube, and thankfully my roommate was an electrical engineering student at CU and lent me his soldering station. I&#x27;ve started the process of soldering extremely tiny surface mounted diodes to the printed circuit boards. All together the project will require over 300 solder joints by my arithmetic. Pretty ambitious for a first project given my meatspace skills are severely deficient. The SMDs are so tiny that I need a magnifying glass to check they are facing the right direction. The project will for sure be sloppy, but I&#x27;m hoping it will indeed function once done. We&#x27;ll see. I put about 18 SMDs on tonight and by the third row of 6 I was markedly improved from the first few. But there is a real skill to this soldering stuff.&lt;&#x2F;p&gt;
&lt;p&gt;Anyway, when I get it done, assuming it functions at all, it will be pretty kick-ass and by far the geekiest thing I have ever done. Tomorrow night I&#x27;ll be taking it with me to the Solid State Depot hacker space open house to work on it some more and maybe get some pro tips.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Quick Tips for Setting Up TLS</title>
          <pubDate>Thu, 17 Oct 2013 18:46:14 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2013/10/quick-tips-for-setting-up-tls/</link>
          <guid>https://peterlyons.com/problog/2013/10/quick-tips-for-setting-up-tls/</guid>
          <description xml:base="https://peterlyons.com/problog/2013/10/quick-tips-for-setting-up-tls/">&lt;p&gt;Here&#x27;s a few quick links that I found really helpful setting up an nginx web site to use a TLS certificate.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;wiki.mozilla.org&#x2F;Security&#x2F;Server_Side_TLS&quot;&gt;Server Side TLS&lt;&#x2F;a&gt; on the Mozilla wiki&lt;&#x2F;li&gt;
&lt;li&gt;Qualys SSL Labs &lt;a href=&quot;https:&#x2F;&#x2F;www.ssllabs.com&#x2F;projects&#x2F;best-practices&#x2F;index.html&quot;&gt;SSL Deployment Best Practices&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Qualys&#x27;s &lt;a href=&quot;https:&#x2F;&#x2F;www.ssllabs.com&#x2F;ssltest&#x2F;index.html&quot;&gt;SSL Server Test&lt;&#x2F;a&gt;. If you follow the Mozilla guidelines, you should score an &quot;A&quot; on this test.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;http:&#x2F;&#x2F;nginxlibrary.com&#x2F;&quot;&gt;nginx library&lt;&#x2F;a&gt; has a few useful sample nginx configuration files. It&#x27;s stale and only has a few posts, but it was a nice idea in theory if not in execution.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Some commands for reference:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;generate a new 2048-bit RSA private key, named with hostname and year
&lt;ul&gt;
&lt;li&gt;Adding the year will make replacing the certificate when it expires an easy symlink swap operation and avoid confusion. Install the new files then swap the symlinks when ready&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;openssl genrsa -out example.com-2013.key 2048&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;lock down its filesystem permissions&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;chown root:root example.com-2013.key&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;chmod 400 example.com-2013.key&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;symlink it for convenience&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;ln -nsf example.com-2013.key example.com.key&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Create a certificate signing request&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;openssl req -new -sha256 -nodes -key example.com.key -out example.com-2013.csr&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;paste that into &lt;a href=&quot;http:&#x2F;&#x2F;namecheap.com&quot;&gt;NameCheap&lt;&#x2F;a&gt; or whichever vendor you prefer to have them sign it&lt;&#x2F;li&gt;
&lt;li&gt;when the certificate arrives, unzip it and concatenate it as follows&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;cat example_com.crt PositiveSSLCA2.crt AddTrustExternalCARoot.crt &amp;gt; example.com-2013.bundle.crt&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;symlink that for convenience&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;ln -nsf example.com-2013.bundle.crt example.com.crt&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;generate a dhparam file&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;openssl dhparam -out example.com-2013.dhparam.pem 2048&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;ln -nsf example.com-2013.dhparam.pem example.com.dhparam.pem&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;For express.js, make sure your session middleware has &lt;code&gt;proxy: true, secure: true, httpOnly: true&lt;&#x2F;code&gt; options set.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Flickr Date Fixer</title>
          <pubDate>Sun, 28 Apr 2013 04:36:41 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2013/04/flickr-date-fixer/</link>
          <guid>https://peterlyons.com/problog/2013/04/flickr-date-fixer/</guid>
          <description xml:base="https://peterlyons.com/problog/2013/04/flickr-date-fixer/">&lt;p&gt;&lt;a href=&quot;http:&#x2F;&#x2F;flickrdatefixer.jit.su&quot;&gt;Try my flickr date fixer app&lt;&#x2F;a&gt;!&lt;&#x2F;p&gt;
&lt;p&gt;So the startup&#x2F;solopreneur community really likes little mantras and axioms. I wrote a little web application that follows mantras such as:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;solve a problem&lt;&#x2F;li&gt;
&lt;li&gt;focus on a small problem in a niche market&lt;&#x2F;li&gt;
&lt;li&gt;scratch your own itch&lt;&#x2F;li&gt;
&lt;li&gt;build the minimum viable product and ship quickly&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;I have built and released a web app that focuses on an itch I have and may serve a small niche market consisting of only myself, but with that understood, my app is able to:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Locate your flickr photos with an incorrect &quot;taken&quot; date&lt;&#x2F;li&gt;
&lt;li&gt;Show you a list of photos with this problem and let you choose to fix them individually or en masse&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;In order to find this app useful, you probably have to:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Take photos on an Android smartphone&lt;&#x2F;li&gt;
&lt;li&gt;Be running Android 2.2 &quot;Froyo&quot; (I suspect other version have resolved this bug)&lt;&#x2F;li&gt;
&lt;li&gt;Upload them to flickr&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Not sure if anyone other than me fits all these criteria, but this is fundamentally a bug in the camera app on these phones and for reasons I cannot fathom, Google seems unwilling or uninterested in releasing an update with a fix. The problem is incorrect EXIF metadata within the photo image files themselves. The bug causes every photo to be marked as December 8, 2002 at noon.&lt;&#x2F;p&gt;
&lt;p&gt;The app is pretty small and gave me a nice opportunity to try some new technologies (as well as use many that have been in my preferred stack for months or years).&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;node.js&lt;&#x2F;li&gt;
&lt;li&gt;coffee-script&lt;&#x2F;li&gt;
&lt;li&gt;express.js&lt;&#x2F;li&gt;
&lt;li&gt;passport.js for OAuth integration with flickr&lt;&#x2F;li&gt;
&lt;li&gt;backbone.js and jQuery in the browser&lt;&#x2F;li&gt;
&lt;li&gt;require.js&lt;&#x2F;li&gt;
&lt;li&gt;grunt.js&lt;&#x2F;li&gt;
&lt;li&gt;nodejitsu&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The nice thing about this app is given that it is all OAuth based and is basically a custom UI using the flickr API for all the business logic, it doesn&#x27;t need any database at all. This means it is easy to deploy. So for the novelty of it, I have chosen to deploy it onto nodejitsu, and in theory they will host it for free since it is open source.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;resources&quot;&gt;Resources&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;http:&#x2F;&#x2F;flickrdatefixer.jit.su&quot;&gt;Flickr Date Fixer&lt;&#x2F;a&gt; running live on nodejitsu&#x27;s &lt;a href=&quot;http:&#x2F;&#x2F;flickrdatefixer.jit.su&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;focusaurus&#x2F;flickr-date-fixer&quot;&gt;flickr-date-fixer source code on github&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;http:&#x2F;&#x2F;nodejitsu.com&quot;&gt;Nodejitsu&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</description>
      </item>
      <item>
          <title>AirPair on TechCrunch</title>
          <pubDate>Mon, 04 Mar 2013 16:30:24 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2013/03/airpair-on-techcrunch/</link>
          <guid>https://peterlyons.com/problog/2013/03/airpair-on-techcrunch/</guid>
          <description xml:base="https://peterlyons.com/problog/2013/03/airpair-on-techcrunch/">&lt;p&gt;TechCrunch has &lt;a href=&quot;http:&#x2F;&#x2F;techcrunch.com&#x2F;2013&#x2F;03&#x2F;04&#x2F;airpair-connects-startups-with-expert-developers-to-get-help-with-code-via-online-sessions&#x2F;&quot;&gt;this article on AirPair&lt;&#x2F;a&gt; today with a nice quote about me. This is my first ever mention on TechCrunch (but hopefully not my first and only!).&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>My Airpair Session was written up</title>
          <pubDate>Tue, 26 Feb 2013 04:28:29 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2013/02/my-airpair-session-was-written-up/</link>
          <guid>https://peterlyons.com/problog/2013/02/my-airpair-session-was-written-up/</guid>
          <description xml:base="https://peterlyons.com/problog/2013/02/my-airpair-session-was-written-up/">&lt;p&gt;&lt;a href=&quot;http:&#x2F;&#x2F;hackerpreneurialism.com&#x2F;post&#x2F;44040862234&#x2F;product-fit-and-success-in-online-marketplaces&quot;&gt;Here&#x27;s a post&lt;&#x2F;a&gt; about my recent experience with the new &lt;a href=&quot;http:&#x2F;&#x2F;airpair.co&quot;&gt;AirPair&lt;&#x2F;a&gt; service connecting entrepreneurs with technology experts.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Airpair</title>
          <pubDate>Sat, 23 Feb 2013 22:26:52 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2013/02/airpair/</link>
          <guid>https://peterlyons.com/problog/2013/02/airpair/</guid>
          <description xml:base="https://peterlyons.com/problog/2013/02/airpair/">&lt;p&gt;So I&#x27;ve just been one of the earliest progragrammers to get involved with &lt;a href=&quot;http:&#x2F;&#x2F;codereview.airpair.co&#x2F;&quot;&gt;AirPair&lt;&#x2F;a&gt;. It&#x27;s a service that pairs entrepreneurs with technology experts for code review and pair programming sessions to help them with their coding projects.&lt;&#x2F;p&gt;
&lt;p&gt;I did some google hangouts with an entrepreneur who built his own Ruby on Rails app that analyzes geology data files for oil &amp;amp; gas companies and is just starting to port it over to node.js. There&#x27;s a &lt;a href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;channel&#x2F;UCOwZvL29sSlUAj6c2i_f53A?v=gdi9xDKZ5qg&quot;&gt;youtube recording video of the 2-hour google hangout session this morning&lt;&#x2F;a&gt; up. You can skip around in that if you want to get a feel for the service and experience.&lt;&#x2F;p&gt;
&lt;p&gt;I&#x27;m perpetually interested in the startup community in general and connecting people across geographies via the Internet, so I was excited to try it out. I&#x27;ve done 3 hour-long sessions so far and so far so good.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>npm modules: development and release versions</title>
          <pubDate>Fri, 01 Feb 2013 20:17:35 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2013/02/npm-modules:-development-and-release-versions/</link>
          <guid>https://peterlyons.com/problog/2013/02/npm-modules:-development-and-release-versions/</guid>
          <description xml:base="https://peterlyons.com/problog/2013/02/npm-modules:-development-and-release-versions/">&lt;p&gt;You are working on two npm modules that are both actively being developed:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;my_app&lt;&#x2F;code&gt;: some application, which depends on &lt;code&gt;my_lib&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;my_lib&lt;&#x2F;code&gt;: a shared library&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;There are 2 modes you&#x27;ll need inside &lt;code&gt;my_app&lt;&#x2F;code&gt;: 1) using a published release of &lt;code&gt;my_lib&lt;&#x2F;code&gt; and 2) using a local development version of &lt;code&gt;my_lib&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Using a published release of &lt;code&gt;my_lib&lt;&#x2F;code&gt; is standard faire:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;list &lt;code&gt;my_lib&lt;&#x2F;code&gt; as a dependency in &lt;code&gt;my_app&#x2F;package.json&lt;&#x2F;code&gt;
1. &lt;code&gt;dependencies: {&quot;my_lib&quot;: &quot;1.2.x&quot;}&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;cd my_app &amp;amp;&amp;amp; npm install&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;npm will populate &lt;code&gt;my_app&#x2F;node_modules&#x2F;my_lib&lt;&#x2F;code&gt; for you as you would expect&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;To use your development version so you can easily make changes to both codebases and have them available with minimal fuss, set up this structure&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;~&amp;#x2F;projects&amp;#x2F;my_app (working directory for the app)
~&amp;#x2F;projects&amp;#x2F;my_lib   (working directory for the lib)
~&amp;#x2F;projects&amp;#x2F;node_modules&amp;#x2F;my_lib (symlink to ..&amp;#x2F;..&amp;#x2F;my_lib)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;You can set this up with&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;mkdir -p ~&amp;#x2F;projects&amp;#x2F;my_app ~&amp;#x2F;projects&amp;#x2F;my_lib ~&amp;#x2F;projects&amp;#x2F;node_modules
cd ~&amp;#x2F;projects&amp;#x2F;node_modules
ln -nsf ..&amp;#x2F;..&amp;#x2F;my_lib
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;So now when developing &lt;code&gt;my_app&lt;&#x2F;code&gt; and you want to use the local development version of &lt;code&gt;my_lib&lt;&#x2F;code&gt;, just do :&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;cd ~&amp;#x2F;projects&amp;#x2F;my_app
npm uninstall my_lib
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Now node&#x27;s &lt;code&gt;require&lt;&#x2F;code&gt; function will load &lt;code&gt;my_lib&lt;&#x2F;code&gt; from &lt;code&gt;~&#x2F;projects&#x2F;node_modules&#x2F;my_lib&lt;&#x2F;code&gt; and you are good to go. To switch back to a public of &lt;code&gt;my_lib&lt;&#x2F;code&gt; just do:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;cd ~&amp;#x2F;projects&amp;#x2F;my_app
npm install
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Of course, there is &lt;a href=&quot;https:&#x2F;&#x2F;npmjs.org&#x2F;doc&#x2F;link.html&quot;&gt;the npm link command&lt;&#x2F;a&gt; which does something nearly identical to this, so far I prefer this approach as I find it a little simpler.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Hilarious Recruiter Email</title>
          <pubDate>Wed, 12 Dec 2012 21:49:52 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2012/12/hilarious-recruiter-email/</link>
          <guid>https://peterlyons.com/problog/2012/12/hilarious-recruiter-email/</guid>
          <description xml:base="https://peterlyons.com/problog/2012/12/hilarious-recruiter-email/">&lt;p&gt;Here is the full text of a recruiter email I received today.&lt;&#x2F;p&gt;
&lt;hr&#x2F;&gt;
Dear Pete,
&lt;p&gt;Wouldn&#x27;t it feel great to wake up each day knowing that you&#x27;re part of a dynamic organization that&#x27;s on the cutting edge of innovation web products? That’s what the team experiences and you could too!&lt;&#x2F;p&gt;
&lt;p&gt;What you will find challenging about this job is to develop revolutionary and challenging web applications using object oriented javascript frameworks like backbone.js, angular.js,  HTML, and CSS.&lt;&#x2F;p&gt;
&lt;p&gt;As a result, there are many exciting decisions to be made to keep it in line with demands. You won&#x27;t find formulaic thinking here. As a company that specializes in innovation, they want the best and brightest creative visionaries who think so far out of the box that the box isn&#x27;t even in the picture anymore. Where others say &quot;can&#x27;t&quot;, you say &quot;how&quot;.&lt;&#x2F;p&gt;
&lt;p&gt;Keeping in mind that most successful people find opportunity not when they&#x27;re looking but when it knocks,  how&#x27;s my timing?&lt;&#x2F;p&gt;
&lt;p&gt;I&#x27;d be happy to talk to you further about it. I&#x27;m reaching out to key individuals before I go on a full scale search for this. You have an ideal background and I think it&#x27;s a good match for you geographically.&lt;&#x2F;p&gt;
&lt;p&gt;Fortune Favors the bravo!&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Find closer OKCupid matches with this bookmarklet</title>
          <pubDate>Tue, 23 Oct 2012 03:15:05 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2012/10/okcupid-closer/</link>
          <guid>https://peterlyons.com/problog/2012/10/okcupid-closer/</guid>
          <description xml:base="https://peterlyons.com/problog/2012/10/okcupid-closer/">&lt;p&gt;I have written a little bookmarklet that will allow you to filter your &lt;a href=&quot;http:&#x2F;&#x2F;okcupid.com&quot;&gt;OKCupid&lt;&#x2F;a&gt; matches by a smaller geographic region. By default, their search filters allow no smaller than a 25-mile radius. Drag the bookmarklet link below onto your bookmarks toolbar, then click it when you are on the OKC search page, and voilà, you will have the options for 5 and 10-mile searches. 10-mile radius will be selected, so just click &quot;search&quot; and you will have your results.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a href=&quot;javascript:(function(){var%20opt5=document.createElement(&#x27;option&#x27;);opt5.value=5;opt5.text=&#x27;5%20miles&#x27;;var%20opt10=document.createElement(&#x27;option&#x27;);opt10.value=10;opt10.text=&#x27;10%20miles&#x27;;var%20radius=document.getElementById(&#x27;radius&#x27;);radius.insertBefore(opt10,radius.firstChild);radius.insertBefore(opt5,radius.firstChild);opt10.selected=true;var%20label=document.getElementById(&#x27;location_interface_button_text&#x27;);label.textContent=label.textContent.replace(&#x2F;\d+&#x2F;,&#x27;10&#x27;);})();&quot;&gt;OKCupid Closer Matches&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Some notes:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;If you have no idea WTF a bookmarklet is or how to use one, read &lt;a href=&quot;http:&#x2F;&#x2F;support.mozilla.org&#x2F;en-US&#x2F;kb&#x2F;bookmarklets-perform-common-web-page-tasks&quot;&gt;this&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;I live close to both Boulder and Denver Colorado. These are two quite different worlds, but they are both within 25 miles of my town. Since Denver&#x27;s population is 10x the size of Boulder&#x27;s, most of my search results are Denver residents, when I&#x27;m really only insterested in folks within biking distance.&lt;&#x2F;li&gt;
&lt;li&gt;After you click search, the default search form will re-appear and it will look like your search has a 25 mile radius. Never fear, the query string has &lt;code&gt;filter3=10&lt;&#x2F;code&gt;, which is what is required, and based on my results, yes, the OKC servers do seem to actually respect this query string parameter even if the value is not offered in the search form.&lt;&#x2F;li&gt;
&lt;li&gt;You need to re-click the bookmarklet for each new search. Sorry. Send feature requests to OKCupid until they add this permanently. Technically I could make this into a Chrome extension and have it always work, so I may do that at some point.&lt;&#x2F;li&gt;
&lt;li&gt;Thanks to &lt;a href=&quot;http:&#x2F;&#x2F;chris.zarate.org&#x2F;bookmarkleter&quot;&gt;Chris Zarate&#x27;s handy Bookmarkleter utility&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;And for you web dev nerds out there, I even wrote this with (gasp!) &lt;a href=&quot;http:&#x2F;&#x2F;vanilla-js.com&#x2F;&quot;&gt;Vanilla JS&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</description>
      </item>
      <item>
          <title>Great Support from Herman Miller</title>
          <pubDate>Thu, 27 Sep 2012 16:48:31 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2012/09/great-support-from-herman-miller/</link>
          <guid>https://peterlyons.com/problog/2012/09/great-support-from-herman-miller/</guid>
          <description xml:base="https://peterlyons.com/problog/2012/09/great-support-from-herman-miller/">&lt;p&gt;So I use one of the famous &lt;a href=&quot;http:&#x2F;&#x2F;www.hermanmiller.com&#x2F;&quot;&gt;Herman Miller&lt;&#x2F;a&gt; &lt;a href=&quot;http:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Aeron_chair&quot;&gt;Aeron chairs&lt;&#x2F;a&gt; in my home office. I love it primarily because the mesh seat prevents &lt;a href=&quot;http:&#x2F;&#x2F;www.urbandictionary.com&#x2F;define.php?term=swampass&quot;&gt;swampass&lt;&#x2F;a&gt;, but of course overall it is a highly ergonomic chair, nicely adjustable, rugged, and holds its value well. My specific chair was made in 2000 and I bought it used on craigslist for $250 in 2009. Today exactly the 12-year warranty finally expires. About two weeks ago it suddenly broke. A thin rod broke free of its clamp and started protruding from the bottom such that the casters wouldn&#x27;t hit the ground anymore. I was worried that I would need to replace the chair, but given my trend toward more DIY and frugality, I called up Herman Miller support to see what they would say. Staff was out at training that day so I left a voice mail and called again Monday. I spoke with a woman who had heard my voice mail and had me on her list to call back. I told her what my chair was doing and she shipped me out a repair kit for the main problem as well as a set of repair kits to fix the arms, which wouldn&#x27;t hold their adjusted height. These arrived a bit later with hand-drawn instructions on how to do the repair. The diagrams weren&#x27;t entirely clear (earth to furniture industry: we have these things called photographs. Stop sending out line drawing pencil art instructions.), but after some futzing I was able to properly repair the main hydraulic rod as well as the arms. I managed to bust the tilt adjuster in the process of putting my chair on its side many times, though. The back was now stuck dead vertical. I called support again. The rep&#x27;s business card and direct phone number were including in the package of repair kits. She instructed me to stand on the back legs of the chair and lift the chair back up to unlock it. Now my Aeron is back to working condition, and it didn&#x27;t cost me a time. Herman Miller shipped the repair kits free and the support consultation was free. That&#x27;s great service!&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Managing Per-Project Interpreters and the PATH</title>
          <pubDate>Sun, 02 Sep 2012 21:35:19 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2012/09/managing-per-project-interpreters-and-the-path/</link>
          <guid>https://peterlyons.com/problog/2012/09/managing-per-project-interpreters-and-the-path/</guid>
          <description xml:base="https://peterlyons.com/problog/2012/09/managing-per-project-interpreters-and-the-path/">&lt;p&gt;&lt;strong&gt;Updated: 2016-04-14&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;So let&#x27;s talk about managing runtimes and interpreters for projects and applications. This post comes about after I have seen a vast jungle of non-solutions and dead ends out there for managing installations of interpreters such as Python, Ruby, node.js, etc. First, let&#x27;s clear the air of a bunch of nonesense you may find out there that makes this problem confusing.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;never-use-the-interpreter-provided-by-your-operating-system&quot;&gt;Never use the interpreter provided by your operating system&lt;&#x2F;h2&gt;
&lt;p&gt;If you are building a web application or any other project that should by all rights be cross-platform and doesn&#x27;t ship with OS, then you should have absolutely nothing to do with the interpreter that may be included with the OS. The OS interpreters are there for components of the OS itself written in those languages. They have no business being used by third party projects and applications that run on top of the OS as opposed to being part of the OS. Forget about the Ruby that comes with OS X. Forget about the Python that comes with Ubuntu. Forget about the Debian packages for node.js (slightly different&#x2F;better, but still, ignore them).&lt;&#x2F;p&gt;
&lt;p&gt;The reasoning behind this guideline is as follows.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Exact version&lt;&#x2F;strong&gt;: Applications need exact and strict control of the version of their interpreter. You should be using the exact same version of your interpreter across all of your development, test, staging, and production environments. This will avoid problems which are easily-avoidable, so do it.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Modern version&lt;&#x2F;strong&gt;: OSes tend to ship versions of these interpreters that are significantly behind the latest stable version. New applications should be written to work with the latest stable version and should keep up with ongoing releases, never getting more than 3 months behind.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Independence&lt;&#x2F;strong&gt; Applications need independence from one another. If you have 3 Django projects on the same machine, each one needs to have the ability to use whatever interpreter &lt;strong&gt;version&lt;&#x2F;strong&gt; it needs on its own independent &lt;strong&gt;schedule&lt;&#x2F;strong&gt;. Due to this fact, that means the correct location for these interpreters is within your application&#x27;s directory alongside your application code, which is why I advise you to ignore the node.js debian packages you may find out there because it installs into a shared location, which does not meet our goals here.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;keep-the-app-specific-interpreter-within-the-application-install-directory&quot;&gt;Keep the app-specific interpreter within the application install directory&lt;&#x2F;h2&gt;
&lt;p&gt;Again, don&#x27;t let the OS&#x27;s notion of shared interpreters in a shared location distract you from the right layout here. The app-specific interpreter installation belongs inside you project&#x27;s installation directory.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;project_root&#x2F;python&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;project_root&#x2F;node&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;project_root&#x2F;ruby&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Basically, the old school unix principles have gone stale on us. Years ago, sysadmins had rules for filesystem layout with different goals. For example, sysadmins wanted to be able to NFS mount a shared directory where binaries could live, be maintained in a single place, and be mounted and run by many additional servers. They wanted to do this to be efficient with disk space and to be able to make manual changes in one place and have them to affect immediately on an arbitrary number of servers that use the same NFS volume.&lt;&#x2F;p&gt;
&lt;p&gt;Now we use automated tools to manage deployments to clusters of independent servers, and disk space is cheap, so we want each server to have its own copy of what it needs to run with as few external dependencies as possible. We want to be able to do rolling deploys across a cluster or run 1&#x2F;2 the cluster on the new code and half on the old code.  Disk space is cheap and plentiful, so if we have 5 or 10 apps running on the same staging server, we could not care less about a few megabytes of duplication to handle a bunch of python installations.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;or-use-tools-like-nvm-and-rbenv&quot;&gt;OR use tools like nvm and rbenv&lt;&#x2F;h2&gt;
&lt;p&gt;Some but not all of the interpreter managers are fully compatible with the requirements outlined above, so it&#x27;s OK to use &lt;code&gt;nvm&lt;&#x2F;code&gt; or &lt;code&gt;rbenv&lt;&#x2F;code&gt; as long as each project gets to specify it&#x27;s exact version.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;never-use-npm-g&quot;&gt;Never use npm -g&lt;&#x2F;h2&gt;
&lt;p&gt;This is a follow up to my &lt;a href=&quot;&#x2F;problog&#x2F;2011&#x2F;12&#x2F;no-need-for-npm-g&quot;&gt;earlier blog post about avoiding npm -g&lt;&#x2F;a&gt;, now improved and revised. For the most part, I believe &lt;a href=&quot;https:&#x2F;&#x2F;npmjs.org&#x2F;&quot;&gt;npm&lt;&#x2F;a&gt; to be the state-of-the-art package management system and to be superior to the messes available for python and ruby. However, the &lt;code&gt;-g&lt;&#x2F;code&gt; switch, which installs commands &lt;code&gt;globally&lt;&#x2F;code&gt;, should be avoided in favor of the system described here. You don&#x27;t want to have to upgrade all your apps at once to a new version of &lt;code&gt;eslint&lt;&#x2F;code&gt;, so give them each their own copy of the &lt;code&gt;eslint&lt;&#x2F;code&gt; command.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;provide-a-single-script-to-launch-your-application-commands&quot;&gt;Provide a single script to launch your application commands&lt;&#x2F;h2&gt;
&lt;p&gt;Encapsulate each version with a wrapper shell script that understands the project directory layout and manages your PATH appropriately. I tend to call this file &lt;code&gt;project_root&#x2F;bin&#x2F;go&lt;&#x2F;code&gt; but &lt;code&gt;project_root&#x2F;bin&#x2F;tasks.sh&lt;&#x2F;code&gt; or similar are good locations for this. This script should handle your service operations like start, stop, reload, etc, as well as any one-off commands you make have like clearing a cache, regenering static files, and so forth.&lt;&#x2F;p&gt;
&lt;p&gt;Here&#x27;s a snippet of my &lt;code&gt;project_root&#x2F;bin&#x2F;go&lt;&#x2F;code&gt; script which locates the correct installation of python and fabric and passes control to them.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#!&amp;#x2F;bin&amp;#x2F;sh -e
cd $(dirname &amp;quot;${0}&amp;quot;)
exec .&amp;#x2F;python&amp;#x2F;bin&amp;#x2F;fab &amp;quot;${@}&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Thus I can run this script from any directory, or from an init&#x2F;upstart script, with any PATH, and the application correctly handles its own required settings. The above is the bare bones and the crux of the separation of concerns in the design. I normally have some other code in there to bootstrap the project&#x27;s dependencies, but I&#x27;ll save that topic for another blog post.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;for-local-development-manage-your-path-intelligently-and-automatically&quot;&gt;For local development, manage your PATH intelligently and automatically&lt;&#x2F;h2&gt;
&lt;p&gt;As you work on many projects which contain their own interpreter installations, you don&#x27;t want to always have to A) work from the project root directory and B) run commands like &lt;code&gt;.&#x2F;python&#x2F;bin&#x2F;python myapp.py&lt;&#x2F;code&gt;. So here are some utilities that can intelligently manage your PATH similar to what is done by &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;sstephenson&#x2F;rbenv&quot;&gt;rbenv&lt;&#x2F;a&gt;, but not tied to ruby and based on you changing project directories.&lt;&#x2F;p&gt;
&lt;p&gt;First, here&#x27;s how I set up my &lt;code&gt;PATH&lt;&#x2F;code&gt; in my &lt;code&gt;~&#x2F;.zshrc&lt;&#x2F;code&gt; file (works equally well for bash or bourne shell). I&#x27;ve added extra explanatory comments inline.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#This helper function will add a directory to the PATH if it exists
#This is a simple way to handle different machines, OSes, and configurations
addPath() {
    if [ -d &amp;quot;${1}&amp;quot; ]; then
        if [ -z &amp;quot;${PATH}&amp;quot; ]; then
            export PATH=&amp;quot;${1}&amp;quot;
        else
          export PATH=$PATH:&amp;quot;${1}&amp;quot;
        fi
    fi
}

setup_path() {
  PATH=
  # Normal system stuff comes first for security
  # So npm packages can&amp;#x27;t override basic commands like ls

  # Homebrew
  add_path &amp;quot;&amp;#x2F;usr&amp;#x2F;local&amp;#x2F;bin&amp;quot;

  add_path &amp;quot;&amp;#x2F;bin&amp;quot;
  add_path &amp;quot;&amp;#x2F;usr&amp;#x2F;bin&amp;quot;
  add_path &amp;quot;&amp;#x2F;sbin&amp;quot;
  add_path &amp;quot;&amp;#x2F;usr&amp;#x2F;sbin&amp;quot;
  add_path &amp;quot;&amp;#x2F;usr&amp;#x2F;X11&amp;#x2F;bin&amp;quot;

  # Personal home dir stuff
  add_path &amp;quot;${HOME}&amp;#x2F;projects&amp;#x2F;dotfiles&amp;#x2F;bin&amp;quot;
  add_path &amp;quot;${HOME}&amp;#x2F;bin&amp;quot;

  # Local pwd stuff
  add_path &amp;quot;${PWD}&amp;#x2F;script&amp;quot;
  add_path &amp;quot;${PWD}&amp;#x2F;bin&amp;quot;

  # For node
  add_path &amp;quot;${PWD}&amp;#x2F;node_modules&amp;#x2F;.bin&amp;quot;
  add_path &amp;quot;${HOME}&amp;#x2F;shared_node.js&amp;#x2F;node_modules&amp;#x2F;.bin&amp;quot;
  add_path &amp;quot;${HOME}&amp;#x2F;shared_node.js&amp;#x2F;node&amp;#x2F;bin&amp;quot;

  # For per-project python virtualenvs
  add_path &amp;quot;${PWD}&amp;#x2F;python&amp;#x2F;bin&amp;quot;
  add_path &amp;quot;${PWD}&amp;#x2F;env&amp;#x2F;bin&amp;quot;

  add_path &amp;quot;${HOME}&amp;#x2F;.rbenv&amp;#x2F;bin&amp;quot;

  export PATH
  [[ -d &amp;quot;${HOME}&amp;#x2F;.rbenv&amp;#x2F;bin&amp;quot; ]] &amp;amp;&amp;amp; eval &amp;quot;$(rbenv init -)&amp;quot;
}
# Run this during shell startup.
# Can be re-run as needed manually as well
setup_path
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;OK, so that&#x27;s how the &lt;code&gt;PATH&lt;&#x2F;code&gt; gets built up, but we want to change the PATH as we move our current working directory between projects. For that we use a shell hook function. What this does is try to detect if we&#x27;ve changed into a project directory, and if so, rebuild the &lt;code&gt;PATH&lt;&#x2F;code&gt;, which will put our project-specific directories early in the &lt;code&gt;PATH&lt;&#x2F;code&gt; list, so when we type &lt;code&gt;node&lt;&#x2F;code&gt; or &lt;code&gt;python&lt;&#x2F;code&gt; or &lt;code&gt;coffee&lt;&#x2F;code&gt;, etc, we get the project specific one under the project root. Because this adds absolute paths and only changes the &lt;code&gt;PATH&lt;&#x2F;code&gt; when we &lt;code&gt;cd&lt;&#x2F;code&gt; to a project root, we can cd to subdirectories within the project and still be running the correct project-specific interpreter. This does breakdown, however, if you cd directly into a project subdirectory without stopping in the project root. I don&#x27;t hit that problem because I&#x27;m not in the habit of doing that, but YMMV. Here&#x27;s the zsh version, which uses the &lt;a href=&quot;http:&#x2F;&#x2F;www.refining-linux.org&#x2F;archives&#x2F;42&#x2F;ZSH-Gem-8-Hook-function-chpwd&#x2F;&quot;&gt;chpwd&lt;&#x2F;a&gt; hook function.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;if [ -n &amp;quot;${ZSH_VERSION}&amp;quot; ]; then
  chpwd() {
    [ -d .git -o \
      -d  node_modules&amp;#x2F;.bin -o \
      -d python&amp;#x2F;bin -o \
      -d node&amp;#x2F;bin ] &amp;amp;&amp;amp; setupPath
  }
fi
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Bash users, &lt;a href=&quot;http:&#x2F;&#x2F;stackoverflow.com&#x2F;questions&#x2F;3276247&#x2F;is-there-a-hook-in-bash-to-find-out-when-the-cwd-changes&quot;&gt;you&#x27;re on your own&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Here&#x27;s an example of this at work.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;~-&amp;gt; cd projects&amp;#x2F;peterlyons.com
~&amp;#x2F;projects&amp;#x2F;peterlyons.com-&amp;gt; which node
&amp;#x2F;Users&amp;#x2F;plyons&amp;#x2F;projects&amp;#x2F;peterlyons.com&amp;#x2F;node&amp;#x2F;bin&amp;#x2F;node
~&amp;#x2F;projects&amp;#x2F;peterlyons.com-&amp;gt; cd ..&amp;#x2F;craft
~&amp;#x2F;projects&amp;#x2F;craft-&amp;gt; which node
&amp;#x2F;Users&amp;#x2F;plyons&amp;#x2F;projects&amp;#x2F;craft&amp;#x2F;node&amp;#x2F;bin&amp;#x2F;node
~&amp;#x2F;projects&amp;#x2F;craft-&amp;gt; cd ..&amp;#x2F;othenticate.com
~&amp;#x2F;projects&amp;#x2F;othenticate.com-&amp;gt; which node
&amp;#x2F;Users&amp;#x2F;plyons&amp;#x2F;projects&amp;#x2F;othenticate.com&amp;#x2F;node&amp;#x2F;bin&amp;#x2F;node
~&amp;#x2F;projects&amp;#x2F;othenticate.com-&amp;gt; cd ..&amp;#x2F;m-cm&amp;#x2F;indivo_provision
~&amp;#x2F;projects&amp;#x2F;m-cm&amp;#x2F;indivo_provision-&amp;gt; which python
&amp;#x2F;Users&amp;#x2F;plyons&amp;#x2F;projects&amp;#x2F;m-cm&amp;#x2F;indivo_provision&amp;#x2F;python&amp;#x2F;bin&amp;#x2F;python
~&amp;#x2F;projects&amp;#x2F;m-cm&amp;#x2F;indivo_provision-&amp;gt; cd .&amp;#x2F;conf
~&amp;#x2F;projects&amp;#x2F;m-cm&amp;#x2F;indivo_provision&amp;#x2F;conf-&amp;gt; which python
&amp;#x2F;Users&amp;#x2F;plyons&amp;#x2F;projects&amp;#x2F;m-cm&amp;#x2F;indivo_provision&amp;#x2F;python&amp;#x2F;bin&amp;#x2F;python
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;bonus-item-keep-variable-files-for-data-and-logging-under-your-project-directory&quot;&gt;(Bonus item) Keep variable files for data and logging under your project directory&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;project_root&#x2F;var&#x2F;log&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;project_root&#x2F;var&#x2F;data&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;This is a mindset shift from traditional unix administration best practices. It&#x27;s in my opinion a less complex and more application-centric design that makes better sense given our focus on applications that tend to be providing network services and generally are less tightly coupled to the underlying OS these days. Traditional unix administration (as documented in the &lt;a href=&quot;http:&#x2F;&#x2F;www.pathname.com&#x2F;fhs&#x2F;&quot;&gt;Filesystem Heirarchy Standard&lt;&#x2F;a&gt;) has a strong and system-wide distinction that runtime variable data like data files and log files go under &lt;code&gt;&#x2F;var&lt;&#x2F;code&gt; and everything else except for &lt;code&gt;&#x2F;home&lt;&#x2F;code&gt; and &lt;code&gt;&#x2F;tmp&lt;&#x2F;code&gt; is static data. Again, this no longer applies to modern applications. These rules had to do with preventing key filesystems from filling up, primarily. They wanted application data to be static and allocate a certain amount of space that had separate filesystem limits from the variable data, which they wanted organized centrally under &lt;code&gt;&#x2F;var&lt;&#x2F;code&gt; so they could manage log file growth and space disk space centrally. There were reasons for these designs at the time that made sense given the constraints and goals, but times have changed.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>CoffeeScript and Progress</title>
          <pubDate>Mon, 05 Mar 2012 04:36:35 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2012/03/coffeescript-and-progress/</link>
          <guid>https://peterlyons.com/problog/2012/03/coffeescript-and-progress/</guid>
          <description xml:base="https://peterlyons.com/problog/2012/03/coffeescript-and-progress/">&lt;p&gt;If you like &lt;a href=&quot;http:&#x2F;&#x2F;jashkenas.github.com&#x2F;coffee-script&quot;&gt;CoffeeScript&lt;&#x2F;a&gt;, you should write applications in CoffeeScript. If you prefer JavaScript, you should write applications in JavaScript. In this post I want to address the common attitude I have found among JavaScript programmers that CoffeeScript is a priori somehow wrong or inferior or a bad choice. I&#x27;ll briefly recount some of the technical reasons why I like and use CoffeeScript at the end of this post, but the focus is a rejection of the attitude above on philosophical grounds. To be clear, the position I&#x27;m taking issue with is the position that JavaScript is the one and only language we should be writing in for the browser and node.js.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;philosophical-rejection-of-monoglotism&quot;&gt;Philosophical Rejection of Monoglotism&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;Availability of a multitude of highly varied programming languages is good
&lt;ul&gt;
&lt;li&gt;Check out &lt;a href=&quot;http:&#x2F;&#x2F;dartr.com&#x2F;&quot;&gt;Dart&lt;&#x2F;a&gt;, &lt;a href=&quot;http:&#x2F;&#x2F;haxe.org&#x2F;&quot;&gt;Haxe&lt;&#x2F;a&gt;, &lt;a href=&quot;http:&#x2F;&#x2F;jashkenas.github.com&#x2F;coffee-script&quot;&gt;CoffeeScript&lt;&#x2F;a&gt;, &lt;a href=&quot;http:&#x2F;&#x2F;code.google.com&#x2F;webtoolkit&#x2F;&quot;&gt;GWT&lt;&#x2F;a&gt;, or &lt;a href=&quot;http:&#x2F;&#x2F;caterwauljs.org&#x2F;&quot;&gt;Caterwaul&lt;&#x2F;a&gt; for examples&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;The goal of a single programming language for the browser is a detrimental non-goal&lt;&#x2F;li&gt;
&lt;li&gt;Openness and freedom are important, as are standards and interoperability, but standards that don&#x27;t evolve on an appropriate schedule are detrimental.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;I brought up the topic of Google&#x27;s Dart language at a local tech meetup when Dart was first announced. I asked people what their reaction was. The few who responded seemed to dislike it, not for any particular technical objection to the language or its features, but simply to its existence at all, which surprised me. This struck me as completely unexpected and strange. Why, I asked. &quot;JavaScript is a standard&quot; came back from one person. &quot;Why do you like it?&quot;, he asked. I said I didn&#x27;t yet know much of anything about it, but I liked the creation of new programming languages for the browser as a basic idea. I said that choice of programming tools is a good thing, and JavaScript is clearly flawed in extremely well-understood ways. Why is there this relentless stranglehold on universal and eternal backward compatibility? What is the cost&#x2F;benefit analysis that goes into that?&lt;&#x2F;p&gt;
&lt;p&gt;Software in general, including programming languages and tools, must continually advance, and the faster, the better. Addressing backward compatibility by slowing forward progress is a losing solution. Ye olde for loop, while venerable, has been surpassed. Let it go. I agree with &lt;a href=&quot;http:&#x2F;&#x2F;www.artima.com&#x2F;weblogs&#x2F;viewpost.jsp?thread=221903&quot;&gt;Bruce Eckel&lt;&#x2F;a&gt; here in that once you have a stable version of a language, authors who &quot;don&#x27;t want to deal with those changes&quot; should be able to just continue using that version while the current version of the language advances. If you wrote a JDK 1.2 application in 2000 or so and you still want to run it, just keep running it on JDK 1.2. Don&#x27;t hold the entire java language back for the lifetime of this application which you are unwilling to maintain and advance along with the platform. Browsers should do the same thing with their javascript engines every 2 years or so. If you want to write a codebase once and not update it, ship it on custom hardware that does not include networking capability and call it a day. If you want to write software that works on the Internet, commit to a certain amount of ongoing maintenance. Even if we had perfect technical backward compatible support, the state of everything else: UX, UI, performance, data formats, protocols, continues to advance. Craigslist.org can get away with keeping the same UI for a decade because they make their own rules. Every other site needs to modernize.&lt;&#x2F;p&gt;
&lt;p&gt;I overheard a well-regarded server-side JavaScript programmer complain about CoffeeScript saying something along the lines of &quot;Why would you write a module in some language I don&#x27;t know?&quot;. WTF? How would it sound if I said that about any other server side programming language? It would sound ridiculous, because it is.&lt;&#x2F;p&gt;
&lt;p&gt;This notion that the web is ever going to be some all-purpose panacea built upon a single markup language plus a single stylesheet language plus a single programming language is a false goal. Let it go. Embrace a variety of tools. The vast majority of web sites are not built from direct HTML files. HTML is generated by dozens of highly divergent programming languages across many paradigms with all combinations of trade-offs. So it should be for the application in the browser. It&#x27;s not a problem on the server, and it&#x27;s not going to be a problem in the browser.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;rejection-of-fud&quot;&gt;Rejection of FUD&lt;&#x2F;h1&gt;
&lt;p&gt;CoffeeScript is clearly a well-thought-through and beautiful language. It brilliantly removes the terrible parts of javascript and replaces verbose javascript boilerplate with elegant expressiveness. For example, the function keyword being replaced with the gorgeous arrow symbol &lt;code&gt;-&amp;gt;&lt;&#x2F;code&gt; and the introduction of list comprehensions. It is so small and concise that there are no glaring warts to be ironed out. If you like python or ruby, you will probably like CoffeeScript better than JavaScript. If you try it for 6 months and change your mind, compile it to javascript and use that as your no codebase. No harm, no foul.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;but-but-but&quot;&gt;But but but...&lt;&#x2F;h1&gt;
&lt;p&gt;Debugging. I hear arguments that because the line numbers in the browser stack traces don&#x27;t align with the line numbers in your .coffee files, debugging is harder in CoffeeScript than JavaScript. I don&#x27;t find this a problem. I just don&#x27;t. My variable and function names are preserved. I anchor to a function name and my functions don&#x27;t get exceedingly long. I see that in the browser the error is a few lines down in the &quot;loadUserList&quot; function and I go look at the &quot;loadUserList&quot; function in CoffeeScript and find the problem without a lot of fuss. In my experience there&#x27;s just no impact here. I don&#x27;t really know why people bring this up so often. It&#x27;s just a non-problem for me. We use multiple preprocessing techniques including concatenating JS files together. Line numbers just ain&#x27;t never gonna map back to actual files in your editor. Get over it.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;in-case-you-didn-t-know&quot;&gt;In case you didn&#x27;t know&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;Multiline strings. OMFG yes, duh.&lt;&#x2F;li&gt;
&lt;li&gt;String interpolation. So nice. So nice.&lt;&#x2F;li&gt;
&lt;li&gt;Destructuring assignment. Coming to JavaScript circa 2019...&lt;&#x2F;li&gt;
&lt;li&gt;No 8-letter function keyword. These days we&#x27;re building anonymous functions every 4 lines or so. Skinny arrow FTW!&lt;&#x2F;li&gt;
&lt;li&gt;Array literals with newlines and no commas. I&#x27;m not sure what the obsession with commas is, but people, we don&#x27;t need them. I don&#x27;t even think we need them in one-line lists. We have the mighty space character, which is how you eye parses code already! If we can do &lt;code&gt;[1, 2, 3, 4]&lt;&#x2F;code&gt;, then why can&#x27;t we just do &lt;code&gt;[1 2 3 4]&lt;&#x2F;code&gt;?&lt;&#x2F;li&gt;
&lt;li&gt;Automatic IIFE wrapper. No var keyword. Default values for function parameters. Die boilerplate, die!&lt;&#x2F;li&gt;
&lt;li&gt;It&#x27;s like skinny dipping, but for punctuation! Look at some Jasmine or Mocha BDD tests in JavaScript and then CoffeeScript. One looks like a garbled mess of crap with line after dedented line of &lt;code&gt;}); }); });&lt;&#x2F;code&gt;, and one looks like poetry.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Here&#x27;s a sample BDD spec in CoffeeScript from &lt;a href=&quot;https:&#x2F;&#x2F;raw.github.com&#x2F;gist&#x2F;1379251&#x2F;274de0e881eb736ebf04657c3c1955a00475836a&#x2F;4_math_spec.coffee&quot;&gt;a gist on github&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;describe &amp;#x27;Math:&amp;#x27;, -&amp;gt;
  describe &amp;#x27;fib()&amp;#x27;, -&amp;gt;
    it &amp;#x27;should calculate the numbers correctly up to fib(16)&amp;#x27;, -&amp;gt;
      fib = [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987]
      expect(Math.fib(i)).toEqual fib[i] for i in [0..16]

  describe &amp;#x27;uuid()&amp;#x27;, -&amp;gt;
    it &amp;#x27;should have the proper UUID format&amp;#x27;, -&amp;gt;
      expect(Math.uuid()).toMatch &amp;#x2F;[A-Z0-9]{8}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{13}&amp;#x2F;

    it &amp;#x27;should have always the numer 4 at position 14&amp;#x27;, -&amp;gt;
      expect(Math.uuid()).toMatch &amp;#x2F;[A-Z0-9]{8}-[A-Z0-9]{4}-4[A-Z0-9]{3}-[A-Z0-9]{4}-[A-Z0-9]{13}&amp;#x2F;
      expect(Math.uuid()).toMatch &amp;#x2F;[A-Z0-9]{8}-[A-Z0-9]{4}-4[A-Z0-9]{3}-[A-Z0-9]{4}-[A-Z0-9]{13}&amp;#x2F;
      expect(Math.uuid()).toMatch &amp;#x2F;[A-Z0-9]{8}-[A-Z0-9]{4}-4[A-Z0-9]{3}-[A-Z0-9]{4}-[A-Z0-9]{13}&amp;#x2F;

    it &amp;#x27;should generate a unique uuid for 1000 generated uuids at least&amp;#x27;, -&amp;gt;
      uuids = []
      counter = 0

      while counter &amp;lt; 1000
        uuids.push Math.uuid()
        counter++

      expect(uuids.length).toEqual _.uniq(uuids).length
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;And here&#x27;s a &lt;a href=&quot;https:&#x2F;&#x2F;raw.github.com&#x2F;gist&#x2F;1680082&#x2F;7180717961b2938f00acedbff14b7192561ac2cc&#x2F;gistfile1.js&quot;&gt;similar jasmine spec in JavaScript&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;describe(&amp;quot;NotesView&amp;quot;, function() {
  beforeEach(function() {
    var notes = new NotesCollection();
    spyOn(notes, &amp;quot;fetch&amp;quot;);

    var view = new NotesView({collection: notes});
  });

  describe(&amp;quot;#initialize&amp;quot;, function() {
    it(&amp;quot;should fetch the notes&amp;quot;, function() {
      expect(notes.fetch).toHaveBeenCalled();
    });

    describe(&amp;quot;on notes fetch success&amp;quot;, function() {
      beforeEach(function() {
        var request = mostRecentAjaxRequest();
        request.response({
          status: 200,
          responseText: JSON.stringify([
            {body: &amp;quot;Blog post #1&amp;quot;, id: &amp;quot;1&amp;quot;}
          ])
        });
      });

      it(&amp;quot;should render the view&amp;quot;, function() {
        expect($(view.el).find(&amp;quot;.post&amp;quot;)).toHaveText(&amp;quot;Blog post #1&amp;quot;);
      });
    });
  });
});
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Ouch, my eyes! All those closing punctuation groups!&lt;&#x2F;p&gt;
&lt;p&gt;Reduction of boilerplate and increasing expressiveness are what I want from a language. CoffeeScript delivers strongly in these areas. It compiles to JavaScript and works right now in all browsers and in node.js.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;conclusion&quot;&gt;Conclusion&lt;&#x2F;h1&gt;
&lt;p&gt;It&#x27;s a polyglot world. Every app is going to use more than one programming language. Work toward making the programming languages we use better instead of trying to shame us into continuing to use languages as better alternatives are created. Shame on you. Move forward.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Noding for Sococo</title>
          <pubDate>Tue, 21 Feb 2012 04:17:33 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2012/02/noding-for-sococo/</link>
          <guid>https://peterlyons.com/problog/2012/02/noding-for-sococo/</guid>
          <description xml:base="https://peterlyons.com/problog/2012/02/noding-for-sococo/">&lt;p&gt;So I&#x27;ve taken a new full-time position as a Web Developer at &lt;a href=&quot;http:&#x2F;&#x2F;sococo.com&quot;&gt;Social Communications Company&lt;&#x2F;a&gt;. We make a virtual office communication tool that combines an interactive office map with chat, voice, and meeting features. We are adding a new web client to complement the existing native clients for Mac OS and Windows, and using node.js and HTML5. As a long-time user (sufferer?) of those frustrating conference call phones you find sprinkled throughout corporate America, at my first remote stand-up meeting I could see why this is a much better tool. Aside from the fact that conference speaker phones suffer from audio quality that is bad more often than it is good, it really only works if the remote people recognize all the on-site people by voice. If you are meeting wih unfamiliar people, you have no clue who is saying what. It&#x27;s a big barrier to clear communication and a point of frustration. Add to that the quiet talkers, people not speaking into the microphone, and people trying to slide the phone around the table (which creates a wall of noise for the remote listeners) and talk at the same time, and it&#x27;s pretty much failville. I remember often wishing everyone would just go back to their cubes and dial in individually so at least I could hear them. With Sococo, everyone is at their desk with nicely isolated audio, and everyone is represented on screen. The avatars indicate who is speaking and are labeled with names so it&#x27;s always clear who is talking. Since everyone is always online in the virtual office, it&#x27;s a very low-friction environment for quick voice or text chats about topics as they come up between however many people are necessary. There&#x27;s no long dial-in number and infuriating IVR to deal with. (IVR is Interactive Voice Response, the industry term for phone menus where a voice tells you to dial numbers to navigate through a series of menu choices).&lt;&#x2F;p&gt;
&lt;p&gt;I&#x27;m really excited to be doing node.js work professionally now. I was cleaning out my &lt;code&gt;~&#x2F;Downloads&lt;&#x2F;code&gt; folder last week and found I had downloaded node.js version 0.3.8 on Feb 4, 2011, so I&#x27;ve been working with node.js more than a year now and I find it a really fun environment in which to work. I think node.js is going to play an important role in the toolbox of web developers in the next few years, and I&#x27;m glad to be here on the early side of things.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Node Summit</title>
          <pubDate>Tue, 31 Jan 2012 00:06:04 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2012/01/node-summit/</link>
          <guid>https://peterlyons.com/problog/2012/01/node-summit/</guid>
          <description xml:base="https://peterlyons.com/problog/2012/01/node-summit/">&lt;p&gt;So I flew out to San Francisco last week for the &lt;a href=&quot;http:&#x2F;&#x2F;nodesummit.com&#x2F;&quot;&gt;Node Summit&lt;&#x2F;a&gt; conference. I wasn&#x27;t sure exactly what to expect. The first event was the &lt;a href=&quot;http:&#x2F;&#x2F;nodeup.com&quot;&gt;NodeUp&lt;&#x2F;a&gt; live pod cast at the Bottom of the Hill bar. I was in the Mission immediately prior to that and it was a long walk (after 5.5 hours of walking) to get there. The crowd seemed to be almost all developers and it was surprisingly international. I got to meet some folks I admire from the Internet like &lt;a href=&quot;http:&#x2F;&#x2F;metaduck.com&#x2F;&quot;&gt;Pedro Teixeira&lt;&#x2F;a&gt;. Tim Caswell and TJ Holowaychuck (my current programming hero) were both there and Ryan Dahl eventually arrived as well. I learned an interesting thing about myself: even if you take a bar and fill it up with ubernerds, I still feel uncomfortable and count the minutes until I can make a quiet exit.&lt;&#x2F;p&gt;
&lt;p&gt;Tuesday morning I caught a shuttle from a hotel near my room to the event, which was a really nice service to have (although I guess I would have happily walked as well). The conference was a lot bigger and more extravagant than I was anticipating. Many large companies were represented including Microsoft, Ebay, Yahoo, LinkedIn, Sabre, Visa, Google, and more. The discussion panels were pretty hit or miss (mostly miss I guess), with a few being really interesting. I did a lot of networking and hopefully some of these connections will prove valuable in the near future. I went to both of the &lt;a href=&quot;http:&#x2F;&#x2F;nodejitsu.com&quot;&gt;Nodejitsu&lt;&#x2F;a&gt; workshops, which were packed beyond capacity.&lt;&#x2F;p&gt;
&lt;p&gt;Wednesday was more of the same but there was a good panel with Ryan Dahl and Brendan Eich on the future of JavaScript. I asked a question to the panel of Platform as a Service companies a question and the moderator, &lt;a href=&quot;http:&#x2F;&#x2F;blog.jolieodell.com&#x2F;&quot;&gt;Jolie O&#x27;Dell&lt;&#x2F;a&gt; said &quot;and by the way you&#x27;re nail polish is bitchin&#x27;&quot; as I returned to my seat. :-)&lt;&#x2F;p&gt;
&lt;p&gt;I didn&#x27;t manage to find any after parties via the twitters, although I probably would have opted to go home and nap anyway. I did get to hang out with the Nodejitsu crew Thursday evening though, which was fun.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Build a Consumer App With Node.js</title>
          <pubDate>Sat, 21 Jan 2012 17:57:09 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2012/01/build-a-consumer-app-with-node-js/</link>
          <guid>https://peterlyons.com/problog/2012/01/build-a-consumer-app-with-node-js/</guid>
          <description xml:base="https://peterlyons.com/problog/2012/01/build-a-consumer-app-with-node-js/">&lt;p&gt;It&#x27;s time to start &lt;a href=&quot;http:&#x2F;&#x2F;venturebeat.com&#x2F;2012&#x2F;01&#x2F;07&#x2F;building-consumer-apps-with-node&#x2F;&quot;&gt;building consumer apps with node&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Troubleshooting responsive layout with :after</title>
          <pubDate>Wed, 11 Jan 2012 00:34:06 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2012/01/troubleshooting-media-querie/</link>
          <guid>https://peterlyons.com/problog/2012/01/troubleshooting-media-querie/</guid>
          <description xml:base="https://peterlyons.com/problog/2012/01/troubleshooting-media-querie/">&lt;p&gt;So I&#x27;m working on a new design for my site and using the super-great &lt;a href=&quot;http:&#x2F;&#x2F;learnboost.github.com&#x2F;stylus&#x2F;&quot;&gt;stylus&lt;&#x2F;a&gt; CSS preprocessor along with &lt;a href=&quot;https:&#x2F;&#x2F;gist.github.com&#x2F;1549029&quot;&gt;this great gist for simple responsive layout&lt;&#x2F;a&gt; (again with stylus and the &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;visionmedia&#x2F;nib&quot;&gt;nib&lt;&#x2F;a&gt; library).&lt;&#x2F;p&gt;
&lt;p&gt;Well, I wanted to see which of my CSS3 media queries were in effect.  Here&#x27;s a simple way to do it with the :before selector.&lt;&#x2F;p&gt;
&lt;p&gt;First, put some markup in your HTML near the top, like this (using &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;visionmedia&#x2F;jade&quot;&gt;jade template language&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;body
  .content
    header
      p BUGBUG responsive layout max-width:
      h1
        a(href=&amp;quot;&amp;#x2F;&amp;quot;) Peter Lyons
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Then in your .styl stylesheet, use :after to tell which media query is active.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F; Responsive layout &amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;
@media screen and (max-width: 960px)
  header
    p
      &amp;amp;:after
        content &amp;quot;960&amp;quot;

@media screen and (max-width: 720px)
  header
    p
      &amp;amp;:after
        content &amp;quot;720&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Now load that in your browser and resize the window. The CSS will change the text in the header telling you exactly which rules are firing.  Nice!&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Tips for MongoDB Migrations in Rails</title>
          <pubDate>Tue, 27 Dec 2011 07:04:50 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2011/12/mongodb-migrations/</link>
          <guid>https://peterlyons.com/problog/2011/12/mongodb-migrations/</guid>
          <description xml:base="https://peterlyons.com/problog/2011/12/mongodb-migrations/">&lt;p&gt;I wanted to share a quick and easy method for testing Rails migrations when using the MongoDB database.  The flexibility of mongo and ruby makes this pretty straightforward. In this example, we&#x27;ll be renaming a field in our example &quot;books&quot; collection from &quot;isbn&quot; to &quot;book_number&quot;. This is a pretty common type of migration and once you get the hang of this simple case, more complex migrations follow the same pattern. First, lets generate our timestamped migration script boilerplate.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;rails generate migration rename_isbn_to_book_number
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Then we&#x27;ll edit the file that generated under &lt;code&gt;db&#x2F;migrate&lt;&#x2F;code&gt;.  We&#x27;ll split our code up into several sections as follows.&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Some class constants shared by all methods&lt;&#x2F;li&gt;
&lt;li&gt;The &lt;code&gt;self.up&lt;&#x2F;code&gt; method that does the forward migration&lt;&#x2F;li&gt;
&lt;li&gt;A helper method to set up some sample data to test forward migration&lt;&#x2F;li&gt;
&lt;li&gt;The &lt;code&gt;self.down&lt;&#x2F;code&gt; method that does the rollback&lt;&#x2F;li&gt;
&lt;li&gt;A helper method to set up some sample data (if needed) to test rollback&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;First, just define some constants that our methods will use.  We store the mongo update options hash &lt;code&gt;MultiUpdate&lt;&#x2F;code&gt; for convenience since most migration update operations want upsert false (don&#x27;t create any new documents), multi true (update all matching documents), and safe true.&lt;&#x2F;p&gt;
&lt;p&gt;Then we also define a constant for the collection name.  For the real migration, the collection is &quot;books&quot;, but for testing, we&#x27;ll create a new collection called &quot;test_books_migration&quot; as we develop our code.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;class RenameIsbnToBookNumber &amp;lt; Mongoid::Migration
  MultiUpdate = {:upsert =&amp;gt; false, :multi =&amp;gt; true, :safe=&amp;gt;true}
  Collection = db.collection(&amp;quot;books&amp;quot;) #Final production code
  Collection = db.collection(&amp;quot;test_books_migration&amp;quot;) #Just for testing on the console
  def up
  end

  def down
  end
end
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;OK, that&#x27;s our initial boilerplate.  The next step is to take a backup of our development database if there&#x27;s any data in there we don&#x27;t want to accidentally wreck.  Then we start coding a little helper method to populate our test collection with fake documents resembling what we expect to see in production, but only focusing on the fields relevant to the migration. Add this method to your migration class.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;def mock_data_for_testing_up
  3.times {|number| Collection.insert({&amp;quot;isbn&amp;quot; =&amp;gt; &amp;quot;#{number}&amp;quot;})}
end

def show_collection
  Collection.find({}).each {|_| puts _}
end
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This will create 3 dummy documents we can use for testing.  We are putting this code in a method so it&#x27;s easy to re-run as we tweak and test our migration code.  For complex migrations, many rounds of tweaking to get all the edge cases might be needed.&lt;&#x2F;p&gt;
&lt;p&gt;We can now fire up a rails console and run this code by copying and pasting the 2 class constants and the mock_data_for_testing_up method into the console and then running mock_data_for_testing_up&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ bundle exec rails console
Loading development environment (Rails 3.1.1)
irb(main):001:0&amp;gt; #Paste the following into the console
MultiUpdate = {:upsert =&amp;gt; false, :multi =&amp;gt; true, :safe=&amp;gt;true}
Collection = db.collection(&amp;quot;test_books_migration&amp;quot;) #Just for testing on the console
def mock_data_for_testing_up
  3.times {|number| Collection.insert({&amp;quot;isbn&amp;quot; =&amp;gt; &amp;quot;#{number}&amp;quot;})}
  Collection.find({}).each {|_| puts _}
end
def show_collection
  Collection.find({}).each {|_| puts _}
end
irb(main):008:0&amp;gt; mock_data_for_testing_up
mock_data_for_testing_up
MONGODB app_development[&amp;#x27;test_books_migration&amp;#x27;].insert([{&amp;quot;isbn&amp;quot;=&amp;gt;&amp;quot;0&amp;quot;, :_id=&amp;gt;BSON::ObjectId(&amp;#x27;4ef5f43b2a4397a5d7000001&amp;#x27;)}])
MONGODB app_development[&amp;#x27;test_books_migration&amp;#x27;].insert([{&amp;quot;isbn&amp;quot;=&amp;gt;&amp;quot;1&amp;quot;, :_id=&amp;gt;BSON::ObjectId(&amp;#x27;4ef5f43b2a4397a5d7000002&amp;#x27;)}])
MONGODB app_development[&amp;#x27;test_books_migration&amp;#x27;].insert([{&amp;quot;isbn&amp;quot;=&amp;gt;&amp;quot;2&amp;quot;, :_id=&amp;gt;BSON::ObjectId(&amp;#x27;4ef5f43b2a4397a5d7000003&amp;#x27;)}])
irb(main):009:0&amp;gt; show_collection
MONGODB app_development[&amp;#x27;test_books_migration&amp;#x27;].find({})
{&amp;quot;_id&amp;quot;=&amp;gt;BSON::ObjectId(&amp;#x27;4ef5f3812a4397a5bd000001&amp;#x27;), &amp;quot;isbn&amp;quot;=&amp;gt;&amp;quot;0&amp;quot;}
{&amp;quot;_id&amp;quot;=&amp;gt;BSON::ObjectId(&amp;#x27;4ef5f3812a4397a5bd000002&amp;#x27;), &amp;quot;isbn&amp;quot;=&amp;gt;&amp;quot;1&amp;quot;}
{&amp;quot;_id&amp;quot;=&amp;gt;BSON::ObjectId(&amp;#x27;4ef5f3812a4397a5bd000003&amp;#x27;), &amp;quot;isbn&amp;quot;=&amp;gt;&amp;quot;2&amp;quot;}
=&amp;gt; nil
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;So now we have a separate, well-understood test collection ready to test our simple migration.  Let&#x27;s code up our &lt;code&gt;up&lt;&#x2F;code&gt; method. To do our migration.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;def up
  #We want to rename the book.isbn field to book.book_number
  Collection.find({&amp;quot;isbn&amp;quot; =&amp;gt; {&amp;quot;$exists&amp;quot; =&amp;gt; 1}}).each do |book|
    update_op = {
      &amp;quot;$unset&amp;quot; =&amp;gt; {&amp;quot;isbn&amp;quot; =&amp;gt; 1},
      &amp;quot;$set&amp;quot; =&amp;gt; {&amp;quot;book_number&amp;quot; =&amp;gt; book[&amp;quot;isbn&amp;quot;]}
    }
  Collection.update({&amp;quot;_id&amp;quot; =&amp;gt; book[&amp;quot;_id&amp;quot;]}, update_op, MultiUpdate)
end
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;We can paste that into the console and run it to test our migration.  We can verify the results with &lt;code&gt;show_colletion&lt;&#x2F;code&gt;.  If we want to test other records for the rollback, we can create a &lt;code&gt;mock_data_for_testing_down&lt;&#x2F;code&gt; method.&lt;&#x2F;p&gt;
&lt;p&gt;This should give you a really quick way to experiment and get your migration code working.  Mongo has some advanced query and modify capabilities that can do amazing things, and an easy way to do some trial and error is handy.  If you make a mess of your test data, you can use &lt;code&gt;Collection.drop&lt;&#x2F;code&gt; to get a clean slate. Here&#x27;s the final migration code for reference. &lt;strong&gt;Don&#x27;t forget&lt;&#x2F;strong&gt; to remove the test collection constant and drop the test collection from your database when your ready to start running your code for real with &lt;code&gt;rake db:migrate&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;class RenameIsbnToBookNumber &amp;lt; Mongoid::Migration
  MultiUpdate = {:upsert =&amp;gt; false, :multi =&amp;gt; true, :safe=&amp;gt;true}
  Collection = db.collection(&amp;quot;books&amp;quot;) #Final production code

  def up
    #We want to rename the book.isbn field to book.book_number
    Collection.find({&amp;quot;isbn&amp;quot; =&amp;gt; {&amp;quot;$exists&amp;quot; =&amp;gt; 1}}).each do |book|
      update_op = {
        &amp;quot;$unset&amp;quot; =&amp;gt; {&amp;quot;isbn&amp;quot; =&amp;gt; 1},
        &amp;quot;$set&amp;quot; =&amp;gt; {&amp;quot;book_number&amp;quot; =&amp;gt; book[&amp;quot;isbn&amp;quot;]}
      }
      Collection.update({&amp;quot;_id&amp;quot; =&amp;gt; book[&amp;quot;_id&amp;quot;]}, update_op, MultiUpdate)
    end
  end

  def down
    #We want to rename the book.book_number field to book.isbn
    Collection.find({&amp;quot;book_number&amp;quot; =&amp;gt; {&amp;quot;$exists&amp;quot; =&amp;gt; 1}}).each do |book|
      update_op = {
        &amp;quot;$unset&amp;quot; =&amp;gt; {&amp;quot;book_number&amp;quot; =&amp;gt; 1},
        &amp;quot;$set&amp;quot; =&amp;gt; {&amp;quot;isbn&amp;quot; =&amp;gt; book[&amp;quot;book_number&amp;quot;]}
      }
      Collection.update({&amp;quot;_id&amp;quot; =&amp;gt; book[&amp;quot;_id&amp;quot;]}, update_op, MultiUpdate)
    end
  end

  #These methods are not called by the migration.  Just for manual testing
  #by copy&amp;#x2F;pasting into the console
  #To test (by copy&amp;#x2F;pasting from here to the console)
  #1. Set the MultiUpdate constant. Adjust &amp;quot;Collection&amp;quot; to be a test collection
  #2. Copy&amp;#x2F;paste the 2 methods below
  #2. Run mock_data_for_testing_up
  #3. Run the body of self.up
  #3b. Optionally run the body of self.up again to make sure it is idempotent
  def mock_data_for_testing_up
    3.times {|number| Collection.insert({&amp;quot;isbn&amp;quot; =&amp;gt; &amp;quot;#{number}&amp;quot;})}
  end

  def show_collection
    Collection.find({}).each {|_| puts _}
  end
end&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</description>
      </item>
      <item>
          <title>No Need for npm -g</title>
          <pubDate>Thu, 22 Dec 2011 21:14:45 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2011/12/no-need-for-npm-g/</link>
          <guid>https://peterlyons.com/problog/2011/12/no-need-for-npm-g/</guid>
          <description xml:base="https://peterlyons.com/problog/2011/12/no-need-for-npm-g/">&lt;p&gt;&lt;strong&gt;UPDATE:&lt;&#x2F;strong&gt; please see my new article on &lt;a href=&quot;&#x2F;problog&#x2F;2012&#x2F;09&#x2F;managing-per-project-interpreters-and-the-path&quot;&gt;Managing Per-project Interpreters and the PATH&lt;&#x2F;a&gt; for a new and improved take on this topic.&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;So &lt;a href=&quot;http:&#x2F;&#x2F;npmjs.org&#x2F;&quot;&gt;npm&lt;&#x2F;a&gt; has this &quot;-g&quot; switch to install &quot;global&quot; packages that bundle command line executable scripts.  I&#x27;ve been on a strict project isolation kick lately after dealing with rbenv in the ruby world, and I just don&#x27;t see any need for &lt;code&gt;npm -g&lt;&#x2F;code&gt;.  I want each project to have its own version of node, coffeescript, mocha, or whatever else I need.  Here&#x27;s my principles for a harmonious multi-project system.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;1-install-things-under-your-project-root&quot;&gt;1. Install things under your project root&lt;&#x2F;h2&gt;
&lt;p&gt;Node goes in &lt;code&gt;project&#x2F;node&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Install npm modules without &lt;code&gt;-g&lt;&#x2F;code&gt;.  &lt;code&gt;coffee&lt;&#x2F;code&gt; becomes &lt;code&gt;project&#x2F;node_modules&#x2F;.bin&#x2F;coffee&lt;&#x2F;code&gt;. &lt;code&gt;mocha&lt;&#x2F;code&gt; becomes &lt;code&gt;project&#x2F;node_modules&#x2F;.bin&#x2F;mocha&lt;&#x2F;code&gt;. And so on.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-set-your-path&quot;&gt;2. Set your PATH&lt;&#x2F;h2&gt;
&lt;p&gt;Add &lt;code&gt;.&#x2F;node&#x2F;bin:.&#x2F;node_modules&#x2F;.bin&lt;&#x2F;code&gt; and to your &lt;code&gt;PATH&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;done&quot;&gt;Done&lt;&#x2F;h2&gt;
&lt;p&gt;Here&#x27;s an example.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ mkdir project1 project2
$ cd project1
$ npm install coffee-script@1.0.1
coffee-script@1.0.1 .&amp;#x2F;node_modules&amp;#x2F;coffee-script 
$ which coffee
.&amp;#x2F;node_modules&amp;#x2F;.bin&amp;#x2F;coffee
$ coffee --version
CoffeeScript version 1.0.1
$ cd ..&amp;#x2F;project2
$ npm install coffee-script@1.2.0
coffee-script@1.2.0 .&amp;#x2F;node_modules&amp;#x2F;coffee-script 
$ which coffee
.&amp;#x2F;node_modules&amp;#x2F;.bin&amp;#x2F;coffee
$ coffee --version
CoffeeScript version 1.2.0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Same principle works for &lt;code&gt;node&lt;&#x2F;code&gt; and any scripts you get from npm modules.&lt;&#x2F;p&gt;
&lt;p&gt;While we&#x27;re talking about &lt;code&gt;PATH&lt;&#x2F;code&gt;, here&#x27;s how I set my &lt;code&gt;PATH&lt;&#x2F;code&gt; in my &lt;code&gt;~&#x2F;.zshrc&lt;&#x2F;code&gt;.  It&#x27;s nice because I can throw a bunch of crap in there that may or may not exist on any given computer and only directories that exist get into my &lt;code&gt;PATH&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;########## PATH ##########
PATH=
addPath() {
  if [ -d &amp;quot;${1}&amp;quot; ]; then
    export PATH=$PATH:&amp;quot;${1}&amp;quot;
  fi
}
addPath .&amp;#x2F;node_modules&amp;#x2F;.bin
addPath .&amp;#x2F;node&amp;#x2F;bin
#Repeat addPath lines for each directory...
export PATH
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;security-caveat-about-relative-paths-in-path&quot;&gt;Security Caveat About Relative Paths in PATH&lt;&#x2F;h2&gt;
&lt;p&gt;Having relative directory paths in your &lt;code&gt;PATH&lt;&#x2F;code&gt; is arguably a &lt;a href=&quot;http:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;mac&#x2F;#documentation&#x2F;opensource&#x2F;conceptual&#x2F;shellscripting&#x2F;ShellScriptSecurity&#x2F;ShellScriptSecurity.html&quot;&gt;security vulnerability&lt;&#x2F;a&gt;. &lt;a href=&quot;https:&#x2F;&#x2F;www.securecoding.cert.org&#x2F;confluence&#x2F;pages&#x2F;worddav&#x2F;preview.action?pageId=3524&amp;amp;fileName=Environment+Variables+v3.pdf&quot;&gt;See also slide 20 here&lt;&#x2F;a&gt;.  I&#x27;m not personally too concerned about this one for my personal interactive login shell.  However, this practice is probably not suitable for shell scripts that are run as programs.  Also note that in a production deployment you should launch your &lt;code&gt;node&lt;&#x2F;code&gt; or &lt;code&gt;coffee&lt;&#x2F;code&gt; executable via an absolute path when coding your &lt;a href=&quot;http:&#x2F;&#x2F;upstart.ubuntu.com&#x2F;&quot;&gt;upstart&lt;&#x2F;a&gt; or SysV init script.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>The destiny of complex languages</title>
          <pubDate>Sun, 04 Dec 2011 01:33:01 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2011/12/complex-language/</link>
          <guid>https://peterlyons.com/problog/2011/12/complex-language/</guid>
          <description xml:base="https://peterlyons.com/problog/2011/12/complex-language/">&lt;p&gt;I had an interesting thought about programming languages and complexity yesterday.  I think of languages like Python and CoffeeScript (and even C &lt;sup&gt;&lt;a href=&quot;https:&#x2F;&#x2F;peterlyons.com&#x2F;problog&#x2F;2011&#x2F;12&#x2F;complex-language&#x2F;#endnote1&quot;&gt;1&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt; aim to be small and consistent as a language.  Things like Perl and Ruby are much more complex.  I have been studying both Ruby and CoffeeScript for a while  (I think about a year for Ruby and maybe 6 months for CoffeeScript guessing from unreliable memory), and I feel like my JavaScript&#x2F;CoffeeScript knowledge is solid and with Ruby I&#x27;m mired in intermediate status. However, the fact is I write lots of code in each of these languages.  So what does that indicate?&lt;&#x2F;p&gt;
&lt;p&gt;It seems to me that the more complex your language, the larger portion of the total extant codebase written in that language will have been authored by programmers that do not completely understand the language.  I would venture that if you took all existing Perl code, about 80% of it was written by programmers or sysadmins with cursory knowledge of the language.  And within the Perl community, finding truly competent programmers with solid mastery of the language is definitely the exception, not the rule.  I think the combination of ruby&#x27;s largeness and complexity coupled with the huge popularity of rails means the same is true for Ruby.  But I don&#x27;t think there&#x27;s anything about Perl or Ruby that makes it this way other than just it&#x27;s the nature of having complexity in your language.&lt;&#x2F;p&gt;
&lt;p&gt;I&#x27;m solidly in the camp of tiny, simple languages with programs authored by programmers with mastery, and this post has been interesting food for thought for me on this topic.&lt;&#x2F;p&gt;
&lt;hr&gt;
&lt;aside id=&quot;endnote1&quot;&gt;
1: End Note on C
&lt;p&gt;While C as a language is usually considered small and simple, the POSIX interfaces you need are so old and devilishly tricky that I wouldn&#x27;t trust C code written by beginner or intermediate C programmers.&lt;&#x2F;p&gt;
&lt;&#x2F;aside&gt;
</description>
      </item>
      <item>
          <title>Hands off my window title!</title>
          <pubDate>Tue, 29 Nov 2011 12:23:46 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2011/11/hands-off-my-window-title/</link>
          <guid>https://peterlyons.com/problog/2011/11/hands-off-my-window-title/</guid>
          <description xml:base="https://peterlyons.com/problog/2011/11/hands-off-my-window-title/">&lt;p&gt;So I recently ssh&#x27;ed into a shiny new Ubuntu 11.10 server on EC2 and noticed some new things.  Firstly, Ubuntu seems to have enabled the &lt;a href=&quot;https:&#x2F;&#x2F;help.ubuntu.com&#x2F;11.10&#x2F;serverguide&#x2F;C&#x2F;byobu.html&quot;&gt;byobu&lt;&#x2F;a&gt; terminal multiplexer configuration by default.  This looks potentially handy, but just like &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;robbyrussell&#x2F;oh-my-zsh&quot;&gt;Oh my zsh&lt;&#x2F;a&gt; I don&#x27;t feel motivated to futz with it just now.  It&#x27;s easy to disable byobu with just a quick &lt;code&gt;byobu-disable&lt;&#x2F;code&gt;, which will, somewhat surprisingly, fully exit your shell, log you out and close your ssh session.  But next time you&#x27;ll get a normal shell instead of a byobu&#x2F;screen session.&lt;&#x2F;p&gt;
&lt;p&gt;Now one thing I noticed and finally got motivated with to &quot;research&quot; (and by that I mean ask smarter people on twitter) and fix was that when I connected via ssh, the window title in my iTerm2 tab was dynamically changed from what I had carefully set it to (&quot;asset pipeline&quot;) to the supremely unhelpful &quot;ubuntu@ip-10-11-12-13&quot;.  In some non-cloud situations where the server has a meaningful hostname, this might be handy, but distinguishing dozens of servers by their internal EC2 IP is not appealing to me.  Initial research suggested my &lt;code&gt;PROMPT&lt;&#x2F;code&gt; or &lt;code&gt;PROMPT_COMMAND&lt;&#x2F;code&gt; environment variables, but neither was set.  I read through &lt;a href=&quot;http:&#x2F;&#x2F;www.ibm.com&#x2F;developerworks&#x2F;linux&#x2F;library&#x2F;l-tip-prompt&#x2F;&quot;&gt;this DeveloperWorks article&lt;&#x2F;a&gt; and found the responsible code in the &lt;code&gt;~&#x2F;.bashrc&lt;&#x2F;code&gt; file.  Here&#x27;s the offending excerpt.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# If this is an xterm set the title to user@host:dir
case &amp;quot;$TERM&amp;quot; in
xterm*|rxvt*)
    PS1=&amp;quot;\[\e]0;${debian_chroot:+($debian_chroot)}\u@\h: \w\a\]$PS1&amp;quot;
    ;;
*)
    ;;
esac
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;So you can comment out that line that changes &lt;code&gt;PS1&lt;&#x2F;code&gt; if you prefer your own manually-set window title.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Linkzie gets a stack upgrade</title>
          <pubDate>Thu, 24 Nov 2011 08:17:08 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2011/11/linkzie-gets-a-stack-upgrade/</link>
          <guid>https://peterlyons.com/problog/2011/11/linkzie-gets-a-stack-upgrade/</guid>
          <description xml:base="https://peterlyons.com/problog/2011/11/linkzie-gets-a-stack-upgrade/">&lt;p&gt;So &lt;a href=&quot;https:&#x2F;&#x2F;linkzie.com&quot;&gt;Linkzie&lt;&#x2F;a&gt; has had a nice series of updates to the underlying software that runs it.  It&#x27;s now pretty much bleeding edge latest and greatest.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Ubuntu 11.10&lt;&#x2F;li&gt;
&lt;li&gt;rbenv&lt;&#x2F;li&gt;
&lt;li&gt;Ruby 1.9.3-p0&lt;&#x2F;li&gt;
&lt;li&gt;Rails 3.1.3&lt;&#x2F;li&gt;
&lt;li&gt;Unicorn 4.1.1&lt;&#x2F;li&gt;
&lt;li&gt;jQuery 1.7&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;It was an interesting process.  I just went through a pretty challenging upgrade from Ruby 1.8.7 to 1.9.3 at work and it was pretty destabilizing and chaotic.  So for this one I went slow and steady, changing one thing at a time, and keeping the app running and tests passing.  First I left ruby at 1.8.7 and switched from the Ubuntu ruby to rbenv without changing anything else.  Then I upgraded some of the supporting gems like Capistrano without messing with the key gems (rails).  Then I upgraded to Rails 3.1.1 and got that working.  Then I did a carte blanche bundle update and pulled everything else up to latest.  Only then did I switch to ruby 1.9.3.&lt;&#x2F;p&gt;
&lt;p&gt;Ruby 1.9.3 still has bugs in the debugger, which I find a bit shocking, but I&#x27;m limping along with &quot;pry&quot; as a poor substitute.  I&#x27;m a bit shocked by the issues with Ruby 1.9.2 and 1.9.3 given how old the project is.  I don&#x27;t remember any actual bugs going from Python 2.4 all the way up to 2.7, just the normal code evolution stuff.&lt;&#x2F;p&gt;
&lt;p&gt;In any case, if Linkzie had any users, in theory it might be loading and responding faster for them.  I learned a lot in the process and got my staging server VM to very closely mirror production, which is good.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Thoughts on launching and scaling quickly</title>
          <pubDate>Mon, 24 Oct 2011 22:10:40 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2011/10/thoughts-on-launching-and-scaling-quickly/</link>
          <guid>https://peterlyons.com/problog/2011/10/thoughts-on-launching-and-scaling-quickly/</guid>
          <description xml:base="https://peterlyons.com/problog/2011/10/thoughts-on-launching-and-scaling-quickly/">&lt;p&gt;I have &lt;a href=&quot;http:&#x2F;&#x2F;dojo4.com&#x2F;blog&#x2F;thoughts-on-launching-and-scaling-quickly&quot;&gt;new blog post&lt;&#x2F;a&gt; over on the &lt;a href=&quot;http:&#x2F;&#x2F;dojo4.com&quot;&gt;Dojo4&lt;&#x2F;a&gt; web site.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>New Linkzie Release: Nice UI improvements</title>
          <pubDate>Sat, 30 Jul 2011 21:09:10 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2011/07/linkzie-ui-improvements/</link>
          <guid>https://peterlyons.com/problog/2011/07/linkzie-ui-improvements/</guid>
          <description xml:base="https://peterlyons.com/problog/2011/07/linkzie-ui-improvements/">&lt;p&gt;I just pushed out a new release of &lt;a href=&quot;https:&#x2F;&#x2F;linkzie.com&quot;&gt;&lt;&#x2F;a&gt;.  There is no longer any need to have an &quot;Organize your links&quot; button.  You can always organize you categories by dragging them around by their name.  You can always organize your links by dragging them by a little crosshairs icon that is displayed to the right of each link when you mouse over it.  The commands to edit or delete items are now in a little pop-up context menu to the right of each link and they are more intuitive to use.&lt;&#x2F;p&gt;
&lt;p&gt;I think this UI makes Linkzie very easy to use and the code is much simpler now and the UI should respond more snappily.  Try it out and let me know what you think!&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Web Framework Woes</title>
          <pubDate>Tue, 05 Jul 2011 23:10:46 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2011/07/web-framework-woes/</link>
          <guid>https://peterlyons.com/problog/2011/07/web-framework-woes/</guid>
          <description xml:base="https://peterlyons.com/problog/2011/07/web-framework-woes/">&lt;p&gt;So my attitude for most of this year has been one of reserving judgement in favor of direct experience. As part of this, I have been trying all the latest &quot;Kool-Aid&quot; web development technologies even if at first glance they didn&#x27;t sit right with me. So I started building the &lt;a href=&quot;http:&#x2F;&#x2F;othenticate.com&quot;&gt;Othenticate&lt;&#x2F;a&gt; infrastructure on a shiny stack of mongodb, node.js, express, mongoose, jade, and stylus. For the most part, I love this stack. However, after I while I have come to the conclusion that for a micropreneur project such as mine, traditional RDBMS (PostgreSQL) is going to be more appropriate than dealing with the trade-offs that NoSQL is making. I won&#x27;t need to scale to millions of objects, and I want ACID, a schema, and mature tools at the data layer. So I&#x27;ve decided I want to use PostgreSQL instead of MongoDB. However, along with this came the realization that it seems that there is no mature Object-Relational Mapper (ORM) on the node stack for PostgreSQL. There also does not seem to be a mature schema migration framework. Basically all of the other elements of the node stack are fantastic to work with, but these two might just be deal breakers for me.&lt;&#x2F;p&gt;
&lt;p&gt;So I started thinking along the lines of another stack I might use if I wanted a good PostgreSQL ORM and schema migrator. There seem to be two obvious candidates: Django and Rails, and maybe some smaller ones and probably the option of cobbling together a hybrid stack of python components. In contemplating learning another web development framework (Django) or doing another Rails project, I started doing some analysis. Below are some of my thoughts on these stacks.&lt;&#x2F;p&gt;
&lt;p&gt;First, Node.js and friends. I&#x27;ve been coding in node.js&#x2F;express for a while and for the most part finding it fantastic. The frameworks are cleanly designed and easy to understand and work with. The environment is in general pretty straightforward (compared to rails). Node starts up instantly and has fantastic debugging support that work seamlessly with chrome&#x27;s debugger via the node-inspector module. I can graphically debug the client side code in the browser, the server side code, my jasmine tests, even my server side node.js command line unit tests can be debugged in chrome. I LOVE the fact that I can re-use my model classes across the browser and server. The only real pain points are the lack of ORM, schema migrator, and the hassle of async callbacks (which is mild but it&#x27;s still a hassle). Jade for templating with stylus for CSS is almost perfect.&lt;&#x2F;p&gt;
&lt;p&gt;I&#x27;ve also been working in rails for a while now. Overall the node&#x2F;express environment feels like a better match for me. Ruby has been influenced by Perl and Perl is utter anathema to me. The fact that Rails is written in Ruby is pretty much a kiss of death for me. However, I do love ActiveRecord and the rails schema migration subsystem. But that&#x27;s about it. Rails templating has decent choices although Jade is the clear king in my mind. I don&#x27;t care much for the Rails controller model and routing mechanism either. But perhaps the paramount rails stopping point for me is just the way the community operates. It&#x27;s a mess of gems and fads and hipsterism and uncontrolled software erosion. The documentation can&#x27;t keep up with it so you end up in a sea of partially-clueful blog posts. My overall reaction to this just ends up being &quot;this doesn&#x27;t feel right&quot;.&lt;&#x2F;p&gt;
&lt;p&gt;So I&#x27;ve started to give serious consideration to Django. I do like python quite a lot. I was hoping Ruby would be the next major language I work in extensively, but I don&#x27;t think I&#x27;m going to go down that path much farther. Working with CoffeeScript has made python feel much less elegant to me, but I can deal with it. I hear good things about the South subsystem in Django and the docs look good. I really don&#x27;t want to recode my whole current effort though. So these days I&#x27;m torn. Here&#x27;s a summary of my reactions in bullets.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;node-js-express-etc-the-good&quot;&gt;Node.js&#x2F;Express&#x2F;Etc - the good&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;Love CoffeeScript&lt;&#x2F;li&gt;
&lt;li&gt;Express&#x2F;Connect are elegant. They are libraries you use, not a framework you must inject your code into&lt;&#x2F;li&gt;
&lt;li&gt;Jade and Stylus are rapidly approaching Nirvana status&lt;&#x2F;li&gt;
&lt;li&gt;Debugging support is top-notch&lt;&#x2F;li&gt;
&lt;li&gt;The functional programming sits pretty well with me&lt;&#x2F;li&gt;
&lt;li&gt;High concurrency through solid design is pretty sweet&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;node-js-express-etc-the-bad&quot;&gt;Node.js&#x2F;Express&#x2F;Etc - the bad&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;No good ORM for PostgreSQL&lt;&#x2F;li&gt;
&lt;li&gt;No good schema migrator for PostgreSQL&lt;&#x2F;li&gt;
&lt;li&gt;Event model means stack traces are often useless&lt;&#x2F;li&gt;
&lt;li&gt;No intelligent reloading of changed code on the fly&lt;&#x2F;li&gt;
&lt;li&gt;The async stuff, while manageable, is clearly a hinderance compared to synchronous code. It does force you to be a little more considered than you might otherwise be, though&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;rails-the-good&quot;&gt;Rails - the good&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;ActiveRecord is awesome&lt;&#x2F;li&gt;
&lt;li&gt;Good schema migrator&lt;&#x2F;li&gt;
&lt;li&gt;Lots of handy gems&lt;&#x2F;li&gt;
&lt;li&gt;Culture of thorough testing&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;rails-the-bad&quot;&gt;Rails - the bad&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;Ruby is a kitchen sink language&lt;&#x2F;li&gt;
&lt;li&gt;Not quite optimized for modern web apps&lt;&#x2F;li&gt;
&lt;li&gt;Docs and blog culture don&#x27;t suite me&lt;&#x2F;li&gt;
&lt;li&gt;A big freaking mess with ruby versions, RVM, gems, bundles&lt;&#x2F;li&gt;
&lt;li&gt;Not javascript means code duplication&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;django-the-predicted-good&quot;&gt;Django - the predicted good&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;South&#x2F;ORM&#x2F;Schema are supposedly very good&lt;&#x2F;li&gt;
&lt;li&gt;Python&lt;&#x2F;li&gt;
&lt;li&gt;Docs look good&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;django-the-predicted-bad&quot;&gt;Django - the predicted bad&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;The python community has a long history of web frameworks that don&#x27;t catch on. Django seems to be the winner, but I am still skeptical&lt;&#x2F;li&gt;
&lt;li&gt;Not JavaScript means code duplication&lt;&#x2F;li&gt;
&lt;li&gt;Seems like there might not be any intelligent autoreloading of code during development&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</description>
      </item>
      <item>
          <title>Linkzie 0.7.1 released</title>
          <pubDate>Tue, 05 Jul 2011 07:04:28 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2011/07/linkzie-0-7-1-released/</link>
          <guid>https://peterlyons.com/problog/2011/07/linkzie-0-7-1-released/</guid>
          <description xml:base="https://peterlyons.com/problog/2011/07/linkzie-0-7-1-released/">&lt;p&gt;So after a fairly lengthy period of inattention to my &lt;a href=&quot;https:&#x2F;&#x2F;linkzie.com&quot;&gt;Linkzie&lt;&#x2F;a&gt; bookmark manager, I got frustrated on my current project and turned my focus to Linkzie today for some updates. There&#x27;s a new menu bar shamelessly stolen from the new google bar. A brand new &quot;beach house&quot; color theme has been applied. The &quot;organize&quot; UI has been changed significantly. Lots of other minor UX tweaks have been made. For example, you can now click anywhere on the entire row for a link as opposed to before where you had to click on the actual link text itself. Perhaps the biggest improvement is session expiration detection. Since linkzie is designed to sit open in a browser tab for long periods of time, it was possible for a user&#x27;s server side session to expire with the window open. The UI would let them attempt to make changes, only to fail with a login required situation when trying to save their changes. This resulted in lost work and an error message popping up. Now linkzie will poll the server periodically which will likely just keep your session alive longer, but if it does expire, the UI is updated to reflect that. You can still use your links, but all editing operations are disabled and you are notified to sign in again if you need to make any edits.&lt;&#x2F;p&gt;
&lt;p&gt;Have a look and let me know what you think!&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Commander: Utility Automation</title>
          <pubDate>Sat, 02 Jul 2011 04:24:54 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2011/07/commander/</link>
          <guid>https://peterlyons.com/problog/2011/07/commander/</guid>
          <description xml:base="https://peterlyons.com/problog/2011/07/commander/">&lt;p&gt;Check out my new little automation utility belt app for the keyboard-centric power users. It is hosted on a &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;focusaurus&#x2F;commander&quot;&gt;github project here&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>MicroConf</title>
          <pubDate>Sun, 05 Jun 2011 09:11:46 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2011/06/microconf/</link>
          <guid>https://peterlyons.com/problog/2011/06/microconf/</guid>
          <description xml:base="https://peterlyons.com/problog/2011/06/microconf/">&lt;p&gt;Just a quick note that I&#x27;m off to &lt;a href=&quot;http:&#x2F;&#x2F;www.microconf.com&quot;&gt;MicroConf&lt;&#x2F;a&gt; in Las Vegas tomorrow. A buddy of mine from Denver Hack Nite is also going. It should be a really fun time and hopefully I&#x27;ll learn a lot. I&#x27;ve been coding away like a madman (an excruciating 4-6 hours a day!) preparing a little proof-of-concept for the startup idea I&#x27;m working on. Can&#x27;t wait to have it scutinized and hopefully not completely blasted to bits. I&#x27;ll be blogging and &lt;a href=&quot;https:&#x2F;&#x2F;twitter.com&#x2F;#!&#x2F;focusaurus&quot;&gt;tweeting&lt;&#x2F;a&gt; if there&#x27;s anything interesting and I&#x27;m not too busy concentrating.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Leveling Up: Career Advancement for Software Developers</title>
          <pubDate>Fri, 06 May 2011 00:56:18 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2011/05/leveling-up/</link>
          <guid>https://peterlyons.com/problog/2011/05/leveling-up/</guid>
          <description xml:base="https://peterlyons.com/problog/2011/05/leveling-up/">&lt;p&gt;I have written a piece to help software developers accelerate their career. It lives on its own page, &lt;a href=&quot;&#x2F;leveling_up&quot;&gt;over here&lt;&#x2F;a&gt;, but please share your comments on this blog post. There are some other comments &lt;a href=&quot;http:&#x2F;&#x2F;news.ycombinator.com&#x2F;item?id=2518519&quot;&gt;on Hacker News&lt;&#x2F;a&gt; as well.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Great Design: Fat Brain Toys Order Confirmation</title>
          <pubDate>Mon, 02 May 2011 23:04:19 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2011/05/great-design-fat-brain-toys-order-confirmation/</link>
          <guid>https://peterlyons.com/problog/2011/05/great-design-fat-brain-toys-order-confirmation/</guid>
          <description xml:base="https://peterlyons.com/problog/2011/05/great-design-fat-brain-toys-order-confirmation/">&lt;p&gt;&lt;a href=&quot;http:&#x2F;&#x2F;www.fatbraintoys.com&#x2F;&quot;&gt;Fat Brain Toys&lt;&#x2F;a&gt; has the best-looking order confirmation page I have seen. Click the image for a larger version.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;problog&#x2F;images&#x2F;2011&#x2F;fat_brain_confirmation_redacted_small.png&quot; alt=&quot;Fat Brain Toys order confirmation&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Good node.js things are happenning</title>
          <pubDate>Wed, 13 Apr 2011 10:38:39 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2011/04/good-node-js-things/</link>
          <guid>https://peterlyons.com/problog/2011/04/good-node-js-things/</guid>
          <description xml:base="https://peterlyons.com/problog/2011/04/good-node-js-things/">&lt;p&gt;I hope to post some details on this soon, but just a note that my ongoing node.js hacking is turning up some real gems. I migrated my application level tests from Zombie to Phantom with great results. phantom.js is pretty rad. Also got remote graphical debugging in the browser working and learned how to properly drop in a REPL for quick and dirty investigations, which is cool. Maybe on Thursday I&#x27;ll have time to post some of the details, but at this point my node.js stack is pretty fierce.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Node docs vs. Rails docs</title>
          <pubDate>Tue, 05 Apr 2011 21:15:51 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2011/04/node-docs-vs-rails-docs/</link>
          <guid>https://peterlyons.com/problog/2011/04/node-docs-vs-rails-docs/</guid>
          <description xml:base="https://peterlyons.com/problog/2011/04/node-docs-vs-rails-docs/">&lt;p&gt;I&#x27;ve been working with &lt;a href=&quot;http:&#x2F;&#x2F;nodejs.org&quot;&gt;Node.js&lt;&#x2F;a&gt; a lot recently and enjoying it. However, I&#x27;m not sure entirely WHY I am enjoying it. I&#x27;m writing it in &lt;a href=&quot;http:&#x2F;&#x2F;jashkenas.github.com&#x2F;coffee-script&#x2F;&quot;&gt;CoffeeScript&lt;&#x2F;a&gt;, which is pleasant enough, but it doesn&#x27;t change the fact that javascript is lacking is basic language&#x2F;library features like a Set object or hashes with non-string keys and so forth. The other day I realized part of my good feelings about node are from my experience with the docs, which normally goes as follows.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Each project has a single authoritative home page
&lt;ul&gt;
&lt;li&gt;often the main github repo or a unique domain that just links to github&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;The docs are usually a 1-pager that explains all you need to get started&lt;&#x2F;li&gt;
&lt;li&gt;The examples are clear enough&lt;&#x2F;li&gt;
&lt;li&gt;That&#x27;s all it takes. You can get up and running with that.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;I really enjoy that aspect of this. Case studies include &lt;a href=&quot;http:&#x2F;&#x2F;documentcloud.github.com&#x2F;underscore&#x2F;&quot;&gt;underscore&lt;&#x2F;a&gt;, &lt;a href=&quot;http:&#x2F;&#x2F;documentcloud.github.com&#x2F;backbone&#x2F;&quot;&gt;backbone&lt;&#x2F;a&gt;, &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;visionmedia&#x2F;jade&quot;&gt;jade&lt;&#x2F;a&gt;, &lt;a href=&quot;http:&#x2F;&#x2F;jashkenas.github.com&#x2F;coffee-script&#x2F;&quot;&gt;coffeescript&lt;&#x2F;a&gt;, and &lt;a href=&quot;http:&#x2F;&#x2F;expressjs.com&#x2F;guide.html&quot;&gt;express&lt;&#x2F;a&gt;. Also, the docs are the right length (compared to rails). They aren&#x27;t one-off blog posts on how to do X, nor are they a book-length comprehensive tutorial that builds an app from scratch. They are both full API references and guides coupled with enough tiny snippets to get you going.&lt;&#x2F;p&gt;
&lt;p&gt;Now rails on the other hand, and this might be my own fault, seems to be problematic here. For one, the gems you need often don&#x27;t have enough documentation to get up and running on their github project. You are forced out into the harsh unpleasant jungle of umpteen out of date, contradictory or incorrect blog posts. One nice thing about having the main docs on github (in node land) is that all the info there is guaranteed to be current and the outdated stuff is not there (but it is still in the git history if you need it). The problem with information rot on rails related blog posts and screencasts is really annoying. Rails DOES have nice docs on the other end of the spectrum, the comprehensive gigantic tutorial, but for whatever reason these days I never read those end to end and I usually just get myself into trouble with impatient skimming. If I&#x27;m going to spend hours and hours reading something, I probably still prefer to get a physical book than read a gigantic web based tutorial.&lt;&#x2F;p&gt;
&lt;p&gt;This is just one taste of the different experience working with these two technologies. I still think I&#x27;ll continue to view rails as basically the top web development framework out there (unless I try Django, maybe, but I learned rails first to force me to learn ruby), but there are definitely aspects of the rails ecosystem that rub me the wrong way.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Baby steps in node.js</title>
          <pubDate>Thu, 24 Mar 2011 00:51:22 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2011/03/baby-steps-in-node-js/</link>
          <guid>https://peterlyons.com/problog/2011/03/baby-steps-in-node-js/</guid>
          <description xml:base="https://peterlyons.com/problog/2011/03/baby-steps-in-node-js/">&lt;p&gt;So I&#x27;ve been working on some toy projects in &lt;a href=&quot;http:&#x2F;&#x2F;nodejs.org&#x2F;&quot;&gt;node.js&lt;&#x2F;a&gt; for a few weeks. Node is pretty hot in the tech media these days (see &lt;a href=&quot;http:&#x2F;&#x2F;www.theregister.co.uk&#x2F;2011&#x2F;03&#x2F;01&#x2F;the_rise_and_rise_of_node_dot_js&#x2F;&quot;&gt;this article&lt;&#x2F;a&gt;, among many others), and I&#x27;ve been having fun learning it and opening my brain to the world of async and callbacks. This post will just note a handful of the interesting things I&#x27;ve found thus far.&lt;&#x2F;p&gt;
&lt;p&gt;OK, so one of the first things I got enticed by was &lt;a href=&quot;http:&#x2F;&#x2F;jashkenas.github.com&#x2F;coffee-script&#x2F;&quot;&gt;CoffeeScript&lt;&#x2F;a&gt; by Jeremy Ashkenas. It&#x27;s a pretty little javascript dialect and does a good job of taking the &lt;a href=&quot;http:&#x2F;&#x2F;oreilly.com&#x2F;catalog&#x2F;9780596517748&quot;&gt;JavaScript: The Good Parts&lt;&#x2F;a&gt; book, smashing it together with python indentation aesthetics, and making a language out of it. It all just compiles to javascript though, so there&#x27;s no deep magic here. Most node.js libraries and utilities can directly work with .coffee source files without needing a manual intermediate step of running &lt;code&gt;coffee --compile&lt;&#x2F;code&gt; to get from .coffee to .js source code, which is key for me. There is also &lt;code&gt;coffee --compile --watch mydir&lt;&#x2F;code&gt; which will use FSevents to watch all .coffee files under the mydir directory and regenerate the corresponding javascript instantly every time you save them. Currently, the only thing in my stack that actually needs the .js versions are my jasmine in-browser tests.&lt;&#x2F;p&gt;
&lt;p&gt;Next I stumbled upon the CSS compiler &lt;a href=&quot;http:&#x2F;&#x2F;learnboost.github.com&#x2F;stylus&#x2F;&quot;&gt;stylus&lt;&#x2F;a&gt;, which is fantastic. I also looked at &lt;a href=&quot;http:&#x2F;&#x2F;sass-lang.com&#x2F;&quot;&gt;SASS&#x2F;SCSS&lt;&#x2F;a&gt;, but there&#x27;s no good and complete node.js implementation at this time as well as &lt;a href=&quot;http:&#x2F;&#x2F;lesscss.org&#x2F;&quot;&gt;less CSS&lt;&#x2F;a&gt;. All of these are great and beyond adequate. I ended up choosing stylus because it had a utility to convert .css files to .styl files automatically, makes all of the extra CSS syntax optional, meaning it&#x27;s easy to start with .css and gradually refactor to .styl, the command line utility is nice, and it works great with node.js and express.js. Again, any one of these will do just fine and improve greatly upon CSS. It seems at this point the CSS problem is pretty much solved and I don&#x27;t have to spend much energy on it, which is great.&lt;&#x2F;p&gt;
&lt;p&gt;Next of course we need some templates. This is where most of the variation and choice seems to exist. I looked at several. I started with &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;mauricemach&#x2F;coffeekup#readme&quot;&gt;CoffeeKup&lt;&#x2F;a&gt; because I was enamored with CoffeeScript and wanted to have my entire codebase in CoffeeScript. I eventually started to find all those &quot;-&amp;gt;&quot; characters cumbersome though, as well as one bug that slowed my development. So I switched to &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;visionmedia&#x2F;jade&quot;&gt;Jade&lt;&#x2F;a&gt;, which improves upon &lt;a href=&quot;http:&#x2F;&#x2F;haml-lang.com&#x2F;&quot;&gt;HAML&lt;&#x2F;a&gt;. Jade is so far pretty good and very minimal. It&#x27;s not perfect, but it&#x27;s really pretty and I&#x27;m liking it. I also thought about &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;caolan&#x2F;weld&quot;&gt;Weld&lt;&#x2F;a&gt;, which looks very cool but I haven&#x27;t tried it yet. I &lt;a href=&quot;http:&#x2F;&#x2F;www.yuiblog.com&#x2F;blog&#x2F;2010&#x2F;09&#x2F;29&#x2F;video-glass-node&#x2F;&quot;&gt;watched this video on using server side YUI&lt;&#x2F;a&gt; and honestly this looks like the shizzle. I&#x27;ve done some experimenting with jsdom and jquery but haven&#x27;t gotten huge traction yet. I really like the idea of rendering the basic document and then after the fact doing some jquery-style changes before sending to the client.&lt;&#x2F;p&gt;
&lt;p&gt;On the middleware side I&#x27;m using &lt;a href=&quot;http:&#x2F;&#x2F;expressjs.com&#x2F;guide.html&quot;&gt;Express&lt;&#x2F;a&gt; which seems to be widely adopted. It works great with no fuss and no muss (so far, but my apps are still microscopic). For testing, it&#x27;s &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;mhevery&#x2F;jasmine-node&quot;&gt;jasmine-node&lt;&#x2F;a&gt; on the server and &lt;a href=&quot;http:&#x2F;&#x2F;pivotal.github.com&#x2F;jasmine&#x2F;&quot;&gt;jasmine&lt;&#x2F;a&gt; in the browser. The &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;noblesamurai&#x2F;jasbin&quot;&gt;jasbin&lt;&#x2F;a&gt; command line utility is handy for this. I&#x27;ve been using &lt;a href=&quot;http:&#x2F;&#x2F;zombie.labnotes.org&#x2F;&quot;&gt;zombie.js&lt;&#x2F;a&gt; for application level testing with good success.&lt;&#x2F;p&gt;
&lt;p&gt;Today I did my first real async work on the server side. I used &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;caolan&#x2F;async&quot;&gt;async&lt;&#x2F;a&gt; to help with this successfully. The results are great, but not stellar. The code is still a bit foreign in its layout but overall I&#x27;m grokking the async world sufficiently to make forward progress. &lt;a href=&quot;http:&#x2F;&#x2F;documentcloud.github.com&#x2F;underscore&#x2F;&quot;&gt;underscore.js&lt;&#x2F;a&gt; has some handy utilities, but this is where coffeescript&#x2F;javascript falls short. Consider taking a list of photo objects and extracting a list of captions from that in these languages:&lt;&#x2F;p&gt;
&lt;div class=&quot;code&quot;&gt;
&lt;pre&gt;#Javascript + underscore.js
captions = _.pluck(photos, &quot;caption&quot;)
&lt;&#x2F;pre&gt;
&lt;&#x2F;div&gt;
&lt;div class=&quot;code&quot;&gt;
&lt;pre&gt;#CoffeeScript
captions = photo.caption for photo in photos
&lt;&#x2F;pre&gt;
&lt;&#x2F;div&gt;
&lt;div class=&quot;code&quot;&gt;
&lt;pre&gt;#python
captions = [photo.caption for photo in photos]
&lt;&#x2F;pre&gt;
&lt;&#x2F;div&gt;
&lt;div class=&quot;code&quot;&gt;
&lt;pre&gt;#Ruby, better than javascript but not as clean as python
#The block mechanism is more generically powerful though
captions = photos.collect {|photo| photo.caption}
&lt;&#x2F;pre&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;I&#x27;ve written my business logic in &lt;a href=&quot;http:&#x2F;&#x2F;backbonejs.org&quot;&gt;backbone.js&lt;&#x2F;a&gt; and so far so good. After some head scratching and namespace tweaking, I am able to run the exact same model code on the browser and in node on the server, which is sort of the holy grail that this whole effort was seeking. Hurray for only coding data validation and error messages once. So far so good, but again I&#x27;m still at the baby steps stage of this.&lt;&#x2F;p&gt;
&lt;p&gt;Overall my attitude about the node.js ecosystem is highly optimistic. I hope in the next year or two it gets the kind of mainstream support that rails has. Expect some more posts with more tech details sometime soon as my experiments move into the more complex guts of the applications.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Show HN: My bookmarking app</title>
          <pubDate>Thu, 17 Mar 2011 10:21:11 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2011/03/show-hn-my-bookmarking-app/</link>
          <guid>https://peterlyons.com/problog/2011/03/show-hn-my-bookmarking-app/</guid>
          <description xml:base="https://peterlyons.com/problog/2011/03/show-hn-my-bookmarking-app/">&lt;p&gt;So the &lt;a href=&quot;http:&#x2F;&#x2F;ycombinator.com&#x2F;newsguidelines.html&quot;&gt;Hacker News Guidelines&lt;&#x2F;a&gt; tell me when submitting something I should write a blog post about it and link to that. OK, here it is. I&#x27;d like to get feedback from the HN community about my simple but effective bookmarking web application: &lt;a href=&quot;https:&#x2F;&#x2F;linkzie.com&quot;&gt;Linkzie&lt;&#x2F;a&gt;. I built it mostly during nights&#x2F;weekends while holding down a day job doing enterprise software. I know there are a lot of apps out there for this problem, especially in the recent months following the delicious.com leaked &quot;sunset&quot; presentation. I missed that boat to announce the app, but anyway here it is. I think the interface is very clean and straightforward and the functionality is simple. There&#x27;s nothing here that can&#x27;t be explained in a handful of words. You put in your URLs, it lets you lay them out on one big page. There&#x27;s drag and drop organization and in-place editing. You can try the app using sample data right on the home page without needing to sign in (click no the &quot;Next&quot; link in the tour to see the edit mode).&lt;&#x2F;p&gt;
&lt;p&gt;The app was written mostly as an &quot;organic&quot; project since I have been maintaining a home page of links using a little python script to generate static HTML since 2003 or thereabouts. I wanted to learn modern web development (spent 2004-2010 in back end enterprise software) with rails and jquery. My initial thought was this space was completely saturated but the success of Pinboard has made me rethink that assumption. Linkzie doesn&#x27;t have any whiz-bang fancy features currently, but maybe the spacial layout and clean design will appeal to some users.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>My Lifestyle Business Hero</title>
          <pubDate>Thu, 10 Mar 2011 23:56:12 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2011/03/music-teachers-helper/</link>
          <guid>https://peterlyons.com/problog/2011/03/music-teachers-helper/</guid>
          <description xml:base="https://peterlyons.com/problog/2011/03/music-teachers-helper/">&lt;p&gt;Brandon Pierce of &lt;a href=&quot;http:&#x2F;&#x2F;musicteachershelper.com&quot;&gt;Music Teacher&#x27;s Helper&lt;&#x2F;a&gt; has what looks to me like the ultimate lifestyle business. &lt;a href=&quot;http:&#x2F;&#x2F;www.fourhourworkweek.com&#x2F;blog&#x2F;2011&#x2F;03&#x2F;04&#x2F;engineering-a-%E2%80%9Cmuse%E2%80%9D-%E2%80%93-volume-3-case-studies-of-successful-cash-flow-businesses&#x2F;&quot;&gt;check out the case study on Tim Ferriss&#x27;s blog&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Startup Weekend Boulder: Bridge My Path</title>
          <pubDate>Mon, 28 Feb 2011 03:20:21 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2011/02/swboulder-bridgemypath/</link>
          <guid>https://peterlyons.com/problog/2011/02/swboulder-bridgemypath/</guid>
          <description xml:base="https://peterlyons.com/problog/2011/02/swboulder-bridgemypath/">&lt;p&gt;I spent all weekend at &lt;a href=&quot;http:&#x2F;&#x2F;boulder.startupweekend.org&#x2F;&quot;&gt;Startup Weekend Boulder&lt;&#x2F;a&gt;. This is an event where people pitch their business ideas, vote on the top ideas, and then form teams and try to get the ideas from zero to launch in 54 intense hours. It was my first time participating in Startup Weekend. Overall, it was a really fun experience and I met a lot of really smart and fun people. Friday was pitch night where somewhere just shy of 40 people gave 1-minute pitches. There was good variety here from already established projects to things conceived just moments earlier. There were gaming ideas, social networking ideas, education, finance, retail, even a medical marijuana related business and an infrared steak griller. I pitched one of the ideas I have sitting around on my big long &lt;a href=&quot;https:&#x2F;&#x2F;workflowy.com&quot;&gt;WorkFlowy&lt;&#x2F;a&gt; business idea lists: ThreeShopper, a curated shopping site to end the paradox of choice and narrow things down to exactly three choices. My idea got zero votes. :-) I think there were around 60 total attendees including folks from Nova Scotia and South Africa.&lt;&#x2F;p&gt;
&lt;p&gt;After the top 7 or so pitches were elected we each had a chance to choose a project and join that team. I joined up with &lt;a href=&quot;http:&#x2F;&#x2F;about.me&#x2F;marshallhayes&quot;&gt;Marshall Hayes&lt;&#x2F;a&gt; who was working on his mentoring networking idea. We ended up gathering a very large team. It was nine people even after a few joined and then switched to other projects. Folks were enthusiastic about promoting the concept of mentorship but I felt like the idea needed refinement. So we spent a lot of time just debating whether it was going to be consumer focused or licensed to universities for alumni networks amongst other variations. Saturday morning as these discussions continued and the idea seemed to be heading towards a profile-based &quot;linkedin for mentoring&quot;, which was not at all appealing to me, I suggested we pivot the focus and instead of focusing on the mentors themselves, focus on a skill path and connect people along the same skill progression, facilitating mentorship from those ahead of you, connection with peers at the same place as you, and also helping those following you on the learning path. The team was receptive to the pivot, and we started refining that idea. Eventually we ended up with &lt;a href=&quot;http:&#x2F;&#x2F;bridgemypath.com&quot;&gt;Bridge My Path&lt;&#x2F;a&gt; as the company name and got to work on wireframing, graphic design, market research, copy writing, etc.&lt;&#x2F;p&gt;
&lt;p&gt;In the next day and a half we built the home page and refined a lot of the idea, as well as creating the demo pitch for Sunday evening. Our MVP was our graphic designer &lt;a href=&quot;http:&#x2F;&#x2F;warbweb.com&quot;&gt;Carly Gloge&lt;&#x2F;a&gt; from WarbWeb. She created a beautiful site and graphics. &lt;a href=&quot;http:&#x2F;&#x2F;www.techstars.org&#x2F;mentors&#x2F;nglaros&#x2F;&quot;&gt;Nicole Glaros&lt;&#x2F;a&gt; complemented her during the pitches and said we had the best design of all the teams. I think the aspiration of startup weekend is to actually get a functioning product up and working end to end, but given the lack of initial clarity in the idea and my years working in enterprise software, I am very skeptical of the utility of this goal. So we ended up just building a static HTML site to launch the concept. I put together most of the HTML&#x2F;CSS using coffeekup templates and rendering them via node.js&#x2F;express, then just posting the static HTML files.&lt;&#x2F;p&gt;
&lt;p&gt;Sunday evening teams had 5 minutes to pitch their product. Wow. What a difference a day and a half make! Everything was much more refined and lots of teams had at least a nice home page designed and live. Some teams had working mobile apps. I don&#x27;t recall any of the web apps having much to show in terms of code written over the weekend (SnapGames had some pre-existing code). The panel of judges commented liberally on each idea and gave a lot of good feedback. &lt;a href=&quot;http:&#x2F;&#x2F;www.suzanbond.com&#x2F;2011&#x2F;02&#x2F;in-the-thick-of-things-startup-weekend-boulder&#x2F;&quot;&gt;Suzan Bond&lt;&#x2F;a&gt; was there blogging and tweeting the event as well as conducting some video interviews (which I can&#x27;t easily locate on her site just yet). After the event, I took a long overdue nap, grabbed some ribs at the Rib House, and then hung out at the Spark Palace after party.&lt;&#x2F;p&gt;
&lt;p&gt;Again, this was a really fun and valuable event. In general I like high-commitment events (like Burning Man), so the fact that all the participants sprung for a $75 ticket and committed to locking themselves in the bunker all weekend makes a great dynamic. A few lamers did pitch their idea and then jet when they didn&#x27;t get selected, but what are you going to do? If you like those early days of product ideas, I definitely recommend giving it a try.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Linkzie gets a bookmarklet</title>
          <pubDate>Sun, 20 Feb 2011 10:17:43 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2011/02/linkzie-gets-a-bookmarklet/</link>
          <guid>https://peterlyons.com/problog/2011/02/linkzie-gets-a-bookmarklet/</guid>
          <description xml:base="https://peterlyons.com/problog/2011/02/linkzie-gets-a-bookmarklet/">&lt;p&gt;I have added a bookmarklet feature to &lt;a href=&quot;https:&#x2F;&#x2F;linkzie.com&quot;&gt;Linkzie&lt;&#x2F;a&gt;. In the &quot;organize your links&quot; area you will see a little link there you can drag to your browser&#x27;s bookmarks bar. Then if you are looking at a web page and want to save it into Linkzie, just click the bookmarklet. You will be able to set the name and category for the link and then be taken right back to the original page you were looking at. This should make it easier to get lots of links into Linkzie. Enjoy!&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>House of Genius</title>
          <pubDate>Sat, 19 Feb 2011 02:16:31 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2011/02/house-of-genius/</link>
          <guid>https://peterlyons.com/problog/2011/02/house-of-genius/</guid>
          <description xml:base="https://peterlyons.com/problog/2011/02/house-of-genius/">&lt;p&gt;I got to participate in a cool new brainstorming event called &lt;a href=&quot;http:&#x2F;&#x2F;houseofgenius.org&quot;&gt;House of Genius&lt;&#x2F;a&gt; last night. I won&#x27;t say too much about it since the founders seem to prefer if the participants don&#x27;t know exactly what to expect. But it was populated by a great group of smart and interesting people from many different age groups and backgrounds. It was in a super-swank enormous apartment downtown and was overall very unusual, engaging, and fun. It&#x27;s funny, last week&#x27;s Denver Hack Nite meetup was also in an inappropriately-fancy mansion. Maybe all my tech meetups will gradually start shifting from coffee shops and garages to luxury homes with helipads....&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Going all in on Google Calendar and Android</title>
          <pubDate>Fri, 18 Feb 2011 22:00:53 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2011/02/google-calendar/</link>
          <guid>https://peterlyons.com/problog/2011/02/google-calendar/</guid>
          <description xml:base="https://peterlyons.com/problog/2011/02/google-calendar/">&lt;p&gt;So I bought an &lt;a href=&quot;https:&#x2F;&#x2F;www.cheap-phones.com&#x2F;wp-content&#x2F;uploads&#x2F;2012&#x2F;05&#x2F;lg-optimus-v-vm670-virgin.jpg&quot;&gt;LG Optimus V&lt;&#x2F;a&gt; Android phone this weekend. I said if I finished the redesigns for peterlyons.com and linkzie.com I would treat myself. I&#x27;ve been a fairly die-hard PalmOS fan since the early days of the PalmPilot Personal. I think I got my first palm while still in high school probably in 1997 or so. I learned the Palm Graffiti input method. I soaked it right up. I continued on with a Kyocera cell phone&#x2F;PDA based on PalmOS which I loved passionately and then onto the Treo for work and finally the Centro which I bought in March 2008 and have been using for the past 3 years. Throughout this whole time I maintained the same contact database and calendar. I had phone numbers in there of high school friends from before 10-digit dialing with area code was required. I had 4-digit on-campus extensions for random people I met in college filed under their first name only. It was amusing to go and look through the archives. I was pretty anal-retentive about putting everything in there. Guy who conducted my Region Band in high school: in there. That girl who cut my hair for $5 one time in college: in there. The number for the King 101 CS lab: in there. The same goes for Calendar. I think during one of the numerous computer data migrations I did over the years, I abandoned the early years of calendar data, but I still have data going back to as early as September 1999. I can tell you that I had a saxophone quartet rehearsal at 6:30pm on Wednesday September 22, 1999. Some 7000+ calendar records have all been migrated into my google calendar. And now they&#x27;re all synced to my phone. w00t!&lt;&#x2F;p&gt;
&lt;p&gt;So anyway for the contact list I had to go through and trim down the people I hadn&#x27;t been in contact with for a decade and then I hired someone on odesk.com to figure out how to migrate it into my gmail contacts list. The whole keeping track of people&#x27;s email addresses and phone numbers is pretty much moot at this point since tracking folks down by name online is usually straightforward, but we all have those handful of folks that still have little or no online presence. So at this point keeping my contact book maintained basically makes sure I can easily call the people in my immediate network and perhaps more importantly the caller ID works properly when they call. But clearly we&#x27;re close to the point where any manual address book maintenance will be a thing of the past.&lt;&#x2F;p&gt;
&lt;p&gt;So far the Optimus V seems like an awesome deal, especially for mobile-phobes like me who are late late adopters. The phone costs $150 and you get a prepaid $25 plan with no contract that includes unlimited data and TXT plus 300 voice minutes. That&#x27;s saving me $15&#x2F;month over my already pretty cheap AT&amp;amp;T plan which has no data at all. The savings on the plan will pay for the phone in 10 months. Plus now that I&#x27;m already done with my AT&amp;amp;T contract I don&#x27;t have to start a new one. We&#x27;ll see how the coverage is: Virgin Mobile uses a subset of the Sprint towers, I&#x27;m told. So far no issues and AT&amp;amp;T is not great in this area anyway.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;www.cheap-phones.com&#x2F;wp-content&#x2F;uploads&#x2F;2012&#x2F;05&#x2F;lg-optimus-v-vm670-virgin.jpg&quot; alt=&quot;LG Optimus V&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Perhaps the best part is how much functionality I get for my $150. I get a full GPS device with updated maps. No thanks, Garmin, I won&#x27;t be paying you $75 to update the maps on my Nuvi. I get a new Sonos controller. No thanks, Sonos, I won&#x27;t be buying a $300 dedicated extra controller. I get another Rhapsody&#x2F;Pandora streaming music endpoint. Nope, don&#x27;t need another Ibiza Rhapsody MP3 player for $160. Plus now I can develop and test Android apps, which I just might do. Plus since I can get a 32 GB microSD card for $100, I can &lt;em&gt;finally&lt;&#x2F;em&gt; have an MP3 player that can hold pretty much all of my music. I bought and loved a Creative Nomad Xen Xtra around 2005 probably. It&#x27;s the size of an old cassette tape player, and on the rare occasions when I bring it out into public, people think I&#x27;m a hipster listening to 80s cassettes or something. But it was cheap and good and it took years and years for Apple and other mainstream mp3 players to hit 64GB. If there was a 64GB microSD card I could fit all my ripped MP3s from my CD collection (30GB or so) plus a ton of Rhapsody stuff and still have room for some photos and videos. It seems microSD is topping out at 32GB though, so next year I&#x27;ll probably buy another cheap Android phone with microSDHXC and then achieve portable device Nirvana.&lt;&#x2F;p&gt;
&lt;p&gt;On Tuesday I spent all day in Boulder working at Coffee shops in between my three (3!) meetups in one day. Thus I had to move my car about three or four times during the day to park for free and not get a ticket. What this meant is that when my last meetup concluded around 8:30pm I had forgotten which of the locations was the most recent one and spent about 20 minutes walking a grid in the dark looking for my car. Yesterday I downloaded the parked car locator app. Never again will I have to wonder around! (Assuming I remember to actually tag my car&#x27;s location during the parking process).&lt;&#x2F;p&gt;
&lt;p&gt;Any other Optimus-V users out there? Let me hear from ya!&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>New web designs</title>
          <pubDate>Wed, 16 Feb 2011 02:35:08 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2011/02/new-web-designs/</link>
          <guid>https://peterlyons.com/problog/2011/02/new-web-designs/</guid>
          <description xml:base="https://peterlyons.com/problog/2011/02/new-web-designs/">&lt;p&gt;As you may notice, the &lt;a href=&quot;&#x2F;&quot;&gt;peterlyons.com&lt;&#x2F;a&gt; site has been redesigned. I worked with &lt;a href=&quot;http:&#x2F;&#x2F;stephaniehenderson.com&quot;&gt;Stephanie Henderson&lt;&#x2F;a&gt; to get a new look now that I am an independent professional. I&#x27;m pleased with the results, and the blogs are more seamlessly integrated now.&lt;&#x2F;p&gt;
&lt;p&gt;I have also released a new version of &lt;a href=&quot;https:&#x2F;&#x2F;linkzie.com&quot;&gt;Linkzie&lt;&#x2F;a&gt; today featuring a brand new web design. The original layout of categories was based on rows and that has been changed to columns. Check it out!&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Going all in on gmail</title>
          <pubDate>Thu, 10 Feb 2011 13:13:50 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2011/02/going-all-in-on-gmail/</link>
          <guid>https://peterlyons.com/problog/2011/02/going-all-in-on-gmail/</guid>
          <description xml:base="https://peterlyons.com/problog/2011/02/going-all-in-on-gmail/">&lt;p&gt;So somewhere in college around 2000 I started running my own IMAP mail server and storing my own mail. Around 2007 I got tired of fighting the spam battle and switched to gmail. I migrated some of my current messages into gmail using IMAP, but I didn&#x27;t want to dump everything in there. Today I realized I no longer have convenient access to those messages sitting in mbox format files on the linux laptop I rarely use anymore, so I hooked Thunderbird up to gmail and dragged my mail archives into gmail. That was about 12,000 messages between 2000 and 2007. Now it&#x27;s all in one happy, searchable, spam-free place. As long as Google doesn&#x27;t spontaneously disappear gmail, I should be in good shape.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>node.js and command line jQuery</title>
          <pubDate>Wed, 09 Feb 2011 03:41:10 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2011/02/node-js-and-command-line-jquery/</link>
          <guid>https://peterlyons.com/problog/2011/02/node-js-and-command-line-jquery/</guid>
          <description xml:base="https://peterlyons.com/problog/2011/02/node-js-and-command-line-jquery/">&lt;p&gt;node.js combined with the &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;visionmedia&#x2F;query&quot;&gt;new query utility&lt;&#x2F;a&gt; which makes jQuery-like functionality available from the command line. This type of thing used to take a 30-line script. Brilliant!&lt;&#x2F;p&gt;
&lt;div class=&quot;code&quot;&gt;
&lt;pre&gt;for N in {1..10}; do curl --silent http:&#x2F;&#x2F;news.ycombinator.com | query td.title a get &quot;${N}&quot;; done

YC Partner Harjeet Taggar Gives Insights [Interview]
Update your timezone defs: Russia abolishes winter time (DST)
Want to be a leader? Wash the Dishes When Nobody Else Will. 
How to be 100% sure your startup idea is good
Introducing the Google Translate app for iPhone
In China, alpha males carry designer purses
How To Get Your First 1,000 Users
How Much Money I Made From Side Projects In 2010
Facebook Buys Old Sun Campus in Menlo Park
Government investigation: No evidence Toyota electronic throttles malfunctioned
&lt;&#x2F;pre&gt;
&lt;&#x2F;div&gt;</description>
      </item>
      <item>
          <title>The Perfect Exercise Headphones</title>
          <pubDate>Mon, 07 Feb 2011 07:00:11 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2011/02/exercise-headphones/</link>
          <guid>https://peterlyons.com/problog/2011/02/exercise-headphones/</guid>
          <description xml:base="https://peterlyons.com/problog/2011/02/exercise-headphones/">&lt;p&gt;I believe I have now found a pair of headphones to wear while exercising and traveling that is optimal. I&#x27;ve gone through a lot of headphones over the years as I&#x27;m sure many of you have. I&#x27;ve tried both the extreme low end of the price spectrum (less than $10) and the costs-more-than-your-mp3-player range (I owned some $180 Shure E2C earbuds back when they were $180 instead of $90). Of course I have learned some lessons the hard way. The following list starts out with weightier arguments and then decays into personal preference&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;The $20 ballpark is about right where when they are inevitably lost or broken, you shake it off and move on, but they are still able to sound OK
&lt;ul&gt;
&lt;li&gt;As an aside, my $180 Shure headphones started making static interference two weeks after the warranty expired.&lt;&#x2F;li&gt;
&lt;li&gt;If you want a nicer pair for hi-fi listening at home, by all means go for it and invest in some nice cans, just don&#x27;t bring them when traveling&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;Sound isolating earbuds are the way to go for travel and exercise. I know traditional full-ear headphones are back in popularity with the snowboarders, but in general I want something that will pack up to essentially zero size and block out the exterior noise. I don&#x27;t wear these while skiing where taking them on and off frequently might be required.&lt;&#x2F;li&gt;
&lt;li&gt;It is better if there is a right angle between the plug and the cord. This is a sturdier design less likely to cause static or drop a stereo channel with wear and tear.&lt;&#x2F;li&gt;
&lt;li&gt;The combination of in-ear earbud with behind-the ear wire for stability and security is the best. You don&#x27;t want your earpiece falling out and dangling there while you are working on your downward dog.&lt;&#x2F;li&gt;
&lt;li&gt;The cord should include a small clip you can slide up from the neck to the earbuds to zip up the &quot;Y&quot; section of the cable for storage to prevent tangling&lt;&#x2F;li&gt;
&lt;li&gt;The cord should have a woven fabric sheath instead of plastic or rubber. This makes it easier to untangle.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;So to this end I think the &lt;a href=&quot;http:&#x2F;&#x2F;www.amazon.com&#x2F;MEElectronics-M6-BK-Sport-Sound-Isolating-Headphones&#x2F;dp&#x2F;B0038W0K2K&#x2F;ref=sr_1_2?ie=UTF8&amp;amp;s=electronics&amp;amp;qid=1297036394&amp;amp;sr=1-2&quot;&gt;MEElectronics M6-BK Sport Sound-Isolating In-Ear Headphones&lt;&#x2F;a&gt; are pretty much perfect. The only item they lack from the above list is the woven fabric sheath. The behind the ear wire is minimal but effective and they come with both single and triple flange earpieces.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;workoutheadphonespro.com&#x2F;wp-content&#x2F;uploads&#x2F;2012&#x2F;01&#x2F;meelectronics-m6-sports-headphones.jpg&quot; alt=&quot;MEElectronics M6 Headphones&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>And the bots show up</title>
          <pubDate>Sun, 06 Feb 2011 02:00:35 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2011/02/and-the-bots-show-up/</link>
          <guid>https://peterlyons.com/problog/2011/02/and-the-bots-show-up/</guid>
          <description xml:base="https://peterlyons.com/problog/2011/02/and-the-bots-show-up/">&lt;p&gt;I set up a staging server for a web app I&#x27;m working on yesterday. This morning I find this in my rails log.&lt;&#x2F;p&gt;
&lt;div class=&quot;code&quot;&gt;
&lt;pre&gt;ActionController::RoutingError (No route matches &quot;&#x2F;scripts&#x2F;setup.php&quot;):
ActionController::RoutingError (No route matches &quot;&#x2F;sql&#x2F;scripts&#x2F;setup.php&quot;):
ActionController::RoutingError (No route matches &quot;&#x2F;web&#x2F;scripts&#x2F;setup.php&quot;):
ActionController::RoutingError (No route matches &quot;&#x2F;scripts&#x2F;setup.php&quot;):
ActionController::RoutingError (No route matches &quot;&#x2F;admin&#x2F;scripts&#x2F;setup.php&quot;):
ActionController::RoutingError (No route matches &quot;&#x2F;PMA&#x2F;scripts&#x2F;setup.php&quot;):
ActionController::RoutingError (No route matches &quot;&#x2F;phpmyadmin&#x2F;scripts&#x2F;setup.php&quot;):
&lt;&#x2F;pre&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;Thus the shotgun bot scanning has commenced. It&#x27;s funny how organic and viral (viral in the bad sense) the web is given it&#x27;s all just computer programs.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>dayJob.quit()</title>
          <pubDate>Fri, 28 Jan 2011 00:01:39 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2011/01/dayjob-quit/</link>
          <guid>https://peterlyons.com/problog/2011/01/dayjob-quit/</guid>
          <description xml:base="https://peterlyons.com/problog/2011/01/dayjob-quit/">&lt;p&gt;I&#x27;ve quit my job writing data center automation software for HP. I&#x27;m striking out on my own doing independent web application development. I&#x27;ll be building some information products and then hopefully a subscription based web application targeted at a small niche audience. I had been working on the same product for 6.5 years and I am ready to switch gears from the world of enterprise software to consumer-facing web applications.&lt;&#x2F;p&gt;
&lt;p&gt;I&#x27;ve been learning ruby, rails, and jquery in my free time for the past six months or so. It&#x27;s been pretty enjoyable (especially jQuery), and there is a stark contrast between the slow grind of enterprise software and the lightness and speed of web development. I&#x27;ll have more time and spare brain cycles for blogging and hopefully some more open source contributions, to which I look forward. &lt;a href=&quot;&#x2F;problog&#x2F;feed&#x2F;&quot;&gt;Subscribe to this blog&#x27;s feed&lt;&#x2F;a&gt; to see how things pan out for me in the coming weeks and months. My working plan is to give self-employment a good long try before considering outside work. I plan to work at it at least until Labor Day, which is a full 7 months away. Wish me luck!&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Wiki migration from MoinMoin to gitit</title>
          <pubDate>Mon, 10 Jan 2011 05:08:29 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2011/01/moinmoin_to_giti/</link>
          <guid>https://peterlyons.com/problog/2011/01/moinmoin_to_giti/</guid>
          <description xml:base="https://peterlyons.com/problog/2011/01/moinmoin_to_giti/">&lt;p&gt;So I&#x27;ve been hosting my own &lt;a href=&quot;http:&#x2F;&#x2F;moinmo.in&#x2F;&quot;&gt;MoinMoin&lt;&#x2F;a&gt; wiki for a while. I use it pretty heavily to manage my own little projects, packing lists, and whatnot. I used MoinMoin initially because it is written in python and we were using it at work. It has served me reasonably well, but I decide to shut it down and migrate all my data over to &lt;a href=&quot;http:&#x2F;&#x2F;gitit.johnmacfarlane.net&#x2F;&quot;&gt;gitit&lt;&#x2F;a&gt; instead.&lt;&#x2F;p&gt;
&lt;p&gt;My reasoning for the migration includes the following factors.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;MoinMoin seems as far as I can tell to be impossible if not difficult to run under nginx, and in this world of low-RAM VPSes where I keep all my online stuff, nginx beats apache soundly in this regard, so I&#x27;ve moved everything over to nginx&lt;&#x2F;li&gt;
&lt;li&gt;I never really liked the notion of WikiWords to create links. I wasted a lot of mental cycles worrying about how to prevent MoinMoin from making RedHat a hyperlink unintentionally, etc. Thus I wanted to move to a different markup, such as &lt;a href=&quot;http:&#x2F;&#x2F;daringfireball.net&#x2F;projects&#x2F;markdown&#x2F;&quot;&gt;markdown&lt;&#x2F;a&gt;, which seems to be pretty widely used, especially in github projects for README files.&lt;&#x2F;li&gt;
&lt;li&gt;Using git as the data store is just perfect and brilliant. I&#x27;m only now realizing the magic of git&#x27;s &quot;no single master repo&quot; distributed nature and now I can confidently add stuff to my wiki from multiple machines via either a web browser or a text editor and know that everything will eventually be merged up perfectly. This is one less thing for me to worry about manually rsyncing around the next time I move to a different VPS host.&lt;&#x2F;li&gt;
&lt;li&gt;MoinMoin has several glaring warning signs about programmers running wild with no product sense. For example, they don&#x27;t have configuration files. They have python code you edit. The ruby community is another big fan of this misguided approach. Folks, asking programmers to edit code in a language they know to achieve complicated configuration (a la routes.rb) is fine. Asking sysadmins to configure basic shit like which TCP port your program should listen on is not. Make a configuration file, preferably in dead simple key=value format.&lt;&#x2F;li&gt;
&lt;li&gt;Another glaring warning sign is how MoinMoin handles its default content (help pages, etc), of which there is quite a lot. It just dumps them into your wiki right next to your own home-grown content with no distinction. There is no copy-on-write mechanism and no easy way to extract a list of pages you actually care about. I ended up combining some globbing telling me which pages had at least 2 revisions with a manual scan of page names to fish out my data from the sea of default MoinMoin pages. In hindsight, the smarter thing would have been to install a blank MoinMoin wiki, extract the set of page names, and then do a difference with the set of page names of my existing wiki, but my approach of just &lt;code&gt;ls *&#x2F;revisions&#x2F;00000002&lt;&#x2F;code&gt; worked almost as well, missing only a handful of pages, which a manual scan caught.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Sadly, gitit is not provided as an out of the box Ubuntu x64 package, which I REALLY prefer not to mess with. But alas, as with most webby stuff, I have to accept my fate and deal with manual installation, configuration, maintenance, and upgrade. Other than that fact, gitit has been working well enough so far. I had to write my own init script for it, but that is just annoying as opposed to a serious drawback.&lt;&#x2F;p&gt;
&lt;p&gt;The cool part about this migration was I was able to for the most part script it. I wrote a python script that would take the VERY limited set of MoinMoin syntax that I use (primarily headers, lists, and links) and convert it to markdown. Sadly its seems MoinMoin is one of the few markup formats that pandoc cannot use as input. :-( I also coded my script to start with revision 00000001 (used by MoinMoin) and commit each distinct MoinMoin page revision into git sequentially. Now in my migrated gitit repo, I have the full MoinMoin edit history for every page, which is pretty sweet.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Unit tests are like Ewoks</title>
          <pubDate>Thu, 02 Dec 2010 03:24:43 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2010/12/unit-tests-are-like-ewoks/</link>
          <guid>https://peterlyons.com/problog/2010/12/unit-tests-are-like-ewoks/</guid>
          <description xml:base="https://peterlyons.com/problog/2010/12/unit-tests-are-like-ewoks/">&lt;p&gt;Unit tests are like Ewoks.  They are your friends. They will help you.  They will help you fight formidable enemies.  They are cute and cuddly and adorable.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Managing complexity and chaos</title>
          <pubDate>Tue, 23 Nov 2010 04:02:23 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2010/11/managing-complexity-and-chaos/</link>
          <guid>https://peterlyons.com/problog/2010/11/managing-complexity-and-chaos/</guid>
          <description xml:base="https://peterlyons.com/problog/2010/11/managing-complexity-and-chaos/">&lt;p&gt;I read somewhere (sorry I&#x27;ve forgotten where by now) that as an engineering, our #1 omnipresent mandate is &quot;manage complexity&quot;. I interpret &quot;manage&quot; to mean &quot;reduce&quot; or &quot;prevent&quot; or &quot;minimize&quot; or &quot;mitigate&quot; in this context. When I read this, it resonated very strongly with me.&lt;&#x2F;p&gt;
&lt;p&gt;I was recently asked for feedback on one of the managers I work with and I thought in many was, in the manager role, the mantra is &quot;manage chaos&quot;. I think that fits nicely. When everything is clear, well-understood, prioritized, under control, the team hums along efficiently. As a team gets overwhelmed or sidetracked or confused, chaos ensues, and it seems to me the role of management to prevent&#x2F;reduce&#x2F;minimize&#x2F;mitigate that chaos.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>OS X and CD burning non-smartness</title>
          <pubDate>Tue, 19 Oct 2010 19:46:59 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2010/10/os-x-and-cd-burning-non-smartness/</link>
          <guid>https://peterlyons.com/problog/2010/10/os-x-and-cd-burning-non-smartness/</guid>
          <description xml:base="https://peterlyons.com/problog/2010/10/os-x-and-cd-burning-non-smartness/">&lt;p&gt;So since making the linux to Mac transition, I have this mindset of things on the Mac are supposed to &quot;just work&quot; and be fantastic and marvelous and flawless. This preconception was shattered again when I tried to burn some CDs recently. I downloaded and ISO and tried to burn it with Finder. In Ubuntu, if you try to burn a single .iso file to disc, it prompts you that you probably want to just make a disc from that CD image, and thus does the right thing. Not so on the Mac. It happily spit out a data disc containing a single .iso file. Duh. Then I went to burn a music CD using some .wav files a friend sent. Again, on Ubuntu, if you ask it to burn a CD of just .wav music files, it suggests that you burn an audio CD automatically. Mac, nope. Data CD with .wav files. Facepalm. In case you&#x27;re curious, yes I did figure out that you need to use Disk Utility on the Mac to burn the .iso file and iTunes for the music CD. I thought this was supposed to be seamless?&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>jQuery and Ball World</title>
          <pubDate>Thu, 14 Oct 2010 07:54:43 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2010/10/jquery-and-ball-world/</link>
          <guid>https://peterlyons.com/problog/2010/10/jquery-and-ball-world/</guid>
          <description xml:base="https://peterlyons.com/problog/2010/10/jquery-and-ball-world/">&lt;p&gt;So in my first computer science course, one of the earliest labs with &lt;a href=&quot;http:&#x2F;&#x2F;www.bandgap.cs.rice.edu&#x2F;personal&#x2F;adrice_swong&#x2F;public&#x2F;default.aspx&quot;&gt;Stephen Wong&lt;&#x2F;a&gt; was called Ball World. Professor Wong had set up some fill-in-the-blanks java code for us that brought up a basic frame. We wrote code to add some ball sprites and we had to define their velocity and have code to detect collisions and compute new velocities after balls collided with each other or the boundaries. I still remember the first time you get everything fired up and see two balls collide and bounce off each other realistically. Keep in mind this is somewhere pretty earlier in my first semester ever programming. It was pretty powerful and pretty exciting.&lt;&#x2F;p&gt;
&lt;p&gt;Now here I am with ten years or so professional programming experience. I&#x27;m teaching myself &lt;a href=&quot;http:&#x2F;&#x2F;jquery.com&#x2F;&quot;&gt;jQuery&lt;&#x2F;a&gt; and javascript. Well, I guess I should put &quot;teaching&quot; in quotes because I&#x27;m basically just doing it since there&#x27;s no learning required. I&#x27;ve had about twenty different things I wanted to do so far on my practice project, and each of them involves a 3-word google query like &quot;jquery sort drag&quot;, a very tiny snippet of code, averaging about 2 or 3 lines, and it working perfectly the first time. It&#x27;s amazing. I&#x27;ve got all kinds of HTML5 Ajaxy Web 2.0 goodness going and it works great and it&#x27;s easy. AND there&#x27;s a graphical debugger and rapid development tools. It&#x27;s pretty darn sweet.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Home LAN fails and wins</title>
          <pubDate>Mon, 11 Oct 2010 04:16:55 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2010/10/home-lan-fails-and-wins/</link>
          <guid>https://peterlyons.com/problog/2010/10/home-lan-fails-and-wins/</guid>
          <description xml:base="https://peterlyons.com/problog/2010/10/home-lan-fails-and-wins/">&lt;p&gt;So my trusty Linksys wireless router up and died on me a few months ago. Of course, in a geek&#x27;s world, lack of Wifi at home is a stop-the-line emergency, so of course I just immediately drove to a local brick and mortar store to get a replacement. I bought a Belkin as it was the cheapest. It seems to not have the ability to remember MAC to IP mappings for any significant length of time, which meant all my laptops and VMs were constantly changing IPs. I even ended up writing a little script to update &lt;code&gt;&#x2F;etc&#x2F;issue&lt;&#x2F;code&gt; on my VMs so I could see their current IP without needing to log in. Well, after a while I was finally frustrated with this enough to go see if the &lt;a href=&quot;http:&#x2F;&#x2F;www.dd-wrt.com&quot;&gt;DD-WRT&lt;&#x2F;a&gt; replacement firmware was available. It wasn&#x27;t, so I thought I&#x27;d get a different model so I could enjoy the DHCP with static IP mappings goodness. I clicked my handy bookmark for local craigslist and bam, there&#x27;s a guy in my town selling a Buffalo WHR-G126 for $25 with the latest dd-wrt already installed. w00t! After a conversation over a few emails, calls, and texts, we rendezvoused in town just an hour or so later and completed our transaction.&lt;&#x2F;p&gt;
&lt;p&gt;So now my network is so nice and lovely. All my devices use DHCP, but the router remembers the devices that &quot;live here&quot; and gives them each a static IP address and entry in DNS. Hurray for dd-wrt and craigslist.&lt;&#x2F;p&gt;
&lt;p&gt;In case you might find this useful, here&#x27;s a script to find the current IP address of a machine and put it into &lt;code&gt;&#x2F;etc&#x2F;issue&lt;&#x2F;code&gt; so you can see it on the login screen without actually logging in.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;First, edit &lt;code&gt;&#x2F;etc&#x2F;issue&lt;&#x2F;code&gt; with a placeholder line (as root) like this: &lt;code&gt;echo &quot;IP Address: &quot; &amp;gt;&amp;gt; &#x2F;etc&#x2F;issue&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Second, edit &lt;code&gt;&#x2F;etc&#x2F;rc.local&lt;&#x2F;code&gt; and append this little bit of code.&lt;&#x2F;p&gt;
&lt;div class=&quot;code&quot;&gt;
&lt;pre&gt;#plyons. Display the IP at the login screen so we can SSH in
#without loggin in on the console
getip() {
    IP=`ifconfig eth0 | egrep &quot; inet addr:&quot; | cut -d : -f 2 | cut -d &quot; &quot; -f 1`
}
getip
if [ -z &quot;${IP}&quot; ]; then
    sleep 10 #Wait for network to initialize
    getip
fi
perl -pi -e &quot;s&#x2F;IP Address: .*&#x2F;IP Address: ${IP}&#x2F;&quot; &#x2F;etc&#x2F;issue
&lt;&#x2F;pre&gt;
&lt;&#x2F;div&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Now this has a lot of assumptions (one NIC called eth0, etc) and is in no way a generic solution, but for most VMs, it will probably get the job done as is or with a small tweak.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Cease and desist your password restrictions</title>
          <pubDate>Thu, 09 Sep 2010 22:45:38 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2010/09/password-restrictions/</link>
          <guid>https://peterlyons.com/problog/2010/09/password-restrictions/</guid>
          <description xml:base="https://peterlyons.com/problog/2010/09/password-restrictions/">&lt;p&gt;I just tried to create an account at Bed Bath and Beyond. They said my password could only contain letters and numbers. Many sites impose various restrictions on the password, including&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Password must be exactly some length&lt;&#x2F;li&gt;
&lt;li&gt;Password must be greater than some minimum length&lt;&#x2F;li&gt;
&lt;li&gt;Password must be smaller than some maximum length&lt;&#x2F;li&gt;
&lt;li&gt;Password must contain some minimum number of &quot;complex&quot; characters&lt;&#x2F;li&gt;
&lt;li&gt;Password must NOT contain complex characters&lt;&#x2F;li&gt;
&lt;li&gt;Password is actually a 4-digit PIN&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;What this creates is A) major frustration B) a problem with remembering dozens of totally different passwords and C) inability to have a simple &quot;low importance&quot; password for your dozens of accounts you aren&#x27;t super concerned about.&lt;&#x2F;p&gt;
&lt;p&gt;Stop this madness. Stop your silly password restrictions. They are incompatible, frustrating, and probably not helping. See the articles below for reference, but there are many others out there if you do a web search.&lt;&#x2F;p&gt;
&lt;p&gt;References&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;http:&#x2F;&#x2F;www.nytimes.com&#x2F;2010&#x2F;09&#x2F;05&#x2F;business&#x2F;05digi.html&quot;&gt;A Strong Password Isn’t the Strongest Security&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;http:&#x2F;&#x2F;www.wired.com&#x2F;politics&#x2F;security&#x2F;commentary&#x2F;securitymatters&#x2F;2007&#x2F;01&#x2F;72458&quot;&gt;Secure Passwords Keep You Safer&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</description>
      </item>
      <item>
          <title>A response to &quot;Handling Bugs in an Agile Context&quot;</title>
          <pubDate>Wed, 08 Sep 2010 21:07:27 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2010/09/agile-bugs/</link>
          <guid>https://peterlyons.com/problog/2010/09/agile-bugs/</guid>
          <description xml:base="https://peterlyons.com/problog/2010/09/agile-bugs/">&lt;p&gt;&lt;a href=&quot;http:&#x2F;&#x2F;testobsessed.com&#x2F;2009&#x2F;03&#x2F;13&#x2F;handling-bugs-in-an-agile-context&#x2F;&quot;&gt;Handling Bugs in an Agile Context&lt;&#x2F;a&gt; is a blog post I came across via &lt;a href=&quot;http:&#x2F;&#x2F;news.ycombinator.com&quot;&gt;Hacker News&lt;&#x2F;a&gt; this morning. As is often my experience when reading material on Agile from my perspective of an enterprise software developer, the experience was one of frustration and disbelief.&lt;&#x2F;p&gt;
&lt;p&gt;So let me quote the portions of the article I find untenable in a enterprise software realm.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;Let’s start with the Product Owner. Not all Agile teams use this term. So where my definition says “Product Owner,” substitute in the title or name of the person who, in your organization, is responsible for defining what the software should do. This person might be a Business Analyst, a Product Manager, or some other Business Stakeholder.&lt;&#x2F;p&gt;
&lt;p&gt;This person is not anyone on the implementation team. Yes, the testers or programmers may have opinions about what’s a bug and what’s not. The implementation team can advise the Product Owner. But the Product Owner decides.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;Most of this matches my experience to some degree. Yes we have product owners that are primarily business people. We actually have about two levels of these, we have what we call &quot;Product Managers&quot; who set the more abstract direction for the product overall, and another role called &quot;Functional Architect&quot; that is a mostly technical person that deals with more detailed issues. One way to think about this is the Product Manager decides mostly WHAT the product will do, and the Functional Architect refines that and specifies HOW the product will behave in detail. Note the Functional Architect doesn&#x27;t define how the IMPLEMENTATION will be done, just the more detailed behavior.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;This person is also not the end user or customer. When end users or customers encounter problems in the field, we listen to them. The Product Owner takes their opinions and preferences and needs into account. But the Product Owner is the person who ultimately decides if the customer has found something that violates valid expectations of the behavior of the system.&lt;&#x2F;p&gt;
&lt;p&gt;Yes, that does put a lot of responsibility on the shoulders of the Product Owner, but that’s where the responsibility belongs. Defining what the software should and should not do is a business decision, not a technical decision.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;Well, that would be great if it worked that way, but it doesn&#x27;t. The product owner doesn&#x27;t have enough technical skill or understanding of the details to make decisions on specific bugs. I&#x27;m talking about deep, subtle bugs. Think error handling, file encoding, network performance tuning, etc. If a customer complains that our NFS server should by default be configured for a block size of 32768, I&#x27;m sorry but the product owner is just not equipped to make a decision on that. Yes, the technical team could explain the situation to him or her in enough detail for understanding, but the decision would be a &quot;no brainer&quot; dictated by how the team framed the explanation. There are trade-off decisions where the tech lead for the feature needs to make a trade off between two things that are both desirable, like high throughput and low latency. And it&#x27;s not like we get these once in a blue moon. They happen many times in every sprint. We live them day in and day out, and it&#x27;s the job of the implementation team to independently make good decisions on them. It&#x27;s a bit demeaning to the implementation team to suggest that they are mere instruments of the product owner&#x27;s omniscient will. It&#x27;s a TEAM. EVERYONE takes into account many factors in making decisions on designs, implementations, and bugs every day. The team needs authority and autonomy to make the set of decisions that it is appropriate for them to own, and decisions need to escalate as appropriate for their scope and impact. In my experience, 95% of all bugs are correctly resolved by the team members without any input from the product owner.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;Before we declare a story “Done,” if we find something that would violate the Product Owner’s expectations, we fix it. We don’t argue about it, we don’t debate or triage, we just fix it. This is what it means to have a zero tolerance for bugs. This is how we keep the code base clean and malleable and maintainable. That’s how we avoid accumulating technical debt. We do not tolerate broken windows in our code. And we make sure that there are one or more automated tests that would cover that same case so the problem won’t creep back in. Ever.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;Well, that&#x27;s cute but again, sometimes it&#x27;s not possible to do that. Here&#x27;s a real world example. I built a feature that installed some ZIP packages onto some servers. We tested it. It passed and worked. We moved on to the next sprint. Later on, we found out that if the ZIP file&#x27;s install path contained non-ascii characters, we ran into problems and it failed. OK, so that&#x27;s a bug. So you are saying &quot;we fix it&quot;. You say &quot;We don’t argue about it, we don’t debate or triage, we just fix it&quot;. Well, in this case, after several days of me trying to &quot;just fix it&quot;, I informed the team that in order to correctly fix this bug I would have to re-implement the entire user story taking a very different approach. This would take several days and since it was a de-facto rewrite, all of the tests would need to be re-run. So how does your advice apply here? It doesn&#x27;t. We needed to debate and discuss and plan and adjust the backlog and otherwise deal with this reality. I wish I could go to the fire departments that are currently battling the giant wildfire in the next town over and say &quot;Don&#x27;t debate. Just extinguish it&quot;. But it&#x27;s not a helpful thing to say.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;And since we just fix them as we find them, we don’t need a name for these things. We don’t need to prioritize them. We don’t need to track them in a bug tracking system. We just take care of them right away.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;Sorry, but this is the delusional rubbish the agile community puts out that makes it OK for me to dismiss you as utterly and hopelessly clueless. Have you really never heard of a bug that is time consuming or difficult to fix? Have you never seen a bug that needs multiple completely different fixes tried before one that really works is identified? Are you accepting applications for citizenship in your universe? It sounds nice.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;Usually the motivation for wanting to keep a record of things we won’t fix is to cover our backsides so that when the Product Owner comes back and says “Hey! Why didn’t you catch this?” we can point to the bug database and say “We did too catch it and you said not to fix it. Neener neener neener.” If an Agile team needs to keep CYA records, they have problems that bug tracking won’t fix.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;Well, we have a lot of bugs. My product has been around for almost a decade now. There&#x27;s value in tracking them. For one, it gives management some concrete numbers to understand that they have technical debt, the product has issues, and we&#x27;re accumulating more. For two, people hit the bugs. They hit them over and over again. It&#x27;s a waste to have the issue re-triaged, re-explained every time. We track them so there&#x27;s one place that explains what this bug is, how it is reproduced, why we haven&#x27;t fixed it, and how you can work around it. Whether we track this in a bug tracker or a wiki or a KB or whatever doesn&#x27;t seem as important to me as acknowledging the fact that there are issues that might affect users continually for quite a while (maybe for the rest of the life of the product), and not tracking that data makes the problem worse.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;Further, there is a high cost to such record keeping.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;Arguably. But there is also a cost to just abandoning them and leaving the new team members and customers to encounter them over and over again and refile the same bug. Yes, sometimes these things last a long time and there&#x27;s no cost effective way to solve them. For example, in early versions of my product we installed Windows OSes via a DOS boot environment. There was no other viable alternative at the time. DOS has crippling network issues that we couldn&#x27;t solve, so under some circumstances copying a full OS over the network on DOS would result in DOS hanging. We can&#x27;t fix this. We just waited for Microsoft to announce WinPE, but that took several years. Instead we documented that if you had an Intel NIC and encountered this issue, there was an obscure workaround you had to do. This seems like a perfectly valid use of a long-term bug database to me.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;And if we’re not doing things right, we may find out that there are an overwhelming number of the little critters escaping. That’s when we know that we have a real problem with our process.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;OR, maybe we have a legacy code base with technical debt. Maybe the folks who wrote that code don&#x27;t work here anymore and this is the only way we find the problems with it. This doesn&#x27;t necessarily mean we&#x27;re not doing Agile correctly.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;Stop the bugs at the source instead of trying to corral and manage the little critters.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;Thanks. Tell that to my several million lines of legacy code in a half dozen languages that runs on 72 different operating system versions. Done &quot;stopping them at the source&quot; yet? I&#x27;ll wait. Still not done? Hmm, what&#x27;s the problem?&lt;&#x2F;p&gt;
&lt;p&gt;Anyway, that concludes my response to this blog post. The general theme I see in the Agile world is the practitioners are mostly working from a mindset of small web development projects with a new code base. Building enterprise software has realities that create real struggles for us, and we&#x27;re looking for help, but the pundits out there generally dismiss our struggles without really understanding them and acknowledging their reality. And that&#x27;s what I find so frustrating. The truth is most of these agile folks could probably provide us with real useful insights, but they first need to come to terms that we have some real problems that need to be considered even if they don&#x27;t fit within their idealized vision for agile utopia.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Sonos pricing insanity continued</title>
          <pubDate>Tue, 07 Sep 2010 07:17:01 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2010/09/sonos-pricing-insanity/</link>
          <guid>https://peterlyons.com/problog/2010/09/sonos-pricing-insanity/</guid>
          <description xml:base="https://peterlyons.com/problog/2010/09/sonos-pricing-insanity/">&lt;p&gt;So I&#x27;m a fan of &lt;a href=&quot;http:&#x2F;&#x2F;www.sonos.com&quot;&gt;Sonos&lt;&#x2F;a&gt; music systems, but they&#x27;ve always been expensive. Almost prohibitively so. But now I can either spend &lt;a href=&quot;http:&#x2F;&#x2F;sonos.com&#x2F;products&#x2F;controllers&#x2F;cr200&#x2F;default.aspx?rdr=true&amp;amp;LangType=1033&quot;&gt;$349&lt;&#x2F;a&gt; for an additional controller that controls my sonos and does precisely fuck-all else, or I can buy an iPod Touch on &lt;a href=&quot;http:&#x2F;&#x2F;boulder.craigslist.org&quot;&gt;craigslist&lt;&#x2F;a&gt; for $125 and then download the free Sonos controller application, plus do all the other iPod Touch stuff. I&#x27;m not sure who Sonos thinks is going to buy this thing anymore other than the utterly rich and utterly clueless.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>No one really cares about web accessibility anyway...</title>
          <pubDate>Tue, 07 Sep 2010 05:12:19 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2010/09/web_accessibility/</link>
          <guid>https://peterlyons.com/problog/2010/09/web_accessibility/</guid>
          <description xml:base="https://peterlyons.com/problog/2010/09/web_accessibility/">&lt;p&gt;So if a picture is worth a thousand words, shouldn&#x27;t it be:&lt;&#x2F;p&gt;
&lt;div class=&quot;code&quot;&gt;
&lt;pre&gt;&lt;img src=&quot;&#x2F;some&#x2F;image.jpg&quot;&gt;
    &lt;alt&gt;
        one two three four five six seven....
    &lt;&#x2F;alt&gt;
&lt;&#x2F;img&gt;
&lt;&#x2F;pre&gt;
&lt;&#x2F;div&gt;</description>
      </item>
      <item>
          <title>No squids for me, Bruce</title>
          <pubDate>Sun, 05 Sep 2010 05:16:30 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2010/09/no-squids-for-me-bruce/</link>
          <guid>https://peterlyons.com/problog/2010/09/no-squids-for-me-bruce/</guid>
          <description xml:base="https://peterlyons.com/problog/2010/09/no-squids-for-me-bruce/">&lt;p&gt;&lt;a href=&quot;http:&#x2F;&#x2F;www.schneier.com&#x2F;&quot;&gt;Bruce Schneier&lt;&#x2F;a&gt; is a leading author in the technology security and cryptography fields. He is exceedingly rational and pragmatic. I consider his insights on dealing with terrorism to be bar none the best available. His blog often has great insights. However, for reasons I can&#x27;t understand, he always posts a random post on Fridays about squids. Yes, the sea creature. I don&#x27;t know why, but it stopped being cute long ago.&lt;&#x2F;p&gt;
&lt;p&gt;To that end, I&#x27;ve created a &lt;a href=&quot;http:&#x2F;&#x2F;pipes.yahoo.com&#x2F;peterlyons&#x2F;schneiernosquids&quot;&gt;Yahoo Pipe Feed of the Schneier on Security blog with the Friday Squid Blogging filtered out&lt;&#x2F;a&gt;. Feel free to subscribe and enjoy.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>UNIX is broken</title>
          <pubDate>Sat, 04 Sep 2010 19:40:03 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2010/09/unix-is-broken/</link>
          <guid>https://peterlyons.com/problog/2010/09/unix-is-broken/</guid>
          <description xml:base="https://peterlyons.com/problog/2010/09/unix-is-broken/">&lt;p&gt;Reading &lt;a href=&quot;http:&#x2F;&#x2F;factor-language.blogspot.com&#x2F;2010&#x2F;09&#x2F;two-things-every-unix-developer-should.html&quot;&gt;this blog post&lt;&#x2F;a&gt; just makes me cringe. This is why I am so persistently hesitant to bother with C and low level systems programming. They are broken. They are impossible to get right. If even after numerous decades, the &lt;em&gt;cat&lt;&#x2F;em&gt; program still has bugs in basic system interaction, it&#x27;s not the programmers, it&#x27;s the system. Thanks, but no. I&#x27;ll code in a high level language that lets me focus on delivering functionality to my users, not performing sacrificial rituals to the Gods of K&amp;amp;R, signal processing, and bitmasks.&lt;&#x2F;p&gt;
&lt;p&gt;At work we have certain code bases that no matter how long we tweak them and how many dozens of bugs we fix, they just never reach a point of stability and reliability. You need to just abandon them and rethink the problem from scratch. Come at it with a different approach and a brand new code base and some analysis that actually addresses all of the edge cases.&lt;&#x2F;p&gt;
&lt;p&gt;Interestingly, I think the mobile space (iOS, Android) have potential to finally provide a next generation operating system where applications written by average, normal programmers usually function perfectly. Contrast this to most existing operating systems where most programs can be easily made to crash or misbehave in the course of normal use. We&#x27;ve got to get beyond this notion that in order to make a well-behaved application, the developer needs encyclopedic knowledge of bizzarre archaic minutia of the underlying OS.&lt;&#x2F;p&gt;
&lt;p&gt;Incidentally, that post is written by &lt;a href=&quot;http:&#x2F;&#x2F;factorcode.org&#x2F;slava&#x2F;&quot;&gt;Slava Pestov&lt;&#x2F;a&gt;, the creator of the jEdit text editor that has been my primary editor for many years. This guy is good.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>On flash eating my key browser keyboard commands</title>
          <pubDate>Mon, 30 Aug 2010 19:48:19 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2010/08/on-flash-eating-my-key-browser-keyboard-commands/</link>
          <guid>https://peterlyons.com/problog/2010/08/on-flash-eating-my-key-browser-keyboard-commands/</guid>
          <description xml:base="https://peterlyons.com/problog/2010/08/on-flash-eating-my-key-browser-keyboard-commands/">&lt;p&gt;Dear Universe,&lt;&#x2F;p&gt;
&lt;p&gt;Please manifest a way for me to specify that my key browser keyboard shortcuts will work even when I&#x27;m watching a flash video. Specifically I always want &quot;close window&quot;, &quot;new tab&quot;, and &quot;cycle active window&quot; to work. I hate that after watching a video I have to click on the surrounding non-flash web page to get my keyboard commands working again. Please post a comment on this blog post when you are done manifesting this.&lt;&#x2F;p&gt;
&lt;p&gt;Thanks,&lt;&#x2F;p&gt;
&lt;p&gt;Pete&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>How to Install Ubuntu 10.04 over a LAN</title>
          <pubDate>Sat, 17 Jul 2010 04:40:24 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2010/07/install-ubuntu-lan/</link>
          <guid>https://peterlyons.com/problog/2010/07/install-ubuntu-lan/</guid>
          <description xml:base="https://peterlyons.com/problog/2010/07/install-ubuntu-lan/">&lt;h1 id=&quot;overview-and-requirements&quot;&gt;Overview and Requirements&lt;&#x2F;h1&gt;
&lt;p&gt;So I&#x27;ve volunteered to help &lt;a href=&quot;http:&#x2F;&#x2F;www.bococo.org&#x2F;&quot;&gt;Boulder Community Computers&lt;&#x2F;a&gt; set up some automated Ubuntu OS installations over their local area network. Being as this is a large part of what I do professionally, this is something easy for me to do that should help them be more efficient. Their network layout is going to look something like this I think:&lt;&#x2F;p&gt;
&lt;pre&gt;Internet
   ^
   |
Office LAN (private addresses)&lt;-&gt; Various workstations (DHCP from router)
   ^
   |
Multihomed Netboot Server
   ^
   |
OS Build Network (other private addresses)
   ^
   |
   &lt;-&gt;Bare metal target machines needing  an OS (DHCP from Netboot Server)
&lt;&#x2F;pre&gt;
&lt;p&gt;OK, so let&#x27;s talk about some of the goals&#x2F;requirements:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;I want the build network to be separate from the office network. This will keep things simple, avoid DHCP collisions, prevent users from accidentally provisioning their workstations, and keep the intense network traffic of OS installation off the office LAN&lt;&#x2F;li&gt;
&lt;li&gt;I want to mainly download cached objects from the Netboot server to the targets. I want to avoid downloading the OS over the Internet connection repeatedly.&lt;&#x2F;li&gt;
&lt;li&gt;I don&#x27;t want a lot of ongoing maintenance tasks&lt;&#x2F;li&gt;
&lt;li&gt;I would like to be able to do a fully automatic hands-off installation&lt;&#x2F;li&gt;
&lt;li&gt;I would also like to be able to boot into a live CD to test compatibility&lt;&#x2F;li&gt;
&lt;li&gt;I would like to be able to do an interactive custom install&lt;&#x2F;li&gt;
&lt;li&gt;I do want internet access from the OS Build Network (mostly for NTP, but it&#x27;s just handy in general)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;So overall, Ubuntu can meet all of these goals easily. Here are the details.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;setting-up-the-multihomed-network-install-server&quot;&gt;Setting up the Multihomed Network Install Server&lt;&#x2F;h1&gt;
&lt;p&gt;So we want an Ubuntu server machine with two network interfaces (multihomed) to act as our network install server. This machine will provide DHCP&#x2F;PXE, TFTP, NFS, HTTP, and HTTP proxy services to the target machines. It will also act as their default gateway, routing their traffic from the OS Build Network out to the Office LAN and then out onto the Internet and back. Here&#x27;s how we get this server installed and configured.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;install-a-basic-ubuntu-10-04-server-amd64-host&quot;&gt;Install a basic Ubuntu 10.04 Server amd64 host&lt;&#x2F;h2&gt;
&lt;p&gt;I used a VirtualBox VM for this while testing this setup, and we will probably use that in their &quot;production&quot; environment as well. If you use a physical computer, make sure it has 2 network connections. You can select &quot;OpenSSH Server&quot; during the install or &lt;a href=&quot;https:&#x2F;&#x2F;help.ubuntu.com&#x2F;community&#x2F;InstallingSoftware&quot;&gt;install&lt;&#x2F;a&gt; openssh-server later. Once the OS is up and running, we need to configure both of the network interfaces. We&#x27;ll be using RFC 1918 private addresses for both. In this case we have a 10.0.0.0&#x2F;8 network as eth0 which is the Office LAN and a 192.168.0.0&#x2F;16 network as eth1 which is the OS Build Network. The Office LAN is your typical home or office type network where a router is serving private IP addresses over DHCP and providing Internet access. In VirtualBox I set the first interface to be a bridged interface eth0 and the second interface to an internal only interface eth1. To do this we edit &lt;code&gt;&#x2F;etc&#x2F;network&#x2F;interfaces&lt;&#x2F;code&gt; as root as follows.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;a-quick-note-about-code-samples-and-long-lines&quot;&gt;A quick note about code samples and long lines&lt;&#x2F;h3&gt;
&lt;p&gt;Several of the code samples in this article require very long lines. These don&#x27;t display well on the web, but they are displayed using a CSS overflow horizontal scrollbar. Hopefully they are easy enough to read, copy, and paste.&lt;&#x2F;p&gt;
&lt;div class=&quot;code&quot;&gt;
&lt;pre&gt;# This file describes the network interfaces available on your system
# and how to activate them. For more information, see interfaces(5).

# The loopback network interface
auto lo
iface lo inet loopback

# The primary network interface
auto eth0
iface eth0 inet dhcp

# The internal build network for OS installation
auto eth1
iface eth1 inet static
address 192.168.8.1
netmask 255.255.255.0
&lt;&#x2F;pre&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;After that edit, activate the config with (as root) &lt;code&gt;ifdown eth1; ifup eth1&lt;&#x2F;code&gt;. Verify it&#x27;s working by pinging both of your IP addresses (the 10.X.X.X one and the 192.168.X.X one).&lt;&#x2F;p&gt;
&lt;h2 id=&quot;install-and-configure-pxe-boot-services&quot;&gt;Install and Configure PXE Boot Services&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;help.ubuntu.com&#x2F;community&#x2F;InstallingSoftware&quot;&gt;Install&lt;&#x2F;a&gt; &lt;code&gt;dnsmasq&lt;&#x2F;code&gt; via &lt;code&gt;apt-get install dnsmasq&lt;&#x2F;code&gt;. Then as root do a &lt;code&gt;mkdir &#x2F;var&#x2F;lib&#x2F;tftpboot&lt;&#x2F;code&gt;. Edit &lt;code&gt;&#x2F;etc&#x2F;dnsmasq.conf&lt;&#x2F;code&gt; to look as follows.&lt;&#x2F;p&gt;
&lt;div class=&quot;code&quot;&gt;
&lt;pre&gt;interface=eth1
dhcp-range=192.168.8.100,192.168.8.254,12h
dhcp-boot=pxelinux.0
enable-tftp
tftp-root=&#x2F;var&#x2F;lib&#x2F;tftpboot&#x2F;
&lt;&#x2F;pre&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;Restart it with &lt;code&gt;service dnsmasq restart&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;The next big step is to make the Debian Installer available as a PXE boot image. We&#x27;re basically following &lt;a href=&quot;http:&#x2F;&#x2F;www.debuntu.org&#x2F;how-to-unattended-ubuntu-network-install-pxelinux-p4&quot;&gt;this article&lt;&#x2F;a&gt; here, except we&#x27;re doing Ubuntu Lucid 10.04 instead of Feisty. Do the following.&lt;&#x2F;p&gt;
&lt;div class=&quot;code&quot;&gt;
&lt;pre&gt;cd &#x2F;var&#x2F;lib&#x2F;tftpboot
sudo wget http:&#x2F;&#x2F;archive.ubuntu.com&#x2F;ubuntu&#x2F;dists&#x2F;lucid&#x2F;main&#x2F;installer-i386&#x2F;current&#x2F;images&#x2F;netboot&#x2F;netboot.tar.gz
sudo tar -xzvf netboot.tar.gz
sudo rm netboot.tar.gz
&lt;&#x2F;pre&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;OK, at this point we should be ready to boot our first test target machine into the Debian Installer network boot service OS. PXE boot a machine on the OS Build Network (again, I used a VirtualBox VM). You may have to fit a key like F12 to instruct the machine to boot from a network card. I have to hit F12 then &quot;l&quot; to boot from the LAN on my VirtualBox VM. If all is well, you should see a lovely Ubuntu installer menu like this (you won&#x27;t have the menu items containing &quot;10.04&quot; yet, but we&#x27;ll add them later).&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;problog&#x2F;images&#x2F;ubuntu_installer_pxe_menu.png&quot; alt=&quot;Ubuntu Installer PXE Menu&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;So now we can use this to install Ubuntu onto the target. Well, almost. We don&#x27;t have Internet access from our OS Build Network working yet, so let&#x27;s get that going.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;setting-up-routing-to-the-internet&quot;&gt;Setting up Routing to the Internet&lt;&#x2F;h1&gt;
&lt;p&gt;We want to allow our target machines to connect to the Internet, which means we will configure our Net Boot server as a very basic router and NAT firewall. This is often called Internet Connection Sharing when used for this simple purpose. We&#x27;re basically following &lt;a href=&quot;https:&#x2F;&#x2F;help.ubuntu.com&#x2F;community&#x2F;Internet&#x2F;ConnectionSharing&quot;&gt;Internet Connection Sharing Setup&lt;&#x2F;a&gt; docs from the Ubuntu community wiki. As root, add &lt;code&gt;net.ipv4.ip_forward=1&lt;&#x2F;code&gt; to &lt;code&gt;&#x2F;etc&#x2F;sysctl.conf&lt;&#x2F;code&gt;. Then run these commands.&lt;&#x2F;p&gt;
&lt;div class=&quot;code&quot;&gt;
&lt;pre&gt;sudo iptables -A FORWARD -i eth0 -o eth1 -s 192.168.0.0&#x2F;16 -m conntrack  --ctstate NEW -j ACCEPT
sudo iptables -A FORWARD -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
sudo iptables -A POSTROUTING -t nat -j MASQUERADE 
sudo iptables-save | sudo tee &#x2F;etc&#x2F;iptables.sav
sudo sh -c &quot;echo 1 &gt; &#x2F;proc&#x2F;sys&#x2F;net&#x2F;ipv4&#x2F;ip_forward&quot;
&lt;&#x2F;pre&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;Now add &lt;code&gt;iptables-restore &amp;lt; &#x2F;etc&#x2F;iptables.sav&lt;&#x2F;code&gt; to &lt;code&gt;&#x2F;etc&#x2F;rc.local&lt;&#x2F;code&gt; as root just above the &lt;code&gt;exit 0&lt;&#x2F;code&gt; line. This will get us going again when we reboot.&lt;&#x2F;p&gt;
&lt;p&gt;Now you should be able to PXE boot a target, select &quot;Install&quot; from the menu and do an Ubuntu installation. However, it&#x27;s going to be a normal install over the Internet, which is fine for doing one or two boxes, but since we want to crank these out en masse, we&#x27;ll want to cache the bits and grab them locally.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;caching-the-ubuntu-packages-with-apt-cacher-ng&quot;&gt;Caching the Ubuntu Packages with apt-cacher-ng&lt;&#x2F;h1&gt;
&lt;p&gt;I tried both squid and apt-cacher for a solution to locally cache the bulk of the Ubuntu binary package payload, but neither worked very well. apt-cacher-ng seems to do exactly what we want and nothing more with no configuration. &lt;a href=&quot;https:&#x2F;&#x2F;help.ubuntu.com&#x2F;community&#x2F;InstallingSoftware&quot;&gt;install&lt;&#x2F;a&gt; &lt;code&gt;apt-cacher-ng&lt;&#x2F;code&gt;. This &quot;just works&quot; with no setup. After it&#x27;s installed, if you PXE boot a target, select &quot;Install&quot; and go through the interactive install, when prompted for a proxy, enter &lt;code&gt;http:&#x2F;&#x2F;192.168.8.1:3142&lt;&#x2F;code&gt;. This is the OS Build Network IP of the Net Boot server and the apt-cacher-ng proxy port. Now all your packages will be cached on the OS Boot Server. The first install will download them from the Internet. Subsequent installs will get them from the local cache and thus be much much faster and more efficient.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;hands-off-automatic-os-installation&quot;&gt;Hands-off Automatic OS Installation&lt;&#x2F;h1&gt;
&lt;p&gt;OK, we now have network based OS installs working, but since we&#x27;re planning on doing many of these, we want to fully automate this. To do that, we&#x27;ll use a preseed file to configure the Debian Installer to not ask any questions. Configure a file at &lt;code&gt;&#x2F;var&#x2F;lib&#x2F;tftpboot&#x2F;bococo.seed&lt;&#x2F;code&gt; with the the content linked here: &lt;a href=&quot;&#x2F;problog&#x2F;images&#x2F;bococo.seed&quot;&gt;bococo.seed&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Now we&#x27;ll add a new item to the PXE menu to trigger a hands-off automated OS install. Edit &lt;code&gt;&#x2F;var&#x2F;lib&#x2F;tftpboot&#x2F;ubuntu-installer&#x2F;i386&#x2F;boot-screens&#x2F;text.cfg&lt;&#x2F;code&gt; and add this entry.&lt;&#x2F;p&gt;
&lt;div class=&quot;code&quot;&gt;
&lt;pre&gt;label install-10.04-hands-off 
        menu label ^Install 10.04 Hands Off
        kernel ubuntu-installer&#x2F;i386&#x2F;linux
        append vga=normal initrd=ubuntu-installer&#x2F;i386&#x2F;initrd.gz locale=en_US.UTF-8 debian-installer&#x2F;keymap=us auto hostname=bococo preseed&#x2F;url=http:&#x2F;&#x2F;192.168.8.1&#x2F;bococo.seed -- quiet
&lt;&#x2F;pre&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;In order to get that file over HTTP, we&#x27;ll server it up with the nginx web server. &lt;a href=&quot;https:&#x2F;&#x2F;help.ubuntu.com&#x2F;community&#x2F;InstallingSoftware&quot;&gt;install&lt;&#x2F;a&gt; nginx via &lt;code&gt;apt-get install nginx&lt;&#x2F;code&gt;. Then we can edit &lt;code&gt;&#x2F;etc&#x2F;nginx&#x2F;sites-enabled&#x2F;default&lt;&#x2F;code&gt; and change &lt;code&gt;location &#x2F;&lt;&#x2F;code&gt; block to set the new document root to the tftpboot directory so it looks like this.&lt;&#x2F;p&gt;
&lt;div class=&quot;code&quot;&gt;
&lt;pre&gt;        location &#x2F; {
                root   &#x2F;var&#x2F;lib&#x2F;tftpboot;
                index  index.html index.htm;
        }
&lt;&#x2F;pre&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;Restart nginx via &lt;code&gt;service nginx restart&lt;&#x2F;code&gt;. Now we should be able to PXE boot the target and select the &quot;Install 10.04 Hands Off&quot; option and the entire thing should happen automatically.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;network-booting-into-the-live-image&quot;&gt;Network Booting into the Live Image&lt;&#x2F;h1&gt;
&lt;p&gt;One final bit of utility functionality we want to provide is a network bootable live Ubuntu image. This will be handy for testing hardware compatibility, performance, etc. First we&#x27;ll download the primary Ubuntu Desktop CD image and make it available over NFS. Do the following as root.&lt;&#x2F;p&gt;
&lt;div class=&quot;code&quot;&gt;
&lt;pre&gt;apt-get install nfs-kernel-server
cd &#x2F;var&#x2F;opt
wget &#x27;http:&#x2F;&#x2F;ubuntu.cs.utah.edu&#x2F;releases&#x2F;lucid&#x2F;ubuntu-10.04-desktop-i386.iso&#x27;
mkdir &#x2F;var&#x2F;lib&#x2F;tftpboot&#x2F;ubuntu-10.04-desktop-i386
echo &#x2F;var&#x2F;opt&#x2F;ubuntu-10.04-desktop-i386.iso &#x2F;var&#x2F;lib&#x2F;tftpboot&#x2F;ubuntu-10.04-desktop-i386 auto ro,loop 0 0 &gt;&gt; &#x2F;etc&#x2F;fstab
mount &#x2F;var&#x2F;lib&#x2F;tftpboot&#x2F;ubuntu-10.04-desktop-i386
echo &quot;&#x2F;var&#x2F;lib&#x2F;tftpboot&#x2F;ubuntu-10.04-desktop-i386 *(ro,sync,no_subtree_check)&quot; &gt;&gt; &#x2F;etc&#x2F;exports
service nfs-kernel-server restart
&lt;&#x2F;pre&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;Now add another entry to your PXE menu for the live Session. I also like to move the &lt;code&gt;menu default&lt;&#x2F;code&gt; option to the live session, so my final &lt;code&gt;&#x2F;var&#x2F;lib&#x2F;tftpboot&#x2F;ubuntu-installer&#x2F;i386&#x2F;boot-screens&#x2F;text.cfg&lt;&#x2F;code&gt; looks like this:&lt;&#x2F;p&gt;
&lt;div class=&quot;code&quot;&gt;
&lt;pre&gt;default install-10.04-hands-off
label live
        menu label ^Live Session 10.04
        menu default
        kernel ubuntu-10.04-desktop-i386&#x2F;casper&#x2F;vmlinuz
        append initrd=ubuntu-10.04-desktop-i386&#x2F;casper&#x2F;initrd.lz boot=casper netboot=nfs nfsroot=192.168.8.1:&#x2F;var&#x2F;lib&#x2F;tftpboot&#x2F;ubuntu-10.04-desktop-i386          -- quiet
label install
        menu label ^Install
        kernel ubuntu-installer&#x2F;i386&#x2F;linux
        append vga=normal initrd=ubuntu-installer&#x2F;i386&#x2F;initrd.gz -- quiet 
label install-10.04-hands-off
        menu label ^Install 10.04 Hands Off
        kernel ubuntu-installer&#x2F;i386&#x2F;linux
        append vga=normal initrd=ubuntu-installer&#x2F;i386&#x2F;initrd.gz locale=en_US.UTF-8 debian-installer&#x2F;keymap=us auto hostname=bococo preseed&#x2F;url=http:&#x2F;&#x2F;192.168.8.1&#x2F;bococo.seed -- quiet
label cli
        menu label ^Command-line install
        kernel ubuntu-installer&#x2F;i386&#x2F;linux
        append tasks=standard pkgsel&#x2F;language-pack-patterns= pkgsel&#x2F;install-language-support=falseac vga=normal initrd=ubuntu-installer&#x2F;i386&#x2F;initrd.gz -- quiet
&lt;&#x2F;pre&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;And that&#x27;s it! We can now netbook into a live session, an interactive install, or a fully automated install.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;references&quot;&gt;References&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;http:&#x2F;&#x2F;www.debuntu.org&#x2F;how-to-unattended-ubuntu-network-install-pxelinux-p4&quot;&gt;How-To: Unattended Ubuntu Deployment over Network -- page 4 -- PXELinux&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;help.ubuntu.com&#x2F;community&#x2F;Internet&#x2F;ConnectionSharing&quot;&gt;Ubuntu Internet Connection Sharing&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;http:&#x2F;&#x2F;www.debian.org&#x2F;releases&#x2F;etch&#x2F;i386&#x2F;ch04s07.html.en&quot;&gt;Debian Installer section on Automatic Installation&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;help.ubuntu.com&#x2F;10.04&#x2F;installation-guide&#x2F;example-preseed.txt&quot;&gt;Sample preseed file for Ubuntu 10.04&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;http:&#x2F;&#x2F;www.ubuntugeek.com&#x2F;apt-cacher-ng-http-download-proxy-for-software-packages.html&quot;&gt;Info on apt-cacher-ng&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;acknowledgements&quot;&gt;Acknowledgements&lt;&#x2F;h2&gt;
&lt;p&gt;Thanks to the authors of the above reference web pages and Nick Flores for collaborating on this.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Windows Server 2008 Setup Annoyances</title>
          <pubDate>Sat, 10 Jul 2010 07:12:54 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2010/07/windows-setup-annoyances/</link>
          <guid>https://peterlyons.com/problog/2010/07/windows-setup-annoyances/</guid>
          <description xml:base="https://peterlyons.com/problog/2010/07/windows-setup-annoyances/">&lt;p&gt;So I do a lot of work with automated unattended intallations of Operating Systems, including Windows. Here&#x27;s some of my primary complaints about the new Windows setup program in Windows Server 2008.&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;No good way to validate the unnatend.xml file. Now Microsoft does provide tools to help generate these files, and hopefully any file you generate with those (graphical, Windows-only) tools should be at least well-formed and semantically valid. However, there&#x27;s no way to do a deeper validation that a given XML file is compatible with a particular target machine.&lt;&#x2F;li&gt;
&lt;li&gt;The unattend.xml encodes the processor architecture all over the place for no reason. I have to do a search and replace of &lt;code&gt;processorArchitecture=&quot;x86&quot;&lt;&#x2F;code&gt; with &lt;code&gt;processorArchitecture=&quot;amd64&quot;&lt;&#x2F;code&gt; in every &lt;code&gt;component&lt;&#x2F;code&gt; tag. There&#x27;s pretty much zero information in most unattend.xml files that&#x27;s CPU architecture specific anyway. This is a real nuisance. I should be able to use the exact same file on x86 and amd64 without issues.&lt;&#x2F;li&gt;
&lt;li&gt;Setup doesn&#x27;t do the simple but important hardware compatibility validation that would make users&#x27; lives easier. For example, neither winpe nor windows setup with complain if the target system has insufficient RAM. WinPE will just behave very oddly and things won&#x27;t work. There&#x27;s no checking for sufficient disk space ahead of time or that the disk layout is feasible. There&#x27;s no checking for suitable network or storage drivers. When you don&#x27;t have viable storage drivers, you just reboot out of WinPE into a lovely Blue Screen of Death &lt;code&gt;0x7b&lt;&#x2F;code&gt; stop error. Hurray! Similarly, no one at MS seems to care if you don&#x27;t have a working NIC driver. The OS has the word &quot;Server&quot; in it. You need a network driver or your OS is in a useless void.&lt;&#x2F;li&gt;
&lt;li&gt;The fact that windows setup reboots into an environment with zero networking and zero third party applications allowed is just a mind-boggling recipe for end user frustration. We have to resort to brute time out calculation to even know whether the Windows install worked or not. We can&#x27;t provide a good user experience to people looking to do UNATTENDED installs.&lt;&#x2F;li&gt;
&lt;li&gt;Another one in the &quot;we have no idea what unattended means&quot; even though our configuration file is called &quot;unattend.xml&quot; department: STOP PRESENTING GUI DIALOGS. There are many issues that unattend.xml supposedly allows a &lt;code&gt;showGui=&quot;never&quot;&lt;&#x2F;code&gt; but in my experience they either have no effect (GUI displays and halts the install anyway) or they just flat out break the install. Microsoft just doesn&#x27;t &quot;get it&quot; here. Any automated install isn&#x27;t a fully attended graphical install rejiggered to try not to pop up a GUI. It&#x27;s an entirely different use case. Get it through your thick skull: NO ONE IS LOOKING AT THE CONSOLE. THESE ARE SERVERS. THE INSTALL IS BEING DRIVER AUTOMATICALLY BY SOFTWARE. NEVER EVER SHOW A GUI.&lt;&#x2F;li&gt;
&lt;li&gt;Same category of cluelessness. Windows setup doesn&#x27;t return a non-zero exit code on failure. Duh. I&#x27;m aware of some other mechanisms like &lt;code&gt;setupcomplete.cmd&lt;&#x2F;code&gt; that MS claims to provide for this, but from what I can tell after several attempts, they simply don&#x27;t work.&lt;&#x2F;li&gt;
&lt;li&gt;The log files and error messages are just a mess. And I&#x27;m not even talking about obscure edge case failures. Simple things like an invalid product key or computer name can create weird mysterious failures and behavior.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
</description>
      </item>
      <item>
          <title>Remove your SCM system from your job postings</title>
          <pubDate>Wed, 07 Jul 2010 04:54:58 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2010/07/no-scm-in-job-postings/</link>
          <guid>https://peterlyons.com/problog/2010/07/no-scm-in-job-postings/</guid>
          <description xml:base="https://peterlyons.com/problog/2010/07/no-scm-in-job-postings/">&lt;p&gt;Most job postings will list one or more source code management (SCM) tools in their laundry list of required skills and buzzwords. This seems to both be unnecessary and also to miss the point. As an employer, your concern shouldn&#x27;t be whether or not your candidate is intimately familiar with subversion, git, perforce, starteam, mercurial, CVS, sourcesafe, or whatever system your code happens to reside within at this point in time. Why not? Because it just doesn&#x27;t matter and it doesn&#x27;t help you distinguish good candidates from bad. This in my mind seems akin to asking potential postal employees &quot;Do you have experience driving from the right hand side of a boxy little truck?&quot;. It&#x27;s just not something that&#x27;s going to be a stumbling block. If you can drive, you&#x27;ll get the hang of the postal delivery truck soon enough. It&#x27;s the same with SCM. If you have worked successfully on a few sizeable projects in one or two of them, you&#x27;ll be fine. I&#x27;ve never heard this story: &quot;Oh yeah, we hired this woman Sandra and she wrote this fantastic code for us, but she just could not figure out how to commit it to subversion, so we had to let her go.&quot; It&#x27;s just not going to be the issue. Well, what is going to be the issue?&lt;&#x2F;p&gt;
&lt;p&gt;In this context, it&#x27;s more important to see if the candidate understands branch and release organization and management as generic principles. When and how should the code be branched? What steps do you take when it&#x27;s time to ship a release? When a bug needs to get fixed against an old version of the product, how does that work with SCM? These types of questions are OK sanity checks to spend 2 minutes on in an interview, but they don&#x27;t need to be on your job posting.&lt;&#x2F;p&gt;
&lt;p&gt;Now, here&#x27;s what I see missing that seems in my mind much more likely to actually cause you problems down the road. Does your candidate understand software packaging, distribution, installation, and upgrade? Do they understand the principles of package management systems like RPM or DEB, and the complexities around dependency management, upgrade, and rollback? Do they understand how to package and ship (or not) third party libraries? These questions may be more or less relevant based on the deployment model you use (web vs. bundled vs. embedded, etc). However, for lots of projects, they are key and lots of companies are clearly in the dark here.&lt;&#x2F;p&gt;
&lt;p&gt;So the summary: don&#x27;t bother listing a specific SCM as a requirement in your job posting. Instead, briefly interview your candidates on general SCM methods and principles. And in addition to that, given your deployment model(s) find out how much your candidate knows about packaging and installation.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>On Idempotence, intention, and unix commands</title>
          <pubDate>Fri, 21 May 2010 09:42:05 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2010/05/on-idempotence-intention-and-unix-commands/</link>
          <guid>https://peterlyons.com/problog/2010/05/on-idempotence-intention-and-unix-commands/</guid>
          <description xml:base="https://peterlyons.com/problog/2010/05/on-idempotence-intention-and-unix-commands/">&lt;p&gt;&lt;a href=&quot;http:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Idempotence#In_computing&quot;&gt;Idempotence&lt;&#x2F;a&gt; means that running a command or function several times produces the same result as running it only once. This is an very important design principle that is a blessing when used appropriately and a scourge when not used where warranted.&lt;&#x2F;p&gt;
&lt;p&gt;For analogy, imagine you ask a housemate (or butler if that&#x27;s how you roll) to empty the dishwasher. They dutifully go over there, open the dishwasher door, and find it&#x27;s already empty. How do they react? Do they come back to you shouting in confusion &quot;You fool! How can I empty the dishwasher if there&#x27;s nothing in it! Oh woe is me. What am I to do?&quot;? Or do they just think to themselves &quot;score!&quot; and go on a coffee break, leaving you to go about your business trusting that the dishwasher is now empty?&lt;&#x2F;p&gt;
&lt;p&gt;Another analogy is from the military&#x27;s notion of &quot;management by intent&quot; wherein a commander might order his troops to &quot;have camp fully operational by noon&quot; as opposed to dictating specific tactics that must be taken in order to achieve the intended outcome. This way, the troops can rely on their own abilities to achieve the intent and are empowered to respond to changing or unexpected circumstances independently.&lt;&#x2F;p&gt;
&lt;p&gt;Now, when it comes to computer programs, UNIX has a mixed bag of utilities that understand this and some that don&#x27;t.&lt;&#x2F;p&gt;
&lt;div class=&quot;code&quot;&gt;
&lt;pre&gt;mkdir &#x2F;tmp&#x2F;2;echo $?;mkdir &#x2F;tmp&#x2F;2;echo $?
0
mkdir: cannot create directory `&#x2F;tmp&#x2F;2&#x27;: File exists
1

rm &#x2F;tmp&#x2F;foo;echo $?;rm &#x2F;tmp&#x2F;foo;echo $?
0
rm: cannot remove `&#x2F;tmp&#x2F;foo&#x27;: No such file or directory
1

&lt;&#x2F;pre&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;So the bad examples include &lt;code&gt;mkdir, rmdir, rm, ln, and perhaps kill (debatable)&lt;&#x2F;code&gt;. Think about how much simpler using a command line and writing shell scripts would be if these were idempotent and instead of panicking in horror when the user does not know the current state of the filesystem, just allowed the user to describe the desired end state. I would love to have idempotent and recursive by default commands like &lt;code&gt;mkdir -p&lt;&#x2F;code&gt; or &lt;code&gt;rm -rf&lt;&#x2F;code&gt; in combination with a transactional filesystem with built in undo capabilities.&lt;&#x2F;p&gt;
&lt;p&gt;Good idempotent examples include &lt;code&gt;touch, tar, zip, cp, chmod&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;So the point about design and usability here is &lt;strong&gt;it&#x27;s good to ask oneself &quot;What is the user&#x27;s intent here?&quot;&lt;&#x2F;strong&gt;, and try to do everything in your power to work in concert with that intention. A strong and painful negative example from my career has to do with the fact that the Solaris &lt;code&gt;patchadd&lt;&#x2F;code&gt; program is not idempotent and it doesn&#x27;t return exit codes according to the user&#x27;s intent. So when I run &lt;code&gt;patchadd 123456-01&lt;&#x2F;code&gt;, really my intention is &quot;I want this system to be OK with regard to patch 123456-01&quot;. &lt;code&gt;patchadd&lt;&#x2F;code&gt; will return a non-zero exit code if the patch is already installed or the patch is not applicable to the server or if a newer revision is already installed. As a user of &lt;code&gt;patchadd&lt;&#x2F;code&gt;, I don&#x27;t care. It&#x27;s all success to me, and nor do I want to be bothered with implementation details within patchadd such as not installing a patch if a newer revision is already installed. I think many shell scripts would be a lot smaller and clearer and simpler without always having to wrap &lt;code&gt;mkdir&lt;&#x2F;code&gt; in an &lt;code&gt;if [ ! -d &#x2F;blah&#x2F;dir ]&lt;&#x2F;code&gt; clause to avoid spurious error output.&lt;&#x2F;p&gt;
&lt;p&gt;A few other links on this topic:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;http:&#x2F;&#x2F;devhawk.net&#x2F;2007&#x2F;11&#x2F;09&#x2F;The+Importance+Of+Idempotence.aspx&quot;&gt;The Importance of Idempotence (devhawk)&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;http:&#x2F;&#x2F;www.allapplabs.com&#x2F;glossary&#x2F;idempotent.htm&quot;&gt;Java Glossary entry on Idempotent&lt;&#x2F;a&gt;. I like this quote &quot;Elevator call buttons are also idempotent, though many people think they are not.&quot;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;http:&#x2F;&#x2F;stackoverflow.com&#x2F;questions&#x2F;1077412&#x2F;what-is-an-idempotent-operation&quot;&gt;Stack Overflow: What is an idempotent operation?&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</description>
      </item>
      <item>
          <title>Bleeding Edge and Rotting Core</title>
          <pubDate>Fri, 19 Mar 2010 08:53:53 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2010/03/bleeding-edge-and-rotting-core/</link>
          <guid>https://peterlyons.com/problog/2010/03/bleeding-edge-and-rotting-core/</guid>
          <description xml:base="https://peterlyons.com/problog/2010/03/bleeding-edge-and-rotting-core/">&lt;p&gt;I just wanted to post some thoughts on the topic of selecting software components with regard to the maturity thereof. I think overall the programmer community is by default gung-ho about the bleeding edge. We like the shiny new toys with the bells and whistles. Once something&#x27;s been around enough to have its weaknesses well understsood, we find it very frustrating to have to continue to work with it. I&#x27;m not going to offer any specific recommendations, just some things to keep in mind. The general gist though is that it takes some hard-earned pragmatism and real production experience to understand the value of using older releases of components.&lt;&#x2F;p&gt;
&lt;p&gt;First, let&#x27;s define some terms. We&#x27;re familiar with what is known as the bleeding edge. The new hotness. The stuff straight off the presses instilled with the glimmering light of state of the art knowledge. There&#x27;s probably always been a lot of this, but there seems to have been a flurry in the past five years of so of interest in ruby, rails, erlang, clojure, scala, dozens of python app and web frameworks, etc. On the other hand, we have the old guard, which I&#x27;d like to call the rotting core. Generally we shy away from this, but there are times when it is absolutely the correct choice in certain situations.&lt;&#x2F;p&gt;
&lt;p&gt;So, let&#x27;s look at some pros and cons.&lt;&#x2F;p&gt;
&lt;p&gt;Bleeding edge pros:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;The freshest and (usually) best designs and thinking are made available&lt;&#x2F;li&gt;
&lt;li&gt;Almost always more succinct and expressive&lt;&#x2F;li&gt;
&lt;li&gt;Often more coherent, clean, and consistent&lt;&#x2F;li&gt;
&lt;li&gt;Embodies improvements based on lessons learned from past failings and shortcomings&lt;&#x2F;li&gt;
&lt;li&gt;Development tools and processes are sometimes more productive&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Bleeding edge cons:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Development tools are usually immature and inferior
&lt;ul&gt;
&lt;li&gt;IDE support is likely to lag behind&lt;&#x2F;li&gt;
&lt;li&gt;Debugger may lag behind as may remote graphical debugging&lt;&#x2F;li&gt;
&lt;li&gt;Performance profilers might not be there&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;Deployment issues may not have been well addressed yet&lt;&#x2F;li&gt;
&lt;li&gt;Updates will come more frequently causing churn&lt;&#x2F;li&gt;
&lt;li&gt;Software has not had as broad testing in production and is therefore likely to have more &quot;surprises&quot;. Sometimes these can be showstoppers.&lt;&#x2F;li&gt;
&lt;li&gt;Community size will be smaller&lt;&#x2F;li&gt;
&lt;li&gt;Depth of knowledge in the community will be shallower&lt;&#x2F;li&gt;
&lt;li&gt;Standard library may be undergoing more flux&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Rotting core pros:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Stable, known quantity. It may have warts and bugs, but at least we&#x27;re aware of most of them by now&lt;&#x2F;li&gt;
&lt;li&gt;Development tools generally have solid support including remote graphical debugging, mature performance profilers, etc&lt;&#x2F;li&gt;
&lt;li&gt;Community size will be larger&lt;&#x2F;li&gt;
&lt;li&gt;Community depth of knowledge will be much deeper&lt;&#x2F;li&gt;
&lt;li&gt;Updates are rare and only for occasional major issues or security patches&lt;&#x2F;li&gt;
&lt;li&gt;standard library will be well known and stable&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Rotting core cons:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Less exciting to developers. Yesterday&#x27;s designs and paradigms.&lt;&#x2F;li&gt;
&lt;li&gt;Often tedious compared to the bleeding edge&lt;&#x2F;li&gt;
&lt;li&gt;Support issues. Standard answer may always be &quot;update to the latest version&quot;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;And now, let&#x27;s back this up with some examples and anecdotes. I think when it comes to rotting core technologies, you have both the &quot;oldie but a goodie&quot; category and the &quot;oldie and a baddie&quot; one. Currently my project has a component written against the now ancient Python 1.5.2 runtime, and we have hundreds of thousands of copies of that component installed at customer sites. It is running on something around seventy different OSes. Now, at the time when that component was originally written, this was close to the bleeding edge. We&#x27;ve still not entirely upgraded it because it&#x27;s an oldie and a goodie. We&#x27;ve patched it a bunch and run it under huge loads and huge scales. We know what it can do, and we know what it can&#x27;t do. We even had famous python educator Mark Lutz (Programming Python) come in to train us and give us quizzical looks when we explain that half of what he is saying doesn&#x27;t apply to us since it wasn&#x27;t available in python 1.5.2. Over the years, I&#x27;ve come to see the merits of this and even though its frustrating, the business reality is that every year that stuff continues to run without issue is bettering the return on the initial R&amp;amp;D investment. It ain&#x27;t broke, so we&#x27;re not in a hurry to fix it.&lt;&#x2F;p&gt;
&lt;p&gt;Of course, on the other side, you&#x27;ve got things like Java 1.2, which I also worked with. Python has come a long way since 1.5.2, but really it&#x27;s still basically the same deal, and the design was good from the start. Java has probably come even farther, but the design was a mess from the beginning and they&#x27;ve since seen the error of their ways and made some great improvements. I would put that one in the &quot;oldie but a baddie&quot; category and do what it takes to upgrade.&lt;&#x2F;p&gt;
&lt;p&gt;I remember chatting with a stranger on a plane after we each noticed that we were both programmers and were both actively programming on the plane. This was a few years ago and Ruby was still pretty much bleeding edge. He looked at me with desperation and asked me if I knew anything about debugging deadlocks, threading issues, and core dumps since his production ruby app was regularly hitting issues and his team was basically at a point where they didn&#x27;t have the knowledge or tools to solve them, and it was jeopardizing their whole project. Sadly I couldn&#x27;t offer any help, but I could certainly sympathize.&lt;&#x2F;p&gt;
&lt;p&gt;I also have a friend who used to work at a DNS registry run by someone very much of the &quot;rotting core&quot; philosophy. They ran Solaris 8 and ancient versions of lots of core C&#x2F;unix utilities (bind et al), and to actually run versions that old took significant effort on their part, but it made sense for that project. They are running a piece of the Internet backbone. It&#x27;s not bleeding edge stuff. It just needs stability, stability, stability, and those are the tools they needed to meet their business goals.&lt;&#x2F;p&gt;
&lt;p&gt;So next time you join a new project and start to reflexively freak out when they explain their software stack, supress your urge for a minute and get some information about the choices they have made and the reasoning and circumstances that got them where they are. You might be surprised at the difficult but pragmmatic choices that were made and hopefully you can admire and appreciate the character of those who made them.&lt;&#x2F;p&gt;
&lt;p&gt;And finally, think about the value of being able to look across a broad set of available components and correctly determine where components are in a &quot;sweet spot&quot; of their lifecycle, ripe to be chosen and deployed at length. That is a deep wisdom that is a long time coming.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>MoinMoin Columns Macro</title>
          <pubDate>Mon, 08 Mar 2010 00:43:31 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2010/03/moinmoin-columns/</link>
          <guid>https://peterlyons.com/problog/2010/03/moinmoin-columns/</guid>
          <description xml:base="https://peterlyons.com/problog/2010/03/moinmoin-columns/">&lt;p&gt;I just updated the &quot;Columns&quot; macro for the &lt;a href=&quot;http:&#x2F;&#x2F;moinmo.in&#x2F;&quot;&gt;MoinMoin&lt;&#x2F;a&gt; wiki. This allows you to lay out a wiki page in two to ten columns. This makes it easier to get lots of info on one page in certain situations and I&#x27;ve used it to great benefit on my personal wiki where I organize my stuff.&lt;&#x2F;p&gt;
&lt;p&gt;Here&#x27;s the &lt;a href=&quot;http:&#x2F;&#x2F;moinmo.in&#x2F;MacroMarket&#x2F;Columns#peterlyonsupdate&quot;&gt;MacroMarket page where my update has been posted for discussion&lt;&#x2F;a&gt;. The original author may not like it, so it might not become the canonical fork, but that&#x27;s how it goes with open source.&lt;&#x2F;p&gt;
&lt;p&gt;Here&#x27;s what it looks like with four columns:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;problog&#x2F;images&#x2F;moinmoin_columns.png&quot; alt=&quot;MoinMoin wiki page with Columns macro&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Optional Syntax Should Be Illegal</title>
          <pubDate>Sun, 14 Feb 2010 03:33:31 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2010/02/optional-syntax-should-be-illegal/</link>
          <guid>https://peterlyons.com/problog/2010/02/optional-syntax-should-be-illegal/</guid>
          <description xml:base="https://peterlyons.com/problog/2010/02/optional-syntax-should-be-illegal/">&lt;p&gt;Why in the world do some programming languages include optional syntax? To a true type A engineer, this is incomprehensible and unacceptable. For example, in Adobe&#x27;s ActionScript, statements may optionally be terminated with a semicolon. Usually this is not required, except in a few situations you need it. Evil. The statement that our number one job as software engineers is to manage complexitity really resonates with me, and willy nilly allowing of optional syntax just destroys consistency, predictability, and simplicity for no reason whatsoever. Optional syntax seems to me a bad language design smell that indicates the language authors need to rethink a bit and find something that works always and should be required.&lt;&#x2F;p&gt;
&lt;p&gt;Part of the impetus for this post is my annoyance when my thinking cycles are wasted on unimportant details in a source code file. I&#x27;d rather have a strict format so that whenever I am reading or writing in a language, there will be a strong and deep consistency. I don&#x27;t have to spend time deciding whether I&#x27;m going to use some optional syntax or which of the several ways to express the same thing I&#x27;m going to use. Similarly, when I come upon someone else&#x27;s code and it&#x27;s a mixture of two optional approaches, I feel compelled to go and make it consistent, which is another time waster.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Environment Variables Considered Harmful</title>
          <pubDate>Sun, 14 Feb 2010 03:30:14 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2010/02/environment-variables-considered-harmful/</link>
          <guid>https://peterlyons.com/problog/2010/02/environment-variables-considered-harmful/</guid>
          <description xml:base="https://peterlyons.com/problog/2010/02/environment-variables-considered-harmful/">&lt;p&gt;Many projects reference environment variables at either build time, install time, or run time to handle configuration that can&#x27;t be made to work across all of the target environments.  It is better to use plain text simple configuration files for the reasons that follow.  First, let&#x27;s quickly review common usage of environment variables.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Directory path to supporting tools and libraries (JAVA_HOME, LD_LIBRARY_PATH, CATALINA_HOME, etc)&lt;&#x2F;li&gt;
&lt;li&gt;Customization of build time locations (BUILD_DIR, OUTPUT_DIR, DIST_DIR, etc)&lt;&#x2F;li&gt;
&lt;li&gt;Customization of compiler options and other build time configurations (STATIC_LINK, etc)&lt;&#x2F;li&gt;
&lt;li&gt;Settings that apply OS-wide and to several programs (http_proxy, etc). In theory this would almost make sense.  You set your http_proxy environment variable in one place, and any program that makes HTTP requests respects that setting.  In practice, these settings are more realistically effective higher up in your desktop environment, and AFAIK in the whole GNU&#x2F;Linux&#x2F;UNIX ecosystem, there are only a small handful of cross-program environment variables that are actually used commonly.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;So what&#x27;s the problem with environment variables?&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;The are ephemeral, nebulous, stored in memory within your shell and process tree&lt;&#x2F;li&gt;
&lt;li&gt;How and where they are set is inconsistent across shells (~&#x2F;.bash_profile, ~&#x2F;.zshrc, etc)&lt;&#x2F;li&gt;
&lt;li&gt;The syntax to specify them is needlessly different across different shells (csh vs. bash vs. cmd.exe, etc)&lt;&#x2F;li&gt;
&lt;li&gt;How to fully unset them varies per shell and is often unclear&lt;&#x2F;li&gt;
&lt;li&gt;There is widespread confusion on the distinction between shell variables and environment variables, how to set each, and how each interacts with subprocesses&lt;&#x2F;li&gt;
&lt;li&gt;They are often tied to a user account due to where they are specified above, and can vary between login shell verses non-login shell. They can therefore often vary when a program runs via init compared to run from an interactive root login shell.  This can be difficult to detect and troubleshoot&lt;&#x2F;li&gt;
&lt;li&gt;They are rife with &lt;a href=&quot;https:&#x2F;&#x2F;www.securecoding.cert.org&#x2F;confluence&#x2F;pages&#x2F;worddav&#x2F;preview.action?pageId=3524&amp;amp;fileName=Environment+Variables+v3.pdf&quot;&gt;major security concerns&lt;&#x2F;a&gt; and a common attack vector&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;All of these reasons combined mean that in general environment variables are losers in our goal of managing complexity and making simple, easy to use software that is cross platform.  So what&#x27;s the solution?  The solution, as it so often is, is simple plain text configuration files.  At the end of the day, environment variables end up set in a shell script as KEY=VALUE type pairs, and that&#x27;s where they belong in a configuration file on the filesystem. How does this make things better?&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;One consistent place to set your application&#x27;s configuration&lt;&#x2F;li&gt;
&lt;li&gt;Same syntax regardless of shell, programming language or OS&lt;&#x2F;li&gt;
&lt;li&gt;Files on disk are concrete and reliable. You can email it to someone for help with troubleshooting and be confident about its content&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;So go forth and configure with simple plain text configuration files.  And there will be much rejoicing.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Business hours</title>
          <pubDate>Tue, 22 Sep 2009 10:38:56 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2009/09/business-hours/</link>
          <guid>https://peterlyons.com/problog/2009/09/business-hours/</guid>
          <description xml:base="https://peterlyons.com/problog/2009/09/business-hours/">&lt;p&gt;NOTICE: To all businesses with a single physical location and a web site. You will put your address, phone number, and business hours on your home page. There will not be a &quot;Contact&quot; page. There will not be an &quot;About&quot; page. END OF NOTICE&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>The wheel of not waiting</title>
          <pubDate>Fri, 18 Sep 2009 08:01:00 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2009/09/the-wheel-of-not-waiting/</link>
          <guid>https://peterlyons.com/problog/2009/09/the-wheel-of-not-waiting/</guid>
          <description xml:base="https://peterlyons.com/problog/2009/09/the-wheel-of-not-waiting/">&lt;p&gt;So flash videos are everywhere now. Generally as they load they show some spinning wheel type graphic. The problem is as an end user, I have no visual differentiation between a video that is loading slowly and a video sitting there waiting for TCP from an overloaded server that is simply never going to work, and certainly not in a timeframe smaller than my attention budget. Show me whether or not you are getting any data, and I might be willing to wait, but if you have me watching your spinner until your TCP connection times out, you are just frustrating me.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>How to disable wpautop in WordPress blogs</title>
          <pubDate>Sat, 23 May 2009 19:53:19 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2009/05/disable-wpautop/</link>
          <guid>https://peterlyons.com/problog/2009/05/disable-wpautop/</guid>
          <description xml:base="https://peterlyons.com/problog/2009/05/disable-wpautop/">&lt;p&gt;So, when creating a &lt;a href=&quot;http:&#x2F;&#x2F;wordpress.org&quot;&gt;WordPress&lt;&#x2F;a&gt; blog, even if you are editing in HTML mode, WordPress includes a feature called &quot;wpautop&quot; that will replace any pair of line feed characters in your post markup with a &lt;p&gt; tag. This is helpful I think in general for people who blog mostly paragraphs with some links and images. However, if you blog with more complex markup, this can invalidate your HTML. I run my HTML through the &lt;a href=&quot;http:&#x2F;&#x2F;validator.w3.org&quot;&gt;W3C HTML Validator&lt;&#x2F;a&gt; to check it and wpautop can cause validation to fail. I hunted around online for an easy way to disable this and didn&#x27;t see one, so I made the changes described below.&lt;&#x2F;p&gt;
&lt;p&gt;One thing to keep in mind is that if you HAVE been relying on wpautop and you have not been including your own explicit &lt;p&gt; tags, disabling wpautop will cause all your paragraphs to run together and thus your layout will be broken. To prepare for this, pre-edit all your posts so they have the paragraph tags and remove extra blank lines from them. You can check how they look in that state since when there are no blank lines wpautop won&#x27;t do anything. Once they look good like that, you can disable wpautop.&lt;&#x2F;p&gt;
&lt;p&gt;In your WordPress installation, edit the file &lt;code&gt;wp-includes&#x2F;formatting.php&lt;&#x2F;code&gt;. Search for &quot;function wpautop&quot; and insert the following two lines at the beginning of the function to disable it.&lt;&#x2F;p&gt;
&lt;div class=&quot;code&quot;&gt;
&lt;pre&gt;function wpautop($pee, $br = 1) {
        &#x2F;&#x2F;plyons disabling this. 20090516
        return $pee;
&lt;&#x2F;pre&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;Of course, this change will be undone when you upgrade to a newer WordPress release, so it&#x27;s just a convenient hack. Once you have your posts with proper paragraph tags and no extra line feeds, wpautop should not change your markup and therefore you shouldn&#x27;t have a problem when it is re-enabled after a WordPress upgrade.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Maritz: 1 - Very Dissatisfied</title>
          <pubDate>Sun, 17 May 2009 04:27:56 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2009/05/maritz-1-very-dissatisfied/</link>
          <guid>https://peterlyons.com/problog/2009/05/maritz-1-very-dissatisfied/</guid>
          <description xml:base="https://peterlyons.com/problog/2009/05/maritz-1-very-dissatisfied/">&lt;p&gt;I bought a new car this fall and a few months later I got a follow-up survey in the mail from Maritz Research. Having a few pieces of feedback to give, such as the orange readout on the Bose sound system being invisible through Sunglasses, I endeavored to fill it out. Holy SAT Test, Batman! The survey is nine jam-packed, small-font pages long. There are 76 officially numbered questions, but many questions involve dozens of individual line-items. See the example below where question 58 asks you to rate 67 individual aspects of the vehicle! Sixty frigging seven! I gave up in frustration long before getting there.&lt;&#x2F;p&gt;
&lt;p&gt;This represents a complete failure to do your job as a market research company. This is their business. Did they exert any effort to make the customer do less work? No. Does the survey include dozens and dozens of line items that completely do not apply to my vehicle because it&#x27;s not a pick-up truck, and so forth? Yes. Did they select only the really meaningful things for me to rate? No. For example, I am asked to supply my satisfaction level from 5 &quot;Completely Satisfied&quot; to 1 &quot;Very Dissatisfied&quot; on the topic &quot;absence of engine stalling&quot;. Give me a break. You need to survey you customers to find out A) your cars stall and B) customers find that unsatisfactory. Please. Do they have any section for free-form comments, unprompted feedback, or even brief descriptions? No. Do they have a special &quot;green traffic light&quot; insert stapled in reading &quot;Your Opinion Counts! Pleas Proceed...&quot;? Yes. Apparently my opinion doesn&#x27;t count enough for them to create a survey with less complexity than an income tax form. I&#x27;m hunting around for the section to fill in my &lt;a href=&quot;http:&#x2F;&#x2F;www.imdb.com&#x2F;title&#x2F;tt0365825&#x2F;quotes&quot;&gt;non-farm income&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Please rate your satisfaction with this Maritz Research survey:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;input type=&quot;radio&quot; name=&quot;maritz_sat&quot; value=&quot;5&quot; disabled=&quot;disabled&quot;&gt;5 - Completely Satisfied&lt;br &#x2F;&gt;
&lt;input type=&quot;radio&quot; name=&quot;maritz_sat&quot; value=&quot;4&quot; disabled=&quot;disabled&quot;&gt;4 - Very Satisfied&lt;br &#x2F;&gt;
&lt;input type=&quot;radio&quot; name=&quot;maritz_sat&quot; value=&quot;3&quot; disabled=&quot;disabled&quot;&gt;3 - Satisfied&lt;br &#x2F;&gt;
&lt;input type=&quot;radio&quot; name=&quot;maritz_sat&quot; value=&quot;2&quot; disabled=&quot;disabled&quot;&gt;2 - Somewhat Dissatisfied&lt;br &#x2F;&gt;
&lt;input type=&quot;radio&quot; name=&quot;maritz_sat&quot; value=&quot;1&quot; disabled=&quot;disabled&quot; checked=&quot;checked&quot;&gt;1 - Very Dissatisfied&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;problog&#x2F;images&#x2F;maritz_survey_fail_web.jpg&quot; alt=&quot;Maritz Survey Fail&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>How to zip a directory in python</title>
          <pubDate>Mon, 27 Apr 2009 10:04:03 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2009/04/zip-dir-python/</link>
          <guid>https://peterlyons.com/problog/2009/04/zip-dir-python/</guid>
          <description xml:base="https://peterlyons.com/problog/2009/04/zip-dir-python/">&lt;p&gt;I came across this problem at work and also over on &lt;a href=&quot;http:&#x2F;&#x2F;stackoverflow.com&#x2F;questions&#x2F;458436&#x2F;adding-folders-to-a-zip-file-using-python&#x2F;792199#792199&quot;&gt;this www.stackoverflow.com thread&lt;&#x2F;a&gt;. You have a directory and you want to recursively zip it up. Simple, right? The equivalent of the unix command &quot;zip myDir.zip myDir&quot;. Should be like 5 lines of code? Python even has a built in zipfile module, sweet! Well, as is often the case (see urllib and friends), python&#x27;s &quot;batteries included&quot; slogan is more like &quot;enough batteries for 36 seconds included&quot;. Anyway, it&#x27;s more like 23 lines of functional code, which is still pretty good, but I would have expected the zipfile module to have this included and not have to use os.walk() to do this.&lt;&#x2F;p&gt;
&lt;div class=&quot;code&quot;&gt;
&lt;pre&gt;#!&#x2F;usr&#x2F;bin&#x2F;python
# -*- coding: utf-8 -*-
&quot;&quot;&quot;This is a sample function for zipping an entire directory into a zipfile&quot;&quot;&quot;

#This seems to work OK creating zip files on both windows and linux. The output
#files seem to extract properly on windows (built-in Compressed Folders feature,
#WinZip, and 7-Zip) and linux. However, empty directories in a zip file appear
#to be a thorny issue. The solution below seems to work but the output of
#&quot;zipinfo&quot; on linux is concerning. Also the directory permissions are not set
#correctly for empty directories in the zip archive. This appears to require
#some more in depth research.

#I got some info from:
#http:&#x2F;&#x2F;www.velocityreviews.com&#x2F;forums&#x2F;t318840-add-empty-directory-using-zipfile.html
#http:&#x2F;&#x2F;mail.python.org&#x2F;pipermail&#x2F;python-list&#x2F;2006-January&#x2F;535240.html
import os
import zipfile

def zipdir(dirPath=None, zipFilePath=None, includeDirInZip=True):
    &quot;&quot;&quot;Create a zip archive from a directory.

    Note that this function is designed to put files in the zip archive with
    either no parent directory or just one parent directory, so it will trim any
    leading directories in the filesystem paths and not include them inside the
    zip archive paths. This is generally the case when you want to just take a
    directory and make it into a zip file that can be extracted in different
    locations. 

    Keyword arguments:

    dirPath -- string path to the directory to archive. This is the only
    required argument. It can be absolute or relative, but only one or zero
    leading directories will be included in the zip archive.

    zipFilePath -- string path to the output zip file. This can be an absolute
    or relative path. If the zip file already exists, it will be updated. If
    not, it will be created. If you want to replace it from scratch, delete it
    prior to calling this function. (default is computed as dirPath + &quot;.zip&quot;)

    includeDirInZip -- boolean indicating whether the top level directory should
    be included in the archive or omitted. (default True)

&quot;&quot;&quot;
    if not zipFilePath:
        zipFilePath = dirPath + &quot;.zip&quot;
    if not os.path.isdir(dirPath):
        raise OSError(&quot;dirPath argument must point to a directory. &quot;
            &quot;&#x27;%s&#x27; does not.&quot; % dirPath)
    parentDir, dirToZip = os.path.split(dirPath)
    #Little nested function to prepare the proper archive path
    def trimPath(path):
        archivePath = path.replace(parentDir, &quot;&quot;, 1)
        if parentDir:
            archivePath = archivePath.replace(os.path.sep, &quot;&quot;, 1)
        if not includeDirInZip:
            archivePath = archivePath.replace(dirToZip + os.path.sep, &quot;&quot;, 1)
        return os.path.normcase(archivePath)

    outFile = zipfile.ZipFile(zipFilePath, &quot;w&quot;,
        compression=zipfile.ZIP_DEFLATED)
    for (archiveDirPath, dirNames, fileNames) in os.walk(dirPath):
        for fileName in fileNames:
            filePath = os.path.join(archiveDirPath, fileName)
            outFile.write(filePath, trimPath(filePath))
        #Make sure we get empty directories as well
        if not fileNames and not dirNames:
            zipInfo = zipfile.ZipInfo(trimPath(archiveDirPath) + &quot;&#x2F;&quot;)
            #some web sites suggest doing
            #zipInfo.external_attr = 16
            #or
            #zipInfo.external_attr = 48
            #Here to allow for inserting an empty directory.  Still TBD&#x2F;TODO.
            outFile.writestr(zipInfo, &quot;&quot;)
    outFile.close()
&lt;&#x2F;pre&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;Here&#x27;s some samples of how you use this:&lt;&#x2F;p&gt;
&lt;div class=&quot;code&quot;&gt;
&lt;pre&gt;zipdir(&quot;foo&quot;) #Just give it a dir and get a .zip file
zipdir(&quot;foo&quot;, &quot;foo2.zip&quot;) #Get a .zip file with a specific file name
zipdir(&quot;foo&quot;, &quot;foo3nodir.zip&quot;, False) #Omit the top level directory
zipdir(&quot;..&#x2F;test1&#x2F;foo&quot;, &quot;foo4nopardirs.zip&quot;, False) #exclude some leading dirs
zipdir(&quot;..&#x2F;test1&#x2F;foo&quot;, &quot;foo5pardir.zip&quot;) #Include some leading dirs
&lt;&#x2F;pre&gt;
&lt;&#x2F;div&gt;</description>
      </item>
      <item>
          <title>Thoughts on Scrum</title>
          <pubDate>Fri, 17 Apr 2009 08:15:07 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2009/04/thoughts-on-scrum/</link>
          <guid>https://peterlyons.com/problog/2009/04/thoughts-on-scrum/</guid>
          <description xml:base="https://peterlyons.com/problog/2009/04/thoughts-on-scrum/">&lt;p&gt;So I had a chance to work in the &lt;a href=&quot;http:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Scrum_(development)&quot;&gt;Scrum&lt;&#x2F;a&gt; methodology for about six months recently. I thought I&#x27;d write up some of my thoughts on the experience and the process. Please note that first, this is my first experience working in the Scrum process. I am sure as I use it more these opinions will change. Second, these are strictly my personal thoughts and opinions and in no way represent the experience of my team or anyone else. Third, my project had a specific set of tasks and particulars. On a different project, with more sprints or shorter tasks or other relevant variations, I might have drawn (and may well draw in the future) different conclusions.&lt;&#x2F;p&gt;
&lt;p&gt;Overall, agile and Scrum are a vast improvement over the &lt;a href=&quot;http:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Waterfall_model&quot;&gt;waterfall model&lt;&#x2F;a&gt; traditionally used at large companies. Some of the specifics benefits:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Far less time wasted doing detailed design and estimates for huge amounts of work that will take a long time to build, or very often might not get built at all&lt;&#x2F;li&gt;
&lt;li&gt;Overall a very steady and metered workload. We did hit a few days of mad dash toward the end of one or two sprints preparing for the demo, but much more stable than the wild variances and unpredictability of waterfall&lt;&#x2F;li&gt;
&lt;li&gt;The focus on build and test automation really does enable better agility in the code and prevent regressions&lt;&#x2F;li&gt;
&lt;li&gt;Having something demoable every month is just all around good for all parties involved. I think perhaps this is the single most important piece of the methodology. Ignore this and I bet a substantial amount of the benefit of Scrum would be lost.&lt;&#x2F;li&gt;
&lt;li&gt;Similar to demoable, the notion of &quot;potentially shippable product&quot; really rings true for me and forces you to deal with the issues that often lurk in the shadows in the waterfall model only to jump out at the last minute: installation, upgrade, documentation, fit and finish, etc.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;So the remainder of this will mostly comment on things I found to be problematic or confusing, but I want it to be clear that I am a huge proponent of agile and Scrum and this is not meant to be an argument that the status quo is better. Quite the contrary.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;the-backlog-and-user-stories&quot;&gt;The Backlog and User Stories&lt;&#x2F;h3&gt;
&lt;p&gt;I want to opine a bit about this notion of &quot;User Stories&quot;. Conceptually, this feels like a perfectly healthy way to constantly remind the team to be focused on the important things that will be valuable to the end users. I think this works best for user interfaces and web development where the ratio of engineering effort to visible, tangible change in the end user interface or experience is high. However, I think in many other areas of software development, such as embedded systems, and in our case complex enterprise software, one user story can generate a boatload of tasks and there end up being a lot of backlog items that are just engineering internal stepping stones to functionality. In our team, we were able to bridge this gap and refer to the work items in the backlog just as general &quot;backlog items&quot;, but I still felt a bit guilty adding more raw engineering items to the backlog even though it would be a bit of a stretch to tie them closely to a user story. I think we managed to get beyond this eventually and I don&#x27;t think we strayed off into the woods of unimportant engineering diversions. However, in many if not most backlog items, we were not able to apply the &quot;As a $USER_ROLE, I want to $ACTION so I can $BENEFIT&quot; user story template.&lt;&#x2F;p&gt;
&lt;p&gt;I was just browsing around &lt;a href=&quot;http:&#x2F;&#x2F;www.openagile.com&quot;&gt;www.openagile.com&lt;&#x2F;a&gt; and noticed they call the backlog the &quot;Work Queue&quot;. I like this name better because it more easily allows the notion of &quot;anything that requires work&quot; can go in there and I dislike the word &quot;backlog&quot; because it connotes a buildup of work debt that for me has negative motivational effects.&lt;&#x2F;p&gt;
&lt;p&gt;Secondly, I found some problems trying to implement this idea that a user story should fit on a &quot;story card&quot; the size of an index card and be fleshed out through face to face conversation. The software I develop at work tends to have a high complexity level and attention to detail. It&#x27;s more complex than web development. Our work items often can&#x27;t be clearly expressed with something straightforward like &quot;As a shopper, I want to see the total cost of the items in my shopping cart in the page header&quot;. The problem with discussion is A) our team was in three cities with no more than two people in any single location B) we wanted something in writing that could be referred to over and over again throughout and after the sprint and C) no one could remember the details otherwise. Due to C), if you asked the product owner (1&#x2F;2 me) to clarify a user story through conversation on sprint day 2, 12, and 16, you were liable to get three variations. Our stories ended up being usually at least a few paragraphs worth of detail plus a smattering of acceptance tests, and I think that is OK and worked better for us.&lt;&#x2F;p&gt;
&lt;p&gt;Also, it turns out that populating and maintaining the product backlog is actually a large amount of work. Keeping the backlog items detailed enough, in the right order, with good acceptance tests takes an awful lot of time. Normally the product owner is not on the scrum team. However, at my company we&#x27;re not adopting that aspect at this time (for several reasons), and in this project myself and one other scrum team member acted as the product owner role. Basically after working hard to get the sprint review demo up and working, my co-product-owner and I would have about 2 hours Friday afternoon while we were completely fried and 1 hour Monday morning to try to whip the backlog into shape for the next sprint planning meeting Monday morning. In retrospect, we should have allocated about two full days per sprint just for care and feeding of the product backlog. For the early sprints, this number might be larger - like a week, and then as the effort congeals, the amount of time the product owner needs to allocate to backlog care and feeding will probably grow shorter, unless a major change of requirements comes down the pike.&lt;&#x2F;p&gt;
&lt;p&gt;One final point about the backlog or work queue. After several sprints, we ended up having a fairly enormous work queue with hundreds of items. This became really unwieldy to deal with as a flat list ordered by priority. We ended up wanting to leave the items in the sprint ordered by priority, and about a sprint or two&#x27;s worth of work items ordered by priority in the backlog, but for all the stuff further down the road, it was much easier to group those into folders based on functionality. I guess each product owner&#x27;s mileage may vary here, but my point is do whatever it is you need to do as a product owner to be able to comprehend and manipulate your work queue with facility.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;user-stories-and-product-backlog-summary-and-suggestions&quot;&gt;User Stories and Product Backlog summary and suggestions:&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;Use the terms &quot;Work Queue&quot; and &quot;Work Item&quot; instead&lt;&#x2F;li&gt;
&lt;li&gt;Don&#x27;t feel obligated to use the &quot;As an X, I want to Y, so I can Z&quot; template if it is unnatural (But do stick with it when it is appropriate)&lt;&#x2F;li&gt;
&lt;li&gt;Put how ever much detail is needed into your Work Items&lt;&#x2F;li&gt;
&lt;li&gt;Allocate sufficient Product Owner time to keep the work queue healthy&lt;&#x2F;li&gt;
&lt;li&gt;Do what&#x27;s needed to organize large work queues for easy manipulation by the product owner&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;sprint-planning-estimation-and-time-tracking&quot;&gt;Sprint Planning, Estimation, and Time Tracking&lt;&#x2F;h3&gt;
&lt;p&gt;In terms of planning and estimating, even with the shorter four week sprints, there are still some difficulties built into the Scrum process in my experience. We used story points, a somewhat abstract relative unit of measurement designed to express relative difficulties between two stories, not necessarily any absolute measure of time, effort, or difficulty. For me personally, this just does not feel natural or come easily. First, I don&#x27;t think about estimates in relative comparison normally. I don&#x27;t think task A is easy, and task B is four times as hard as task A. As a general rule, I think most people don&#x27;t think accurately in terms of multiplicative values. Conceptualizing the idea that 13 story points is 6.5 times more effort than 2 story points doesn&#x27;t come naturally for me. Given any two stories, I can tell you which one is harder, and I could probably use that basic operation to sort a list of tasks by difficulty, but computing the relative sizes of two along the Fibonacci scale is awkward for me. Secondly, the seemingly arbitrary and perhaps a bit weird use of the Fibonacci sequence just sticks out a bit to me as obtuse. Here&#x27;s my suggested improvements. When it comes to story points, I really believe you need three and only three values: small, medium, large. Fibonacci gives us 1 2 3 5 8 13 21, but 1s and 2s were not particularly common in our backlog, and when we saw a 13 we generally panicked and broke it into smaller pieces. That&#x27;s what I suggest. If you have three or four stories that are truly super easy (1 or 2 in Fibonacci story points), just stick them all into one story and call it small. If you have something that seems very hard, like it&#x27;s going to take one developer half the sprint to do it, break it up into a large and a few mediums. I think the &quot;small, medium, large&quot; terminology has another benefit of being obvious even when discussing with someone non-technical or not familiar with Scrum or Story Points. No explanation is required. Story Points using Fibonacci are subtle enough to require explanation, and perhaps they don&#x27;t add enough value to justify their subtlety.&lt;&#x2F;p&gt;
&lt;p&gt;OK, so now onto the sprint planning meeting. The default scrum schedule has a single marathon sprint planning meeting at the beginning of each sprint. For our team of five developers with four week sprints, we are looking to estimate somewhere around 600 hours worth of tasks ranging in granularity from 4-30 hours. It&#x27;s just too much to focus on in a single session (again, my opinion). After two and a half hours of this, I&#x27;m bleary-eyed and fried and unable to motivate myself to do the kind of careful thinking required to make the task lists complete and accurate. I start falling into wanting to list the same three tasks for every story: 1. Figure it out 2. Code it 3. Test it. My suggestion is that this be done for one hour a week as many as four times during the sprint if needed (probably 3 of these will get the job done) just to break it up into manageable chunks.&lt;&#x2F;p&gt;
&lt;p&gt;Now, another point on the sprint planning meetings. Our goal was to take user stories that had story points and acceptance criteria and define a full set of tasks and estimates for five developer-months worth of time in a single session, and then try not to have to add&#x2F;remove&#x2F;change during the sprint. (Note, the try not to have to add&#x2F;remove&#x2F;change is my own understanding, although my editor pointed out that Scrum itself has no such restriction and allows for task adjustments - focusing only on remaining work. However, it seems the goal is to define the tasks to whatever degree possible at the beginning or we wouldn&#x27;t bother doing it in the first place, but then adjustments are accomodated). In any case, I wonder whether this is a realistic, achievable goal. Or let me say that in my experience at least about 10% churn in the tasks as development progresses (regardless of what overall methodology I am working within) seems to be my limit and I can&#x27;t get it more accurate than that up front. So if the team is OK with somewhere around 10% task churn mid-sprint, then all is well.&lt;&#x2F;p&gt;
&lt;p&gt;When it comes to estimating in hours an individual task in the backlog, our team was developing a new product from scratch with a team of all highly experienced engineers. So we actually ended up having pretty good interchangeability between developers where several different people could complete a task and generally take about the same amount of time to do it. However, when working on an big existing code base, it can be an order of magnitude difference in the estimate between when someone who has already learned a complex subsystem and implemented a similar change does a task verses someone else doing it for the first time. I think if we want real accuracy we need some way to model this more accurately. I&#x27;m not sure how best to do this. Maybe one task for learning curve that gets skipped if it&#x27;s not needed because the person who completes the task didn&#x27;t need it? I don&#x27;t have a good suggestion to improve this yet. Also, along similar lines, in general I always get nervous when one person (or a team) creates estimates for work that will be completed by another person. I think there&#x27;s just a lot of risk there. Again, I don&#x27;t have an alternative to propose at this time.&lt;&#x2F;p&gt;
&lt;p&gt;With regard to ordering the work queue items in the queue, and thus determining which ones get into the sprint, I think in general the scrum notion that the product owner does this and it&#x27;s based on business value to the product owner is reasonable and beneficial for the most part. However, we also tried to adhere to this order for the order in which items were implemented during the sprint, and I think in many cases this didn&#x27;t work out as well as it could have. Even in the backlog, I feel that a certain amount of the time, a logical or technical dependency will suggest a different backlog item order than strict product owner importance. But for now I&#x27;m OK with the product owner importance ordering for the backlog. However, inside the sprint, I think it might work out better to allow the scrum team a certain amount of discretion and control of the order of implementation on any technical or logistical grounds they feel will help. Within a sprint, we bumped up against a fair number of interdependencies where two people would need to heavily edit the same source code file on the same day, or one person was blocking waiting for another person to complete some code needed to build their feature. I can understand that taking the most important item and implementing it last is probably not acceptable, but I think some well-reasoned adjustment should be allowed. Ultimately, I think more stories will get completed due to the efficiency gains. I think a good guideline would be just to discuss these proposed changes to implementation order with the product owner during the daily Scrum call, and based on the status of the burn down chart and how things seem to be going, the product owner may permit or veto a re-ordering.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;sprint-planning-estimation-and-time-tracking-summary-and-suggestions&quot;&gt;Sprint Planning, Estimation, and Time Tracking summary and suggestions:&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;Use small, medium, large instead of Fibonacci story points&lt;&#x2F;li&gt;
&lt;li&gt;Divide the sprint planning across several meetings throughout the sprint, never exceeding the motivation&#x2F;attention span threshold for this somewhat tedious endeavor&lt;&#x2F;li&gt;
&lt;li&gt;Define criteria under which it is OK for developers to add&#x2F;remove&#x2F;change tasks during the sprint&lt;&#x2F;li&gt;
&lt;li&gt;May need some way to estimate tasks differently depending on who implements them&lt;&#x2F;li&gt;
&lt;li&gt;Within a single sprint, team should be allowed to make implementation order changes that will help with efficiency with consent of the product owner&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</description>
      </item>
      <item>
          <title>Code Conventions</title>
          <pubDate>Sun, 15 Mar 2009 03:06:23 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2009/03/code-conventions/</link>
          <guid>https://peterlyons.com/problog/2009/03/code-conventions/</guid>
          <description xml:base="https://peterlyons.com/problog/2009/03/code-conventions/">&lt;p&gt;Here&#x27;s a link to &lt;a href=&quot;&#x2F;code_conventions&quot;&gt;my article on code conventions&lt;&#x2F;a&gt;. Feel free to post comments here.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>How to run two wordpress blogs on one web site</title>
          <pubDate>Sun, 15 Mar 2009 02:57:53 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2009/03/how-to-run-two-wordpress-blogs-on-one-web-site/</link>
          <guid>https://peterlyons.com/problog/2009/03/how-to-run-two-wordpress-blogs-on-one-web-site/</guid>
          <description xml:base="https://peterlyons.com/problog/2009/03/how-to-run-two-wordpress-blogs-on-one-web-site/">&lt;p&gt;There is ample detailed information out there on installing wordpress. However, I wanted to just provide a small supplement about setting up two distinct wordpress blogs within a single apache2 web site. The system I am using is Ubuntu Linux 8.10, but other than the package installation, the configuration steps should be the same on other linux distributions.&lt;&#x2F;p&gt;
&lt;p&gt;As a first step, read through the &lt;a href=&quot;http:&#x2F;&#x2F;codex.wordpress.org&#x2F;Getting_Started_with_WordPress#Installation&quot;&gt;Wordpress Installation Instructions&lt;&#x2F;a&gt;. You will find them to be thorough and clear. The starting point for my setup is that I already had a web site up and running under apache2. I just wanted to add the Wordpress (and underlying MySQL database) setup and have two separate blogs with separate themes and content.&lt;&#x2F;p&gt;
&lt;p&gt;So here&#x27;s my starting setup:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Ubuntu 8.10 on an amd64 system&lt;&#x2F;li&gt;
&lt;li&gt;apache2 already installed and working&lt;&#x2F;li&gt;
&lt;li&gt;static content for the web site deployed in &lt;code&gt;&#x2F;var&#x2F;www&#x2F;example.com&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;MySQL and Wordpress are not yet installed&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;install-wordpress-and-mysql&quot;&gt;Install wordpress and mysql&lt;&#x2F;h4&gt;
&lt;p&gt;First let&#x27;s install wordpress and mysql. I&#x27;ll do this on the command line using the &lt;code&gt;apt-get&lt;&#x2F;code&gt; program, but you can &lt;a href=&quot;https:&#x2F;&#x2F;help.ubuntu.com&#x2F;8.10&#x2F;add-applications&#x2F;C&#x2F;advanced.html&quot;&gt;use one of the graphical options as well&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;sudo apt-get install wordpress virtual-mysql-server&lt;&#x2F;pre&gt;
&lt;p&gt;You should see a bunch of packages that will get installed and press &lt;code&gt;y&lt;&#x2F;code&gt; to proceed. The MySQL install will prompt you to create a new mysql root account password, so go ahead and do that.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;set-up-the-wordpress-databases&quot;&gt;Set up the wordpress databases&lt;&#x2F;h4&gt;
&lt;p&gt;OK, so let&#x27;s say we are going to call our blogs blog1 and blog2. We need to create mysql databases for them. Note that you may see tutorials telling you you can store the data for two separate blogs in one database. While true, two databases is a much cleaner way to go. End users shouldn&#x27;t be going around making up database table names. So, we are going to use the mysql command line tools to do this. Again, the wordpress docs here are fine and describe graphical alternatives as well. Let&#x27;s connect to mysql (use the password you created above) as root and create the databases. We&#x27;ll call them blog1 and blog2 and we&#x27;ll also set up user accounts inside mysql that wordpress will use to access the databases. Again for simplicity, we&#x27;ll also call the user accounts blog1 and blog2. Replace &quot;MakeUpPassword&quot; with your own chosen password.&lt;&#x2F;p&gt;
&lt;div class=&quot;code&quot;&gt;
&lt;pre&gt;mysql -u root -p
create database blog1;
GRANT ALL PRIVILEGES ON blog1.* TO &quot;blog1&quot;@&quot;localhost&quot; IDENTIFIED BY &quot;MakeUpPassword&quot;;
flush privileges;
create database blog2;
GRANT ALL PRIVILEGES ON blog2.* TO &quot;blog2&quot;@&quot;localhost&quot; IDENTIFIED BY &quot;MakeUpPassword&quot;;
flush privileges;
quit;
&lt;&#x2F;pre&gt;
&lt;&#x2F;div&gt;
&lt;h4 id=&quot;install-wordpress-files-and-configure-db-access&quot;&gt;Install wordpress files and configure DB access&lt;&#x2F;h4&gt;
&lt;p&gt;OK, when we installed wordpress above, Ubuntu put a copy of the wordpress PHP files into &lt;code&gt;&#x2F;usr&#x2F;share&#x2F;wordpress&lt;&#x2F;code&gt;, so now we&#x27;re going to make 2 copies, one for each blog, underneath our web site&#x27;s document root. Note that since these are two blogs in the same web site, we don&#x27;t want either blog to be the top level home page of the site, so each gets its own separate subdirectory.&lt;&#x2F;p&gt;
&lt;div class=&quot;code&quot;&gt;
&lt;pre&gt;sudo mkdir -p &#x2F;var&#x2F;www&#x2F;example.com&#x2F;blog1 &#x2F;var&#x2F;www&#x2F;example.com&#x2F;blog2
sudo cp -r &#x2F;usr&#x2F;share&#x2F;wordpress&#x2F;* &#x2F;var&#x2F;www&#x2F;example.com&#x2F;blog1
sudo cp -r &#x2F;usr&#x2F;share&#x2F;wordpress&#x2F;* &#x2F;var&#x2F;www&#x2F;example.com&#x2F;blog2
sudo chown -R www-data:www-data &#x2F;var&#x2F;www&#x2F;example.com&#x2F;blog*
&lt;&#x2F;pre&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;Now I should note that Debian&#x2F;Ubuntu has a customized wordpress configuration. Often, these are well crafted by the experts and will save you a lot of time and hassle if you follow the patterns they suggest. In this case, I don&#x27;t think their setup exactly matches my goal of two different blogs under one web site, so I am bypassing their pattern and doing my own simple alternative. So what happens is that by default the file &lt;code&gt;&#x2F;usr&#x2F;share&#x2F;wordpress&#x2F;wp-config.php is a symbolic link to &#x2F;etc&#x2F;wordpress&#x2F;wp-config.php&lt;&#x2F;code&gt;, and when we copied these files, that symlink was copied too, which means if we aren&#x27;t careful both of our blogs will point at the same database, making them one blog instead of two! So we do the following:&lt;&#x2F;p&gt;
&lt;div class=&quot;code&quot;&gt;
&lt;pre&gt;sudo rm &#x2F;var&#x2F;www&#x2F;example.com&#x2F;blog*&#x2F;wp-config.php
sudo cp &#x2F;usr&#x2F;share&#x2F;wordpress&#x2F;wp-config-sample.php &#x2F;var&#x2F;www&#x2F;example.com&#x2F;blog1&#x2F;wp-config.php
sudo cp &#x2F;usr&#x2F;share&#x2F;wordpress&#x2F;wp-config-sample.php &#x2F;var&#x2F;www&#x2F;example.com&#x2F;blog2&#x2F;wp-config.php
sudo chown -R www-data:www-data &#x2F;var&#x2F;www&#x2F;example.com&#x2F;blog*
&lt;&#x2F;pre&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;OK, now each blog has it&#x27;s own separate copy of wp-config.php. Go ahead and edit those files to point blog1 at the blog1 database and blog2 at the blog2 database using vi or your text editor of choice. When done, they should look as follows:&lt;&#x2F;p&gt;
&lt;pre&gt;&#x2F;var&#x2F;www&#x2F;example.com&#x2F;blog1&#x2F;wp-config.php&lt;&#x2F;pre&gt;
&lt;div class=&quot;code&quot;&gt;
&lt;pre&gt;&lt;?php
&#x2F;&#x2F; ** MySQL settings ** &#x2F;&#x2F;
define(&#x27;DB_NAME&#x27;, &#x27;blog1&#x27;);    &#x2F;&#x2F; The name of the database
define(&#x27;DB_USER&#x27;, &#x27;blog1&#x27;);     &#x2F;&#x2F; Your MySQL username
define(&#x27;DB_PASSWORD&#x27;, &#x27;yourpasswordhere&#x27;); &#x2F;&#x2F; ...and password
define(&#x27;DB_HOST&#x27;, &#x27;localhost&#x27;);    &#x2F;&#x2F; 99% chance you won&#x27;t need to change this value
define(&#x27;DB_CHARSET&#x27;, &#x27;utf8&#x27;);
define(&#x27;DB_COLLATE&#x27;, &#x27;&#x27;);

&#x2F;&#x2F; Change SECRET_KEY to a unique phrase.  You won&#x27;t have to remember it later,
&#x2F;&#x2F; so make it long and complicated.  You can visit http:&#x2F;&#x2F;api.wordpress.org&#x2F;secret-key&#x2F;1.0&#x2F;
&#x2F;&#x2F; to get a secret key generated for you, or just make something up.
&#x2F;&#x2F; Change this to a unique phrase.
define(&#x27;SECRET_KEY&#x27;, &#x27;put your unique phrase here -- yes you change this now&#x27;);

&#x2F;&#x2F; You can have multiple installations in one database if you give each a unique prefix
$table_prefix  = &#x27;wp_&#x27;;   &#x2F;&#x2F; Only numbers, letters, and underscores please!

&#x2F;&#x2F; Change this to localize WordPress.  A corresponding MO file for the
&#x2F;&#x2F; chosen language must be installed to wp-content&#x2F;languages.
&#x2F;&#x2F; For example, install de.mo to wp-content&#x2F;languages and set WPLANG to &#x27;de&#x27;
&#x2F;&#x2F; to enable German language support.
define (&#x27;WPLANG&#x27;, &#x27;&#x27;);

&#x2F;* That&#x27;s all, stop editing! Happy blogging. *&#x2F;

define(&#x27;ABSPATH&#x27;, dirname(__FILE__).&#x27;&#x2F;&#x27;);
require_once(ABSPATH.&#x27;wp-settings.php&#x27;);
?&gt;
&lt;&#x2F;pre&gt;
&lt;&#x2F;div&gt;
&lt;pre&gt;&#x2F;var&#x2F;www&#x2F;example.com&#x2F;blog2&#x2F;wp-config.php&lt;&#x2F;pre&gt;
&lt;div class=&quot;code&quot;&gt;
&lt;pre&gt;&lt;?php
&#x2F;&#x2F; ** MySQL settings ** &#x2F;&#x2F;
define(&#x27;DB_NAME&#x27;, &#x27;blog2&#x27;);    &#x2F;&#x2F; The name of the database
define(&#x27;DB_USER&#x27;, &#x27;blog2&#x27;);     &#x2F;&#x2F; Your MySQL username
define(&#x27;DB_PASSWORD&#x27;, &#x27;yourpasswordhere&#x27;); &#x2F;&#x2F; ...and password
define(&#x27;DB_HOST&#x27;, &#x27;localhost&#x27;);    &#x2F;&#x2F; 99% chance you won&#x27;t need to change this value
define(&#x27;DB_CHARSET&#x27;, &#x27;utf8&#x27;);
define(&#x27;DB_COLLATE&#x27;, &#x27;&#x27;);

&#x2F;&#x2F; Change SECRET_KEY to a unique phrase.  You won&#x27;t have to remember it later,
&#x2F;&#x2F; so make it long and complicated.  You can visit http:&#x2F;&#x2F;api.wordpress.org&#x2F;secret-key&#x2F;1.0&#x2F;
&#x2F;&#x2F; to get a secret key generated for you, or just make something up.
&#x2F;&#x2F; Change this to a unique phrase.
define(&#x27;SECRET_KEY&#x27;, &#x27;put your unique phrase here -- yes you change this now&#x27;);

&#x2F;&#x2F; You can have multiple installations in one database if you give each a unique prefix
$table_prefix  = &#x27;wp_&#x27;;   &#x2F;&#x2F; Only numbers, letters, and underscores please!

&#x2F;&#x2F; Change this to localize WordPress.  A corresponding MO file for the
&#x2F;&#x2F; chosen language must be installed to wp-content&#x2F;languages.
&#x2F;&#x2F; For example, install de.mo to wp-content&#x2F;languages and set WPLANG to &#x27;de&#x27;
&#x2F;&#x2F; to enable German language support.
define (&#x27;WPLANG&#x27;, &#x27;&#x27;);

&#x2F;* That&#x27;s all, stop editing! Happy blogging. *&#x2F;

define(&#x27;ABSPATH&#x27;, dirname(__FILE__).&#x27;&#x2F;&#x27;);
require_once(ABSPATH.&#x27;wp-settings.php&#x27;);
?&gt;
&lt;&#x2F;pre&gt;
&lt;&#x2F;div&gt;
&lt;h4 id=&quot;permalinks&quot;&gt;Permalinks&lt;&#x2F;h4&gt;
&lt;p&gt;OK, now let&#x27;s make sure we have mod_rewrite enabled for wordpress permalinks, then we restart apache2 to get our new software and config to take effect. We&#x27;ll also make sure we have a writeable .htaccess file so wordpress can set it up for us.&lt;&#x2F;p&gt;
&lt;div class=&quot;code&quot;&gt;
&lt;pre&gt;sudo a2enmod rewrite
sudo apache2ctl restart
sudo touch &#x2F;var&#x2F;www&#x2F;example.com&#x2F;blog1&#x2F;.htaccess
sudo touch &#x2F;var&#x2F;www&#x2F;example.com&#x2F;blog2&#x2F;.htaccess
sudo chown www-data:www-data &#x2F;var&#x2F;www&#x2F;example.com&#x2F;blog*&#x2F;.htaccess
sudo chmod 644 &#x2F;var&#x2F;www&#x2F;example.com&#x2F;blog*&#x2F;.htaccess
&lt;&#x2F;pre&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;Almost done, we can now point a web browser to &lt;a href=&quot;http:&#x2F;&#x2F;localhost&#x2F;blog1&#x2F;wp-admin&#x2F;install.php&quot;&gt;http:&#x2F;&#x2F;localhost&#x2F;blog1&#x2F;wp-admin&#x2F;install.php&lt;&#x2F;a&gt; and fill out that form and launch the wordpress self-install. Repeat the process for blog2 at &lt;a href=&quot;http:&#x2F;&#x2F;localhost&#x2F;blog2&#x2F;wp-admin&#x2F;install.php&quot;&gt;http:&#x2F;&#x2F;localhost&#x2F;blog2&#x2F;wp-admin&#x2F;install.php&lt;&#x2F;a&gt;. Now, this is going to set up the blog in the database and create the admin user with a default password. If your computer has a mail transport agent running, you should get the email with the default password. If not, you won&#x27;t, which is what happened to me, so we can just set the admin password to one of our choosing. First, we need to know the MD5 checksum of our chosen password. You can use &lt;a href=&quot;http:&#x2F;&#x2F;epleweb.com&#x2F;md5&#x2F;&quot;&gt;this online form&lt;&#x2F;a&gt; to get the MD5 of your password, but sending your password to some random web site in clear text is too insecure for me. Therefore, if you have python, you can use this little python one-liner to do it. This will securely prompt you for your password and print out the corresponding MD5. Nothing is sent over the network. You will end up with a 32-character hex string. Use this instead of the For details from wordpress, read &lt;a href=&quot;http:&#x2F;&#x2F;codex.wordpress.org&#x2F;Resetting_Your_Password&quot;&gt;resetting your password&lt;&#x2F;a&gt; in the wordpress online docs. The cheat sheet is below.&lt;&#x2F;p&gt;
&lt;pre&gt;python -c &quot;import getpass,md5;m=md5.new();m.update(getpass.getpass());print m.hexdigest()&quot;&lt;&#x2F;pre&gt;
&lt;p&gt;OK, so let&#x27;s set this password into mysql so we can start managing our blog&lt;&#x2F;p&gt;
&lt;div class=&quot;code&quot;&gt;
&lt;pre&gt;mysql -u root -p
use blog1;
update wp_users set user_pass=&quot;useyour32charhexstringmd5here&quot; where user_nicename=&quot;admin&quot;;
use blog2;
update wp_users set user_pass=&quot;useyour32charhexstringmd5here&quot; where user_nicename=&quot;admin&quot;;
quit;
&lt;&#x2F;pre&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;OK, you are good to go to &lt;a href=&quot;http:&#x2F;&#x2F;localhost&#x2F;blog1&quot;&gt;http:&#x2F;&#x2F;localhost&#x2F;blog1&lt;&#x2F;a&gt;, log in as admin, and start managing your blog. You can go in and set up custom pretty permalinks in the admin section and when you save, wordpress should be able to write the rewrite settings to the .htaccess file we set up for you. Good luck and happy blogging!&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Music subscription and Rhapsody</title>
          <pubDate>Sun, 15 Mar 2009 02:54:03 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2009/03/music-subscription-and-rhapsody/</link>
          <guid>https://peterlyons.com/problog/2009/03/music-subscription-and-rhapsody/</guid>
          <description xml:base="https://peterlyons.com/problog/2009/03/music-subscription-and-rhapsody/">&lt;p&gt;Flat fee music subscription service has changed my life. I wish there was some less dramatic way to put it, but it&#x27;s the honest truth. I don&#x27;t remember exactly when, but somewhere in late 2006 probably, after reading &lt;a href=&quot;http:&#x2F;&#x2F;www.joelonsoftware.com&#x2F;items&#x2F;2006&#x2F;11&#x2F;10.html&quot;&gt;this Joel on Software - The infinite music collection&lt;&#x2F;a&gt; article, I was intrigued by the idea and did a little shopping. I know I looked at Napster, Virgin, Yahoo, and Rhapsody at least. I think I initially went with Rhapsody because of the Sonos integration. I experimented for a bit listening on the computer and then I think that Christmas I asked for a basic set of Sonos gear, which was and still is exorbitantly priced.&lt;&#x2F;p&gt;
&lt;p&gt;OK, so first some comments on the whole notion of subscription music service. It&#x27;s amazing. For someone like me with a voracious appetite for new music cultivated spending long hours every week in the fantastic &lt;a href=&quot;http:&#x2F;&#x2F;www.oberlin.edu&#x2F;library&#x2F;con&#x2F;&quot;&gt;Oberlin Conservatory Music Library&lt;&#x2F;a&gt;, it was complete drooling brain-fry. It was a bit overwhelming at first. There were numerous aspects of this arrangement that were just awesome. Obviously, the size of the library is number one. Having extremely ecclectic and somewhat obscure taste in music, I was worried that it would be basically a top-40 archive that I would quickly grow bored with. This was not the case. The collection of jazz, classical, and less main stream stuff was really quite good. Now, every CD in my personal library is not available, but the service library as a whole is so infinitely broad and compelling that I don&#x27;t care that much.&lt;&#x2F;p&gt;
&lt;p&gt;Secondly, there is the complete removal of the financial penalty for exploring. Before I expound on this let me just state that I listen to whole albums in totality in order. Fuck shuffle. Fuck 30 second previews. I usually listen to complete albums start to finish numerous times before I decide how I feel about them. In addition, when checking out a new artist, I actually prefer to listen to all the albums in chronological order. Sometimes I&#x27;ll scan a &quot;best of&quot; to see if it&#x27;s worth my time, but usually I go straight for the debut album. For years, I would gather suggestions from peers, and then go plunk down $17 for a CD. No more. Now I can try relentlessly at full speed for a flat fee. This is fantastic and significant. Now I can actually listen to artists I know I don&#x27;t care for, if only to better understand what it is about them that I don&#x27;t like.&lt;&#x2F;p&gt;
&lt;p&gt;Third, in the jazz and classical genres, musicians are vastly more prolific compared to their pop&#x2F;rock peers. Go try to get the complete recordings of Miles Davis. Be sure to bring like $1500. With a flat fee subscription, I can go and listen to LOTS of music by folks I like. One of the first things that got me completely hooked on this was listening to a 10-disc set of Steve Reich: Works 1965-1995. This retails at &lt;a href=&quot;http:&#x2F;&#x2F;www.amazon.com&#x2F;Steve-Reich-1965-1995&#x2F;dp&#x2F;B000005J4P&#x2F;ref=pd_bbs_sr_1?ie=UTF8&amp;amp;s=music&amp;amp;qid=1236457622&amp;amp;sr=8-1&quot;&gt;amazon.com for $99.98&lt;&#x2F;a&gt;. So the trade-off for this ONE collection is own this collection forever or get over six months access to millions of songs online.&lt;&#x2F;p&gt;
&lt;p&gt;Fourth, you can compare recordings to your heart&#x27;s content. I have a physical copy of the venerable &lt;a href=&quot;http:&#x2F;&#x2F;www.amazon.com&#x2F;Bela-Bartok-Quartets-Emerson-Quartet&#x2F;dp&#x2F;B000001G9O&#x2F;ref=sr_1_1?ie=UTF8&amp;amp;s=music&amp;amp;qid=1236532264&amp;amp;sr=1-1&quot;&gt;Emerson String Quartet&#x27;s recording of the six Bela Bartok String Quartets&lt;&#x2F;a&gt;. Currently going for $22 on amazon. It is a cherished record in my collection. However, the chances of me buying another recording of this work are slim to none, even though I would love to hear various interpretations. Rhapsody has no less than ten complete recordings of these works available! When it comes to jazz and you want to learn a new song, Rhapsody is going to give you about thirty recordings to choose from. No better way to truly absorb the song&#x27;s full meaning from the repertoire by listening to numerous different recordings.&lt;&#x2F;p&gt;
&lt;p&gt;Fifth, there is no personal music library management. I&#x27;ve spent hours and hours labeling my CD collection (over 800 discs), ripping them, fixing the cddb track metadata, transferring stuff to portable devices, re-transfering it when it gets corrupted, etc. Then there&#x27;s the idea that I have to keep this all backed up. I have so far ripped about 30 GB of music and don&#x27;t really want to deal with backing that up. With a subscription, all the music is just instantly there. It&#x27;s all indexed and searchable. The track metadata is correct. I don&#x27;t have to personally back it all up.&lt;&#x2F;p&gt;
&lt;p&gt;Sixth, it works with portable players in a reasonable way. I think for the next Christmas I asked for the &lt;a href=&quot;http:&#x2F;&#x2F;reviews.cnet.com&#x2F;mp3-players&#x2F;sandisk-sansa-e280r-rhapsody&#x2F;4505-6490_7-32102346.html&quot;&gt;SanDisk Sansa e280R portable MP3 player&lt;&#x2F;a&gt; that is integrated with Rhapsody. You can download tracks to the device and as long as you connect it once a month to verify your subscription is active, you are good to go. It works pretty well and doesn&#x27;t really add any hassle. I&#x27;ll be posting another entry soon about the various devices (basically all bad) I have used with Rhapsody and how they work.&lt;&#x2F;p&gt;
&lt;p&gt;So I&#x27;m totally hooked on this model of flat fee all you can eat subscription music. I was initially concerned that streaming this in real time would not work, but honestly there have been almost no glitches. Either the service is working or it isn&#x27;t, but it doesn&#x27;t do any buffering interrupting the song or anything like that, which would be a complete deal-breaker. To try to listen to this much music on &lt;a href=&quot;http:&#x2F;&#x2F;www.itunes.com&quot;&gt;iTunes&lt;&#x2F;a&gt; would cost me hundreds of dollars a month.&lt;&#x2F;p&gt;
&lt;p&gt;So, let&#x27;s look at some of the cons. There are no linear notes. I do miss that. I also miss detailed personnel listings per track. However, when I really am interested in that, the info is usually available online if I can remember to go look it up. As of now, in order for this to work, it relies on Plays For Sure Digital Right Management (DRM) scheme. Generally I am against DRM, but if it is required in order for the music industry to be willing to make a subscription service available, so be it.&lt;&#x2F;p&gt;
&lt;p&gt;OK, so that is mostly focussing on the subscription model in the abstract. Let&#x27;s talk about the Rhapsody service in particular. After a few years of using it, overall I&#x27;d probably give it about 7 out of 10. It&#x27;s good, but it has some annoyances. On the plus side, the web based Rhapsody online product, which was formerly a proprietary plug-in that gave me a few hassles on Linux, is now entirely standard flash based app. In general it works great on linux however my new laptop I installed 64 bit Ubuntu and the rhapsody flash app won&#x27;t log in currently. I&#x27;ll be contacting support (sigh) to yell at them to make it work.&lt;&#x2F;p&gt;
&lt;p&gt;So some of the annoyances include inability to re-order tracks in your queue&#x2F;playlist. You can append to the end of it and delete items, but you can&#x27;t reorder. Again, since I generally listen to whole albums it doesn&#x27;t bother me much, but sometimes it can be a real pain. Also, their web site used to have a frigging NORMAL LOGIN HTML FORM, thus I could save my credentials in my browser and not be bothered with it. Now the flash app itself prompts for credentials which means I have to re-type my password a few times a day. I asked support to make it go back to the old way but you can guess how that went.&lt;&#x2F;p&gt;
&lt;p&gt;Reliability and bill payment have been good. The Windows application that I need to use to actually transfer music to a portable player is decent. It&#x27;s better than the flash app, but still a bit cumbersome. It&#x27;s got a web browser pane embedded in it, which has all the annoyances of a web browser, but not all the screen components and tools to properly manage it. Here they have a proper playlist editor window. Ideally I would be able to transfer to my portable from a linux application, but I have realistic expectations here. It ain&#x27;t going to happen.&lt;&#x2F;p&gt;
&lt;p&gt;Rhapsody has user and celebrity playlists, both of which are a great way to explore and check out new stuff.&lt;&#x2F;p&gt;
&lt;p&gt;So if you are a big music fan, and still haven&#x27;t tried a subscription service, give it a shot. I can never go back. Look for more posts soon about my experierces with music player devices over the years.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Announcing Pete&#x27;s Points</title>
          <pubDate>Sun, 15 Mar 2009 02:53:24 +0000</pubDate>
          <author>Unknown</author>
          <link>https://peterlyons.com/problog/2009/03/announcing-petes-points/</link>
          <guid>https://peterlyons.com/problog/2009/03/announcing-petes-points/</guid>
          <description xml:base="https://peterlyons.com/problog/2009/03/announcing-petes-points/">&lt;p&gt;I have decided to start a &quot;professional&quot; blog. I use the word &quot;professional&quot; only in the sense of &quot;not personal&quot;. I will post about software engineering, computers, consumer electronics, and music technology primarily. I hope you enjoy it and look forward to any conversations this generates.&lt;&#x2F;p&gt;
</description>
      </item>
    </channel>
</rss>
