Nord Light Theme

I like my terminal to change theme when I ssh into another machine. I have been using Solarized Light and Solarized Dark to facilitate this for some time. But now I have gotten hooked on Nord which has no light version. So I made one, just for Terminal for now at least.

Screenshot of Nord Light theme with colored ls output

There are notes about a “bright ambiance” version of the theme in the Nord docs but no actual themes available. The official Terminal.app theme actually deviates from the core 16-color palette but I didn’t do that here. I just chose the most appropriate color from the 16 color palette for each option in a Terminal theme.

Avoid trailing comma when emitting lists in Mustache templates

In pure Mustache there’s no (simple) way to avoid a trailing comma on a list:

{{#items}}{{.}}, {{/items}}

Which produces something like: “First, Second, Third, “

There are various ways around this. If you’re working in HTML I think the nicest is to model your list as an actual HTML list and use CSS:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8"/>
    <title>Lists</title>
    <style>
      .list {           
        margin: 0;      
        padding: 0;
      }
      .list li {           
        display: inline;             
      }
      .list li:not(:last-child):after {
        content: ", ";               
      }
    </style>
  </head>
  <body>
    <ol class="list">
      <li>First</li>
      <li>Second</li>
      <li>Third</li>
    </ol>
  </body>
</html>

Which produces: “First, Second, Third”.

Python types for a decorator that is agnostic about the wrapped func’s return type


import random
from typing import Callable, ParamSpec, TypeVar

T = TypeVar("T")
P = ParamSpec("P")

def some_global_condition() -> bool:
return random.choice([True, False])

def my_return_type_preserving_decorator(val: T) -> Callable[[Callable[P, T]], Callable[P, T]]:
def decorator(func: Callable[P, T]) -> Callable[P, T]:
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
if some_global_condition():
return val
return func(*args, **kwargs)
return wrapper
return decorator

@my_return_type_preserving_decorator((True, True, True))
def my_func(n: int) -> tuple[bool, bool, bool]:
return n > 10, n > 100, n > 1000

x = my_func(5)
for r in x:
print("yes" if r else "no")

Format Percentage for Display in JavaScript

Input Output
0.12 12%
0.0012345 0.12%
0.0 0

2 significant digits but locale-sensitive, strip meaningless trailing zeroes and decimal dividers.

const formatPercentage = function (value) {
    return new Intl.NumberFormat(
        undefined, // use browser locale
        // Choose the options that get us closest to our desired style – a percentage that tells you enough to be useful but no unnecessary decimal places or trailing zeroes.
        {
            style: "percent", // Treat decimals as percentages, multiplying by 100 and adding a % sign.
            minimumSignificantDigits: 2,
            maximumSignificantDigits: 2
        }
    )
        .format(value)
        // \D here is the locale-agnostic decimals separator.
        .replace(/(\D[0-9]*?)0+%$/, "$1%") // Get rid of trailing zeroes after the decimal separator.
        .replace(/\D%/, "%") // Clean up the scenario where we got rid of everything after the decimal separator and now have something like "4.%.
        .replace(/^0%$/, "0"); // Replace literal "0%" with "0" as zero is unitless.
};

Python Types for Union of String Literal and Other Type

I wanted to add a single special value to another type for a union type, something like this:

from typing import Literal
                                                                            
UNMEASURED = "unmeasured"                                                    
Unmeasured = Literal[UNMEASURED]
Resource = int | Unmeasured

notes: dict[Resource, str] = {1: "foo", 2: "bar", 3: "baz", UNMEASURED: "quux"}
resources = [1, 2, 3] + [UNMEASURED]
for resource in resources:
    print(notes[resource])

This works, but mypy complains: ‘error: Parameter 1 of Literal[…] is invalid [valid-type]’. So to make this pass mypy typechecking you have to repeat “unmeasured” as a literal. Even if you do that:

from typing import Literal

UNMEASURED = "unmeasured"
Unmeasured = Literal["unmeasured"]
Resource = int | Unmeasured

notes: dict[Resource, str] = {1: "foo", 2: "bar", 3: "baz", UNMEASURED: "quux"}
resources = [1, 2, 3] + [UNMEASURED]
for resource in resources:
    print(notes[resource])

mypy will complain: ‘Dict entry 3 has incompatible type “str”: “str”; expected “int | Literal[‘unmeasured’]”: “str” [dict-item]’ and also ‘Invalid index type “str | int” for “dict[int | Literal[‘unmeasured’], str]”; expected type “int | Literal[‘unmeasured’]” [index]’

Eventually I came up with:

from enum import Enum
from typing import Literal

class Aspect(Enum):
    UNMEASURED = "unmeasured"

Resource = int | Aspect

notes: dict[Resource, str] = {1: "foo", 2: "bar", 3: "baz", Aspect.UNMEASURED: "quux"}
resources = [1, 2, 3] + [Aspect.UNMEASURED]
for resource in resources:
    print(notes[resource])

which both produces the same result, and passes the typechecker. I don’t love the weird Enum hanging out on its own but it does at least put the magic string in one place only and pass. I wonder what would be better.

Remember the Milk Alfred Workflow

This uses rtm-cli which you can install with npm.

query=$1
logfile="/Users/bakert/u/scratch/rtm.log"
rtm=/opt/homebrew/bin/rtm
max_lines=1000

# Log the query
echo "[$(date)] $query" >> "$logfile"

# Ensure the logfile does not grow beyond $max_lines
tail -n $max_lines "$logfile" > "$logfile.tmp" && mv "$logfile.tmp" "$logfile"

# Send the task to Remember the Milk
"$rtm" add $query

if [ $? -ne 0 ]; then
  echo "SOMETHING WENT WRONG! Please check that rtm-cli is installed: npm install -g rtm-cli"
else
  echo "[Added] $query"
fi

Gmail Conversations Archiving Has Gotten Worse

You can search for emails in Gmail, “Select All” and Gmail will prompt you, “do you want to select all conversations that match this search?” rather than just the 100 you can see. Then if you do something like use the Archive keyboard shortcut it says “do you want to apply this to all conversations selected?” Previously this was instant. Now it says, “We archived some conversations. We’ll do the same for any remaining conversations in a few minutes. This might take longer, depending on how many conversations are selected.” And if you go back to your Inbox they’re still there. A real downgrade!