Lifting the Fog

To say that 2025 has been a hectic year would be an understatement. Even in my personal corner of the world, my work life has been chaotic, far more than I would have liked. I’ve lost one team member, brought on a new one, weathered changes and challenges at the company level, and overall become far more reactive than proactive than I’d like. Feels like I’ve been trying to steer a ship through the fog, with only the dimmest lighthouse to guide the way.

Part of that is my own confusion about where I wanted my professional life to go. Since being promoted from Staff Engineer to Engineering Manager (and seeing my old team dismantled and handed over to foreign contractors), I’ve been unsure about how much time I wanted to dedicate to maintaining my technical skills. Not only that, but the additional uncertainty present with the proliferation of AI tools and massive layoffs in tech has also caused me to question my place in the industry for the next part of my career.

Am I still an engineer? Was I ever? Am I now just a manager?

I think the answer for the foreseeable future is going to be both… and more.

I’ve been what I would describe as a competent programmer. My history with Objective-C, and experience with Python and other languages along the way, has given me a good foundation for understanding the concepts and syntax needed to build applications. However, most of my professional experience has been focused on scripting and DevOps-centric tasks. Building pipelines, setting up system automations, etc. From time to time, I’ve dabbled in other languages, but I haven’t gone all in on any of them. I’d like to change that. One of the things I’m going to focus on in the new year is learning Rust. I’m not one for making resolutions; instead, I create yearly themes, with a few ideal outcomes for the year. I’m not “resolving” to learn Rust this year, but I am focusing on it as part of my overall theme.

As a sysadmin, and later a DevOps engineer, I’ve always focused on deep technical expertise. That meant understanding filesystems at the kernel level, tracing TCP packets through the system, and occasionally spelunking through the Linux kernel source code. Over the past few years of cloud automation, though, a lot of that has fallen to the wayside to make way for learning the AWS APIs. I’ve missed the low-level operating system work. I haven’t had to manage a RAID array in nearly 10 years, nor have I had to track down why a deleted file still has an open handle. However, I do now have an opportunity to dig back into some hard low-level problems, and I’m looking forward to seeing what new skills I can learn.

Taking on a management role has been one of the most challenging things I’ve done in my career. Not because of my plans for where I’d like the systems we’re responsible for to go, but because of all the other interpersonal tasks that are now my responsibility. Last year I hired the first person on my team, and this year I had to let a person on my team go. Both decisions were difficult, and the second took far longer than it should have. That was a lesson learned, one that cost me months of productivity. There are other management tasks that are difficult, like deciding on metrics to gather, how to build reports that are worthwhile, and how best to accurately reflect the value of our team. Management has a lot of abstractions, time-consuming tasks that attempt to convey an idea, either to my team, to my boss, or to the rest of the company. Difficult, but interesting.

Thinking through these aspects of my career informs my position on AI. I’ve gotten to where I am by prioritizing expertise, technical knowledge, quick learning, and being able to get things done. What I’ve noticed using AI occasionally over the past couple of years is that it makes me intellectually lazy. When faced with a difficult problem, my first impulse isn’t to dig into the problem and discern a solution, it’s to hand the problem over to AI and see what it has to say. This unhealthy habit undermines the very thing my career has been built on, thinking clearly. When I offload a task to AI I’m offloading my own thinking, and that’s something I just can’t do. On top of that, AI has very real environmental problems and a questionable financial future, at least in its current form. I understand the usefulness of AI for many tasks, but for me, personally, I can’t risk my career on outsourcing my brain.

So, that’s 800 words or so on why I’m thinking my next yearly theme should be “The Year of Thinking Clearly”. My focus will be on building my expertise, communicating effectively, and, as an essential part of having a clear head, building and maintaining a consistent and challenging exercise program. Healthy body, healthy mind. I’m looking forward to 2026. I think it could be one of my best years ever.

Why I'm Still Using macOS

I started an email thread with Jeremy Friesen, that we later roped Jack Baty into, discussing our relative computing choices. The thread has gotten lengthy, and honestly was probably always better suited for blogging, so I decided to post my next response here.

Jeremy and Jack have both switched at least part of their computing from macOS to Linux, continuing a decades-old tradition of getting fed up with Apple’s corporate malarkey and jumping ship to the open waters of open source. I don’t want to speak for them, but my impression is that Jeremy has had more luck making the switch than Jack has, but they’ve both wound up with one foot in each pool. I feel the pull of Switcher Season every few years myself, more powerfully in 2025 than in the past, but still not quite strongly enough to convince me that it’d be in my best interest.

There are a few practical reasons I’m still enjoying my walled garden.

Sunk Costs

I’ve spent quite a bit of money on my Apple ecosystem over the years. An investment that I’d like to make the best out of. Most recently an Apple Watch Ultra 2, a decision I struggled over for weeks. I was very close to buying a Garmin Fenix, but in the end decided on the Apple Watch because, again, it was so well integrated with the rest of the Apple ecosystem.

There are also a number of third-party applications that I’ve paid for, most notably OmniFocus, an app for which there really is no equivalent anywhere else.1 There’s also OmniGraffle from the same crew, as well as Transmit, Acorn, and DEVONthink. All in all, the third-party ecosystem might be the most compelling reason not to switch.

Hardware

Linux has an even more difficult time running on Macs since Apple switched to their M-series chips, and the PC world just doesn’t have anything close to the power, efficiency, and build quality of a Mac. Vendors like System76 and Framework each make compelling cases for their machines; in the end, you just won’t get the same battery life and performance as you will running macOS on a MacBook.

One only needs to feel the difference between any PC and a MacBook. Pick one up, set it down, and open the lid; close it again. Pick it back up. The Mac feels sturdy, well-built. Test the trackpad, the keyboard; feel the snap of the MagSafe power cord as it magnetically connects and lights up. The Mac hardware is once again that good.

Also, buying new hardware would be expensive. Especially if I wanted something that was at least close to the build quality and specs of a MacBook. It’d be several thousand dollars invested in a new machine that I’m not entirely sure I want.

Familial Obligations

Over the past two decades of using Apple gear, I inevitably bought some for my kids when they were ready. Starting with iPhones when they were teenagers, then Macs for college if they chose to go, and HomePods or Apple Watches for gifts. I pay for Apple One Premier, which comes with just enough accounts for everyone in our family to share iCloud Drive, Apple TV, and Apple Music. As our family has grown and moved farther away, we rely on FaceTime and Messages to keep in touch, sharing quick photos and videos throughout the day. We also share our workouts in Apple Fitness, so I often get pings from one of my daughters and reply with a quick encouragement.

If I were to start moving away from Apple, and macOS specifically, I’d miss out on some of those little interactions throughout the day. Maybe I’d be more focused at work, but I also wouldn’t have those little pings of connection that make me smile.

Will I Ever Switch?

Given all of that, even after sounding like a commercial for Apple in this post, I still want to switch. Apple’s declining software quality, their horrible design decisions over the past few years, poor treatment of developers, overall greed, and executive embrace of our authoritarian regime have all given me ample reason to move to a computing platform that I can own and control. There’d be so much that I’d miss though. I’m not sure when I’ll ever decide to do it. Maybe if Tim Cook gives Trump another gold trophy. Or if Apple abandons all good taste and decides to put ads in Finder like Microsoft. At that point I’d drop them like a hot potato.


  1. There are obviously an abundance of “to-do" apps and task managers. But let’s be honest, there’s only one OmniFocus, and nothing else works quite like it. And certainly nothing on Linux is going to have the same level of polish and multi-device consistency. To make a poor car analogy, OmniFocus is the Lamborghini in a world of Fords and Chevys. It’s expensive and hard to drive, but once you learn how to handle it you can go very fast. ↩︎

Searching for Non-Printable Characters in Text

One of the systems at work accepts data in csv format, which is essentially plain text with columns separated by commas. Occasionally a client will upload a file with mistakes in it, and while our applications are fairly robust and can handle most issues, sometimes one slips by that we weren’t expecting. When this happens, as it has twice in the past as many days, I’m called in to find out why.

The first issue was with a Python application that was pulling a file from S3, parsing it, and turning it into a tab-separated values file before uploading it back to S3 to be further processed by another system. The error given by Python’s csv package was:

_csv.Error: need to escape, but no escapechar set

Which was odd because we process many, many files during a day and this same code hasn’t needed to set anything else. After a bit of faffing around replicating the error locally, and isolating where in the code the error was occuring, I finally got it in my head to just grep the file for special characters. Sure enough, I found that the file contained tabs inside of the values, which given the logic of our program was causing it to have a bad time of it.

The command grep '\t' in.csv gave me the lines containing the offending tabs, and grep -n '\t' in.csv | cut -d : -f 1 gave me just the line numbers, which is what I was asked for. The cut command lets me select specific parts of a line, in this case I asked for -d : which set the colon as the delimiter, and -f 1 which asked for just the first column.

This morning I was asked to look at another task that had failed, this time using a custom Go binary that, again, parsed a csv file from S3. Thinking I might get lucky I ran the same search for tabs in the file but came up empty. After some looking around I found a Stack Overflow question that pointed me in the right direction, but I first had to install the GNU version of grep.

When Mac OS was merged with NeXTSTEP to create Mac OS X, the NeXT OS brought with it pure Unix underpinnings thanks to BSD1. Thanks to that lineage, the Mac contains all the Unix tools we’d expect, but it does not include the Linux tools you’d expect. There are sometimes subtle differences between the tools, and I’ve found that the (ugg…) GNU/Linux version of grep to be more flexible. Luckily, Homebrew makes it trivially easy to install standard GNU tools, and running brew install grep provided me with the ggrep binary.

Equipped with the right tools for the job, I ran this command:

LC_ALL=C  ggrep --color='auto' -P -n "[\x80-\xFF]" in.csv

I’ve written about setting LC_ALL=C before, so I’ll skip that here. The rest of the command I’ll cover below.

--color='auto', this makes it much easier to spot the matching characters in a long string.

-P, tells grep to use Perl-compatible regular expressions

-n, print the line numbers

"[\x80-\xFF]", This searches the text for the extended ASCII characters ranging from hexadecimal code 80, or €, to code FF, the ÿ, or “Latin small letter y with diaeresis”, according to the ASCII table hosted at ascii-code.com.

Finally, in.csv is just the file name.

After editing the file, re-uploading, and kicking off the job again, it completed successfully. How in the world is it 2025 and we still have text encoding issues?


  1. I thought for years that NeXTSTEP was based on FreeBSD, but Wikipedia tells me that it was actually built initially on the older 4.3BSD-Tahoe. Sometimes I forget how long ago that really was, and how fast Steve Jobs was pushed out of Apple after the announcement of the Mac in 1984. ↩︎

Bright Gold in Dark Times

On August 6th, in the White House, Tim Cook announced that Apple is committing an additional $100 billion dollars in American manufacturing. If only he’d left it at that. If only Cook had simply announced the additional investment, bringing Apple’s commitment up to $600 billion over the next four years, and then politely thanked the president and walked out of the room, he may have held on to his integrity. But that’s not what happened. Instead, Cook kissed Trump’s ass and gave him a gold participation trophy.

The trophy is a glass plaque, “made in America”, and “designed by a former Marine” (probably while a bald eagle flew overhead singing “God Bless the U.S.A.”, wearing the American flag as a cape, with a gun in one claw and the Constitution in the other) that sits in a 24 karat gold base. The trophy itself is, honestly, kind of ugly. It doesn’t give off the “Designed by Apple in California” vibes I would expect from something that they actually put a lot of effort into. It looks like a high-school shop class project. However mediocre the actual object, Cook’s gift represented his, and Apple’s, subjugation. Bending the knee and kissing the ring. I nearly threw my iMac out the window.

But I didn’t, because my mind grinds slow and fine. I thought on it for days before reading Gruber’s take on Daring Fireball, where he says that Cook:

…is keenly aware that trust and reputation are only accrued slowly, but are always at risk of being squandered quickly, and that this applies both to how he is perceived personally and how Apple is perceived as an institution — a pillar of American ingenuity and industry. His life’s work. And that despite all of that, Cook concluded that debasing himself, selling some shares of his own dignity, was the best course of action — for Apple, for Apple’s customers (and, yes, shareholders), and perhaps even for the country. That ruthless practicality is necessary merely to stay afloat in a sea of abject graft, extortion, and cronyism. That’s dark. That requires considering that the problem isn’t the greed of a few billionaires and executives who ought to resist burgeoning corruption, but that Trump and his sycophants in the Republican Party have already succeeded in corrupting the system. That the corruption isn’t happening, but happened. The United States isn’t heading for existential trouble. We’re in it — and a pathway out is not yet clear. That’s not to say all is forever lost, but that we are, in our current political moment, beyond the point where the game can be played successfully on the level. You can choose to play a crooked game straight, but you can’t win. Business is competition. A loser who played above reproach is still a loser. You need to choose your battles. US manufacturing is Cook’s choice.

Once again, Gruber is faster to put into words what I’ve been mulling over.

To be clear, I fully support Apple bringing manufacturing back to the United States. In rural Iowa, small towns have been decimated over the past few decades by big manufacturers leaving the country and building overseas or in Mexico. Newton’s loss of Maytag is the poster child for describing how lives can be upended and entire ways of life lost due to the greed of corporations chasing the cheapest labor. The loss of identity and self-respect associated with losing a career is one of the reasons we’re in this mess with Trump and his ilk to begin with. But, one can both agree with bringing manufacturing back to the US and despise the bootlicking at the same time. With many things with the Trump administration, it’s not just what is being done, but how they are going about doing it.

Yes, bringing manufacturing back to the US is a good and nobel goal. I hope we can actually do it, but I also hope that we can do it in an environmentally clean way, leveraging renewable energy and sustainable materials harvesting whenever possible. The Republicans want to tout new investments in the US and new jobs, but they don’t believe in climate change, and their oil-drilling overlords won’t let them invest in clean energy alternatives. Since the MAGA cult is in charge of everythign for at least the next year and a half, if not till 2029, the United States is in a downward spiral that I don’t see a way out of.

The government is now blatantly, openly corrupt. Gifts like Cooks are now just the way things are done to win favor. The US is doing everything it can to roll back environmental protections, dissuade people from buying electric or investing at all in renewable energy. The health department is making it harder to get vaccinated. The Department of Education is being dismantled, which will make it easier to send public funds to private religious schools. There are literally masked gangs of thugs kidnapping people off the street in broad daylight and putting them in camps. And just to really own the libs, they give the camps fun names like “Alligator Alcatraz”.

Cook’s display of fealty has given me pause when considering future Apple purchases, but changing my computing platform of choice would be like cutting off my nose to spite my face. The answer isn’t in punishing individual companies (which would also be punishing myself and my family) that are playing by the current rules, the answer is to change the rules. To try to rebuild a fair, just, and civil society, free from the corruption of the MAGA movement. The only way we are going to do that is by making sure we vote out every Republican currently in office, so that not a single one of those sycophantic cowards ever get close to holding power again. Is that likely? Probably not. Not anytime soon anyway. The bad guys won. We’re living in their world now.

I hope Trump scratches the base of his little trophy someday to discover that it is only “gold plated”, and as fake as his orange tan.

Random Strings on the Command Line

Browsing through some old files today I came across this note:

To Generate A Random String:tr -dc A-Za-z0-9_ < /dev/urandom | head -c 8 | xargs

Curios if it still worked I pasted it into my terminal and, unsurprisingly, was met with an error:

tr: Illegal byte sequence

The tr utility is one of the many old-school Unix programs with history reaching way back to System V. It stands for “translate characters”, and with the -dc flags on, it should have ignored all input except for alphabet characters A-Z, both upper and lower case, and the integers 0 through 9, and the underscore character. The “Illegal byte sequence” error means it was really not happy with the input it was getting from /dev/urandom.

On macOS, the pseudo-device /dev/urandom is, according to the man page, “a compatibility nod to Linux”. The device generates random bytes, so if we read it we’ll get back raw binary data that looks like:

00010101 01011001 10111101

The reason the command is not working like it used to is because most modern computing system expect text character encoding to be UTF-8. When tr gets the string of random bytes from /dev/urandom, it expects the bytes to be in a specific sequence that it can translate into printable characters on the screen. Since we are intentionally generating random bytes though, we might get a few characters that translate properly, but eventually we’ll encounter the “illegal byte sequence” error above.

To fix the problem, all we need to do is set LC_ALL=C before running tr:

LC_ALL=C tr -dc A-Za-z0-9_ < /dev/urandom | head -c 14 | xargs

Setting LC_ALL=C sets the language setting back to POSIX, or the original C ASCII encoding for text. That means when tr is fed a random string of bytes, it interprets each byte as a character, according to the ASCII table, which looks something like this:

Character ASCII Decimal ASCII Hexadecimal Binary Representation
A 65 41 01000001
B 66 42 01000010
C 67 43 01000011

Now each byte is interpreted as a character that matches the list passed as an argument to tr.

➜ LC_ALL=C tr -dc A-Za-z0-9_ < /dev/urandom | head -c 14 | xargsRhac_WGis7tHzS

So, to break down each command in the pipeline:

  • tr: filters out all characters except those in the sets A-Z, a-z, 0-9, and _
  • head -c 14: displays the first 14 characters of the input from tr
  • xargs: adds a nice newline character at the end of the string, so it’s easy to copy.

This command could be easily adopted to use base64 instead of tr without setting LC_ALL=C if you wanted more random characters in the string:

base64 < /dev/random | head -c 14 | xargs

Expanding head -c to 34 or so makes for a nice password generator.

In fact, I’ve aliased this to pgen in my .zshrc:

pgen(){    base64 < /dev/random | head -c 32 | xargs}

There’s almost certainly easier ways to generate a random string in the shell, but I like this, and it works for me.


Update: The good Dr. Drang suggested ending the pipeline and running echo instead of xargs for clairity, which makes a lot of sense to me. I updated the alias to base64 < /dev/random | head -c 32; echo.

Gross Apple Marketing

I’m not sure what’s going on over in Cupertino for them to think that any of the recent Apple Intelligence ads they’ve been running are a good idea. They’re cringy at best, and honestly just flat out insulting.

In one a schlub writes an email to his boss and uses AI to make it sound ‘more professional’, in another a young woman uses it to lie about remembering an acquaintance’s name. In another the same young woman again uses it to lie about reading an email from a college, to her face, while she’s sitting with her. In yet another, linked to recently by Scott McNulty, a woman uses AI to lie to her husband about getting him something for his birthday.

If this is what Apple thinks their AI is for, I honestly don’t know that I want any part of it.

Compare and contrast with the video I posted yesterday, and with this beautiful animation from Canonical.

An error occurred.

Try watching this video on www.youtube.com, or enable JavaScript if it is disabled in your browser.

I’ve watched that little animation several times, and they tell a better story in a minute twenty-five than all of Apple’s AI commercials combined.

Loading and Indexing SQLite

What a difference a couple of lines of code can make.

I recognize that databases have always been a weak point for me, so I’ve been trying to correct that lately. I have a lot of experience with management of the database engines, failover, filesystems, and networking, but too little working with the internals of the databases themselves. Early this morning I decided I didn’t know enough about how database indexes worked. So I did some reading, got to the point where I had a good mental model for them, and decided I’d like to do some testing myself. I figured 40 million records was a nice round number, so I used fakedata to generate 40 million SQL inserts that looked something like this:

INSERT INTO contacts (name,email,country) VALUES ("Milo Morris","pmeissner@test.tienda","Italy");INSERT INTO contacts (name,email,country) VALUES ("Hosea Burgess","kolage@example.walmart","Dominica");INSERT INTO contacts (name,email,country) VALUES ("Adaline Frank","shaneIxD@example.talk","Slovenia");

I saved this as fakedata.sql and piped it into sqlite3 and figured I’d just let it run in the background. After about six hours I realized this was taking a ridiculously long time, and I estimated I’d only loaded about a quarter of the data. I believe that’s because SQLite was treating each INSERT as a separate transaction.

A transaction in SQLite is a unit of work. SQLite ensures that the write to the database is Atomic, Consistent, Isolated, and Durable, which means that for each of the 40 million lines I was piping into sqlite3, the engine was ensuring that every line was fully committed to the database before moving on to the next line. That’s a lot of work for a very, very small amount of data. So, I did some more reading and found one recommendation of explicitly wrapping the entire load into a single transaction, so my file now looked like:

BEGIN TRANSACTION;INSERT INTO contacts (name,email,country) VALUES ("Milo Morris","pmeissner@test.tienda","Italy");INSERT INTO contacts (name,email,country) VALUES ("Hosea Burgess","kolage@example.walmart","Dominica");INSERT INTO contacts (name,email,country) VALUES ("Adaline Frank","shaneIxD@example.talk","Slovenia");COMMIT;

I set a timer and ran the import again:

➜  var time cat fakedata.sql| sqlite3 test.dbcat fakedata.sql  0.07s user 0.90s system 1% cpu 1:13.66 totalsqlite3 test.db  70.81s user 2.19s system 98% cpu 1:13.79 total

So, that went from 6+ hours to about 71 seconds. And I imagine if I did some more optimization (possibly using the Write Ahead Log?) I might be able to get that import faster still. But a little over a minute is good enough for some local curiosity testing.

Indexes

So… back to indexes.

Indexing is a way of sorting a number of records on multiple fields. Creating an index on a field in a table creates another data structure that holds the field values and a pointer to the record it relates to. Once the index is created it is sorted. This allows binary searches to be performed on the new data structure.

One good analogy is the index of a physical book. Imagine that a book has ten chapters and each chapter has 100 pages. Now imagine you’d like to find all instances of the word “continuum” in the book. If the book doesn’t have an index, you’d have to read through every page in every chapter to find the word.

However, if the book is already indexed, you can find the word in the alphabetical list, which will then have a pointer to the page numbers where the word can be found.

The downside to the index is that it does take additional space. In the book analogy, while the book itself is 1000 pages, we’d need another ten or so for the index, bringing up the total size to 1010 pages. Same with a database, the additional index data structure requires more space to hold both the original data field being indexed, and a small (4-byte, for example) pointer to the record.

Oh, and the results of creating the index are below.

SELECT * from contacts WHERE name is 'Hank Perry';Run Time: real 2.124 user 1.771679 sys 0.322396CREATE INDEX IF NOT EXISTS name_index on contacts (name);Run Time: real 22.129 user 16.048308 sys 2.274184SELECT * from contacts WHERE name is 'Hank Perry';Run Time: real 0.003 user 0.001287 sys 0.001598

That’s a massive improvement. And now I know a little more than I did.

The Perfect ZSH Config

If you spend all day in the terminal like I do, you come to appreciate it’s speed and efficiency. I often find myself in Terminal for mundane tasks like navigating to a folder and opening a file; it’s just faster to type where I want to go than it is to click in the Finder, scan the folders for the one I want, double-click that one, scan again… small improvements to the speed of my work build up over time. The speed is increased exponentially with the correct configuration for your shell, in my case, zsh.

zsh is powerful and flexible, which means that it can also be intimidating to try to configure yourself. Doubly-so when there are multiple ‘frameworks’ available that will do the bulk of the configuration for you. I used Oh My Zsh for years, but I recently abandoned it in favor of maintaining my own configuration using only the settings that I need for the perfect configuration for my use.

I’ve split my configuration into five files:

  • apple.zsh-theme
  • zshenv
  • zshrc
  • zsh_alias
  • zsh_functions

I have all five files in a dotfiles git repository, pushed to a private Github repository.

The zshenv file is read first by zsh when starting a new shell. It contains a collection of environmental variables I’ve set, mainly for development. For example:

export PIP_REQUIRE_VIRTUALENV=trueexport PIP_DOWNLOAD_CACHE=$HOME/.pip/cacheexport VIRTUALENV_DISTRIBUTE=true

The next file is zshrc, which contains the main bulk of the configurations. My file is 113 lines, so let’s take it a section at a time.

source /Users/jonathanbuys/Unix/etc/dotfiles/apple.zsh-themesource /Users/jonathanbuys/Unix/etc/dotfiles/zsh_aliassource /Users/jonathanbuys/Unix/etc/dotfiles/zsh_functions

The first thing I do is source the other three files. The first is my prompt, which is cribbed entirely from Oh My Zsh. It’s nothing fancy, but I consider it to be elegant and functional. I don’t like the massive multi-line prompts. I find them to be far too distracting for what they are supposed to do.

My prompt looks like this:

 ~/Unix/etc/dotfiles/ [master*] 

It gives me my current path, what git branch I’ve checked out, and if that branch has been modified since the last commit.

The next two files, as their names suggest, contain aliases and functions. I have three functions and 16 aliases. I won’t go into each of them here, as they are fairly mundane and only specific for my setup. The three functions are to print the current path of the open Finder window, to use Quicklook to preview a file, and to generate a uuid string.

The next few lines establish some basic settings.

autoload -U colors && colorsautoload -U zmvsetopt AUTO_CDsetopt NOCLOBBERsetopt SHARE_HISTORYsetopt HIST_IGNORE_DUPSsetopt HIST_IGNORE_SPACE

The autoload lines setup zsh to use pretty colors, and to enable the extremely useful zmv command for batch file renaming. The interesting parts of the setopt settings are the ones dealing with command history. These three commands allow the sharing of command line history between open windows or tabs. So if I have multiple Terminal windows open, I can browse the history of both from either window. I find myself thinking that the environment is broken if this is not present.

Next, I setup some bindings:

  # start typing + [Up-Arrow] - fuzzy find history forward  bindkey '^[[A' up-line-or-search  bindkey '^[[B' down-line-or-search    # Use option as meta  bindkey "^[f" forward-word  bindkey "^[b" backward-word    # Use option+backspace to delete words  x-bash-backward-kill-word(){      WORDCHARS='' zle backward-kill-word    }  zle -N x-bash-backward-kill-word  bindkey '^W' x-bash-backward-kill-word    x-backward-kill-word(){      WORDCHARS='*?_-[]~\!#$%^(){}<>|`@#$%^*()+:?' zle backward-kill-word  }  zle -N x-backward-kill-word  bindkey '\e^?' x-backward-kill-word

These settings let me use the arrow keys to browse history, and to use option + arrow keys to move one word at a time through the current command, or to use option + delete to delete one word at a time. Incredibly useful, use it all the time. Importantly, this also lets me do incremental searching through my command history with the arrow keys. So, if I type aws, then arrow up, I can browse all of my previous commands that start with aws. And when you have to remember commands that have 15 arguments, this is absolutely invaluable.

The next section has to do with autocompletion.

# Better autocomplete for file namesWORDCHARS=''unsetopt menu_complete   # do not autoselect the first completion entryunsetopt flowcontrolsetopt auto_menu         # show completion menu on successive tab presssetopt complete_in_wordsetopt always_to_endzstyle ':completion:*:*:*:*:*' menu select# case insensitive (all), partial-word and substring completionif [[ "$CASE_SENSITIVE" = true ]]; then  zstyle ':completion:*' matcher-list 'r:|=*' 'l:|=* r:|=*'else  if [[ "$HYPHEN_INSENSITIVE" = true ]]; then    zstyle ':completion:*' matcher-list 'm:{[:lower:][:upper:]-_}={[:upper:][:lower:]_-}' 'r:|=*' 'l:|=* r:|=*'  else    zstyle ':completion:*' matcher-list 'm:{[:lower:][:upper:]}={[:upper:][:lower:]}' 'r:|=*' 'l:|=* r:|=*'  fifiunset CASE_SENSITIVE HYPHEN_INSENSITIVE# Complete . and .. special directorieszstyle ':completion:*' special-dirs truezstyle ':completion:*' list-colors ''zstyle ':completion:*:*:kill:*:processes' list-colors '=(#b) #([0-9]#) ([0-9a-z-]#)*=01;34=0=01'zstyle ':completion:*:*:*:*:processes' command "ps -u $USERNAME -o pid,user,comm -w -w"# disable named-directories autocompletionzstyle ':completion:*:cd:*' tag-order local-directories directory-stack path-directories# Use caching so that commands like apt and dpkg complete are useablezstyle ':completion:*' use-cache yeszstyle ':completion:*' cache-path $ZSH_CACHE_DIRzstyle ':completion:*:*:*:users' ignored-patterns \        adm amanda apache at avahi avahi-autoipd beaglidx bin cacti canna \        clamav daemon dbus distcache dnsmasq dovecot fax ftp games gdm \        gkrellmd gopher hacluster haldaemon halt hsqldb ident junkbust kdm \        ldap lp mail mailman mailnull man messagebus  mldonkey mysql nagios \        named netdump news nfsnobody nobody nscd ntp nut nx obsrun openvpn \        operator pcap polkitd postfix postgres privoxy pulse pvm quagga radvd \        rpc rpcuser rpm rtkit scard shutdown squid sshd statd svn sync tftp \        usbmux uucp vcsa wwwrun xfs '_*'if [[ ${COMPLETION_WAITING_DOTS:-false} != false ]]; then  expand-or-complete-with-dots() {    # use $COMPLETION_WAITING_DOTS either as toggle or as the sequence to show    [[ $COMPLETION_WAITING_DOTS = true ]] && COMPLETION_WAITING_DOTS="%F{red}…%f"    # turn off line wrapping and print prompt-expanded "dot" sequence    printf '\e[?7l%s\e[?7h' "${(%)COMPLETION_WAITING_DOTS}"    zle expand-or-complete    zle redisplay  }  zle -N expand-or-complete-with-dots  # Set the function as the default tab completion widget  bindkey -M emacs "^I" expand-or-complete-with-dots  bindkey -M viins "^I" expand-or-complete-with-dots  bindkey -M vicmd "^I" expand-or-complete-with-dotsfi# automatically load bash completion functionsautoload -U +X bashcompinit && bashcompinit

That’s a long section, but in a nutshell this lets me type one character, then hit tab, and be offered a menu of all the possible completions of that character. It is case-insensitive, so b would match both boring.txt and Baseball.txt. I can continue to hit tab to cycle through the options, and hit enter when I’ve found the one I want.

The last section sources a few other files:

[ -f ~/.fzf.zsh ] && source ~/.fzf.zsh[ -f "/Users/jonathanbuys/.ghcup/env" ] && source "/Users/jonathanbuys/.ghcup/env" # ghcup-env[ -s "/Users/jonathanbuys/.bun/_bun" ] && source "/Users/jonathanbuys/.bun/_bun"source /Users/jonathanbuys/Unix/src/zsh-autosuggestions/zsh-autosuggestions.zshsource /Users/jonathanbuys/Unix/src/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh

If I’m experimenting with Haskell, I’d like to load the ghcup-env variables. If I have bun installed (a way, way faster npm), than use that. The final two sources are for even more enhanced autosuggestions and command line syntax highlighting. So, typos or commands that don’t exist will be red, good commands where zsh can find the executable will be green. The autosuggestions take commands from my history and suggest them, I can type right-arrow to accept the suggestion, or keep typing to ignore it.

Taken together, I’ve been able to remove Oh My Zsh, but keep all of the functionality. My shell configuration is constantly evolving as I find ways to make things faster and more efficient. I don’t consider myself a command line zealot, but I do appreciate how this setup gets out of my way and helps me work as fast as I can think.


p.s. A lot of this configuration was taken from other sources shared around the internet, as well as the zsh documentation. I regret that I haven’t kept references to the origins of some of these configs. If I can find the links I’ll post them here.

Future Work and AI

I’ve been trying to wrap my small monkey brain around what ChatGPT will mean in the long run. I’m going to try to think this through here. In many ways the advances we’ve seen in AI this past year perpetuate the automation trend that’s existed since… well, since humans started creating technology. I’ve seen arguments that seem to be on two ends of a spectrum, that the AI is often wrong and unreliable, and we shouldn’t use it for anything important, to AI is so good that it’s going to put us all out of jobs. As with most truths, I think the reality is somewhere in between.

It’s my opinion that jobs that AI can replace, it probably will replace a lot of. But not all. Referring back to our discussion about the current state of Apple news sites, if the site is a content farm pumping out low-value articles for hit counts and views, I can see AI handling that. If your site is well thought out opinions and reviews about things around the Apple ecosystem, that I think will be safe. Because it’s the person’s opinion that gives the site value.

For more enterprise-y jobs, I could see fewer low and mid-level developers. Fewer managers, fewer secretaries, fewer creatives. Not all gone, but certainly less than before. If your job is to create stock photos and put together slide shows, you might want to expand your skill set a bit.

I think… the kind of jobs that will survive are the type that bring real value. The kind of value that can’t be replicated by a computer. Not just the generation of some text or code, but coming up with the why. What needs to be made, and why does it need to be made?

Maybe AI will help free us up to concentrate on solving really hard problems. Poverty, clean water, famine, climate change. Then again, maybe it’ll make things worse. I suppose in the end that’s up to us.

GPG Signing Git Commits

On my way towards completing another project I needed to setup gpg public key infrastructure. There are many tutorials and explanations about gpg on the web, so I won’t try to explain what it is here. My goal is to simply record how I went about setting it up for myself to securely sign my Git commits.

Most everything here I gathered from this tutorial on dev.to, but since I’m sure I’ll never be able to find it again after today, I’m going to document it here.

First, install gpg with Homebrew:

brew install gpg

Next, generate a new Ed25519 key:

gpg --full-generate-key --expert

We pick option (9) for the first prompt, Elliptic Curve Cryptography, and option (1) for the second, Curve 25519. Pick the defaults for the rest of the prompts, giving the key a descriptive name.

Once finished you should be able to see your key by running:

gpg --list-keys --keyid-format short

The tutorial recommends using a second subkey generated from the first key to actually do the signing. So, we edit the master key by running:

gpg --expert --edit-key XXXXXXX

Replacing XXXXX with the ID of your newly generated key. Once in the gpg command line, enter addkey, and again select ECC and Curve 25519 for the options. Finally, enter save to save the key and exit the command line.

Now when we run gpg --list-keys --keyid-format short we should be able to see a second key listed with the designation [S] after it. The ID will look similar to this:

sub   ed25519/599D272D 2021-01-02 [S]

We will need the part after ed25519/, in this case 599D272D. Add that to your global Git configuration file by running:

git config --global user.signingkey 599D272D

If you’d like git to sign every commit, you can add this to your config file:

git config --global commit.gpgsign true

Otherwise, pass the -S flag to your git command to sign individual commits. I’d never remember to do that, so I just sign all of them.

Make sure that gpg is unlocked and ready to use by running:

echo "test"  | gpg --clearsign

If that fails, run export GPG_TTY=$(tty) and try again. You should be prompted to unlock GPG with the passphrase set during creation of the key. Enter the export command in your ~/.zshrc to fix this issue.

Finally, Github has a simple way to add gpg keys, but first we’ll need to export the public key:

gpg --armor --export 599D272D

Copy the entire output of that command and enter it into the Github console under Settings, “SSH and GPG keys”, and click on “New GPG key”. Once that’s finished, you should start seeing nice green “Verified” icons next to your commits.