<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom"><generator uri="https://jekyllrb.com/" version="3.8.7">Jekyll</generator><link href="https://nathanfriend.io/feed.xml" rel="self" type="application/atom+xml"/><link href="https://nathanfriend.io/" rel="alternate" type="text/html"/><updated>2023-06-29T11:00:13-04:00</updated><id>https://nathanfriend.io/feed.xml</id><title type="html">Nathan Friend</title><subtitle>Remote Software Engineer at Stripe and cellist based out of Ontario. Previously at GitLab. Fascinated with building usable, delightful software.</subtitle><entry><title type="html">I built a weird keyboard</title><link href="https://nathanfriend.io/2023/06/26/i-built-a-weird-keyboard.html" rel="alternate" type="text/html" title="I built a weird keyboard"/><published>2023-06-26T20:00:00-04:00</published><updated>2023-06-26T20:00:00-04:00</updated><id>https://nathanfriend.io/2023/06/26/i-built-a-weird-keyboard</id><content type="html" xml:base="https://nathanfriend.io/2023/06/26/i-built-a-weird-keyboard.html">&lt;figure&gt; &lt;img loading=&quot;lazy&quot; src=&quot;/assets/img/i-built-a-weird-keyboard/half_rainbow.jpg&quot; alt=&quot;A picture of half a Dactyl Manuform keyboard&quot; /&gt; &lt;/figure&gt; &lt;p&gt;&lt;br /&gt;I spent most of my free time over the last 10 months building this bizarre keyboard from scratch. It’s a &lt;a href=&quot;https://github.com/abstracthat/dactyl-manuform&quot;&gt;Dactyl Manuform&lt;/a&gt; - a split keyboard with a highly sculpted design that is somehow simulataneously the ugliest and most eye-catching object I’ve ever seen.&lt;/p&gt; &lt;p&gt;The goal of this keyboard design is to place keys exactly along each finger’s natural axis of motion. The consequences of this approach are downward-sloping (A.K.A “tented”) rows to match the natural angle of the wrists, a deeper middle finger column to compensate for this finger’s relative length, an offset pinky column to minimize stretching, and a thumb cluster with multiple keys to take advantage of its opposable nature.&lt;/p&gt; &lt;p&gt;There’s a reason most keyboards don’t look like this. It’s difficult to mass-produce curved keyboards since they can’t use the stiff, flat &lt;a href=&quot;https://en.wikipedia.org/wiki/Printed_circuit_board&quot;&gt;PCBs&lt;/a&gt; that most keyboards use to wire the keys to the microcontroller. Every Dactyl Manuform is a unique piece of art, painstakingly hand-wired by a human being. Here’s how I built mine:&lt;/p&gt; &lt;h2 id=&quot;build-log&quot;&gt;Build log&lt;/h2&gt; &lt;p&gt;The first step was to design and 3D print the case. There are a number of Dactyl Manuform model generators out there; I ended up using &lt;a href=&quot;https://github.com/ibnuda/dactyl-keyboard/tree/refaktor&quot;&gt;this one&lt;/a&gt; since it had a few extra features I wanted (e.g. wide pinky keys). Some things I was looking for in my design:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;A key layout similar to the &lt;a href=&quot;https://ergodox-ez.com/&quot;&gt;ErgoDox EZ&lt;/a&gt; keyboards I already own&lt;/li&gt; &lt;li&gt;An aggressive tent angle (I went with π/8 = 22.5°)&lt;/li&gt; &lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Hot_swapping#Keyboards&quot;&gt;Hot-swap&lt;/a&gt; sockets&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;I gave up on the hot-swap sockets after failing to coax the generator script to produce valid hot-swap socket holders. I’m glad I did - in the end I don’t think they would have worked anyway.&lt;/p&gt; &lt;p&gt;Once I had a model that looked good on the screen, I printed a draft version to see it in real life.&lt;/p&gt; &lt;figure&gt; &lt;img loading=&quot;lazy&quot; src=&quot;/assets/img/i-built-a-weird-keyboard/draft.jpg&quot; alt=&quot;A draft print of the right half&quot; /&gt; &lt;figcaption&gt;Printed with a .8mm nozzle, .32mm layer height, and lightning infill&lt;/figcaption&gt; &lt;/figure&gt; &lt;p&gt;&lt;br /&gt;&lt;/p&gt; &lt;p&gt;Overall, I was really happy with this first draft. I only made a few tweaks to the model before printing the real thing:&lt;/p&gt; &lt;ol&gt; &lt;li&gt;I enabled the “external microcontroller” option, which let me kick the can on deciding which microcontroller and connection types to use&lt;/li&gt; &lt;li&gt;I added my own screw holes; the holes generated by the script were awkwardly placed&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;To mount the base plates to the body of the keyboard, I used &lt;a href=&quot;https://a.co/d/6wMZmwo&quot;&gt;these heat set inserts&lt;/a&gt;. It’s a neat system; the threaded inserts are melted into the 3D print using a soldering gun, producing threaded screw holes much stronger and smaller than anything that would be possible using only 3D printing.&lt;/p&gt; &lt;p&gt;To determine the ideal hole size for these inserts, I made a test print.&lt;/p&gt; &lt;figure&gt; &lt;img loading=&quot;lazy&quot; src=&quot;/assets/img/i-built-a-weird-keyboard/insert_tester.jpg&quot; alt=&quot;A photo of a print for determining the correct insert tolerance&quot; /&gt; &lt;figcaption&gt;A test print with insert holes ranging from 3.8mm to 4.2mm in diameter&lt;/figcaption&gt; &lt;/figure&gt; &lt;p&gt;&lt;br /&gt;&lt;/p&gt; &lt;p&gt;I’m glad I tested this; &lt;em&gt;all&lt;/em&gt; of the hole options were too small! A second print with bigger holes was more successful.&lt;/p&gt; &lt;figure&gt; &lt;img loading=&quot;lazy&quot; src=&quot;/assets/img/i-built-a-weird-keyboard/insert_tester_after.jpg&quot; alt=&quot;A photo of another insert hole test print; this one includes installed screws&quot; /&gt; &lt;figcaption&gt;A second test print with insert holes ranging from 4.2mm to 4.6mm in diameter. I decided to go with 4.5mm&lt;/figcaption&gt; &lt;/figure&gt; &lt;p&gt;&lt;br /&gt;&lt;/p&gt; &lt;p&gt;With this last detail resolved, I began printing the real halves. I used my favorite material - wood PLA - which looks (and even smells!) quite similar to real wood, once properly post-processed.&lt;/p&gt; &lt;figure&gt; &lt;img loading=&quot;lazy&quot; src=&quot;/assets/img/i-built-a-weird-keyboard/cura.jpg&quot; alt=&quot;A screenshot of the final STL being sliced in Cura&quot; /&gt; &lt;figcaption&gt;The final STL file being sliced in Cura. 24 hours per half!&lt;/figcaption&gt; &lt;/figure&gt; &lt;p&gt;&lt;br /&gt;&lt;/p&gt; &lt;figure&gt; &lt;img loading=&quot;lazy&quot; src=&quot;/assets/img/i-built-a-weird-keyboard/on_print_bed.jpg&quot; alt=&quot;A picture of the left half of the keyboard on the 3D printer bed&quot; /&gt; &lt;figcaption&gt;The left half, fresh off the printer&lt;/figcaption&gt; &lt;/figure&gt; &lt;p&gt;&lt;br /&gt;&lt;/p&gt; &lt;figure&gt; &lt;img loading=&quot;lazy&quot; src=&quot;/assets/img/i-built-a-weird-keyboard/printed_no_sanding.jpg&quot; alt=&quot;A picture of both halves of the keyboard&quot; /&gt; &lt;figcaption&gt;Both halves printed and slightly cleaned up&lt;/figcaption&gt; &lt;/figure&gt; &lt;p&gt;&lt;br /&gt;&lt;/p&gt; &lt;p&gt;I melted the inserts into the holes, which was nerve-wracking; one bad insert would have likely ruined the whole print. It was awkward to try and hold the keyboard &lt;em&gt;and&lt;/em&gt; the insert in place while pressing the insert into the plastic with the soldering gun. Somehow I managed to install all ten without issue!&lt;/p&gt; &lt;figure&gt; &lt;img loading=&quot;lazy&quot; src=&quot;/assets/img/i-built-a-weird-keyboard/insert_installed.jpg&quot; alt=&quot;A picture of an installed heat-set insert&quot; /&gt; &lt;figcaption&gt;An installed heat-set insert&lt;/figcaption&gt; &lt;/figure&gt; &lt;p&gt;&lt;br /&gt;&lt;/p&gt; &lt;p&gt;I ordered the transparent acrylic base plates from &lt;a href=&quot;https://www.ponoko.com/&quot;&gt;ponoko.com&lt;/a&gt; and was quite happy with the result.&lt;/p&gt; &lt;figure&gt; &lt;img loading=&quot;lazy&quot; src=&quot;/assets/img/i-built-a-weird-keyboard/acrylic_plate.jpg&quot; alt=&quot;The bottom plate of the keyboard, made from acrylic&quot; /&gt; &lt;figcaption&gt;Acrylic base plate from &lt;a href=&quot;https://www.ponoko.com/&quot;&gt;ponoko.com&lt;/a&gt;&lt;/figcaption&gt; &lt;/figure&gt; &lt;p&gt;&lt;br /&gt;&lt;/p&gt; &lt;p&gt;Next up was post-processing. I sanded each half with 80-grit sandpaper, which was a &lt;em&gt;ton&lt;/em&gt; of work - there are a lot of nooks and crannies that make this a tedious job.&lt;/p&gt; &lt;figure&gt; &lt;img loading=&quot;lazy&quot; src=&quot;/assets/img/i-built-a-weird-keyboard/sanded_side.jpg&quot; alt=&quot;A picture of both keyboard halves, sanded&quot; /&gt; &lt;figcaption&gt;Both halves, sanded&lt;/figcaption&gt; &lt;/figure&gt; &lt;p&gt;&lt;br /&gt;&lt;/p&gt; &lt;p&gt;In order to sand the inside of the key holes, I printed a little attachment that I could wrap with sandpaper and fit on my screwdriver.&lt;/p&gt; &lt;figure&gt; &lt;img loading=&quot;lazy&quot; src=&quot;/assets/img/i-built-a-weird-keyboard/screw_driver.jpg&quot; alt=&quot;A screwdriver with a 3D-printed accessory installed&quot; /&gt; &lt;figcaption&gt;My custom SuperSander™ (patent pending)&lt;/figcaption&gt; &lt;/figure&gt; &lt;p&gt;&lt;br /&gt;&lt;/p&gt; &lt;p&gt;Normally this would be an unnecessary step - no one sees the inside of the key holes - but the fit was a &lt;em&gt;bit&lt;/em&gt; too tight; most of the holes required some sanding before the switch would fit properly. (This is why I’m glad I didn’t bother with hot-swap sockets - the fit is so tight, I’d never be able to get the switches out anyway.)&lt;/p&gt; &lt;figure&gt; &lt;img loading=&quot;lazy&quot; src=&quot;/assets/img/i-built-a-weird-keyboard/sanded_top.jpg&quot; alt=&quot;Another picture of both keyboard halves, sanded&quot; /&gt; &lt;figcaption&gt;Another shot, because sanding these took too much time not to show off&lt;/figcaption&gt; &lt;/figure&gt; &lt;p&gt;&lt;br /&gt;&lt;/p&gt; &lt;p&gt;The next step was to stain and clear coat the prints. I used this &lt;a href=&quot;https://www.homedepot.ca/product/1000844900&quot;&gt;gel stain&lt;/a&gt; and &lt;a href=&quot;https://a.co/d/do2gUQm&quot;&gt;glossy polyurethane&lt;/a&gt; to give the prints a rich, polished wood color.&lt;/p&gt; &lt;figure&gt; &lt;img loading=&quot;lazy&quot; src=&quot;/assets/img/i-built-a-weird-keyboard/finish_complete_one_side.jpg&quot; alt=&quot;A picture of a keyboard half, finished with stain and polyurethane&quot; /&gt; &lt;figcaption&gt;The finished product&lt;/figcaption&gt; &lt;/figure&gt; &lt;p&gt;&lt;br /&gt;&lt;/p&gt; &lt;figure&gt; &lt;img loading=&quot;lazy&quot; src=&quot;/assets/img/i-built-a-weird-keyboard/finish_complete_one_side_closeup.jpg&quot; alt=&quot;Another picture of a keyboard half from a different angle, finished with stain and polyurethane&quot; /&gt; &lt;figcaption&gt;Some nice faux woodgrain&lt;/figcaption&gt; &lt;/figure&gt; &lt;p&gt;&lt;br /&gt;&lt;/p&gt; &lt;p&gt;This was a time consuming step, as each half required three coats of stain (minimum 24 hours to dry per coat) and at least 3 coats of polyurethane (a few hours to dry per coat). I did this in the dead of winter which made drying these in the outdoors challenging (a space heater may have been involved).&lt;/p&gt; &lt;p&gt;The hard work paid off, though - I’m really happy with how these look! The layer lines even give the illusion of a wood grain.&lt;/p&gt; &lt;p&gt;The next decision was which key switches to use. I’m a clicky switch guy; the noisier the better. I bought a Kailh switch tester so I could make an informed decision and decided on Kailh Box Whites.&lt;/p&gt; &lt;figure&gt; &lt;img loading=&quot;lazy&quot; src=&quot;/assets/img/i-built-a-weird-keyboard/switch_tester.jpg&quot; alt=&quot;Two keyboard switch testers&quot; /&gt; &lt;figcaption&gt;Regular and low-profile Kailh switch testers. Not pictured: Gateron and Boba testers&lt;/figcaption&gt; &lt;/figure&gt; &lt;p&gt;&lt;br /&gt;&lt;/p&gt; &lt;p&gt;Compared to other clicky switches (e.g. Cherry MX Blues), Box Whites are &lt;em&gt;extra&lt;/em&gt; clicky (they click &lt;em&gt;twice&lt;/em&gt; per key press). Perfect for working remotely! If I ever make a silent/office-friendly version of this board, I’ll go with Boba U4 Silents.&lt;/p&gt; &lt;p&gt;At this point, I was able to set the switches and keycaps in place and get a feel for what it would be like to type on this monstrosity. I’ll admit it felt about as weird as it looks.&lt;/p&gt; &lt;figure&gt; &lt;img loading=&quot;lazy&quot; src=&quot;/assets/img/i-built-a-weird-keyboard/finish_complete_both_sides.jpg&quot; alt=&quot;A picture of the keyboard; half has just the switches installed, and the other half has both switches and key caps installed&quot; /&gt; &lt;figcaption&gt;Switches and keycaps (temporarily) installed&lt;/figcaption&gt; &lt;/figure&gt; &lt;p&gt;&lt;br /&gt;&lt;/p&gt; &lt;p&gt;There was one last detail to work out before I could begin wiring up the halves. I wanted to install a rotary encoder (“volume knob”) on each half, but these don’t click into a standard keyboard hole out of the box. I had to 3D print a special adapter for each.&lt;/p&gt; &lt;figure&gt; &lt;img loading=&quot;lazy&quot; src=&quot;/assets/img/i-built-a-weird-keyboard/encoder_adapters.jpg&quot; alt=&quot;3D-printed adapters for rotary encoders&quot; /&gt; &lt;figcaption&gt;The end result, with chunks of glass attached to the bottoms&lt;/figcaption&gt; &lt;/figure&gt; &lt;p&gt;&lt;br /&gt;&lt;/p&gt; &lt;p&gt;I printed these with PETG and learned the hard way to &lt;em&gt;always&lt;/em&gt; use glue stick when printing with PETG. The adapters adhesed to the bed so strongly they took chunks out of my glass bed when I finally pried them off. I was able to salvage the adapters with some sanding, but the printer bed was unfortunately beyond repair.&lt;/p&gt; &lt;figure&gt; &lt;img loading=&quot;lazy&quot; src=&quot;/assets/img/i-built-a-weird-keyboard/broken_bed.jpg&quot; alt=&quot;A picture of the 3D printer bed with chips in it from the overly-adhesive PETG print&quot; /&gt; &lt;figcaption&gt;😭&lt;/figcaption&gt; &lt;/figure&gt; &lt;p&gt;&lt;br /&gt;&lt;/p&gt; &lt;p&gt;Despite their rather violent effects on my printer, the adapters did their job quite nicely!&lt;/p&gt; &lt;figure&gt; &lt;img loading=&quot;lazy&quot; src=&quot;/assets/img/i-built-a-weird-keyboard/encoders_installed.jpg&quot; alt=&quot;Two pictures of rotary encoders; one without a cap and one with a cap&quot; /&gt; &lt;figcaption&gt;Rotary encoder with and without the cap&lt;/figcaption&gt; &lt;/figure&gt; &lt;p&gt;&lt;br /&gt;&lt;/p&gt; &lt;p&gt;Finally, it was time to start wiring it up! First, I wrapped diodes around one pin on each switch.&lt;/p&gt; &lt;figure&gt; &lt;img loading=&quot;lazy&quot; src=&quot;/assets/img/i-built-a-weird-keyboard/diodes_installed.jpg&quot; alt=&quot;A keyboard half with diodes bent around one pin of the switch&quot; /&gt; &lt;figcaption&gt;Diodes wrapped&lt;/figcaption&gt; &lt;/figure&gt; &lt;p&gt;&lt;br /&gt;&lt;/p&gt; &lt;p&gt;I soldered the diodes into place and snipped the extra leg.&lt;/p&gt; &lt;figure&gt; &lt;img loading=&quot;lazy&quot; src=&quot;/assets/img/i-built-a-weird-keyboard/diodes_soldered.jpg&quot; alt=&quot;A keyboard half with diodes soldered to one pin of the switch&quot; /&gt; &lt;figcaption&gt;Diodes soldered and snipped&lt;/figcaption&gt; &lt;/figure&gt; &lt;p&gt;&lt;br /&gt;&lt;/p&gt; &lt;p&gt;I soldered the remaining diode legs together to form the rows of the &lt;a href=&quot;https://en.wikipedia.org/wiki/Keyboard_matrix_circuit&quot;&gt;key matrix&lt;/a&gt;.&lt;/p&gt; &lt;figure&gt; &lt;img loading=&quot;lazy&quot; src=&quot;/assets/img/i-built-a-weird-keyboard/diodes_soldered_together.jpg&quot; alt=&quot;A keyboard half with diodes soldered together to form rows of the keyboard matrix&quot; /&gt; &lt;figcaption&gt;Diode legs soldered together into rows&lt;/figcaption&gt; &lt;/figure&gt; &lt;p&gt;&lt;br /&gt;&lt;/p&gt; &lt;p&gt;I used small, individual pieces of insulated wire to form the columns.&lt;/p&gt; &lt;figure&gt; &lt;img loading=&quot;lazy&quot; src=&quot;/assets/img/i-built-a-weird-keyboard/columns_soldered.jpg&quot; alt=&quot;A keyboard half with wires soldered to the switches to form columns of the keyboard matrix&quot; /&gt; &lt;figcaption&gt;Don't look too closely; I'm really bad at soldering&lt;/figcaption&gt; &lt;/figure&gt; &lt;p&gt;&lt;br /&gt;&lt;/p&gt; &lt;p&gt;I installed &lt;a href=&quot;https://en.wikipedia.org/wiki/Jump_wire&quot;&gt;DuPont connectors&lt;/a&gt; so I didn’t have to solder directly to the microcontroller. This saved me a &lt;em&gt;lot&lt;/em&gt; of headache since it took quite a bit of trial and error to get all pins in the right spot.&lt;/p&gt; &lt;figure&gt; &lt;img loading=&quot;lazy&quot; src=&quot;/assets/img/i-built-a-weird-keyboard/dupont_connectors_attached.jpg&quot; alt=&quot;A keyboard half with DuPont connectors installed to each row and column of the keyboard matrix&quot; /&gt; &lt;figcaption&gt;DuPont connectors installed&lt;/figcaption&gt; &lt;/figure&gt; &lt;p&gt;&lt;br /&gt;&lt;/p&gt; &lt;p&gt;I flashed a basic &lt;a href=&quot;https://qmk.fm/&quot;&gt;QMK&lt;/a&gt; firmware to the microcontroller and had the incredibly satisfying experience of seeing a letter appear on the screen when I pressed a key. I also got the LED strip working!&lt;/p&gt; &lt;figure&gt; &lt;img loading=&quot;lazy&quot; src=&quot;/assets/img/i-built-a-weird-keyboard/it_lives.jpg&quot; alt=&quot;A keyboard half with a LED strip lit in rainbow colors&quot; /&gt; &lt;figcaption&gt;IT LIVES!!&lt;/figcaption&gt; &lt;/figure&gt; &lt;p&gt;&lt;br /&gt;&lt;/p&gt; &lt;p&gt;I was getting &lt;em&gt;really&lt;/em&gt; close at this point. I designed a custom holder for the microcontroller since the one that was supposed to be compatible with my case didn’t fit for some reason.&lt;/p&gt; &lt;figure&gt; &lt;img loading=&quot;lazy&quot; src=&quot;/assets/img/i-built-a-weird-keyboard/holder.jpg&quot; alt=&quot;A 3D printed holder for the microcontroller&quot; /&gt; &lt;figcaption&gt;Custom-designed microcontroller holder&lt;/figcaption&gt; &lt;/figure&gt; &lt;p&gt;&lt;br /&gt;&lt;/p&gt; &lt;p&gt;I spliced some wires together since a few of the microcontroller pins had to be shared by more than one connection.&lt;/p&gt; &lt;figure&gt; &lt;img loading=&quot;lazy&quot; src=&quot;/assets/img/i-built-a-weird-keyboard/spaghetti.jpg&quot; alt=&quot;A mess of wires soldered together&quot; /&gt; &lt;figcaption&gt;I'm frankly shocked this thing works&lt;/figcaption&gt; &lt;/figure&gt; &lt;p&gt;&lt;br /&gt;&lt;/p&gt; &lt;p&gt;After assembling all the pieces, a bit of software configuration, and a lot of trial and error… I had a working keyboard!&lt;/p&gt; &lt;figure&gt; &lt;img loading=&quot;lazy&quot; src=&quot;/assets/img/i-built-a-weird-keyboard/desk.jpg&quot; alt=&quot;A picture of the finished keyboard on a desk&quot; /&gt; &lt;figcaption&gt;Finally. Done.&lt;/figcaption&gt; &lt;/figure&gt; &lt;p&gt;&lt;br /&gt;&lt;/p&gt; &lt;figure&gt; &lt;img loading=&quot;lazy&quot; src=&quot;/assets/img/i-built-a-weird-keyboard/bottom_and_side.jpg&quot; alt=&quot;A picture of the finished keyboard, with one half showing the bottom and one half showing the side&quot; /&gt; &lt;figcaption&gt;This was way too much work.&lt;/figcaption&gt; &lt;/figure&gt; &lt;p&gt;&lt;br /&gt;&lt;/p&gt; &lt;figure&gt; &lt;img loading=&quot;lazy&quot; src=&quot;/assets/img/i-built-a-weird-keyboard/pink.jpg&quot; alt=&quot;Another picture of the finished keyboard, with pink underglow&quot; /&gt; &lt;figcaption&gt;Never again. Probably.&lt;/figcaption&gt; &lt;/figure&gt; &lt;p&gt;&lt;br /&gt;&lt;/p&gt; &lt;figure&gt; &lt;video controls=&quot;&quot; width=&quot;500&quot;&gt; &lt;source src=&quot;/assets/img/i-built-a-weird-keyboard/alert.mp4&quot; /&gt; &lt;/video&gt; &lt;figcaption&gt;ALERT&lt;/figcaption&gt; &lt;/figure&gt; &lt;p&gt;&lt;br /&gt;&lt;/p&gt; &lt;h2 id=&quot;how-does-it-feel&quot;&gt;How does it feel?&lt;/h2&gt; &lt;p&gt;Weird, but good! I’ve only been typing on it for a few work days, so my muscle memory hasn’t fully adjusted. I keep reaching for keys in the wrong places; in particular, my fingers naturally stretch too far when reaching for the bottom row. I also made a few modifications to my &lt;a href=&quot;https://gitlab.com/nfriend/qmk_firmware/-/blob/nfriend-dactyl-manuform/keyboards/handwired/dactyl_manuform/5x7/keymaps/nfriend/keymap.c&quot;&gt;QMK layout&lt;/a&gt; to take advantage of the more accessible thumb clusters compared to my ErgoDox EZ. I think I’ll really like it once I’m used to it.&lt;/p&gt; &lt;h2 id=&quot;was-it-worth-it&quot;&gt;Was it worth it?&lt;/h2&gt; &lt;p&gt;Umm… I think so? The end result was fantastic, but it was an insane amount of work. I don’t recommend this project to anyone who isn’t interested in the process itself. If you’re just looking for a great ergonomic keyboard, I’d recommend buying an &lt;a href=&quot;https://ergodox-ez.com/&quot;&gt;ErgoDox EZ&lt;/a&gt;, a &lt;a href=&quot;https://www.zsa.io/moonlander/&quot;&gt;Moonlander&lt;/a&gt;, a &lt;a href=&quot;https://kinesis-ergo.com/shop/adv360/&quot;&gt;Kinesis Advantage360&lt;/a&gt; or a &lt;a href=&quot;https://ohkeycaps.com/products/built-to-order-dactyl-manuform-keyboard&quot;&gt;prebuilt Dactyl Manuform&lt;/a&gt;, all of which will cost about the same as this project (see below).&lt;/p&gt; &lt;h2 id=&quot;cost-breakdown&quot;&gt;Cost breakdown&lt;/h2&gt; &lt;p&gt;Cost of all items, including tax and shipping.&lt;/p&gt; &lt;table&gt; &lt;thead&gt; &lt;tr&gt; &lt;th&gt;Description&lt;/th&gt; &lt;th&gt;Cost (CAD)&lt;/th&gt; &lt;th&gt;Link&lt;/th&gt; &lt;/tr&gt; &lt;/thead&gt; &lt;tbody&gt; &lt;tr&gt; &lt;td&gt;Wood PLA filament for 3D printed case&lt;/td&gt; &lt;td&gt;$40.44&lt;/td&gt; &lt;td&gt;&lt;a href=&quot;https://a.co/d/eUWep4w&quot;&gt;amazon.ca&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;Kailh switch tester&lt;/td&gt; &lt;td&gt;$21.46&lt;/td&gt; &lt;td&gt;&lt;a href=&quot;https://www.aliexpress.com/item/32898546644.html&quot;&gt;aliexpress.com&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;Kailh low-profile switch tester&lt;/td&gt; &lt;td&gt;$9.68&lt;/td&gt; &lt;td&gt;&lt;a href=&quot;https://www.aliexpress.com/item/4001204818828.html&quot;&gt;aliexpress.com&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;Pro Micro controller (x2)&lt;/td&gt; &lt;td&gt;$44.98&lt;/td&gt; &lt;td&gt;&lt;a href=&quot;https://a.co/d/1GsSJXg&quot;&gt;amazon.ca&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;Kailh BOX White switches (x90)&lt;/td&gt; &lt;td&gt;$51.26&lt;/td&gt; &lt;td&gt;&lt;a href=&quot;https://www.aliexpress.com/item/1005004522909300.html&quot;&gt;aliexpress.com&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;M3 heat-seated inserts (x100)&lt;/td&gt; &lt;td&gt;$12.42&lt;/td&gt; &lt;td&gt;&lt;a href=&quot;https://a.co/d/6wMZmwo&quot;&gt;amazon.ca&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;M3 screws (x100)&lt;/td&gt; &lt;td&gt;$12.02&lt;/td&gt; &lt;td&gt;&lt;a href=&quot;https://a.co/d/enX1CBA&quot;&gt;amazon.ca&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;EC11 rotary encoder (x4)&lt;/td&gt; &lt;td&gt;$15.80&lt;/td&gt; &lt;td&gt;&lt;a href=&quot;https://a.co/d/eAp729Y&quot;&gt;amazon.ca&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;LED strip (1m)&lt;/td&gt; &lt;td&gt;$16.37&lt;/td&gt; &lt;td&gt;&lt;a href=&quot;https://www.aliexpress.com/item/32682015405.html&quot;&gt;aliexpress.com&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;1N4148 Diode (x100)&lt;/td&gt; &lt;td&gt;$8.80&lt;/td&gt; &lt;td&gt;&lt;a href=&quot;https://www.digikey.ca/en/products/detail/onsemi/1N4148/458603&quot;&gt;digikey.ca&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;Reset button (x3)&lt;/td&gt; &lt;td&gt;$10.82&lt;/td&gt; &lt;td&gt;&lt;a href=&quot;https://www.digikey.ca/en/products/detail/e-switch/PS1057ABLK/46305&quot;&gt;digikey.ca&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;22AWG Wire (25’)&lt;/td&gt; &lt;td&gt;$7.24&lt;/td&gt; &lt;td&gt;&lt;a href=&quot;https://www.digikey.ca/en/products/detail/sparkfun-electronics/PRT-08866/6833926&quot;&gt;digikey.ca&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;TRRS jack, female (x3)&lt;/td&gt; &lt;td&gt;$12.65&lt;/td&gt; &lt;td&gt;&lt;a href=&quot;https://www.digikey.ca/en/products/detail/switchcraft-inc/35RASMT5CHNTRX/16569698&quot;&gt;digikey.ca&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;Jumper wire (x60)&lt;/td&gt; &lt;td&gt;$11.74&lt;/td&gt; &lt;td&gt;&lt;a href=&quot;https://www.digikey.ca/en/products/detail/sparkfun-electronics/PRT-12796/5993861&quot;&gt;digikey.ca&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;Soldering iron&lt;/td&gt; &lt;td&gt;$59.87&lt;/td&gt; &lt;td&gt;&lt;a href=&quot;https://www.homedepot.ca/product/1001649527&quot;&gt;homedepot.ca&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;Solder&lt;/td&gt; &lt;td&gt;$28.23&lt;/td&gt; &lt;td&gt;&lt;a href=&quot;https://www.homedepot.ca/product/1001652943&quot;&gt;homedepot.ca&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;Wire stripper&lt;/td&gt; &lt;td&gt;$11.29&lt;/td&gt; &lt;td&gt;&lt;a href=&quot;https://www.canadiantire.ca/en/pdp/mastercraft-20-30-gauge-wire-stripper-comfort-grip-handles-high-carbon-heat-treated-steel-0584508p.html?loc=plp&quot;&gt;canadiantire.ca&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;Acrylic base plate (x4)&lt;/td&gt; &lt;td&gt;$54.88&lt;/td&gt; &lt;td&gt;&lt;a href=&quot;https://www.ponoko.com/&quot;&gt;ponoko.com&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;Gel wood stain&lt;/td&gt; &lt;td&gt;$19.93&lt;/td&gt; &lt;td&gt;&lt;a href=&quot;https://www.homedepot.ca/product/1000844900&quot;&gt;homedepot.ca&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;Glossy polyurethane&lt;/td&gt; &lt;td&gt;$27.11&lt;/td&gt; &lt;td&gt;&lt;a href=&quot;https://a.co/d/do2gUQm&quot;&gt;amazon.ca&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;Keycap set (x2)&lt;/td&gt; &lt;td&gt;$57.32&lt;/td&gt; &lt;td&gt;&lt;a href=&quot;https://www.amazon.ca/gp/product/B0B3QQ2468&quot;&gt;amazon.ca&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;Electrical tape&lt;/td&gt; &lt;td&gt;$5.37&lt;/td&gt; &lt;td&gt;&lt;a href=&quot;https://www.amazon.ca/dp/B001AXD0EY&quot;&gt;amazon.ca&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;Rubber feet&lt;/td&gt; &lt;td&gt;$13.55&lt;/td&gt; &lt;td&gt;&lt;a href=&quot;https://www.amazon.ca/gp/product/B08GLMTL77&quot;&gt;amazon.ca&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;&lt;strong&gt;Only keyboard materials&lt;/strong&gt;&lt;/td&gt; &lt;td&gt;&lt;strong&gt;$450.93&lt;/strong&gt;&lt;/td&gt; &lt;td&gt;≈ $340 USD&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;&lt;strong&gt;All items&lt;/strong&gt; (including tools, testers, etc.)&lt;/td&gt; &lt;td&gt;&lt;strong&gt;$553.23&lt;/strong&gt;&lt;/td&gt; &lt;td&gt;≈ $417 USD&lt;/td&gt; &lt;/tr&gt; &lt;/tbody&gt; &lt;/table&gt; &lt;p&gt;As you can see, building your own keyboard is &lt;em&gt;not&lt;/em&gt; a good way to save money 💸&lt;/p&gt; &lt;h2 id=&quot;linksresources&quot;&gt;Links/resources&lt;/h2&gt; &lt;ul&gt; &lt;li&gt;All &lt;code class=&quot;highlighter-rouge&quot;&gt;.stl&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;.svg&lt;/code&gt; files: &lt;a href=&quot;https://www.thingiverse.com/thing:6099418&quot;&gt;https://www.thingiverse.com/thing:6099418&lt;/a&gt;&lt;/li&gt; &lt;li&gt;A helpful Hacker News comment: &lt;a href=&quot;https://news.ycombinator.com/item?id=23445208&quot;&gt;https://news.ycombinator.com/item?id=23445208&lt;/a&gt;&lt;/li&gt; &lt;li&gt;The generator I used to generate the keyboard case model: &lt;a href=&quot;https://github.com/ibnuda/dactyl-keyboard/tree/refaktor&quot;&gt;https://github.com/ibnuda/dactyl-keyboard/tree/refaktor&lt;/a&gt;&lt;/li&gt; &lt;li&gt;The firmware that powers the keyboard: &lt;a href=&quot;https://qmk.fm/&quot;&gt;https://qmk.fm/&lt;/a&gt;&lt;/li&gt; &lt;li&gt;Some helpul build logs: &lt;ul&gt; &lt;li&gt;&lt;a href=&quot;https://sachee.medium.com/building-my-first-keyboard-and-you-can-too-512c0f8a4c5f&quot;&gt;https://sachee.medium.com/building-my-first-keyboard-and-you-can-too-512c0f8a4c5f&lt;/a&gt;&lt;/li&gt; &lt;li&gt;&lt;a href=&quot;https://youtu.be/UerP5bxGL3c&quot;&gt;https://youtu.be/UerP5bxGL3c&lt;/a&gt; (and subsequent videos)&lt;/li&gt; &lt;li&gt;&lt;a href=&quot;https://nickgreen.info/dactyl-manuform-build-log/&quot;&gt;https://nickgreen.info/dactyl-manuform-build-log/&lt;/a&gt;&lt;/li&gt; &lt;li&gt;&lt;a href=&quot;https://medium.com/swlh/complete-idiot-guide-for-building-a-dactyl-manuform-keyboard-53454845b065&quot;&gt;https://medium.com/swlh/complete-idiot-guide-for-building-a-dactyl-manuform-keyboard-53454845b065&lt;/a&gt;&lt;/li&gt; &lt;/ul&gt; &lt;/li&gt; &lt;/ul&gt;</content><author><name></name></author><summary type="html">I spent most of my free time over the last 10 months building this bizarre keyboard from scratch. It’s a Dactyl Manuform - a split keyboard with a highly sculpted design that is somehow simulataneously the ugliest and most eye-catching object I’ve ever seen.</summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://nathanfriend.io/assets/img/i-built-a-weird-keyboard/half_rainbow.jpg"/><media:content medium="image" url="https://nathanfriend.io/assets/img/i-built-a-weird-keyboard/half_rainbow.jpg" xmlns:media="http://search.yahoo.com/mrss/"/></entry><entry><title type="html">Wordle Bot</title><link href="https://nathanfriend.io/2022/01/25/wordle.html" rel="alternate" type="text/html" title="Wordle Bot"/><published>2022-01-25T05:25:10-05:00</published><updated>2022-01-25T05:25:10-05:00</updated><id>https://nathanfriend.io/2022/01/25/wordle</id><content type="html" xml:base="https://nathanfriend.io/2022/01/25/wordle.html">&lt;p&gt;Like everyone else, I’ve been obsessed with &lt;a href=&quot;https://www.powerlanguage.co.uk/wordle/&quot;&gt;Wordle&lt;/a&gt; lately.&lt;/p&gt; &lt;p&gt;I was &lt;em&gt;really&lt;/em&gt; bad at it when I began. My original strategy was to use the first 3 or 4 guesses to uncover as many letters as possible, and then use the last few guesses to unscramble the solution. This turned out to be a terrible approach, and I failed most of the challenges.&lt;/p&gt; &lt;p&gt;This got me thinking: what &lt;em&gt;is&lt;/em&gt; the optimal Wordle strategy?&lt;/p&gt; &lt;p&gt;And &lt;em&gt;that&lt;/em&gt; immediately led to a second thought: can I automate it?&lt;/p&gt; &lt;p&gt;The answer is a resounding &lt;strong&gt;of course&lt;/strong&gt;! I present to you: &lt;a href=&quot;https://twitter.com/bot_wordle/&quot;&gt;Wordle Bot&lt;/a&gt;!&lt;/p&gt; &lt;figure&gt; &lt;img src=&quot;/assets/img/wordle/wordle-bot.png&quot; alt=&quot;The Wordle Bot logo&quot; /&gt; &lt;/figure&gt; &lt;p&gt;&lt;br /&gt;&lt;/p&gt; &lt;h2 id=&quot;how-it-works&quot;&gt;How it works&lt;/h2&gt; &lt;p&gt;The general strategy is to repeatedly pick the word that is most likely to reveal new letters. To do this, I use the frequency of each letter in the set of possible solutions to assign a score to each potential guess, and choose the option with the highest score.&lt;/p&gt; &lt;p&gt;The set of all possible solutions is conveniently hardcoded in Wordle’s source code, so my first step is &lt;a href=&quot;https://gitlab.com/nfriend/wordle-solver/-/blob/a2e90c02e6acbf4c002356dfe1fca9851c6915e2/src/index.ts#L2&quot;&gt;to simply hardcode this list into my own script&lt;/a&gt;.&lt;/p&gt; &lt;p&gt;Next, &lt;a href=&quot;https://gitlab.com/nfriend/wordle-solver/-/blob/a2e90c02e6acbf4c002356dfe1fca9851c6915e2/src/index.ts#L4-19&quot;&gt;I loop through each potential solution&lt;/a&gt; and count how many times each letter occurs. This results in a mapping like this:&lt;/p&gt; &lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;e&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1233&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;a&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;979&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;r&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;899&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;o&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;754&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;t&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;729&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;l&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;719&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;i&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;671&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;s&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;669&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;n&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;575&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;c&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;477&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;u&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;467&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;y&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;425&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;d&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;393&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;h&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;389&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;p&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;367&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;m&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;316&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;g&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;311&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;b&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;281&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;f&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;230&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;k&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;210&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;w&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;195&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;v&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;153&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;z&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;40&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;x&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;37&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;q&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;29&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;j&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;27&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;So, for example, a word like “HELLO” would score 3,814 (389 + 1,233 + 719 + 719 + 754).&lt;/p&gt; &lt;p&gt;Using this method, the bot would &lt;em&gt;always&lt;/em&gt; start with the word “EERIE”, due to its high score of 5,269 (1,233 + 1,233 + 899 + 671 + 1,233).&lt;/p&gt; &lt;p&gt;However, this actually isn’t a very useful guess! It only gives us information about 3 letters. A more strategic method is to pick the highest scoring word &lt;em&gt;that contains no repeating letters&lt;/em&gt;. Using this approach, the best first play is “LATER”, which still scores quite high (4,559 = 719 + 979 + 729 + 1,233 + 899) &lt;em&gt;and&lt;/em&gt; provides information about 5 different letters.&lt;/p&gt; &lt;p&gt;(Side note: Even though I know I &lt;em&gt;should&lt;/em&gt; start with “LATER” when playing on my own, I can’t help but start with “ADIEU” - too many vowels to pass up!)&lt;/p&gt; &lt;p&gt;Each subsequent guess is generated using the same algorithm, except that the list of possible solutions is shrunk by eliminating all options that contradict the results of previous guesses. A side effect of this elimination method is that it effectively plays the game on Hard Mode. (Hard Mode = “Any revealed hints must be used in subsequent guesses”).&lt;/p&gt; &lt;p&gt;In later guesses, it’s possible there &lt;em&gt;won’t&lt;/em&gt; be a valid option that includes no repeating letters, so in this case the bot &lt;a href=&quot;https://gitlab.com/nfriend/wordle-solver/-/blob/a2e90c02e6acbf4c002356dfe1fca9851c6915e2/src/index.ts#L170&quot;&gt;falls back to the highest-scoring word regardless of letter repetition&lt;/a&gt;.&lt;/p&gt; &lt;h2 id=&quot;demo&quot;&gt;Demo&lt;/h2&gt; &lt;p&gt;Here’s an example of the script working in &lt;a href=&quot;https://en.wikipedia.org/wiki/Bookmarklet&quot;&gt;bookmarklet&lt;/a&gt; form:&lt;/p&gt; &lt;figure&gt; &lt;img src=&quot;/assets/img/wordle/wordle-demo.gif&quot; alt=&quot;A demo of Wordle being solved by the script in bookmarklet form&quot; /&gt; &lt;/figure&gt; &lt;p&gt;(Instruction for setting up this script as a bookmarklet can be found &lt;a href=&quot;https://gitlab.com/nfriend/wordle-solver#usage&quot;&gt;here&lt;/a&gt;.)&lt;/p&gt; &lt;h2 id=&quot;how-good-is-it&quot;&gt;How good is it?&lt;/h2&gt; &lt;p&gt;The short answer is: quite good! (&lt;em&gt;Much&lt;/em&gt; better than me.) But not perfect!&lt;/p&gt; &lt;p&gt;At the time of writing, Wordle Bot has solved 220 puzzles. Its guess distribution looks like this:&lt;/p&gt; &lt;figure&gt; &lt;img src=&quot;/assets/img/wordle/guess-distribution.png&quot; alt=&quot;A chart showing Wordle Bot's guess distribution&quot; /&gt; &lt;/figure&gt; &lt;p&gt;80% of the time, Wordle Bot can solve the daily puzzle in 4 tries or less. Not bad!&lt;/p&gt; &lt;p&gt;But why isn’t it perfect? I was intrigued by the two puzzles it failed to solve. In both cases, Wordle Bot failed in a similar fashion. By guess 3, it had nailed down letters 2 - 5. It spent the remaining guesses cycling through different starting letters, and failed to find the correct one before it ran out of guesses.&lt;/p&gt; &lt;p&gt;Here is its attempt at puzzle 113:&lt;/p&gt; &lt;figure&gt; &lt;img src=&quot;/assets/img/wordle/puzzle-113.jpg&quot; alt=&quot;A screenshot of Wordle Bot's solution to puzzle 113&quot; /&gt; &lt;figcaption&gt;Wordle 113&lt;/figcaption&gt; &lt;/figure&gt; &lt;p&gt;Even though the correct solution (“HATCH”) scores higher than all the options it tried (“PATCH”, “MATCH”, “BATCH”, and “WATCH”), it chose not to guess “HATCH” since it includes a repeated letter (“H”). As mentioned earlier, the algorithm always prefers guesses without repetition if they exist.&lt;/p&gt; &lt;p&gt;A similar thing happened on day 123:&lt;/p&gt; &lt;figure&gt; &lt;img src=&quot;/assets/img/wordle/puzzle-123.jpg&quot; alt=&quot;A screenshot of Wordle Bot's solution to puzzle 123&quot; /&gt; &lt;figcaption&gt;Wordle 123&lt;/figcaption&gt; &lt;/figure&gt; &lt;p&gt;In this case, the solution “JAUNT” scored lower than all the options it tried (“DAUNT”, “HAUNT”, “GAUNT”, and “VAUNT”), so it simply ran out of time.&lt;/p&gt; &lt;h2 id=&quot;bringing-the-bot-to-life&quot;&gt;Bringing the bot to life&lt;/h2&gt; &lt;p&gt;Turning the script into a Twitter bot was just a matter of automation.&lt;/p&gt; &lt;p&gt;First, I built a &lt;a href=&quot;https://gitlab.com/nfriend/wordle-solver/-/blob/a2e90c02e6acbf4c002356dfe1fca9851c6915e2/twitter-bot/index.js&quot;&gt;Puppeteer script&lt;/a&gt; to launch Wordle in an instance of headless Chrome, inject my bookmarklet script, and solve the puzzle. Once it’s finished, the script &lt;a href=&quot;https://gitlab.com/nfriend/wordle-solver/-/blob/a2e90c02e6acbf4c002356dfe1fca9851c6915e2/twitter-bot/screenshot-and-upload-to-imgur.js&quot;&gt;takes a screenshot, uploads it to Imgur&lt;/a&gt;, and &lt;a href=&quot;https://gitlab.com/nfriend/wordle-solver/-/blob/a2e90c02e6acbf4c002356dfe1fca9851c6915e2/twitter-bot/index.js#L182-188&quot;&gt;tweets the results&lt;/a&gt;:&lt;/p&gt; &lt;blockquote class=&quot;twitter-tweet&quot;&gt;&lt;p lang=&quot;en&quot; dir=&quot;ltr&quot;&gt;Wordle 220 3/6&lt;br /&gt;&lt;br /&gt;⬜🟨⬜⬜🟩&lt;br /&gt;🟩⬜⬜🟩🟩&lt;br /&gt;🟩🟩🟩🟩🟩&lt;br /&gt;&lt;br /&gt;Full solution [&lt;a href=&quot;https://twitter.com/hashtag/SPOILER?src=hash&amp;amp;ref_src=twsrc%5Etfw&quot;&gt;#SPOILER&lt;/a&gt;!]: &lt;a href=&quot;https://t.co/kXojDt5u3i&quot;&gt;https://t.co/kXojDt5u3i&lt;/a&gt;&lt;/p&gt;&amp;mdash; Wordle Bot (@bot_wordle) &lt;a href=&quot;https://twitter.com/bot_wordle/status/1485940275180711940?ref_src=twsrc%5Etfw&quot;&gt;January 25, 2022&lt;/a&gt;&lt;/blockquote&gt; &lt;script async=&quot;&quot; src=&quot;https://platform.twitter.com/widgets.js&quot; charset=&quot;utf-8&quot;&gt;&lt;/script&gt; &lt;p&gt;Then, I configured a &lt;a href=&quot;https://docs.gitlab.com/ee/ci/pipelines/schedules.html&quot;&gt;scheduled GitLab pipeline&lt;/a&gt; to run &lt;a href=&quot;https://gitlab.com/nfriend/wordle-solver/-/blob/a2e90c02e6acbf4c002356dfe1fca9851c6915e2/.gitlab-ci.yml&quot;&gt;the project’s pipeline&lt;/a&gt; every day at 3:00 AM ET.&lt;/p&gt; &lt;p&gt;Even though the end result is fairly simple, I ran into a number of speed bumps along the way:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Wordle shares its results by copying them to the clipboard, but I couldn’t figure out a way to access the clipboard’s contents inside Puppeteer (it always returned my &lt;em&gt;system&lt;/em&gt; clipboard contents instead). To get around this, I had to &lt;a href=&quot;https://gitlab.com/nfriend/wordle-solver/-/blob/a2e90c02e6acbf4c002356dfe1fca9851c6915e2/twitter-bot/index.js#L101-152&quot;&gt;build the “share” text myself&lt;/a&gt;.&lt;/li&gt; &lt;li&gt;The &lt;a href=&quot;https://github.com/KenEucker/imgur&quot;&gt;Imgur JavaScript library&lt;/a&gt; I use required Node 14+, but &lt;a href=&quot;https://github.com/alekzonder/docker-puppeteer&quot;&gt;the Puppeteer Docker image&lt;/a&gt; I was using inside my GitLab pipeline was stuck on Node 12. Switching to &lt;a href=&quot;https://github.com/buildkite/docker-puppeteer&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;buildkite/puppeteer&lt;/code&gt;&lt;/a&gt; fixed this.&lt;/li&gt; &lt;li&gt;The pipeline would randomly fail while trying to connect to the Wordle website: &lt;code class=&quot;highlighter-rouge&quot;&gt;Error: net::ERR_NETWORK_CHANGED at https://www.powerlanguage.co.uk/wordle/&lt;/code&gt;. I “solved” this by adding a &lt;a href=&quot;https://docs.gitlab.com/ee/ci/yaml/#timeout&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;timeout&lt;/code&gt;&lt;/a&gt; and &lt;a href=&quot;https://docs.gitlab.com/ee/ci/yaml/#retry&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;retry&lt;/code&gt;&lt;/a&gt; to &lt;a href=&quot;https://gitlab.com/nfriend/wordle-solver/-/blob/a2e90c02e6acbf4c002356dfe1fca9851c6915e2/.gitlab-ci.yml#L21-22&quot;&gt;my GitLab job&lt;/a&gt;.&lt;/li&gt; &lt;/ul&gt; &lt;h2 id=&quot;absurdal&quot;&gt;Absurdal&lt;/h2&gt; &lt;p&gt;Oh, did I mention the bot is also surprisingly good at solving the &lt;a href=&quot;https://qntm.org/files/wordle/index.html&quot;&gt;Absurdal&lt;/a&gt; variant?&lt;/p&gt; &lt;figure&gt; &lt;img src=&quot;/assets/img/wordle/absurdal-demo.gif&quot; alt=&quot;A demo of Absurdal being solved by the script in bookmarklet form&quot; /&gt; &lt;/figure&gt; &lt;p&gt;&lt;br /&gt;&lt;/p&gt; &lt;h2 id=&quot;links&quot;&gt;Links&lt;/h2&gt; &lt;ul&gt; &lt;li&gt;Wordle Solver GitLab project: &lt;a href=&quot;https://gitlab.com/nfriend/wordle-solver&quot;&gt;https://gitlab.com/nfriend/wordle-solver&lt;/a&gt;&lt;/li&gt; &lt;li&gt;Wordle Bot Twitter account: &lt;a href=&quot;https://twitter.com/bot_wordle/&quot;&gt;https://twitter.com/bot_wordle/&lt;/a&gt;&lt;/li&gt; &lt;li&gt;Another Wordle solver I discovered after building my version that uses an almost identical algorithm: &lt;a href=&quot;https://lockwood.dev/wordle/python/2022/01/23/wordle-solved-average-3-64.html&quot;&gt;https://lockwood.dev/wordle/python/2022/01/23/wordle-solved-average-3-64.html&lt;/a&gt;&lt;/li&gt; &lt;li&gt;An open-source Wordle Clone written in React, which I later discovered was written by my coworker’s partner! &lt;a href=&quot;https://github.com/hannahcode/wordle&quot;&gt;https://github.com/hannahcode/wordle&lt;/a&gt;&lt;/li&gt; &lt;/ul&gt;</content><author><name></name></author><summary type="html">Like everyone else, I’ve been obsessed with Wordle lately.</summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://nathanfriend.io/assets/img/wordle/wordle-bot.png"/><media:content medium="image" url="https://nathanfriend.io/assets/img/wordle/wordle-bot.png" xmlns:media="http://search.yahoo.com/mrss/"/></entry><entry><title type="html">Herding Gits</title><link href="https://nathanfriend.io/2021/08/26/herding-gits.html" rel="alternate" type="text/html" title="Herding Gits"/><published>2021-08-26T19:42:44-04:00</published><updated>2021-08-26T19:42:44-04:00</updated><id>https://nathanfriend.io/2021/08/26/herding-gits</id><content type="html" xml:base="https://nathanfriend.io/2021/08/26/herding-gits.html">&lt;p&gt;Juggling multiple Git identities can be tricky.&lt;/p&gt; &lt;figure&gt; &lt;img src=&quot;/assets/img/herding-gits/git-mosaic.png&quot; alt=&quot;A screenshot of the Git logo&quot; /&gt; &lt;/figure&gt; &lt;p&gt;For example, at &lt;a href=&quot;https://stripe.com/&quot;&gt;Stripe&lt;/a&gt;, we encourage developers to create a separate GitHub account for Stripe-related open source activity. For me, this means I now own both a &lt;a href=&quot;https://github.com/nfriend/&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;nfriend&lt;/code&gt;&lt;/a&gt; &lt;em&gt;and&lt;/em&gt; a &lt;a href=&quot;https://github.com/nfriend-stripe/&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;nfriend-stripe&lt;/code&gt;&lt;/a&gt; GitHub profile.&lt;/p&gt; &lt;p&gt;While setting up my dev environment, I had a few goals:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Use both Git identities on the same machine&lt;/li&gt; &lt;li&gt;Sign commits with separate GPG keys&lt;/li&gt; &lt;li&gt;Connect to remotes using different SSH keys&lt;/li&gt; &lt;li&gt;Have all of this 👆 happen automatically without me having to think about it&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;strong&gt;Good news!&lt;/strong&gt; This is possible with a little &lt;code class=&quot;highlighter-rouge&quot;&gt;.gitconfig&lt;/code&gt; magic ✨&lt;/p&gt; &lt;h2 id=&quot;the-setup&quot;&gt;The setup&lt;/h2&gt; &lt;p&gt;&lt;em&gt;Note: The instructions below rely on Git’s &lt;a href=&quot;https://git-scm.com/docs/git-config#_conditional_includes&quot;&gt;conditional includes&lt;/a&gt;, which are only available in Git 2.13 and beyond.&lt;/em&gt;&lt;/p&gt; &lt;h3 id=&quot;1-set-up-separate-ssh-and-gpg-keys-for-each-identity&quot;&gt;1. Set up separate SSH and GPG keys for each identity&lt;/h3&gt; &lt;p&gt;I won’t go into details since this is already covered in great detail by other tutorials. GitHub’s tutorials are particularly well-presented:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;a href=&quot;https://docs.github.com/en/github/authenticating-to-github/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent&quot;&gt;Generating a new SSH key and adding it to the ssh-agent&lt;/a&gt;&lt;/li&gt; &lt;li&gt;&lt;a href=&quot;https://docs.github.com/en/github/authenticating-to-github/managing-commit-signature-verification/signing-commits&quot;&gt;Signing commits&lt;/a&gt;&lt;/li&gt; &lt;/ul&gt; &lt;h3 id=&quot;2-create-separate-directories-for-each-identity&quot;&gt;2. Create separate directories for each identity&lt;/h3&gt; &lt;p&gt;For example, a &lt;code class=&quot;highlighter-rouge&quot;&gt;~/github-personal&lt;/code&gt; and a &lt;code class=&quot;highlighter-rouge&quot;&gt;~/github-work&lt;/code&gt; directory.&lt;/p&gt; &lt;h3 id=&quot;3-create-a-gitconfig_include-file-in-each&quot;&gt;3. Create a &lt;code class=&quot;highlighter-rouge&quot;&gt;.gitconfig_include&lt;/code&gt; file in each&lt;/h3&gt; &lt;p&gt;Inside each of these new directories, create a new file named &lt;code class=&quot;highlighter-rouge&quot;&gt;.gitconfig_include&lt;/code&gt; with the following content:&lt;/p&gt; &lt;div class=&quot;language-ini highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nn&quot;&gt;[user]&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Your Name&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;email&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;your-name@example.com&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;signingkey&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;0123456789ABCDEF&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;[core]&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;sshCommand&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ssh -i ~/.ssh/id_rsa_example -F /dev/null&lt;/span&gt; &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;Update each file with the name, email, and signing key for the corresponding Git identity.&lt;/p&gt; &lt;p&gt;Additionally, update the command in the &lt;code class=&quot;highlighter-rouge&quot;&gt;sshCommand&lt;/code&gt; option to reference the appropriate key file.&lt;/p&gt; &lt;h3 id=&quot;4-reference-these-files-from-the-global-gitconfig&quot;&gt;4. Reference these files from the global &lt;code class=&quot;highlighter-rouge&quot;&gt;.gitconfig&lt;/code&gt;&lt;/h3&gt; &lt;p&gt;In your global &lt;code class=&quot;highlighter-rouge&quot;&gt;.gitconfig&lt;/code&gt; (i.e. &lt;code class=&quot;highlighter-rouge&quot;&gt;~/.gitconfig&lt;/code&gt;), configure Git to conditionally include the correct &lt;code class=&quot;highlighter-rouge&quot;&gt;.gitconfig_include&lt;/code&gt; file based on the current directory:&lt;/p&gt; &lt;div class=&quot;language-ini highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nn&quot;&gt;[includeIf &quot;gitdir:~/github-personal/&quot;]&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;path&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;~/github-personal/.gitconfig_include&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;[includeIf &quot;gitdir:~/github-work/&quot;]&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;path&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;~/github-work/.gitconfig_include&lt;/span&gt; &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;h3 id=&quot;5-test-it&quot;&gt;5. Test it!&lt;/h3&gt; &lt;p&gt;Create a test project with both identities. Ensure you can:&lt;/p&gt; &lt;ol&gt; &lt;li&gt;Clone the repository from the remote&lt;/li&gt; &lt;li&gt;Make a commit&lt;/li&gt; &lt;li&gt;Push the commit to the remote&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;If you’re using a web UI like GitLab or GitHub, check to see that your commits are being signed correctly and are labeled as “Verified”:&lt;/p&gt; &lt;figure&gt; &lt;img src=&quot;/assets/img/herding-gits/commit-verified.png&quot; alt=&quot;A screenshot of GitLab showing a 'Verified' label next to a commit&quot; /&gt; &lt;/figure&gt; &lt;p&gt;&lt;br /&gt;&lt;/p&gt; &lt;h2 id=&quot;helpful-links&quot;&gt;Helpful links&lt;/h2&gt; &lt;p&gt;Some things I found helpful while setting this up:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Setting &lt;code class=&quot;highlighter-rouge&quot;&gt;.gitconfig&lt;/code&gt; settings on a per-directory basis: &lt;a href=&quot;https://stackoverflow.com/a/48088291/1063392&quot;&gt;https://stackoverflow.com/a/48088291/1063392&lt;/a&gt;&lt;/li&gt; &lt;li&gt;Using Git’s &lt;code class=&quot;highlighter-rouge&quot;&gt;core.sshCommand&lt;/code&gt; configuration to select an SSH key: &lt;a href=&quot;https://superuser.com/a/912281/144803&quot;&gt;https://superuser.com/a/912281/144803&lt;/a&gt;&lt;/li&gt; &lt;/ul&gt; &lt;h2 id=&quot;feedback&quot;&gt;Feedback&lt;/h2&gt; &lt;p&gt;Thoughts? Let me know in &lt;a href=&quot;https://gitlab.com/nfriend/website-3.0/-/issues/7&quot;&gt;this GitLab issue&lt;/a&gt;!&lt;/p&gt;</content><author><name></name></author><summary type="html">Juggling multiple Git identities can be tricky.</summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://nathanfriend.io/assets/img/herding-gits/git-mosaic.png"/><media:content medium="image" url="https://nathanfriend.io/assets/img/herding-gits/git-mosaic.png" xmlns:media="http://search.yahoo.com/mrss/"/></entry><entry><title type="html">It’s finally here! 🎉</title><link href="https://nathanfriend.io/2021/05/07/its-finally-here.html" rel="alternate" type="text/html" title="It’s finally here! 🎉"/><published>2021-05-07T00:01:01-04:00</published><updated>2021-05-07T00:01:01-04:00</updated><id>https://nathanfriend.io/2021/05/07/its-finally-here</id><content type="html" xml:base="https://nathanfriend.io/2021/05/07/its-finally-here.html">&lt;p&gt;It’s been almost seven years since &lt;a href=&quot;https://nathanfriend.io/inspiral-web/&quot;&gt;Inspiral Web&lt;/a&gt; was released.&lt;/p&gt; &lt;p&gt;For most of those seven years, the site included a “Get the app” button that popped open a message that read “Inspiral Web isn’t mobile friendly… yet” and included a link to join the mailing list.&lt;/p&gt; &lt;p&gt;Over 25,000 people joined that mailing list. The sheer number of interested people, combined with just a &lt;em&gt;bit&lt;/em&gt; of pandemic cabin fever, spurred me to &lt;em&gt;finally&lt;/em&gt; begin work in earnest on the mobile app.&lt;/p&gt; &lt;p&gt;After five months of feverish, bleary-eyed, nights-and-weekends coding, I’m thrilled to announce that Inspiral is now available for iOS and Android!&lt;/p&gt; &lt;figure&gt; &lt;img src=&quot;/assets/img/its-finally-here/inspiral.jpg&quot; alt=&quot;A screenshot of the Inspiral app&quot; /&gt; &lt;/figure&gt; &lt;p&gt;Give it a try on your platform of choice:&lt;/p&gt; &lt;p style=&quot;display: flex;&quot;&gt; &lt;a style=&quot;flex: 1 1 0; padding-right: 0.5rem; min-width: 0&quot; href=&quot;https://play.google.com/store/apps/details?id=io.nathanfriend.inspiral&quot;&gt; &lt;img style=&quot;width: 100%&quot; src=&quot;/assets/img/its-finally-here/google-play-badge.svg&quot; alt=&quot;Get it on Google Play&quot; /&gt; &lt;/a&gt; &lt;a style=&quot;flex: 1 1 0; padding-left: 0.5rem; min-width: 0&quot; href=&quot;https://apps.apple.com/us/app/inspiral-gear-art/id1558340425&quot;&gt; &lt;img style=&quot;width: 100%&quot; src=&quot;/assets/img/its-finally-here/app-store-badge.svg&quot; alt=&quot;Download on the App Store&quot; /&gt; &lt;/a&gt; &lt;/p&gt; &lt;p&gt;Or, &lt;a href=&quot;https://inspiral.nathanfriend.io/&quot;&gt;check out the app’s website,&lt;/a&gt; which includes links to the app’s social media presence.&lt;/p&gt; &lt;p&gt;The app’s &lt;a href=&quot;https://gitlab.com/nfriend/inspiral&quot;&gt;source code&lt;/a&gt; is public. If you find issues or have a feature request, please &lt;a href=&quot;https://gitlab.com/nfriend/inspiral/-/issues&quot;&gt;open an issue&lt;/a&gt;!&lt;/p&gt;</content><author><name></name></author><summary type="html">It’s been almost seven years since Inspiral Web was released.</summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://nathanfriend.io/assets/img/its-finally-here/inspiral.jpg"/><media:content medium="image" url="https://nathanfriend.io/assets/img/its-finally-here/inspiral.jpg" xmlns:media="http://search.yahoo.com/mrss/"/></entry><entry><title type="html">Capturing Alexa Errors with Sentry and GitLab</title><link href="https://nathanfriend.io/2020/11/18/capturing-alexa-errors-with-sentry-and-gitlab.html" rel="alternate" type="text/html" title="Capturing Alexa Errors with Sentry and GitLab"/><published>2020-11-18T09:22:10-05:00</published><updated>2020-11-18T09:22:10-05:00</updated><id>https://nathanfriend.io/2020/11/18/capturing-alexa-errors-with-sentry-and-gitlab</id><content type="html" xml:base="https://nathanfriend.io/2020/11/18/capturing-alexa-errors-with-sentry-and-gitlab.html">&lt;p&gt;Diagnosing issues with a live Alexa skill can be tricky.&lt;/p&gt; &lt;figure&gt; &lt;img src=&quot;/assets/img/capturing-alexa-errors-with-sentry-and-gitlab/uh-oh.jpg?v=2&quot; alt=&quot;The Amazon Echo logo with a speech bubble saying &amp;quot;Uh oh&amp;quot;&quot; /&gt; &lt;/figure&gt; &lt;p&gt;Most users who run into issues will simply uninstall your skill. A few unusually inspired users may even leave helpful reviews like this:&lt;/p&gt; &lt;figure&gt; &lt;img class=&quot;bordered&quot; src=&quot;/assets/img/capturing-alexa-errors-with-sentry-and-gitlab/this-skill-is-broken.png&quot; alt=&quot;An Amazon review that says &amp;qout;This skill is broken&amp;qout;&quot; /&gt; &lt;/figure&gt; &lt;p&gt;How do you go about figuring out what’s wrong?&lt;/p&gt; &lt;p&gt;By plugging a few open source tools together, you can get great visibility into what’s going wrong.&lt;/p&gt; &lt;h2 id=&quot;1-implement-an-errorhandler-in-your-skill-code&quot;&gt;1. Implement an &lt;code class=&quot;highlighter-rouge&quot;&gt;ErrorHandler&lt;/code&gt; in your skill code&lt;/h2&gt; &lt;p&gt;First, you’ll need a way to globally catch errors in your Alexa skill. The ASK SDK provides an &lt;code class=&quot;highlighter-rouge&quot;&gt;ErrorHandler&lt;/code&gt; interface that does just this. Create a new file for your &lt;code class=&quot;highlighter-rouge&quot;&gt;ErrorHandler&lt;/code&gt; implementation:&lt;/p&gt; &lt;div class=&quot;language-ts highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// lambda/src/handlers/ErrorHandler.ts&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Alexa&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;ask-sdk-core&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ErrorHandler&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;implements&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Alexa&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ErrorHandler&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;canHandle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Handle _all_ exceptions&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;handle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;handlerInput&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Alexa&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;HandlerInput&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;`~~~~ Error handled: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;stack&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;speech&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Sorry, something went wrong! Can you please try again?&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;handlerInput&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;responseBuilder&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;speak&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;speech&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;reprompt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;speech&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getResponse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;(I’m using &lt;a href=&quot;https://www.typescriptlang.org/&quot;&gt;TypeScript&lt;/a&gt; in this example, but a vanilla JS implementation shouldn’t be much different.)&lt;/p&gt; &lt;p&gt;Next, register this error handler in your skill’s entrypoint:&lt;/p&gt; &lt;div class=&quot;language-ts highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// lambda/src/index.ts&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Alexa&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;ask-sdk-core&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ErrorHandler&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;./handlers/ErrorHandler&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;handler&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Alexa&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;SkillBuilders&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;custom&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;addRequestHandlers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;cm&quot;&gt;/* ...your request handlers here... */&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;addErrorHandlers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ErrorHandler&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// ← add this&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;lambda&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt; &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;This already gets you pretty close! If anything goes wrong, you’ll have a nice stack trace in your &lt;a href=&quot;https://aws.amazon.com/cloudwatch/&quot;&gt;CloudWatch&lt;/a&gt; logs, and the user will get a polite message informing them something went wrong.&lt;/p&gt; &lt;h2 id=&quot;2-ship-error-details-to-sentry&quot;&gt;2. Ship error details to Sentry&lt;/h2&gt; &lt;p&gt;&lt;a href=&quot;https://sentry.io/welcome/&quot;&gt;Sentry&lt;/a&gt; is an open source monitoring platform that does a great job of tracking and organizing software errors.&lt;/p&gt; &lt;p&gt;After creating a free account at &lt;a href=&quot;https://sentry.io/&quot;&gt;https://sentry.io&lt;/a&gt;, create a new Sentry project with the “Node.js” platform option. Install the dependencies it recommends:&lt;/p&gt; &lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;yarn add @sentry/node @sentry/tracing &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;Next, set up &lt;code class=&quot;highlighter-rouge&quot;&gt;@sentry/node&lt;/code&gt; with the tracking info it needs. You can do this at the beginning of every Alexa request by creating a new &lt;a href=&quot;https://developer.amazon.com/blogs/alexa/post/0e2015e1-8be3-4513-94cb-da000c2c9db0/what-s-new-with-request-and-response-interceptors-in-the-alexa-skills-kit-sdk-for-node-js&quot;&gt;request interceptor&lt;/a&gt;:&lt;/p&gt; &lt;div class=&quot;language-ts highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// lambda/src/interceptors/SentryInterceptor.ts&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Alexa&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;ask-sdk-core&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Sentry&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;@sentry/node&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;SentryInterceptor&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;implements&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Alexa&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;RequestInterceptor&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;process&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Sentry&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;init&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;dsn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&amp;lt;your DSN here&amp;gt;&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;tracesSampleRate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;1.0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;(Your &lt;a href=&quot;https://docs.sentry.io/product/sentry-basics/dsn-explainer/&quot;&gt;Sentry DSN&lt;/a&gt; will be provided to you when setting up your Sentry project.)&lt;/p&gt; &lt;p&gt;Don’t forget to register this interceptor, similar to how you registered your &lt;code class=&quot;highlighter-rouge&quot;&gt;ErrorHandler&lt;/code&gt; above:&lt;/p&gt; &lt;div class=&quot;language-ts highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// lambda/src/index.ts&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Alexa&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;ask-sdk-core&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ErrorHandler&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;./handlers/ErrorHandler&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;handler&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Alexa&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;SkillBuilders&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;custom&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;addRequestHandlers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;cm&quot;&gt;/* ...your request handlers here... */&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;addRequestInterceptors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;SentryInterceptor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// ← add this&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;addErrorHandlers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ErrorHandler&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;lambda&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt; &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;Finally, in the error handler you created earlier, send the error to Sentry:&lt;/p&gt; &lt;div class=&quot;language-ts highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// lambda/src/handlers/ErrorHandler.ts&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Alexa&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;ask-sdk-core&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Sentry&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;@sentry/node&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// ← add this&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ErrorHandler&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;implements&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Alexa&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ErrorHandler&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;canHandle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Handle _all_ exceptions&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;handle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;handlerInput&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Alexa&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;HandlerInput&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;`~~~~ Error handled: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;stack&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Sentry&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;captureException&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// ← also add this&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;speech&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Sorry, something went wrong! Can you please try again?&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;handlerInput&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;responseBuilder&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;speak&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;speech&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;reprompt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;speech&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getResponse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;That’s it! Now all that’s left is to…&lt;/p&gt; &lt;h2 id=&quot;3-test-it&quot;&gt;3. Test it!&lt;/h2&gt; &lt;p&gt;In your &lt;code class=&quot;highlighter-rouge&quot;&gt;LaunchRequest&lt;/code&gt; handler, do something silly like this:&lt;/p&gt; &lt;div class=&quot;language-ts highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;anObject&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;any&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{};&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;anObject&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;aMethodThatDoesntExist&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt; &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;(I’m explicitly specifying &lt;code class=&quot;highlighter-rouge&quot;&gt;any&lt;/code&gt; here, otherwise TypeScript won’t let me get away with this!)&lt;/p&gt; &lt;p&gt;Deploy your skill and give it a spin!&lt;/p&gt; &lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;❯ ask dialog User &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; Open &amp;lt;your skill name here&amp;gt; Alexa &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; Sorry, something went wrong! Can you please try again? &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;Jump back into your Sentry project - you should now be the proud owner of a new issue:&lt;/p&gt; &lt;figure&gt; &lt;img class=&quot;bordered&quot; src=&quot;/assets/img/capturing-alexa-errors-with-sentry-and-gitlab/sentry-screenshot.png&quot; alt=&quot;A screenshot of the Sentry dashboard with a new issue&quot; /&gt; &lt;/figure&gt; &lt;p&gt;&lt;br /&gt;&lt;/p&gt; &lt;h2 id=&quot;4-integrate-sentry-with-gitlab-optional&quot;&gt;4. Integrate Sentry with GitLab &lt;em&gt;(optional)&lt;/em&gt;&lt;/h2&gt; &lt;p&gt;If you host you skill’s code on &lt;a href=&quot;https://about.gitlab.com/&quot;&gt;GitLab&lt;/a&gt; you can take advantage of GitLab’s &lt;a href=&quot;https://docs.gitlab.com/ee/operations/error_tracking.html&quot;&gt;first-class Sentry integration&lt;/a&gt; to see error details directly in your GitLab project:&lt;/p&gt; &lt;ol&gt; &lt;li&gt;From your GitLab project, navigate to &lt;strong&gt;Settings &amp;gt; Operations&lt;/strong&gt;&lt;/li&gt; &lt;li&gt;Expand the &lt;strong&gt;Error tracking&lt;/strong&gt; section&lt;/li&gt; &lt;li&gt;Check the &lt;strong&gt;Active&lt;/strong&gt; checkbox&lt;/li&gt; &lt;li&gt;Assuming you are using Sentry’s hosted solution, enter &lt;code class=&quot;highlighter-rouge&quot;&gt;https://sentry.io/&lt;/code&gt; in the &lt;strong&gt;Sentry API URL&lt;/strong&gt; field&lt;/li&gt; &lt;li&gt;Paste your Sentry auth token into the &lt;strong&gt;Auth Token&lt;/strong&gt; field. To generate an auth token in Sentry: &lt;ol&gt; &lt;li&gt;Navigate to your Sentry dashboard&lt;/li&gt; &lt;li&gt;Click the ▼ next to your name and select &lt;strong&gt;API keys&lt;/strong&gt;&lt;/li&gt; &lt;li&gt;Click &lt;strong&gt;Create New Token&lt;/strong&gt;&lt;/li&gt; &lt;li&gt;Leave the default scopes as they are and click &lt;strong&gt;Create Token&lt;/strong&gt;&lt;/li&gt; &lt;li&gt;Copy the big string of gibberish&lt;/li&gt; &lt;/ol&gt; &lt;/li&gt; &lt;li&gt;Jump back to GitLab and click &lt;strong&gt;Connect&lt;/strong&gt; and select your Sentry project&lt;/li&gt; &lt;li&gt;Click &lt;strong&gt;Save changes&lt;/strong&gt;&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;That’s it! Navigate to &lt;strong&gt;Operations &amp;gt; Error Tracking&lt;/strong&gt;. You should see the same set of Sentry errors nicely displayed inside your GitLab project.&lt;/p&gt; &lt;figure&gt; &lt;img class=&quot;bordered&quot; src=&quot;/assets/img/capturing-alexa-errors-with-sentry-and-gitlab/gitlab-error-tracking-screenshot.png?v=2&quot; alt=&quot;A screenshot of GitLab's Error Tracking page showing an issue's details&quot; /&gt; &lt;/figure&gt; &lt;p&gt;&lt;br /&gt;&lt;/p&gt; &lt;h2 id=&quot;source&quot;&gt;Source&lt;/h2&gt; &lt;p&gt;See this code in action at &lt;a href=&quot;https://gitlab.com/nfriend/days-until&quot;&gt;https://gitlab.com/nfriend/days-until&lt;/a&gt;, or give my &lt;a href=&quot;https://www.amazon.com/dp/B0759KJ8D2&quot;&gt;Days Until&lt;/a&gt; skill a try for yourself. &lt;a href=&quot;https://alexa-skills.amazon.com/apis/custom/skills/amzn1.ask.skill.a904b878-9342-4031-9e86-a81ac29bddca/launch&quot;&gt;Here’s a quick link to the skill!&lt;/a&gt;&lt;/p&gt;</content><author><name></name></author><summary type="html">Diagnosing issues with a live Alexa skill can be tricky.</summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://nathanfriend.io/assets/img/capturing-alexa-errors-with-sentry-and-gitlab/uh-oh.jpg?v=2"/><media:content medium="image" url="https://nathanfriend.io/assets/img/capturing-alexa-errors-with-sentry-and-gitlab/uh-oh.jpg?v=2" xmlns:media="http://search.yahoo.com/mrss/"/></entry><entry><title type="html">Ridiculous Refs</title><link href="https://nathanfriend.io/2019/10/19/ridiculous-refs.html" rel="alternate" type="text/html" title="Ridiculous Refs"/><published>2019-10-19T18:27:01-04:00</published><updated>2019-10-19T18:27:01-04:00</updated><id>https://nathanfriend.io/2019/10/19/ridiculous-refs</id><content type="html" xml:base="https://nathanfriend.io/2019/10/19/ridiculous-refs.html">&lt;p&gt;How many different special characters can you jam into a &lt;a href=&quot;https://git-scm.com/book/en/v2/Git-Internals-Git-References&quot;&gt;Git ref&lt;/a&gt; name (i.e. the full name of a &lt;a href=&quot;https://git-scm.com/book/en/v2/Git-Basics-Tagging&quot;&gt;tag&lt;/a&gt; or a &lt;a href=&quot;https://git-scm.com/book/en/v2/Git-Branching-Basic-Branching-and-Merging&quot;&gt;branch&lt;/a&gt;) before Git will complain?&lt;/p&gt; &lt;figure&gt; &lt;img src=&quot;/assets/img/ridiculous-refs/mind-blown.png&quot; alt=&quot;The git logo with a exploding head inside&quot; /&gt; &lt;figcaption&gt;As if Git isn't complicated enough &lt;em&gt;without&lt;/em&gt; testing its limits.&lt;/figcaption&gt; &lt;/figure&gt; &lt;p&gt;The answer is… a &lt;em&gt;lot&lt;/em&gt;.&lt;/p&gt; &lt;p&gt;Here’s a list of ref names that Git considers valid, ranging from pedestrian to ludicrous:&lt;/p&gt; &lt;ol&gt; &lt;li&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;a/b&lt;/code&gt;&lt;/li&gt; &lt;li&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;A/B&lt;/code&gt;&lt;/li&gt; &lt;li&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;a/b/c/d&lt;/code&gt;&lt;/li&gt; &lt;li&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;a/b.c&lt;/code&gt;&lt;/li&gt; &lt;li&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;0/1/2&lt;/code&gt;&lt;/li&gt; &lt;li&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;!/&quot;#%&amp;amp;'{}+,-.&amp;lt;&amp;gt;;=@]_`{|}&lt;/code&gt;&lt;/li&gt; &lt;li&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;🙂🚀😂🇺🇸💩🇨🇦💯👍❤️/🤦&lt;/code&gt;&lt;/li&gt; &lt;li&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;(╯°□°)/╯︵┻━┻&lt;/code&gt;&lt;/li&gt; &lt;li&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;¯|_(ツ)_/¯&lt;/code&gt;&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;The opposite is a little less flashy, but equally useful. All of these are &lt;em&gt;invalid&lt;/em&gt; ref names:&lt;/p&gt; &lt;ol&gt; &lt;li&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;a&lt;/code&gt;: At least one &lt;code class=&quot;highlighter-rouge&quot;&gt;/&lt;/code&gt; is required&lt;/li&gt; &lt;li&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;a/.b&lt;/code&gt;: Slash-separated components can’t begin with a &lt;code class=&quot;highlighter-rouge&quot;&gt;.&lt;/code&gt;&lt;/li&gt; &lt;li&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;a/b.&lt;/code&gt;: No ending with a &lt;code class=&quot;highlighter-rouge&quot;&gt;.&lt;/code&gt;&lt;/li&gt; &lt;li&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;a/.lock&lt;/code&gt;: No ending with &lt;code class=&quot;highlighter-rouge&quot;&gt;.lock&lt;/code&gt;&lt;/li&gt; &lt;li&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;a/b..c&lt;/code&gt;: Consecutive dots (&lt;code class=&quot;highlighter-rouge&quot;&gt;..&lt;/code&gt;) are not allowed&lt;/li&gt; &lt;li&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;a/~^:?*[\&lt;/code&gt;: None of the characters after &lt;code class=&quot;highlighter-rouge&quot;&gt;a/&lt;/code&gt; are allowed&lt;/li&gt; &lt;li&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;a/b/&lt;/code&gt;: No ending with a slash&lt;/li&gt; &lt;li&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;/a/b&lt;/code&gt;: No beginning with a slash&lt;/li&gt; &lt;li&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;a/b//c&lt;/code&gt;: No consecutive slashes&lt;/li&gt; &lt;li&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;a/@{&lt;/code&gt;: The &lt;code class=&quot;highlighter-rouge&quot;&gt;@{&lt;/code&gt; sequence is not allowed&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;There’s some nuance to this second list - some of these rules can be relaxed in special situations. For the complete specification, check out &lt;a href=&quot;https://git-scm.com/docs/git-check-ref-format&quot;&gt;the documentation for the &lt;code class=&quot;highlighter-rouge&quot;&gt;check-ref-format&lt;/code&gt; command&lt;/a&gt;.&lt;/p&gt; &lt;h2 id=&quot;wait-ive-created-lots-of-branches-and-tags-that-dont-contain-a-&quot;&gt;Wait, I’ve created lots of branches and tags that don’t contain a &lt;code class=&quot;highlighter-rouge&quot;&gt;/&lt;/code&gt;!&lt;/h2&gt; &lt;p&gt;Right! When you create a branch named &lt;code class=&quot;highlighter-rouge&quot;&gt;my-feature&lt;/code&gt;, Git actually creates a ref named &lt;code class=&quot;highlighter-rouge&quot;&gt;refs/heads/my-feature&lt;/code&gt;.&lt;/p&gt; &lt;p&gt;This is a bit of a tangent, but what Git is &lt;em&gt;actually&lt;/em&gt; doing under the hood is creating a new file named &lt;code class=&quot;highlighter-rouge&quot;&gt;my-feature&lt;/code&gt; inside your repo’s &lt;code class=&quot;highlighter-rouge&quot;&gt;.git&lt;/code&gt; directory at &lt;code class=&quot;highlighter-rouge&quot;&gt;.git/refs/heads/my-feature&lt;/code&gt;. You can see this for yourself by opening up &lt;code class=&quot;highlighter-rouge&quot;&gt;.git/refs/heads&lt;/code&gt; in a file explorer. Understanding this makes it more obvious why the &lt;code class=&quot;highlighter-rouge&quot;&gt;/&lt;/code&gt; character is required.&lt;/p&gt; &lt;h2 id=&quot;why-should-i-care-about-this&quot;&gt;Why should I care about this?&lt;/h2&gt; &lt;p&gt;If all you do with Git is &lt;code class=&quot;highlighter-rouge&quot;&gt;pull&lt;/code&gt;, &lt;code class=&quot;highlighter-rouge&quot;&gt;commit&lt;/code&gt;, &lt;code class=&quot;highlighter-rouge&quot;&gt;push&lt;/code&gt;, and maybe the occasional &lt;code class=&quot;highlighter-rouge&quot;&gt;rebase&lt;/code&gt;, you can ignore these edge cases. Just keep using nice, simple, boring names like &lt;code class=&quot;highlighter-rouge&quot;&gt;my-feature-branch&lt;/code&gt; or &lt;code class=&quot;highlighter-rouge&quot;&gt;v1.2&lt;/code&gt;.&lt;/p&gt; &lt;p&gt;However, if you’re building a tool that &lt;em&gt;interacts&lt;/em&gt; with Git refs, you might want to throw a few of the crazier names listed above into your test cases.&lt;/p&gt; &lt;p&gt;In fact, this was my motivation for compiling these lists. I built &lt;a href=&quot;https://gitlab.com/gitlab-org/gitlab/merge_requests/18761&quot;&gt;a new GitLab Issue search feature that involved Git tag names&lt;/a&gt;, and I needed to make sure it handled any tag name the user threw at it. I was hoping to find something like the &lt;a href=&quot;https://github.com/minimaxir/big-list-of-naughty-strings&quot;&gt;Big List of Naughty Strings&lt;/a&gt; for Git refs, but I couldn’t find anything beyond Git’s technical documentation.&lt;/p&gt; &lt;h2 id=&quot;your-list-is-wrong&quot;&gt;Your list is wrong!&lt;/h2&gt; &lt;p&gt;I wouldn’t be surprised! &lt;a href=&quot;https://gitlab.com/nfriend/website-3.0/issues/new?issue[title]=There%27s%20a%20problem%20with%20your%20Ridiculous%20Refs%20list!&quot;&gt;Please let me know by opening an issue!&lt;/a&gt;&lt;/p&gt; &lt;h2 id=&quot;referencesattributions&quot;&gt;References/Attributions&lt;/h2&gt; &lt;p&gt;The “exploding head” icon was created by Anniken &amp;amp; Andreas from the Noun Project.&lt;/p&gt;</content><author><name></name></author><summary type="html">How many different special characters can you jam into a Git ref name (i.e. the full name of a tag or a branch) before Git will complain?</summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://nathanfriend.io/assets/img/ridiculous-refs/mind-blown.png"/><media:content medium="image" url="https://nathanfriend.io/assets/img/ridiculous-refs/mind-blown.png" xmlns:media="http://search.yahoo.com/mrss/"/></entry><entry><title type="html">PDF Gotchas with Headless Chrome</title><link href="https://nathanfriend.io/2019/04/15/pdf-gotchas-with-headless-chrome.html" rel="alternate" type="text/html" title="PDF Gotchas with Headless Chrome"/><published>2019-04-15T05:20:55-04:00</published><updated>2019-04-15T05:20:55-04:00</updated><id>https://nathanfriend.io/2019/04/15/pdf-gotchas-with-headless-chrome</id><content type="html" xml:base="https://nathanfriend.io/2019/04/15/pdf-gotchas-with-headless-chrome.html">&lt;p&gt;Generating PDF reports is one of those features that &lt;em&gt;every&lt;/em&gt; enterprise developer will implement at some point in their career. I had my turn on a project with my previous employer. After exploring the available options, I settled on using &lt;a href=&quot;https://developers.google.com/web/updates/2017/04/headless-chrome&quot;&gt;Chrome’s headless mode&lt;/a&gt; to render HTML and save the result as a PDF.&lt;/p&gt; &lt;figure&gt; &lt;img src=&quot;/assets/img/pdf-gotchas-with-headless-chrome/minifigure-chrome.jpg&quot; alt=&quot;A headless minifigure and a Chrome logo&quot; /&gt; &lt;figcaption&gt;&quot;Headless Chrome&quot; just sounds so.... gruesome.&lt;/figcaption&gt; &lt;/figure&gt; &lt;p&gt;This approach seems kind of weird and a bit overkill at first, but it has a number of pretty huge advantages:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;You can build your PDFs using the most popular layout system in the world (HTML/CSS)&lt;/li&gt; &lt;li&gt;Anything that can be rendered in a webpage can be used in a PDF, including: &lt;ul&gt; &lt;li&gt;images&lt;/li&gt; &lt;li&gt;custom fonts&lt;/li&gt; &lt;li&gt;links&lt;/li&gt; &lt;li&gt;3&lt;sup&gt;rd&lt;/sup&gt; party JavaScript libraries (think visualization libraries like &lt;a href=&quot;https://d3js.org/&quot;&gt;D3.js&lt;/a&gt; or graphing libraries like &lt;a href=&quot;https://developers.google.com/chart/&quot;&gt;Google Charts&lt;/a&gt;)&lt;/li&gt; &lt;li&gt;&lt;em&gt;etc…&lt;/em&gt;&lt;/li&gt; &lt;/ul&gt; &lt;/li&gt; &lt;li&gt;If you’re building a web application, you can reuse components from your application in your PDF&lt;/li&gt; &lt;li&gt;Printing to a PDF is a &lt;a href=&quot;https://developers.google.com/web/updates/2017/04/headless-chrome#create_a_pdf_dom&quot;&gt;supported use case&lt;/a&gt; of Chrome’s headless mode&lt;/li&gt; &lt;li&gt;Google’s own &lt;a href=&quot;https://pptr.dev/&quot;&gt;Puppeteer&lt;/a&gt; library gives you full control over the headless instance of Chrome&lt;/li&gt; &lt;li&gt;You can develop your PDF layout in Chrome - with full access to Chrome’s dev tools - instead of continually regenerating the PDF to see changes&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;It’s not all unicorns and rainbows, though. Below are a few of the gotchas I discovered while building a real PDF using headless Chrome.&lt;/p&gt; &lt;h2 id=&quot;headers-and-footers-cant-use-external-resources&quot;&gt;Headers and footers can’t use external resources&lt;/h2&gt; &lt;p&gt;This is the big one. If you try and place an &lt;code class=&quot;highlighter-rouge&quot;&gt;&amp;lt;img&amp;gt;&lt;/code&gt; tag in your header or footer (a pretty common use case for a header or footer):&lt;/p&gt; &lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;img&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/assets/logo.jpg&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt; &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;…your image won’t show up. This is because Chrome won’t make any requests for external resources that appear in the header or footer templates.&lt;/p&gt; &lt;p&gt;One workaround is to encode the image into the template as a base64’d string:&lt;/p&gt; &lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;img&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;data:image/png;base64, iVBORw0KGg...&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt; &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;I’ve found &lt;a href=&quot;https://www.base64-image.de/&quot;&gt;this site&lt;/a&gt; handy for converting an image into an &lt;code class=&quot;highlighter-rouge&quot;&gt;&amp;lt;img&amp;gt;&lt;/code&gt;-compatible base64 string.&lt;/p&gt; &lt;h2 id=&quot;headers-and-footers-dont-inherit-styles-from-the-rest-of-the-page&quot;&gt;Headers and footers don’t inherit styles from the rest of the page&lt;/h2&gt; &lt;p&gt;Headers and footers are specified at PDF render time by passing HTML strings to the &lt;code class=&quot;highlighter-rouge&quot;&gt;page.pdf()&lt;/code&gt; method:&lt;/p&gt; &lt;div class=&quot;language-ts highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;pdf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;headerTemplate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&amp;lt;h1&amp;gt;This is the header!&amp;lt;/h1&amp;gt;&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;footerTemplate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&amp;lt;h1&amp;gt;This is the footer!&amp;lt;/h1&amp;gt;&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt; &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;These templates are rendered in a separate context than the content of the webpage. Because of this, the CSS styles that apply to the content won’t apply to the header and the footer. Any styles that apply to the content of your PDF that you would like to also apply to your header and footer must be repeated in each of your header and footer templates. And unfortunately, you can’t just reference a common stylesheet using a &lt;code class=&quot;highlighter-rouge&quot;&gt;&amp;lt;link&amp;gt;&lt;/code&gt; element - see point #1 above.&lt;/p&gt; &lt;h2 id=&quot;headers-and-footers-require-explicit-margins-to-be-visible&quot;&gt;Headers and footers require explicit margins to be visible&lt;/h2&gt; &lt;p&gt;This one took me a while to figure out. Chrome won’t automatically resize your content to make space for the header and footer templates. You’ll need to make space for your header and footer by specifying a fixed margin at the top and bottom of your page:&lt;/p&gt; &lt;div class=&quot;language-ts highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;pdf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;headerTemplate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&amp;lt;h1&amp;gt;This is the header!&amp;lt;/h1&amp;gt;&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;footerTemplate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&amp;lt;h1&amp;gt;This is the footer!&amp;lt;/h1&amp;gt;&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;margin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;top&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;100px&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;bottom&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;50px&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt; &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;Without these margins, the content will be rendered on top of your header and footer, leaving you wondering why your header and footer templates aren’t showing up.&lt;/p&gt; &lt;h2 id=&quot;page-breaks-can-be-a-pain&quot;&gt;Page breaks can be a pain&lt;/h2&gt; &lt;p&gt;CSS provides some rules that determine where a page break should be placed when printing, for example:&lt;/p&gt; &lt;div class=&quot;language-css highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;@media&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;print&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;.page&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nl&quot;&gt;page-break-after&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;always&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;These rules work - but they can be finicky. You may run into problem when trying to page break inside of&lt;sup&gt;[1]&lt;/sup&gt;:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;tables&lt;/li&gt; &lt;li&gt;floating elements&lt;/li&gt; &lt;li&gt;inline-block elements&lt;/li&gt; &lt;li&gt;block elements with borders&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;I also had issues using &lt;code class=&quot;highlighter-rouge&quot;&gt;page-break-after&lt;/code&gt; inside of a flexbox layout.&lt;/p&gt; &lt;h2 id=&quot;some-advanced-layouts-simply-arent-possible&quot;&gt;Some advanced layouts simply aren’t possible&lt;/h2&gt; &lt;p&gt;There are a few edge cases - mostly dealing with headers/footers and page wrapping - that you simply can’t control. For example, want to place a special footer only on pages 2, 4, and 7? Not possible. (If it is, &lt;a href=&quot;mailto:hello@nathanfriend.io&quot;&gt;let me know how!&lt;/a&gt;)&lt;/p&gt; &lt;h2 id=&quot;the-page-needs-to-finish-loading&quot;&gt;The page needs to finish loading&lt;/h2&gt; &lt;p&gt;If the page being screenshotted requires time to load, (for example, if the page has JavaScript that makes an AJAX request for some data), you’ll need to wait for this initialization to complete before triggering the screenshot. If you simply screenshot the page right after the initial load, your PDF will be filled with loading bars and missing data.&lt;/p&gt; &lt;p&gt;I worked around this by setting a global flag in the webpage once all initialization work is finished:&lt;/p&gt; &lt;div class=&quot;language-ts highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// in the web page&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;init&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;dataService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;user&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;userService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getUserProfile&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// ...etc...&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;window&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;isReadyForPDF&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;Then, using Puppeteer’s &lt;a href=&quot;https://pptr.dev/#?product=Puppeteer&amp;amp;version=v1.10.0&amp;amp;show=api-pagewaitforfunctionpagefunction-options-args&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;page.waitForFunction()&lt;/code&gt;&lt;/a&gt; method, we can wait for this global variable to bet set:&lt;/p&gt; &lt;div class=&quot;language-ts highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// on the server&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;waitForFunction&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;window.isReadyForPDF&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// now we know the page is ready for a screenshot&lt;/span&gt; &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;h2 id=&quot;the-page-might-require-authentication&quot;&gt;The page might require authentication&lt;/h2&gt; &lt;p&gt;If the page you’re screenshotting is part of a web application, it’s likely there’s an authentication step that’s required to view the page. This can be a bit of a pain to work around, but fortunately, Puppeteer provides enough control to programmatically log in to the application:&lt;/p&gt; &lt;div class=&quot;language-ts highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;waitForSelector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;#username&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;waitForSelector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;#password&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;evaluate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;querySelector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;#username&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;my-username&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;querySelector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;#password&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;my-password&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;querySelector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;#log-in-button&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;click&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt; &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;There are some downsides to this approach, though:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;You now need to maintain a system account for logging in to the application for PDF screenshots&lt;/li&gt; &lt;li&gt;The extra login step adds quite a bit of time to the PDF generation process&lt;/li&gt; &lt;li&gt;Your PDF generation is now dependent on your authentication/authorization system&lt;/li&gt; &lt;li&gt;It just feels kind of wrong&lt;/li&gt; &lt;/ul&gt; &lt;hr /&gt; &lt;p&gt;Disclaimer: my PDF generator was written in .NET Core, so I actually used a library called &lt;a href=&quot;https://www.puppeteersharp.com/&quot;&gt;Puppeteer Sharp&lt;/a&gt; which aims to replicate the API of the official &lt;a href=&quot;https://pptr.dev/&quot;&gt;Puppeteer library&lt;/a&gt; (which runs on Node). Some of the code examples above might be slightly off since I translated them from C♯ into JavaScript.&lt;/p&gt; &lt;hr /&gt; &lt;p&gt;&lt;strong&gt;References/Attributions&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;[1]: &lt;a href=&quot;https://stackoverflow.com/a/26265549/1063392&quot;&gt;https://stackoverflow.com/a/26265549/1063392&lt;/a&gt;&lt;/p&gt; &lt;p&gt;Minifigure/Chrome image from &lt;a href=&quot;https://hackernoon.com/so-many-testing-frameworks-so-little-time-b03c707b8f90&quot;&gt;https://hackernoon.com/so-many-testing-frameworks-so-little-time-b03c707b8f90&lt;/a&gt;&lt;/p&gt;</content><author><name></name></author><summary type="html">Generating PDF reports is one of those features that every enterprise developer will implement at some point in their career. I had my turn on a project with my previous employer. After exploring the available options, I settled on using Chrome’s headless mode to render HTML and save the result as a PDF.</summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://nathanfriend.io/assets/img/pdf-gotchas-with-headless-chrome/minifigure-chrome.jpg"/><media:content medium="image" url="https://nathanfriend.io/assets/img/pdf-gotchas-with-headless-chrome/minifigure-chrome.jpg" xmlns:media="http://search.yahoo.com/mrss/"/></entry><entry><title type="html">The Next Chapter</title><link href="https://nathanfriend.io/2018/12/04/the-next-chapter.html" rel="alternate" type="text/html" title="The Next Chapter"/><published>2018-12-04T15:30:12-05:00</published><updated>2018-12-04T15:30:12-05:00</updated><id>https://nathanfriend.io/2018/12/04/the-next-chapter</id><content type="html" xml:base="https://nathanfriend.io/2018/12/04/the-next-chapter.html">&lt;p&gt;I’m incredibly excited to announce that I will be joining the &lt;a href=&quot;https://about.gitlab.com/&quot;&gt;GitLab&lt;/a&gt; team in just a few days!&lt;/p&gt; &lt;figure&gt; &lt;img src=&quot;/assets/img/the-next-chapter/gitlab-logo.jpg&quot; alt=&quot;The GitLab logo&quot; /&gt; &lt;/figure&gt; &lt;p&gt;I’m a big fan of GitLab - both the product and the company. I will be working as a Senior Frontend Engineer on the Release (Continuous Delivery) team.&lt;/p&gt; &lt;p&gt;If you’re not familiar with GitLab, &lt;a href=&quot;https://about.gitlab.com/company/&quot;&gt;here’s some info about the company&lt;/a&gt;.&lt;/p&gt;</content><author><name></name></author><summary type="html">I’m incredibly excited to announce that I will be joining the GitLab team in just a few days!</summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://nathanfriend.io/assets/img/the-next-chapter/gitlab-logo.jpg"/><media:content medium="image" url="https://nathanfriend.io/assets/img/the-next-chapter/gitlab-logo.jpg" xmlns:media="http://search.yahoo.com/mrss/"/></entry><entry><title type="html">ES6 Object Literal Shorthand Is Fun And Kind Of Scary</title><link href="https://nathanfriend.io/2018/11/16/es6-object-literal-shorthand-is-fun-and-kind-of-scary.html" rel="alternate" type="text/html" title="ES6 Object Literal Shorthand Is Fun And Kind Of Scary"/><published>2018-11-16T14:53:36-05:00</published><updated>2018-11-16T14:53:36-05:00</updated><id>https://nathanfriend.io/2018/11/16/es6-object-literal-shorthand-is-fun-and-kind-of-scary</id><content type="html" xml:base="https://nathanfriend.io/2018/11/16/es6-object-literal-shorthand-is-fun-and-kind-of-scary.html">&lt;figure&gt; &lt;img src=&quot;/assets/img/es6-object-literal-shorthand-is-fun-and-kind-of-scary/object-literal.jpg&quot; alt=&quot;An ES6 object literal with two emojis in it&quot; /&gt; &lt;/figure&gt; &lt;p&gt;&lt;br /&gt;One of the small but important features of ES6 is a shorthand syntax for creating object literals:&lt;/p&gt; &lt;div class=&quot;language-ts highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;person&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;age&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;hairColor&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt; &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;I can only assume this syntax sugar was added to the language because the ECMAScript gods were tired of looking at ES5 code like this:&lt;/p&gt; &lt;div class=&quot;language-ts highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;person&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;age&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;age&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;hairColor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;hairColor&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt; &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;This seems like an obvious win - why would you want to write six lines of code when you could just write one?&lt;/p&gt; &lt;h2 id=&quot;the-kind-of-scary-part&quot;&gt;The kind of scary part&lt;/h2&gt; &lt;p&gt;There’s one downside to this syntax that’s not immediately obvious. Let’s say you have some code like this:&lt;/p&gt; &lt;div class=&quot;language-ts highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;getPerson&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;height&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;182&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// ... 100 more lines of code ...&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;age&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;hairColor&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;After working with this code a bit, you realize that it’s not super clear what unit the &lt;code class=&quot;highlighter-rouge&quot;&gt;height&lt;/code&gt; variable uses, so you decide to use your editor’s auto-refactor feature to rename this variable from &lt;code class=&quot;highlighter-rouge&quot;&gt;height&lt;/code&gt; to &lt;code class=&quot;highlighter-rouge&quot;&gt;heightInCm&lt;/code&gt;:&lt;/p&gt; &lt;div class=&quot;language-ts highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;getPerson&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;heightInCm&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;182&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// ... 100 more lines of code ...&lt;/span&gt; &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;But wait! By changing the name of an internal variable, you’ve now changed the &lt;em&gt;public&lt;/em&gt; interface of your function!&lt;/p&gt; &lt;div class=&quot;language-ts highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt; &lt;span class=&quot;c1&quot;&gt;// ... 100 more lines of code ...&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;age&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;heightInCm&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;hairColor&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;If you’re not using a compile-safe language like &lt;a href=&quot;https://www.typescriptlang.org/&quot;&gt;TypeScript&lt;/a&gt;, you might forget to update the consumers of this function. Or even if you &lt;em&gt;are&lt;/em&gt; using TypeScript, you might forget to update an HTML template that references this variable name. This has bitten me a few times in the past.&lt;/p&gt; &lt;p&gt;Like I said, it’s only &lt;em&gt;kind&lt;/em&gt; of scary.&lt;/p&gt;</content><author><name></name></author><summary type="html">One of the small but important features of ES6 is a shorthand syntax for creating object literals:</summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://nathanfriend.io/assets/img/es6-object-literal-shorthand-is-fun-and-kind-of-scary/object-literal.jpg"/><media:content medium="image" url="https://nathanfriend.io/assets/img/es6-object-literal-shorthand-is-fun-and-kind-of-scary/object-literal.jpg" xmlns:media="http://search.yahoo.com/mrss/"/></entry><entry><title type="html">Shell-ing With TypeScript</title><link href="https://nathanfriend.io/2018/06/17/shelling-with-typescript.html" rel="alternate" type="text/html" title="Shell-ing With TypeScript"/><published>2018-06-17T03:59:12-04:00</published><updated>2018-06-17T03:59:12-04:00</updated><id>https://nathanfriend.io/2018/06/17/shelling-with-typescript</id><content type="html" xml:base="https://nathanfriend.io/2018/06/17/shelling-with-typescript.html">&lt;figure&gt; &lt;img src=&quot;/assets/img/shelling-with-typescript/shebang.jpg&quot; alt=&quot;The shebang text for running scripts using ts-node.&quot; /&gt; &lt;figcaption&gt;Here's a TL;DR for my future self when I inevitably forget the correct shebang to use.&lt;/figcaption&gt; &lt;/figure&gt; &lt;p&gt;If you’re like me, most of your time writing &lt;a href=&quot;https://en.wikipedia.org/wiki/Bash_(Unix_shell)&quot;&gt;bash&lt;/a&gt; scripts is spent Googling basic things like “how to loop in bash” while grumbling to yourself how easy this would be in a sane language like &lt;a href=&quot;https://www.typescriptlang.org/&quot;&gt;TypeScript&lt;/a&gt;.&lt;/p&gt; &lt;p&gt;As it turns out, you &lt;em&gt;can&lt;/em&gt; write shell scripts using TypeScript, and it isn’t even that hard! Here’s how:&lt;/p&gt; &lt;h2 id=&quot;1-install-dependencies&quot;&gt;1. Install dependencies&lt;/h2&gt; &lt;p&gt;If you haven’t already, download and install &lt;a href=&quot;https://nodejs.org/&quot;&gt;node&lt;/a&gt;.&lt;/p&gt; &lt;p&gt;After node is installed, install both &lt;a href=&quot;https://www.npmjs.com/package/typescript&quot;&gt;TypeScript&lt;/a&gt; and &lt;a href=&quot;https://www.npmjs.com/package/ts-node&quot;&gt;ts-node&lt;/a&gt; globally using &lt;code class=&quot;highlighter-rouge&quot;&gt;npm&lt;/code&gt;:&lt;/p&gt; &lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;npm &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;typescript ts-node &lt;span class=&quot;nt&quot;&gt;-g&lt;/span&gt; &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;h2 id=&quot;2-write-your-shell-script-in-typescript&quot;&gt;2. Write your shell script in TypeScript&lt;/h2&gt; &lt;p&gt;Create a new &lt;code class=&quot;highlighter-rouge&quot;&gt;.ts&lt;/code&gt; file with a &lt;a href=&quot;https://en.wikipedia.org/wiki/Shebang_(Unix)&quot;&gt;shebang&lt;/a&gt; as its first line that points to &lt;code class=&quot;highlighter-rouge&quot;&gt;ts-node&lt;/code&gt;:&lt;/p&gt; &lt;div class=&quot;language-ts highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;#!/usr/bin/env ts-node &lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;Then, add some TypeScript-y stuff:&lt;/p&gt; &lt;div class=&quot;language-ts highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;#!/usr/bin/env ts-node &lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Hello from TypeScript!&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;h2 id=&quot;3-make-your-script-runnable&quot;&gt;3. Make your script runnable&lt;/h2&gt; &lt;p&gt;After saving your TypeScript file, you’ll need to update its permissions to allow it to be executed:&lt;/p&gt; &lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;chmod &lt;/span&gt;u+x your-shell-script.ts &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;h2 id=&quot;4-run-your-typescript-file&quot;&gt;4. Run your TypeScript file&lt;/h2&gt; &lt;p&gt;You can now run the script as you would any other command-line utility:&lt;/p&gt; &lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; ./your-shell-script.ts &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;…which should result in a friendly message in your terminal:&lt;/p&gt; &lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; ./my-shell-script Hello from TypeScript! &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;h2 id=&quot;5-prove-to-yourself-that-its-working&quot;&gt;5. Prove to yourself that it’s working&lt;/h2&gt; &lt;p&gt;That seemed a bit too easy - shouldn’t there be an intermediate build step in there somewhere? As a sanity check, update your &lt;code class=&quot;highlighter-rouge&quot;&gt;.ts&lt;/code&gt; file with something that &lt;em&gt;shouldn’t&lt;/em&gt; compile:&lt;/p&gt; &lt;div class=&quot;language-ts highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;#!/usr/bin/env ts-node &lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Hello from TypeScript!&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// TypeScript compiler error:&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Type '4' is not assignable to type 'string'.&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;myStr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;Rerunning your script will now result in something like this:&lt;/p&gt; &lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;/Users/nathanfriend/.nvm/versions/node/v7.10.0/lib/node_modules/ts-node/src/index.ts:250 &lt;span class=&quot;k&quot;&gt;return &lt;/span&gt;new TSError&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;diagnosticText, diagnosticCodes&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; ^ TSError: ⨯ Unable to compile TypeScript: test.ts&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;5,5&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;: error TS2322: Type &lt;span class=&quot;s1&quot;&gt;'4'&lt;/span&gt; is not assignable to &lt;span class=&quot;nb&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'string'&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;.&lt;/span&gt; at createTSError &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;/Users/nathanfriend/.nvm/versions/node/v7.10.0/lib/node_modules/ts-node/src/index.ts:250:12&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; at getOutput &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;/Users/nathanfriend/.nvm/versions/node/v7.10.0/lib/node_modules/ts-node/src/index.ts:358:40&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; at Object.compile &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;/Users/nathanfriend/.nvm/versions/node/v7.10.0/lib/node_modules/ts-node/src/index.ts:546:11&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; at Module.m._compile &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;/Users/nathanfriend/.nvm/versions/node/v7.10.0/lib/node_modules/ts-node/src/index.ts:430:43&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; at Module._extensions..js &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;module.js:580:10&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; at Object.require.extensions.&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;anonymous &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;as .ts] &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;/Users/nathanfriend/.nvm/versions/node/v7.10.0/lib/node_modules/ts-node/src/index.ts:433:12&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; at Module.load &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;module.js:488:32&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; at tryModuleLoad &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;module.js:447:12&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; at Function.Module._load &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;module.js:439:3&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; at Function.Module.runMain &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;module.js:605:10&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;This is good! You shouldn’t be able to run a TypeScript file if it contains compile-time errors.&lt;/p&gt; &lt;h3 id=&quot;so-where-is-that-compile-step&quot;&gt;So where is that compile step?&lt;/h3&gt; &lt;p&gt;There isn’t one! Well, not one that you have to explicitly run, anyway. This is the magic of &lt;a href=&quot;https://www.npmjs.com/package/ts-node&quot;&gt;ts-node&lt;/a&gt; - it compiles and runs TypeScript files &lt;em&gt;on the fly&lt;/em&gt; much like regular node runs JavaScript files.&lt;/p&gt;</content><author><name></name></author><summary type="html">Here's a TL;DR for my future self when I inevitably forget the correct shebang to use. If you’re like me, most of your time writing bash scripts is spent Googling basic things like “how to loop in bash” while grumbling to yourself how easy this would be in a sane language like TypeScript.</summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://nathanfriend.io/assets/img/shelling-with-typescript/shebang.jpg"/><media:content medium="image" url="https://nathanfriend.io/assets/img/shelling-with-typescript/shebang.jpg" xmlns:media="http://search.yahoo.com/mrss/"/></entry></feed>