GeistHaus
log in · sign up

https://sal.dev/feed.xml

rss
30 posts
Polling state
Status active
Last polled May 19, 2026 18:46 UTC
Next poll May 20, 2026 17:48 UTC
Poll interval 86400s
ETag W/"dfc2dc6e85e94f50ae5958725c25af7c"

Posts

Setting Up macOS for a Developer
macosdevelopermacos

In this post, I’ll share the key tools and configurations I use when setting up a new macOS environment for software engineering.

Applications

I’m a fan of Firefox and a bunch of other tools, but I’ll keep this to developer-related tools.

iTerm2

I believe it is better than the built-in Terminal. Probably.

Configure the following shortcuts from this tutorial.

  • ⌥ ⌫ for deleting a word
  • ⌥ ← for going back a word
  • ⌥ → for going forward a word

Set the Login command in profile to open tmux or reuse an existing session. When I accidentally close the window, this keeps me from terminating a long-running program.

tmux attach -t base || tmux new -s base
1Password

Managing passwords and keys is 100x easier with 1Password and they’re more secure than a lot of the other options out there. Their SSH key management and git signing support is a must-use.

Folders ~/Developer

I used to call the folder ~/dev or ~/Development, but if you call it “Developer”, then Apple will reward you with a special folder icon.

Pretty cute Developer folder

~/Screenshots

If you also take a lot of screenshots, you’ll want to see this post about setting up a screenshot folder.

Developer Tools

macOS changed the default from bash to zsh in 2019 so if your previous computer was an Intel-based MacBook you’re in for a very-similar experience.

Oh My Zsh

It is a framework for managing shell plugins.

# .zshrc, line ~73
plugins=(
  asdf
  brew
  git
  z
)
asdf

The one-stop-shop for managing Java/Python/Ruby/whatever versions. If you use Ruby, I recommend enabling the legacy versions so you can use the other version manager .ruby-version files.

# $HOME/.asdfrc
legacy_version_file = yes
Homebrew

It is the most popular way to install command line tools.

Starship

It makes my command prompt look nice.

sal.dev on  main [!?] via 💎 v2.7.8
❯
z

Command line tool for jumping back in to a project you were working on. For example, if I type z ad, it drops me back into the Advent of Coding project.

~
❯ z ad

advent-of-code-2023
❯
Other

I also like to keep this command handy for clearing out git branches that have been merged to main.

git branch --merged main | grep -v "\* main" | xargs -n 1 git branch -d
Addendum

This post was more for me to remember what I use than for others, but I hope some folks found it useful anyway!

https://sal.dev/macos/setting-up-macbook-pro/
Running Rust on Android with UniFFI
androidrustuniffiintroandroid

You don’t want rust in your android, but you might want Rust in your Android.

Background

I like Kotlin, and I’m very impressed with the content being written in Rust. I knew it should be possible to call Rust from my Android app. Because I love fighting with the compiler I wanted to see if I could get it working for fun. (I got it working!) I wrote this blog post so others could try it out, and so I could refer back when I try to do something again in the future.

The star of the show is Mozilla’s UniFFI library that does a lot of the hard work. A high level view is that it generates Rust and Kotlin1 that are made for each other. That way your Kotlin code can invoke the Rust methods without worrying about Foreign Function Interface (FFI) for talking cross-language.

Glossing over a lot of detail here.

The rest of this post will walk through

  • configuring your development environment
  • creating a basic Rust library with UniFFI-generated scaffolding
  • generating Kotlin using UniFFI
  • integrating the Rust and Kotlin in an Android app

I’ll assume you have a basic Rust (via cargo) and Android (via Android Studio) environment installed.

Step 1 - Configure your Rust + NDK environment

This was (I believe) the most annoying part to get right. You can either manually configure the Android Native Development Kit (NDK) or you can use cross that downloads a Docker image that’s ready to go. I’d recommend setting up the NDK locally (builds faster2), but falling back on cross (easier default setup) if you get stuck.

Option A - Use Docker-based cross
  1. Install Docker Desktop, OrbStack, Rancher Desktop, or your favorite tool. If you can run docker run --rm hello-world, then you’re good.
  2. Install cross.
  3. If you’re happy with the minSdkVersion on cross (seen here), you’re done. Otherwise, you’ll need to build new Docker images with the desired Android version (instructions here)
  4. That’s it! Go to “Step 2 - Make a Rust library”.
Option B - Configure Android NDK locally

Open Android Studio, and navigate to SDK Manager > SDK Tools > NDK (Side by Side) as laid out on the Android Developer site.

You can also click "Show package details" to get a specific version.

Locate which NDK version you have…

❯ ls $ANDROID_HOME/ndk
23.1.7779620    25.2.9519653

… and set it to your NDK_PATH environment variable.

❯ NDK_PATH=$ANDROID_HOME/ndk/25.2.9519653

<⚠️> Android replaced libgcc with libuwind in NDK 23 which breaks the compilation step. Fortunately there’s a workaround3 that I’ll summarize. If you’re using NDK 23.x or higher, you’ll either need to use a nightly version of Rust or run the following from your terminal.

# if your NDK version is ≥ 23 run this
# snippet that fixes the "broken" NDK issue
❯ find $NDK_PATH -name 'libunwind.a' | \
  sed 's@libunwind.a$@libgcc.a@' | \
  while read x; do
    echo "INPUT(-lunwind)" > $x
  done

</⚠️>

You’ll be able to see the C libraries for each of the architecture-Android version combinations. I’ve modified the output to be more readable.

❯ find $NDK_PATH/toolchains/llvm -name "*-linux-android*-clang" | sort -r
$NDK_PATH/path/to/aarch64-linux-android33-clang
$NDK_PATH/path/to/aarch64-linux-android32-clang
$NDK_PATH/path/to/aarch64-linux-android31-clang
# ...

I’m going to build for an Android minSdkVersion of 24, so these are the four libraries I’ll use.

❯ find $NDK_PATH/toolchains/llvm -name "*-linux-android*24-clang" | sort -r
$NDK_PATH/path/to/x86_64-linux-android24-clang
$NDK_PATH/path/to/i686-linux-android24-clang
$NDK_PATH/path/to/armv7a-linux-androideabi24-clang
$NDK_PATH/path/to/aarch64-linux-android24-clang

Open (or create) your $HOME/.cargo/config file. Add each of the target linkers. Please note:

  • The path has to be absolute.
  • armv7a’s target name and clang name are different and it is “androideabi” as opposed to “android”.
# ~/.cargo/config
# ...
[target.x86_64-linux-android]
linker = "/Users/sal/Library/Android/sdk/ndk/25.2.9519653/toolchains/llvm/prebuilt/darwin-x86_64/bin/x86_64-linux-android24-clang"

[target.i686-linux-android]
linker = "/Users/sal/Library/Android/sdk/ndk/25.2.9519653/toolchains/llvm/prebuilt/darwin-x86_64/bin/i686-linux-android24-clang"

[target.armv7-linux-androideabi]
linker = "/Users/sal/Library/Android/sdk/ndk/25.2.9519653/toolchains/llvm/prebuilt/darwin-x86_64/bin/armv7a-linux-androideabi24-clang"

[target.aarch64-linux-android]
linker = "/Users/sal/Library/Android/sdk/ndk/25.2.9519653/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android24-clang"

Finally, add the targets to your Rust environment.

❯ rustup target add \
    x86_64-linux-android \
    i686-linux-android \
    armv7-linux-androideabi \
    aarch64-linux-android
Step 2 - Make a Rust library

For our example, we’re going to make a simple library that has two methods: reverse a string (“hello” -> “olleh”) and reverse an integer (123 -> 321).

Let’s start by making the library using cargo.

cargo new reverse-rs --lib

Inside the generated src/lib.rs file, I throw in some (ChatGPT-assisted) Rust code to reverse a string and integer as well as some tests.

# reverse-rs/src/lib.rs
pub fn reverse_string(input_string: &str) -> String {
    input_string.chars().rev().collect()
}

pub fn reverse_integer(input_integer: i32) -> i32 {
    let reversed = input_integer.to_string().chars().rev().collect::<String>();
    reversed.parse::<i32>().unwrap()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_reverses_strings() {
        let result = reverse_string("hello world");
        assert_eq!(result, "dlrow olleh");
    }

    #[test]
    fn it_reverses_integers() {
        let result = reverse_integer(123);
        assert_eq!(result, 321);
    }
}

From the reverse-rs/ folder, run cargo test and make sure everything looks good.

Step 3 - Prepare the Rust for Android

Here’s where the UniFFI magic comes in! We’re going to define our reverse string and integer methods in UniFFI’s special language which we’ll then use to generate both the Rust and Kotlin code.

Update dependencies

Update the Cargo.toml file to look like this.

# reverse-rs/Cargo.toml
[package]
name = "reverse-rs"
version = "0.1.0"
edition = "2021"

[lib]
name = "reverse"
crate-type = ["cdylib"]

[dependencies]
uniffi = { version = "0.23.0" }

[build-dependencies]
uniffi = { version = "0.23.0", features = [ "build" ] }

This snippet does three key things.

  1. Make the library a cdylib crate. I dropped the -rs from the name because hyphens aren’t allowed.
  2. Add uniffi as a dependency.
  3. Add uniffi as a build dependency.
Write the UDL file

UniFFI uses it’s own special UniFFI Definition Language (UDL) for describing interfaces. I made src/reverse.udl.

// reverse-rs/src/reverse.udl
namespace reverse {
  string reverse_string([ByRef] string input_string);
  i32 reverse_integer(i32 input_integer);
};
Write the Rust generator

Create a build file in the the top level folder (i.e. reverse-rs/build.rs) and have it point to the UDL file.

# reverse-rs/build.rs
fn main() {
    uniffi::generate_scaffolding("./src/reverse.udl").unwrap();
}

Add the uniffi::include_scaffolding macro on the top of the lib.rs file, to generate the Rust scaffolding.

# reverse-rs/src/lib.rs
uniffi::include_scaffolding!("reverse");

pub fn reverse_string(input_string: &str) -> String {
// ...
Step 4 - Compile the Rust library

If on step 1 you setup cross use that, or if you went through all the NDK-related steps, use cargo build ....

# reverse-rs/
# if you're using cross (step 1, option A)
❯ cross build --target x86_64-linux-android && \
    cross build --target i686-linux-android && \
    cross build --target armv7-linux-androideabi && \
    cross build --target aarch64-linux-android

# if you have the NDK setup (step 1, option B)
❯ cargo build --lib \
    --target x86_64-linux-android \
    --target i686-linux-android \
    --target armv7-linux-androideabi \
    --target aarch64-linux-android

The end result will be a .so file in your corresponding target/ folder!

# reverse-rs/
❯ for binary in target/*/*/libreverse.so; do file $binary; done
target/aarch64-linux-android/debug/libreverse.so: ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked, with debug_info, not stripped
target/armv7-linux-androideabi/debug/libreverse.so: ELF 32-bit LSB shared object, ARM, EABI5 version 1 (SYSV), dynamically linked, with debug_info, not stripped
target/i686-linux-android/debug/libreverse.so: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), dynamically linked, with debug_info, not stripped
target/x86_64-linux-android/debug/libreverse.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, with debug_info, not stripped

To get these ready for the Android app you’ll need to:

  1. move everything to the appropriate Android ABI directory in a jniLibs/ folder
  2. rename libreverse.so to libuniffi_reverse.so

Here’s a command that will do all of it for you.

# reverse-rs/
❯ mkdir -p jniLibs/arm64-v8a/ && \
  cp target/aarch64-linux-android/debug/libreverse.so jniLibs/arm64-v8a/libuniffi_reverse.so && \
  mkdir -p jniLibs/armeabi-v7a/ && \
    cp target/armv7-linux-androideabi/debug/libreverse.so jniLibs/armeabi-v7a/libuniffi_reverse.so && \
  mkdir -p jniLibs/x86/ && \
    cp target/i686-linux-android/debug/libreverse.so jniLibs/x86/libuniffi_reverse.so && \
  mkdir -p jniLibs/x86_64/ && \
    cp target/x86_64-linux-android/debug/libreverse.so jniLibs/x86_64/libuniffi_reverse.so

Here’s where you’ll be at the end.

# reverse-rs/
❯ tree jniLibs
jniLibs
├── arm64-v8a
│   └── libuniffi_reverse.so
├── armeabi-v7a
│   └── libuniffi_reverse.so
├── x86
│   └── libuniffi_reverse.so
└── x86_64
    └── libuniffi_reverse.so

5 directories, 4 files
Step 5 - Generate the Kotlin methods

Add the following to the bottom of your Cargo.toml file.

# reverse-rs/Cargo.toml
# ...

[[bin]]
name = "uniffi-bindgen"
path = "uniffi-bindgen.rs"

Make the reverse-rs/uniffi-bindgen.rs file.

# reverse-rs/uniffi-bindgen.rs
fn main() {
    uniffi::uniffi_bindgen_main()
}

Then generate the Kotlin code!

# reverse-rs/
❯ cargo run --features=uniffi/cli \
    --bin uniffi-bindgen \
    generate src/reverse.udl \
    --language kotlin

This creates a new file reverse-rs/src/uniffi/reverse/reverse.kt with a ton of boilerplate but also our methods!

// reverse-rs/src/uniffi/reverse/reverse.kt
// ...
fun `reverseString`(`inputString`: String): String {
    return FfiConverterString.lift(
    rustCall() { _status ->
    _UniFFILib.INSTANCE.reverse_b8c9_reverse_string(FfiConverterString.lower(`inputString`), _status)
})
}

fun `reverseInteger`(`inputInteger`: Int): Int {
    return FfiConverterInt.lift(
    rustCall() { _status ->
    _UniFFILib.INSTANCE.reverse_b8c9_reverse_integer(FfiConverterInt.lower(`inputInteger`), _status)
})
}
Step 6 - Create the Android app

For demonstration purposes, I’m going to make a new project via Android Studio > File > New Project… and use the “Empty Activity” template, but I’m assuming you’re familiar with Android development and can make your own choices.

Add the JNA dependency

The UniFFI library depends on Java Native Access (JNA), so add the @aar dependency.

// reverse-android/app/build.gradle
// ...
dependencies {
  // ...
  implementation "net.java.dev.jna:jna:5.13.0@aar"
  // ...
}

Make sure to sync your Gradle files.

Copy over generated files
  1. Move the reverse-rs/jniLibs/ folder into app/src/main/.
  2. Move the reverse-rs/src/uniffi/ folder into app/src/main/java/.

You should end up here.

Use the generate Kotlin library

Your IDE will now autocomplete, and you’ll have access to uniffi.reverse.reverseString and uniffi.reverse.reverseInteger. Here’s what my class looks like.

class MainActivity : AppCompatActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    // ...
    val helloWorld = uniffi.reverse.reverseString("Hello World!")
    val oneTwoThree = uniffi.reverse.reverseInteger(123)
    textView.text = "'Hello World!' & '123' becomes '$helloWorld' & '$oneTwoThree'"
  }
}

Run it and 🤞🏼 that you don’t have any errors!

We did it!

Congratulations! You’re running Rust in Android!

Bonus - Suggestions and Resources

There are a few tweaks that you can do and other things I came across that you might find interesting/helpful.

Optimize with --release

When you cross build or cargo build, adding the --release flag really cuts down on size (but it ~doubles the build time).

❯ ls -lh target/*/*/libreverse.so
 37M target/aarch64-linux-android/debug/libreverse.so
4.2M target/aarch64-linux-android/release/libreverse.so

 35M target/armv7-linux-androideabi/debug/libreverse.so
3.5M target/armv7-linux-androideabi/release/libreverse.so

 34M target/i686-linux-android/debug/libreverse.so
3.5M target/i686-linux-android/release/libreverse.so

 37M target/x86_64-linux-android/debug/libreverse.so
4.0M target/x86_64-linux-android/release/libreverse.so
Move uniffi-bindgen to its own crate

If you want to iterate faster on your Rust + Kotlin, you’ll need to have the uniffi-bindgen logic in it’s own crate. Otherwise, you’ll hit this error.

Helpful Docker guide

Guillaume Endignoux’s very thorough blog post, Compiling Rust libraries for Android apps: a deep dive, was super helpful for me. It is much more comprehensive that my post.

More than just UniFFI

There is a neat alternative to UniFFI called Diplomat for which Mark Hammond(from Mozilla) wrote a nice comparison, Comparing UniFFI with Diplomat.

I’m personally excited for uniffi-kotlin-multiplatform-bindings which is still new-ish but could really move the Kotlin ecosystem forward.

2023-07-05 Update

Lammert Westerhoff helpfully pointed out that if you run cargo build with the --lib flag (in step 4), the subsequent bin additions to the Cargo.toml (in step 5) won’t break future attempts at cargo building. I’ve updated the code block in step 4 to include the --lib flag.

heinrich5991 also mentioned something similar earlier, but I did not apply their feedback to my blog post. 🤦

Thank you to my friends

Special thanks to my friends who helped me with this post.

  • Richard Moot - workshopping the title and hook
  • Gary Guo - correcting my poor grammar and helping with the flow
  • Ray Ryan - trying the recipe out, finding quite a few issues, and letting me know about them before I embarrassed myself
Let me know what you think!

Please feel free to reach out on email, the Fediverse @sal@fedi.sal.dev, or Twitter @SalTesta14.


  1. … and other languages

  2. On my 2016 MacBook Pro, the cargo build took ~1.5 minutes while the cross build took ~6 minutes. 

  3. Thank you to Tilmann Meyer in this GitHub thread for laying out the problem! Thank you to ssrlive and Caleb James DeLisle for the fixes

https://sal.dev/android/intro-rust-android-uniffi/
Set up a free Mastodon-ready server in under 20 minutes with Pleroma + Fly.io
pleromaflyfediversefediverse

Run a Pleroma server on Fly.io’s free tier. Pleroma can use Mastodon apps and federate with Mastodon servers.


I wanted to run my own ActivityPub server. I like self hosting. I don’t like spending money. For a while I had tried setting up Mastodon but I didn’t get super far. I had a pretty rough cold for the last two days and suddenly I had a free schedule to figure this out. I ended up pounding my head on the wall a bit, but here we go!

Instructions

This assumes you have the flyctl (aka “fly”) command line tool and you’ve already run fly auth signup or fly auth login. Also, this uses the command line (Terminal on macOS).

1. Initiate the Fly.io app 🐣

Create an application. We’ll be using Pleroma because it’s light-weight enough to run in the free tier constraints. When prompted with the command below, you’ll need to launch:

  • 🐘 with a Postgres instance
  • 🧑‍💻 “Development - Single node” configuration
  • 👎 without Redis
  • 🏃 Yes, deploy! It’ll take ~2 minutes.
fly launch --image salvatoret/fly-pleroma:v2.4.4

This will generate a fly.toml. When you run the fly command make sure the fly.toml is in the same directory.

Save the Postgres configuration information! Once you’ve done that, treat yourself with a “hello world”.

fly open

If you don’t want to purchase a domain, the one you see in the browser will work (e.g. your-app-1234.fly.dev).

You should see something like this.

2. (Optional) Configure DNS and Certs 🏷

If you’re happy with your-app-1234.fly.dev, go to the “Create storage” step.

Create your (one free) IPv4 and IPv6 addresses.

fly ips allocate-v4
fly ips allocate-v6

Add the IPv4 and IPv6 address to the A and AAAA records respectively to your DNS. Do not proxy.

Here's my DNS. Your values should be different.

fly certs create <your-domain>

Whenever you want to see the status run fly certs check <your-domain>. This should go quickly, but you can continue with these instructions on if you’re in a hurry.

3. Create storage 📦

The data in the volume stays between deployments. It needs to be in the same region as step 1.

fly volumes create pleroma_storage --size 1

In your fly.toml file, add the following environment variable and volume information.

[env]
  PLEROMA_CONFIG_PATH = "/mount/config/config.exs"

[mounts]
  source = "pleroma_storage"
  destination = "/mount"

Publish the changes.

fly deploy
4. Generate your configs 🧙

Connect to your server as the pleroma user.

fly ssh console --command "su pleroma --shell /bin/bash"

You’ll need to make some choices about your server. Have the following Postgres configuration info ready.

  • hostname of your database (e.g. your-app-1234-db.internal)
  • password used to connect to your database (e.g. ao09u87ao0e9u)
# from the `fly shell console`
/opt/pleroma/bin/pleroma_ctl instance gen \
  --output /mount/config/config.exs \
  --output-psql /tmp/setup_db.psql \
  --dbname pleroma \
  --dbuser postgres \
  --rum N \
  --uploads-dir /mount/uploads \
  --static-dir /mount/static \
  --listen-ip 0.0.0.0 \
  --listen-port 8080
# still in the `fly shell console`
psql -f /tmp/setup_db.psql $DATABASE_URL
5. Configure the database 🗄

Add the ssl: false and socket_options: [:inet6] to the Pleroma.Repo section using vim. You’ll mainly need i to get to insert mode, arrow keys to navigate, and write/quitting vim by hitting the ESC key and then typing :wq.

# from the `fly shell console`
vim +27 /mount/config/config.exs

It should look like this before…

config :pleroma, Pleroma.Repo,
  adapter: Ecto.Adapters.Postgres,
  username: "postgres",
  # ...

… and this afterwards.

config :pleroma, Pleroma.Repo,
  adapter: Ecto.Adapters.Postgres,
  ssl: false,
  socket_options: [:inet6],
  username: "postgres",
  # ...

Once you’ve updated the config.exs file, run the database migration and exit the shell.

# still in the `fly shell console`
/opt/pleroma/bin/pleroma_ctl migrate
exit

Download and save your config.exs in a safe place.

flyctl ssh sftp get /mount/config/config.exs config.exs
6. Launch the (empty) Pleroma instance! 🚀

Add START_PLEROMA to the environment variable of your fly.toml.

[env]
  # ...
  START_PLEROMA = "1"

Push up the changes and see the progress!

fly deploy
fly open

Check out those defaults!

7. Add yourself and configure 🪞

ssh back into the app and add your account as an admin.

fly ssh console --command "su pleroma --shell /bin/bash"
# from the server
/opt/pleroma/bin/pleroma_ctl user new <username> <email-address> --admin
exit

You’ll get a reset password URL. If you’re setting up your own domain, and your certs have not been issued fly certs check <your domain>, then you might need to manually update the URL to use your Fly.io domain (your-app-1234.fly.dev) for now.

8. Celebrate! 🎉

You have a running Pleroma instance! Feel free to follow me by putting @sal@fedi.sal.dev in the Pleroma search box.

Here's what I see after configuring my lonely server.

You can also use Fedifinder to get your Twitter contacts and populate your feed.

It's much better with content.

Operating Notes

A lot of these are codified in the config.exs

  • “Open registration” is on by default. I recommend turning it off first.
  • Fly.io limits you to 1 volume per app. You can’t run the app in multiple regions because of the volume.
  • The volume is mounted at /mount.
    • static files are in /mount/static.
    • uploads are in /mount/uploads.
    • configs are in /mount/config/config.exs.

Resources I found helpful Bonus

If you want more control over your Docker image, you can clone it and run fly launch from inside the folder.

Bonus Bonus

If you want to host Pleroma at subdomain.example.com but want to use yourname@example.com, here’s the guide. If you get it to work, let me know.

Extra credit

There’s probably a way to use s3fs with Cloudflare’s R2 (that comes with 10GB free) instead of using a Fly.io volume. I tried but couldn’t get it working. If you get that to work, definitely let me know.

https://sal.dev/fediverse/running-pleroma-on-fly-io/
4 macOS Screenshot Tricks To Impress Your Co-Workers
macosimagemagickscreenshotmacos

Here are 4 tricks that I’ve found to make my life easier and help me communicate better with my co-workers.


1. Store screenshots in a folder on your Dock 📂

How do you keep your desktop from being overrun with screenshots? The macOS default is to just dump the images next to everything else you store. If, like me, you take a lot of screenshots, your desktop can quickly fill up.

A mess.

The solution: store them in a Dock folder like Steve Jobs intended.

So organized.

(This one is from a tweet I wrote a year ago.)

  1. Create a “Screenshots” folder Mine is next to my "Documents" folder.
  2. Put the folder on your Dock Next to "Applications" works well.
  3. Open screenshot settings (⌘ ⇧ 5) It's the little bar at the bottom.
  4. Set it to your new “Screenshots” folder It's the little bar at the bottom.
  5. Right click the folder to make it more usable. Sort by “Date Added”, and select “View content as: Fan”. You might need to click "Date Added" twice.
2. Remove screenshot shadow 🕶

How can you make full-app screenshots (⌘ ⇧ 4 then space bar) only include the relevant content without the extra gradient border? If your plan is to share the image, the person receiving the image probably doesn’t care about the feeling that the screen is floating. Remove the shadow!

It looks nice here but silly when you drop it in Slack.

Run the following command from Terminal (Applications -> Utilities -> Terminal).

defaults write com.apple.screencapture disable-shadow -bool true; killall SystemUIServer

To bring back the shadow, change true to false.

defaults write com.apple.screencapture disable-shadow -bool false; killall SystemUIServer
3. Take screenshots as JPEGs, not PNGs ⚖️

How can you make your screenshots take up less space? If you’re sharing the images to places with resource constrains or just want faster uploads, you’re going to want the images to be smaller. One easy way is to change your default screenshot format from .png to .jpg.

You could always use more cats.

Run the following command from Terminal.

defaults write com.apple.screencapture type jpg; killall SystemUIServer

If you change your mind later, you can always go back with.

defaults write com.apple.screencapture type png; killall SystemUIServer

I took a screenshot of a Google image search for “cat” and the PNG was 5.7MB while the JPEG was 1.4MB. I got less impressive results when I took screenshots of mostly solid color screens.

⚠️ The main downside of this setting is that transparent parts of screenshots will be turned black.

4. Show side-by-side comparisons with ImageMagick 🧑‍🤝‍🧑

How can you easily show the before-and-after of an image? You could use Photoshop or GIMP, but that takes a fair amount of time. If you’re comfortable enough to use Terminal, you can very quickly make a side-by-side with ImageMagick.

Winston looking right. Winston looking left. (png, 614KB)

ImageMagick has a helpful set of image manipulation tools including montage. If you don’t yet have Homebrew installed, I recommend doing that first. You can then install ImageMagick by running brew install imagemagick from Terminal.

To make a 2x1 image with a 20 pixel buffer around each image and a transparent background, run the following from Terminal (with the <entries> changed out). You’ll want the output to be a .png if you want a transparent background.

montage <image1> <image2> -tile 2x1 -geometry +20+20 -background none <ouput.png>

Of course you can change to -geometry +0+0 if you want no space or have the output with a .jpg extension if you want a smaller image size.

Winston looking right. Winston looking left. (jpg, 136KB)

Instead of typing out the <image> file names, you can also drag the file onto the Terminal window.

Typing out the whole path is less fun.


If you liked this, please tell a friend. If you hated it, keep it to yourself. Thanks to Richard for feedback on this before I sent it out.


Update (2022-06-16T14:25:03-0700): The Hacker News post has other hot tips that will further impress your co-workers. I’ve also corrected some typos the internet helpfully pointed out. 😅

https://sal.dev/macos/macos-screenshotting-tips-and-tricks/
How I added the Kotlin Interactive Shell to Homebrew (and you can too)
homebrewkotlinjavaopen-source

I really like ki, but I didn’t want to run the shell script every time. Other folks felt the same, so I ended up writing this Homebrew formula. Here’s an abbreviated version of how I did it.

How does Homebrew work?

Homebrew is a package manager and artifact builder. All of the programs/tools/packages that core Homebrew knows about are located at brew --repo homebrew/core repository.

For me, it's "/usr/local/Homebrew/Library/Taps/homebrew/homebrew-core"

The repository contains instructions (called formulae) for the Homebrew build servers to make and test the programs. Once the build servers build the formulae, they store the result as “bottles”. When you run brew install <formula>, you download the bottle along with any other dependency.

Creating the Ki formula

Getting started is super easy because Homebrew has a command to generate a template formula. In my case, I ran the following by pointing at the released version of ki.

brew create https://github.com/Kotlin/kotlin-interactive-shell/archive/refs/tags/v0.3.3.tar.gz

… which resulted in (more-or-less) this:

class KotlinInteractiveShell < Formula
  desc "Kotlin Language Interactive Shell"
  url "https://github.com/Kotlin/kotlin-interactive-shell/archive/refs/tags/v0.3.3.tar.gz"
  sha256 "46913b17c85711213251948342d0f4d0fec7dc98dd11c1f24eedb0409338e273"
  license "Apache-2.0"

  def install
    # ...
  end

  test do
    # ...
  end
end

I renamed the generated kotlin-interactive-shell.rb file to ki.rb and changed the KotlinInteractiveShell class name to Ki because I wanted the command line tool to be ki.

Build Ki locally (think globally)

The only build dependency is Maven, and the runtime is Java, so those are the only two packages I needed to call out.

depends_on "maven" => :build
depends_on "openjdk"

I then took the build instructions from the ki README and plopped them into the install block. I put the Java artifact into the libexec folder to avoid name collisions, which I learned about from this StackExchange post.

Homebrew has some nice pre-built commands including one for wiring up JAR files, so making the command line script is also very little work.

def install
  system "mvn", "-DskipTests", "package"
  libexec.install "lib/ki-shell.jar"
  bin.write_jar_script libexec/"ki-shell.jar", "ki"
end

To test out my script, I made my laptop pretend it was a Homebrew build server, and built the package.

brew install --build-from-source ki

I didn’t actually write the correct code the first time, so I ran the build command with an additional --debug flag that let me try things out in the build environment. I spent most of my time doing this.

Test it out

The test block of the formula is a very basic check to see if the tool runs at all. I tried to not over-think it.

When you run the ki shell, and then close it, this is the output.

$ ki
ki-shell 0.3/1.4.32
type :h for help
[0] :q

Bye!

For my test, I just wanted to make sure the initial “ki-shell” and final “Bye!” message appeared.

test do
  output = pipe_output(bin/"ki", ":q")
  assert_match "ki-shell", output
  assert_match "Bye!", output
end

I then tested with brew test ki. My first draft of the test also didn’t work, so I used the --debug flag here too.

When the test worked, I made sure the automated auditor passed.

brew audit --strict --online ki
Open the pull request

Last, but not least I opened up a pull request against the Homebrew/homebrew-core repo and watched the tests run.

Green tests are the best tests

Once all of the tests passed, and I got sign-off from the reviewers, the formula was merged, and Homebrew built ki for the various macOS environments.

I ran brew install ki, and was very excited to see it work.

Look around and borrow

Most of everything I did, I figured out by looking at other formulae, trial, and error. If you ever decide to write a formula, you might want to do the same.

https://sal.dev/open-source/adding-kotlin-interactive-shell-to-homebrew/
How to make a live wallpaper on Android
androidintrowallpaperandroid

Let’s create a very very very basic live wallpaper on Android.

Background on Backgrounds

Android has had live wallpapers since Android 2.1 (Eclair, API 7) which was released in January of 2010 1. If you were using Android back then, you might have remembered this gem of a wallpaper.2

Gosh it was so darn cool.

Step 0 - Prep an Android project

Create an Android project using the creation wizard. I used all of Android Studio’s default settings with Kotlin, and it went well. If you already have a project lying around, you could probably use that too.

You know the drill

Step 1 - Initialize the WallpaperService

The main class that runs the live wallpaper is the WallpaperService.Engine which is an inner class of WallpaperService.

Start by making an implementation of WallpaperService with an inner class that implements WallpaperService.Engine. We’ll come back to it later.

class MyWallpaperService : WallpaperService() {
  override fun onCreateEngine(): Engine = WallpaperEngine()

  inner class WallpaperEngine : WallpaperService.Engine() {
  }
}
Step 2 - Modify your various XML files

For your app to provide the phone with your live wallpaper, you’ll need to make a resource for it and declare your WallpaperService in your AndroidManifest.xml file.

Make an XML file in your resources folder main/res/xml/my_wallpaper.xml. It only really needs a <wallpaper /> tag, but this is where you can define a thumbnail, settings activity, and other neat things.

<?xml version="1.0" encoding="utf-8"?>
<wallpaper />

Jump over to your AndroidManifest.xml file. You’ll need to let Android know that you’re using the live wallpaper API.

<?xml version="1.0" encoding="utf-8"?>
<manifest>
  <!--  ... some stuff ...  -->
  
  <uses-feature
      android:name="android.software.live_wallpaper"
      android:required="true" />

  <!--  ... other stuff  -->
</manifest>

In that same Android Manifest, you’ll also need to declare your WallpaperService to give it permissions to do wallpaper-y things.

<?xml version="1.0" encoding="utf-8"?>
<manifest>
  <application>
    <!-- ... some stuff ... -->

    <service
        android:name="MyWallpaperService"
        android:enabled="true"
        android:permission="android.permission.BIND_WALLPAPER">
      <intent-filter>
        <action android:name="android.service.wallpaper.WallpaperService" />
      </intent-filter>

      <meta-data
          android:name="android.service.wallpaper"
          android:resource="@xml/my_wallpaper" />
    </service>

    <!-- ... other stuff ... -->
  </application>
</manifest>
Step 3 - Launch your empty wallpaper

Now that the system knows about your fun (currently blank) wallpaper, it’s time to get it going! To wire it up, you need to start the WallpaperManager.ACTION_CHANGE_LIVE_WALLPAPER action with your WallpaperService. You can do this from within an OnClickListener, or anywhere that can start an Activity.

val intent = Intent(WallpaperManager.ACTION_CHANGE_LIVE_WALLPAPER)
intent.putExtra(
    WallpaperManager.EXTRA_LIVE_WALLPAPER_COMPONENT,
    ComponentName(this, MyWallpaperService::class.java)
)
startActivity(intent)

It should look something like this.

This could be the end of the tutorial if you like blank wallpapers.

Step 4 - Have fun with ✨art✨

Go back to your WallpaperService and WallpaperService.Engine implementation from Step 1. We’ll just make changes in these classes for the rest of this tutorial.

Solid Background

To make a background, get the canvas, draw on it, and then post the update. In our first example, override the WallpaperService.Engine#onSurfaceCreated method.

class MyWallpaperService : WallpaperService() {
  override fun onCreateEngine(): Engine = WallpaperEngine()

  private inner class WallpaperEngine : WallpaperService.Engine() {

    override fun onSurfaceCreated(holder: SurfaceHolder) {
      val canvas = holder.lockCanvas()
      // ... do ✨art✨ stuff
      holder.unlockCanvasAndPost(canvas)
    }
  }
}

To set everything as one color, draw a Paint object with fill style and color of your choice.

override fun onSurfaceCreated(holder: SurfaceHolder) {
  val canvas = holder.lockCanvas()
  val paint = Paint().apply {
    color = Color.CYAN
    style = Paint.Style.FILL
  }
  canvas.drawPaint(paint)
  holder.unlockCanvasAndPost(canvas)
}

I would maybe pick a different color if I were you.

It’s (a)live!

To show the wallpaper updating as we touch our device, we’re going to override the WallpaperService.Engine#onTouchEvent callback. We want to only update when we actually touch our device and not when we also lift our finger up, so we’ll filter for the MotionEvent.ACTION_DOWN action.

class MyWallpaperService : WallpaperService() {
  override fun onCreateEngine(): Engine = WallpaperEngine()

  private inner class WallpaperEngine : WallpaperService.Engine() {

    override fun onTouchEvent(event: MotionEvent?) {
      // on finder press events
      if (event?.action == MotionEvent.ACTION_DOWN) {
        // get the canvas from the Engine or leave
        val canvas = surfaceHolder?.lockCanvas() ?: return
        // ... do ✨art✨ stuff
        // update the surface
        surfaceHolder.unlockCanvasAndPost(canvas)
      }
    }
  }
}

To show the touch events are working, let’s change the color to a random one every time. Here’s a quick and easy way to pick a random color between #000000 and #FFFFFF.

// value between #000000 and #FFFFFF
val randomColor = Random.nextInt(16_777_216)
  // convert it to hex/base 16
  .toString(16)
  // gurantee it's six characters long
  .padStart(6, '0')
  // still need to prefix with a '#',
  // but you get the idea.

And when we put it all together…

class MyWallpaperService : WallpaperService() {
  override fun onCreateEngine(): Engine = WallpaperEngine()

  private inner class WallpaperEngine : WallpaperService.Engine() {

    override fun onTouchEvent(event: MotionEvent?) {
      if (event?.action == MotionEvent.ACTION_DOWN) {
        val canvas = surfaceHolder?.lockCanvas() ?: return

        val paint = Paint().apply {
          val randomColor = Random.nextInt(16_777_216)
            .toString(16)
            .padStart(6, '0')
          color = Color.parseColor("#$randomColor")
          style = Paint.Style.FILL
        }
        canvas.drawPaint(paint)

        surfaceHolder.unlockCanvasAndPost(canvas)
      }
    }
  }
}

… we get a live wallpaper! Here’s a bunch of colors that come up randomly on my first few taps.3

So many colors!

Keep going!

Now that you have a basic live live wallpaper, try things out! Lars Vogel also wrote a nice tutorial which I learned from before writing this. Their post has more on using a Handler and Runnable which will be useful if you want your live wallpaper to move continuously. If you end up making anything (including this example), please record it and share it with me on Twitter.

Thanks for your time, and I hope you enjoyed this post!


  1. The Wikipedia article for Android incorrectly lists Android 2.0 (API 5) as the origin of the live wallpaper. However, if you read the release notes for Android 2.0 (API 5) and Android 2.1 (API 7), you’ll see what I’m talking about. 

  2. I couldn’t find an emulator running Android 2.1, but I did find Nexus Revamped, a strikingly similar wallpaper. 

  3. To make a tiled image like this, I installed ImageMagick and ran the following montage command: montage folder-with-screenshots/* -tile 7x4 -geometry +40+40 -background none tiled-wallpaper-montage.png 

https://sal.dev/android/android-live-wallpaper/
Powers of Tau Ceremony
zcashcryptocrypto

I got to take part in a crypto ceremony that I heard about on Radiolab. It was surprisingly easy to participate.

What is the Powers of Tau Ceremony?

I won’t pretend to understand. The digital currency group, The Zcash Foundation, posted an announcement late last year that explains it, but the main thing I got was this.

The best part is that the Powers of Tau […] can scale to hundreds (or even thousands) of participants. As the number of participants grows, it becomes implausible that all of them could be compromised.

Basically they were looking for a lot of people to do something.

Why did I do it?

Early in 2017, my friends Corinne, Jeremy, and Lexi, and they told me about this cool Radiolab podcast episode called “The Ceremony”. It was a very intriguing story, but apart from looking up Zcash, I didn’t think much of it.

Then, earlier this week, my co-worker Alok emailed out about the second iteration of “The Ceremony”. He was participating, and he showed how easy it was: you download a file, run a script, and upload the result. If anyone wanted to participate they just needed to email a group and let the group know.

How did I get involved?

I fired-off a quick email.

I’d like to help out. I’m available any day of the week except Thursdays.

Less than an hour later, I got an email from one of the organizers, Jason.

Great. I do actually have a slot this Friday (16th) at the moment. Would that work for you? What time zone are you in? We normally give each participant 24 hours from the point they receive the challenge file. I will send you further instructions when it’s your turn.

On Friday, I got an email with setup instructions and a link to the site where I downloaded the challenge and needed to upload the response.

this is what secrecy looks like

Then I got started.

Setting-up Hardware

I installed “Raspbian Strech with Desktop” via torrent (sha 64c4103316efe2a85fd2814f2af16313abac7d4ad68e3d95ae6709e2e894cc1b) onto my Raspberry Pi 3.

~ $ openssl sha -sha256 ~/Downloads/2017-11-29-raspbian-stretch.zip
SHA256(/Users/sal/Downloads/2017-11-29-raspbian-stretch.zip)= 64c4103316efe2a85fd2814f2af16313abac7d4ad68e3d95ae6709e2e894cc1b

imaging the SD card with Etcher

Once I formatted the SD card, I enabled SSH’ing into my RPi.

~ $ cd /Volumes/boot/
boot $ touch ssh
Installing Software

I installed Rust onto the Raspberry Pi…

pi@raspberrypi:~ $ curl https://sh.rustup.rs > install-rust.sh
pi@raspberrypi:~ $ openssl sha256 install-rust.sh
SHA256(install-rust.sh)= 22aa1f7f4c4b9be99a9d7e13ad45b2aec6714165a0578dd5ef81ca11f55ea24e
pi@raspberrypi:~ $ bash install-rust.sh

… and cloned powersoftau (commit d47a1d3d1f007063cbcc35f1ab902601a8b3bd91) and downloaded my challenge file via wget.

Next, I cd‘d to the powersoftau directory and started the program.

cargo run --release --bin compute

This downloaded and installed everything I needed. Once all the network requests were done, I unplugged my router from the wall, so I could still SSH into my RPi, but there was no internet connection (fortunately my roommates were out of town).

Less than two minutes into running, the Rust program crashed unceremoniously with a when the OS decided it had enough.

Killed

I threw a similar setup on my laptop (MacBook Pro (15-inch, 2016) running 10.13.3) with the same version of Rust and the GitHub repo. I turned the internet connection back off on my laptop and disabled my router.

I covered the laptop in a silver mylar blanket. I don't think it made anything more secure, but it made me feel safer.

Running the Program

To get entropy for the program, I went to the local transit station (16th/Mission BART) and asked people for random numbers. Only a handful of people were willing to talk to me, so I eventually resorted to messaging a bunch of my friends saying “Please send me a number” over Signal and Facebook Messenger (using the Signal option). I also went karaokeing, and I added some of the songs as well.

I think this counts as entropy.

The program took a few hours to run and resulted in this.

Writing your contribution to `./response`...
Done!

Your contribution has been written to `./response`

The BLAKE2b hash of `./response` is:
        1f65d9db a726e65f 96e97235 3eb58707
        48bf26e2 d04575b4 e2f95cd6 5ce4fb65
        c7157dfe 497559b9 bd8f453a 6fbe1c68
        daced14e 09e51975 64773fdb 437d8ac7
Thanks

Thank you to Alok for telling me about this and to all of the people who gave me seed numbers for the ceremony: Alen, Amod, Annirudh, Anton, Axel, Christian, Conor, Corinne, Hailey, JB, Katrina, Leila, Matt, Maximillian, Mike, Mike, Reva, Waseem, and Will.

https://sal.dev/crypto/powers-of-tau/
Updating Jekyll Formats
hackjekyllmediatorhack

It’s 2017; my site needed to stop looking so 2013.

Thank you, Andrew

Three and a half years ago, my buddy who is into design, Andrew, had a pretty swanky personal website. I thought it looked nice, so with his permission, I forked it, and pretty much took out his content and added my own.

This is what swank looks like.

At first I was proud of it, and tried to add content to it. I even wrote a post on how to change the keyboard icon which still gets most of the traffic to this site.

I don't know what sharebutton.to is, but robots like it.

Eventually, I lost interest and stopped paying attention to the site. Side projects that I had finished or abandoned were still featured front and center. Two and a half years after I graduated, it still said I was a Senior in college.

Enter, Mediator

All the cool kids use the publishing platform, Medium, these days, or they were until Medium started getting a bit more flack for laying-off a third of their company. I don’t know if it’s still cool, but I’m hoping someone will tell me. Regardless, a few of my friends started exporting their work from the site and started looking for a new place to play ball. This made me wonder if it would be feasible to make something that looked as polished (cool) as Medium but hosted easily.

This is what cool looks like.

I was already using Jekyll (site building) on GitHub Pages (free hosting if you know how to use git) because that’s what Andrew used; I wanted my site to look more like Medium because that’s the new standard for what looks good; and I wanted to exert minimal effort because I’ve demonstrated to myself that I spend much time on this kind of thing (yet).

It's the top two results!

The top results were Mediator, and a copy of Mediator that used Google’s fancy new caching thing-a-ma-bob. I could do almost no work and use the first option, so we had a winner!

This is what winners look like.

Mostly Seamless

For the most part, moving between Jekyll projects went off without a hitch. There were a few minor problems: the URL structure was different and I didn’t want to make links to my site break (if they exist), and images in posts weren’t centered by default. Five minutes of work later, one scare with CNAMES on GitHub pages, and I was all setup.

My knee-jerk reaction is that this is a pretty good configuration, and I hope it inspires me to write more posts this year.

https://sal.dev/hack/mediator-jekyll-blog/
Customizing Your Mac Input Source Icon
input sourcehackflaghack
Motivation

Pair programming is pretty common around Square, and I’ve had the fortunate experience of pairing with my manager, Xavier. Now, if (you have a Mac and) you’ve ever had to type in a different language, you know that there is a flag in the top right corner that signifies the layout. Qwerty The U.S. traditional Qwerty layout that we all know and love.

Dvorak A layout for people who want to try new things.

Pinyin The layout I had to use for the one semester I took Chinese.

Colemak The layout that I never heard of before Square and a layout that pretty much only Xavier uses.

This brings us back to pair programming. Xavier is very proficient with his keyboard layout and added it to the configurations. When I first the little ‘CO’ in the top corner, I joked that it stood for ‘communist’, and from then on, it was referred to as Xavier’s communist layout. To drive the point home, I changed the display from this… … to this …

How to make a custom layout Step 1: Make an icon.

Select an image for your layout, and visit iConvert. I tried their software with no luck, but you might have a better outcome. Browse to your image and hit convert. Once it does its magic, click the “Download .icns” button. Download icns

Step 2: Make the keyboard layout

Download the Unicode Keyboard Layout Editor, Ukelele (version 2.2.4 worked for me), and open it. Once you have it open, make sure your keyboard layout is the one you want to replicate.

Ukelele's icon is a ukulele

If it isn’t you can change it by navigating to  > System Preferences > Language & Text > Input Sources and checking the box for the layout you want. Then, click the layout icon in the top right corner of your screen and select the one you want.

check language

In Ukelele, select File > New From Current Input Source which should open a new keyboard window with your current keyboard layout.

Then, set your icon file by going to Keyboard > Attach Icon File… and layout name by going to Keyboard > Set Keyboard Name….

Finally save your layout by going to File > Save As Bundle….

save as bundle

Step 3: Use the layout

To install the layout, take your newly minted .bundle file and store it in your Library/Keyboard Layouts folder. If you can’t find this folder, go to Applications > Utilities > Terminal and paste this line in:

open ~/Library/Keyboard\ Layouts/

open image library

Once you have your bundle in the layout folder, restart your computer, and your new layout will be listed under  > System Preferences > Language & Text > Input Sources. Check your custom keyboard, select it from the top corner.

select layout

Step 4 (optional): Share

When you share your layout bundle, make sure to upload it to Dropbox or Skydrive and send the link to the bundle. I tried emailing the bundle and Gmail striped-out some important piece that prevented it from working.

Here is the Communist Colmak and Jedi Dvorak that I made for Xavier and this blog post respectively.

https://sal.dev/hack/customizing-mac-input-source-icon/
Eventful First Sunday in San Francisco
Bay to BreakerIke's SandwichesBARTMOMASFsummer-13
Good Morning

Having gone to bed early, David and I get up with no problem and head on down towards Market Street around 8:15AM. Google Maps informs us that our BART (Bay Area Rapid Transit) train will arrive at 8:29AM and will whisk us over to Akeem’s place where we’ll meet up with some other people before heading over to a parade. I took a good long glance at the map, and then cleared my phone away as we descend the escalator steps to the train platform. At 8:29AM, a train arrives that we think is our train and with inadequate time to verify, we hustle and get on the train. After a minute and a half of tracing our fingers over the color-coded map and mumbling to ourselves, we come to the correct conclusion that this is indeed our train and realize that the old couple a few yards away who went unnoticed probably think we are crazy. It didn’t matter because we were victorious.

Parade

After everyone arrived at Akeem’s apartment, we grab a Lyft to Alamo Square to watch the Bay to Breakers “race”.

Dennis leads the way

The Parade

While I’m positive that the first people to run in this event do indeed treat it as a foot race, the remaining 99.9% of people are clearly not in a hurry. Everyone was wearing costumes with a San Francisco spectrum of ideas: there were people in Victorian Era outfits, a guy dressed as though he was in a hot air balloon, Teenage Mutant Ninja Turtles, naked dudes, a surprisingly large number of lifeguards, pirates, sharks, Cards Against Humanity, Ghost Busters, and many cross dressing variations on childhood TV shows.

Street View We camped out on that hill in the background.

Ghost Busters There is something strange in this neighborhood.

Some semblance of a band Rag-tag band

Cymbal Monkeys Some cross between clever and creepy

Party Scanner “I’m sorry sir, I can’t let you through here without the proper amount of party.”

Group Shot Hey, hey, the gangs all here.

Food and the Park

Ike's Place We hit up this super-tasty sandwich shop called Ike’s Place (no relation to a Mike, unfortunately). I got a sandwich called “Name of Girl I’m Dating”, and it was fantastic. We got out of Ike’s just as half of the city decided to show up, and we headed to Dolores Park to enjoy our lunches and hung out for a while.

SF MOMA

My friend Nathan joined the group at the park a bit after lunch, and he, my roommate David headed over to check out The San Francisco Museum of Modern Art before it closed for two and a half years. David paying the fare I believe this is a picture of the first time David used public transit in San Francisco.

Nathan Crossing At this point I realized candid photos weren’t easy/cool. Unlike the way Nathan dresses. Nathan is always cool.

Scroll Art The scrolls were pretty sweet and would make quite the discussion piece for anyone’s living room.

Black Canvas If you look really close, you can see the eagle.

Toilet Art Fortunately, SF MOMA had restroom facilities in the middle of a gallery.

And thus concluded my first full day in San Francisco for the summer.

https://sal.dev/summer-13/eventful-first-sunday-in-San-Francisco/