fx

2024-01-02

fx is my favorite terminal-based JSON tool. Let's look at how it lets us interactively explore, query, filter, and edit JSON. To wrap things up, we'll compare it to jq, and I'll explain why I prefer fx.

To show off fx, we'll need some JSON. We'll start by fetching JSON of Harry Potter characters.

curl https://hp-api.onrender.com/api/characters > hp.json

If we cat hp.json, we see that it is a single line of JSON (formatted for computers, not for human readability).

Explore interactively

Running fx with the JSON file's name will let you explore it interactively with human-friendly formatting.

fx hp.json
fx interactive view

You can navigate using up and down arrows or vim-style j/k navigation. You can search with / and go to previous/next matches with n/N

Note that fx shows the current JSON path in the bottom left as you move around. Press . to enter "dig" mode to fuzzy-search for a path.

Pressing y lets you copy the value, path, or key under the cursor.

q, ctrl-c or esc will exit fx.

fx --help will show you how to collapse/expand nodes and the rest of the shortcuts.

Querying and filtering

fx querying will feel like the JavaScript map/filter/etc. you already know. fx also provides some optional syntactic-sugar.

Here are some example commands with their non-sugary and sugary versions.

# get a list of wand woods
fx hp.json '.map(x => x.wand.wood)'
fx hp.json .[].wand.wood
fx hp.json @.wand.wood

# wand woods without blank wood names
fx hp.json '.map(x => x.wand.wood).filter(x => x)'
fx hp.json .[].wand.wood '.filter(x => x)'
fx hp.json @.wand.wood '.filter(x => x)'

# living blonde wizard names
fx hp.json '.filter(x => x.alive && x.hairColour === "blonde" && x.wizard).map(x => x.name).sort()'
fx hp.json '.filter(x => x.alive && x.hairColour === "blonde" && x.wizard)' @.name sort

Built-ins and custom functions

sort is a built-in function (there's also uniq and more). fx also allows you to define your own functions in ~/.fxrc.js (or a local .fxrc.js).

Using .filter(x => x) to remove blanks is common enough that I've written a custom present function in my ~/.fxrc.js

// reject any falsy/blank values from an array
global.present = (arr) => arr.filter((x) => x);

To use a custom function or built-in function, you add it as an argument to the fx command.

# unique wand woods
fx hp.json @.wand.wood sort uniq present

Because you can run any JavaScript in your custom functions, you can write functions to do things like filter out secrets or sensitive information.

Let's imagine we never want to see Voldemort's name, and we never want to leak our Stripe API key while recording a screencast. We can avoid both disasters by building a confidential function.

// define secrets and their replacements
const secrets = {
  "Lord Voldemort": "He-Who-Must-Not-Be-Named",
  [process.env.STRIPE_API_KEY]: "********",
};

// replace secrets with their replacements
global.confidential = (x) => {
  let stringVersion = JSON.stringify(x);

  Object.keys(secrets).forEach((secret) => {
    stringVersion = stringVersion.replaceAll(secret, secrets[secret]);
  });

  return JSON.parse(stringVersion);
};

Here's some example usages:

# without confidential
fx hp.json '.filter(x => x.house === "Slytherin")' '.[3].name'
Lord Voldemort

# with confidential
fx hp.json '.filter(x => x.house === "Slytherin")' confidential
He-Who-Must-Not-Be-Named

# Set a fake stripe API to test marking those as confidential
export STRIPE_API_KEY="Potter"

# without confidential
fx hp.json .[0].name
Harry Potter

# with confidential
fx hp.json .[0].name confidential
Harry ********

Modifying JSON

You use map to modify JSON.

e.g., here's how we'd replace blank strings for wand.wood with nulls.

fx hp.json '.map(c => { c.wand.wood = c.wand.wood || null; return c })' > modified.json

fx versus jq

jq is a great tool. If you already know it, you'll probably be happy continuing to use it (though you can benefit from the interactive parts of fx).

However, learning jq is daunting because it is a DSL. I've found I can read a jq command reasonably easily but struggle to write one from scratch because I haven't internalized their DSL yet. I could learn jq's DSL, but I haven't made it a priority yet.

While jq is a DSL you have to learn, fx is just using JavaScript with some completely optional syntactic sugar.

Compare the following examples:

jq '[.[] | select(.hairColour == "blonde" and .alive == true and .wizard == true) | .name ] | sort' hp.json
fx hp.json '.filter(x => x.alive && x.hairColour === "blonde" && x.wizard).map(x => x.name).sort()'

Here's that same fx command with the sugar.

fx hp.json '.filter(x => x.alive && x.hairColour === "blonde" && x.wizard).map(x => x.name).sort()'
fx hp.json '.filter(x => x.alive && x.hairColour === "blonde" && x.wizard)' @.name sort

While the syntactic sugar lets you be more succinct, you don't have to learn it to use fx successfully.


fx is a power tool with the ergonomics I'm already familiar with. The ability to write custom functions is super compelling. You should give it a try.


2023 Wrap Up

2023-12-21

I haven't posted here all year, so I figured I could at least aggregate some links to other things I've done this year.

2023 started with me going all-in with some cofounders on Prefab. I'm super enjoying building developer tools. I've been able to go deep into the Language Server Protocol, CLIs, and VS Code/Neovim tooling to help build the best feature flags, live config, and dynamic log levels for developers. You should try it out -- it'd mean a lot to me 🫶

Here are some blog posts I wrote for the Prefab blog.

I started a youtube channel. There is only one video so far, but I plan to hit it hard in 2024. Like and subscribe and all that.

I've also started an LSP Newsletter if you'd like to join me on my journey into developer tools.


Layers

2022-04-01

The problem

Pressing (not holding) ⌘-tab is useful for swapping between apps. If you ⌘-tab from your editor to your browser, you can then ⌘-tab back to your editor. You can jump back and forth between your editor and browser with just a quick ⌘-tab keystroke. It is a great workflow as long as you're using exactly two apps.

Then you get a Slack message and after giving Slack focus, ⌘-tab takes you back to your browser, the next ⌘-tab sends you back to Slack.

Sigh. OK, so we hold ⌘ and push tab until we're back to our editor and then hold ⌘ and push tab until we're back to our browser and now we can hit ⌘-tab to quickly jump between the two again. Order is restored.

This is mostly a minor nuisance except that this can happen dozens of times throughout the day. Little frictions add up and break flow.

The journey

I tried using Spaces a few times before but the animation was honestly slightly nauseating. If you turn on reduce motion then Spaces changes from a dizzying sliding animation to a more subtle fade animation. The fade animation is pretty tolerable and you can drag whatever applications you want to the proper Spaces and jump around with the keyboard. Success!

Except that somehow the reduce motion setting stopped working for me after a reboot -- it is still checked, it just doesn't do anything and I'm back to the abrasive sliding animation.

Gross. I started looking at other options. yabai looks powerful but requires you to disable System Integrity Protection (permanently, not just during installation) and that's a non-starter for me.

The solution

Taking a step back, what am I really trying to accomplish? I want to be able to press a key and jump to an app (or group of apps). Ideally I don't have to wrangle app the apps into the right workspace like I did with Spaces after every reboot.

Fine. I'll write some AppleScript. After lots of Googling and some optimization, I ended up with a script I'm calling Layers.

How does it work? Create a file in your home directory named .layers. Each line of the file should include a comma-separated list of applications in that layer. Running the layers script will generate compiled AppleScripts to focus applications and spit out a command to bind to a keyboard shortcut.

My .layers file looks like this:

Alacritty
Firefox, Google Chrome, Safari
Slack
Messages, Music, Stream Deck
zoom.us

The layers output is

Add keyboard shortcuts for the following:
ls /Users/ship/.layers_scripts/layer_0*_compiled | xargs -L 1 -P 8 osascript
ls /Users/ship/.layers_scripts/layer_1*_compiled | xargs -L 1 -P 8 osascript
ls /Users/ship/.layers_scripts/layer_2*_compiled | xargs -L 1 -P 8 osascript
ls /Users/ship/.layers_scripts/layer_3*_compiled | xargs -L 1 -P 8 osascript
ls /Users/ship/.layers_scripts/layer_4*_compiled | xargs -L 1 -P 8 osascript

I assign each line starting with ls to a shortcut via an Alfred workflow but you can use any tool that lets you map bash commands to keyboard shortcuts. I'm using ctrl-1 through ctrl-5 since those worked well enough for me with Spaces.

The ls ... command finds all the compiled scripts for a layer and passes them to xargs to run in parallel (up to 8 at a time).

The compilation + parallelization felt like overengineering at first, but it took little time to implement and has a real impact on responsiveness.

Pressing ctrl-2 will bring Firefox, Google Chrome, and Safari to the forefront of my screen. It skips any apps that aren't running. I can press ctrl-1 to get back to Alacritty where my editor is running.

This is better for me than ⌘-tab because it is predictable -- the positions of things never change. This is better for me than Spaces because there's no distracting animations.

There's one downside I can think of when compared to Spaces -- you can't have different windows of an Application mapped to different Layers. I didn't use this functionality in Spaces so I don't miss it.

Why "Layers"? Spaces are effectively distinct virtual screens. Layers don't have this same isolation and apps overlap. This overlap allows for composability and use-cases not allowed by space.

I've been using it a few days now and it feels fantastic. Take it for a spin and let me know how it works for you.

My goofy face

Hi, I'm Jeffrey Chupp.
I solve problems, often with code.