Aerospace as Windows Manager for OS X

Recently I discovered Aerospace as a Window Manager. This is the first manager I really like. Tried loop, Rectangle and several others.

Aearospace just feels right when your a (Neo)Vim user. Using Alt + VIM keys to navigate the windows. Please checkout this youtube video to get an idea how it works.

What I didn't like is that everything was tiled directly.
By default I would like to keep the OS X Behaviour of the windows. (I'm very used to it).

To prevent Aerospace from tiling new Windows I let them float by default. (default OS X Behaviour)
With Alt-F when I can toggle the float status to tiling (and back).

I'm using the default configuration file. With the following adjustments
I added the following line to the bottom of the configuration file:

[[on-window-detected]]
run = 'layout floating'

Then I added two shortcuts to make unfloat / floating easier. (Remember to remove the other alt-f / alt-shift-f bindings). Also add a fullscreen toggle key.

alt-f = "layout floating tiling"
alt-shift-f = "fullscreen"

Further I added the tool Yanky Borders to show a nice border around the active windows. Which is nice to know where the keyboard focus is.

after-startup-command = [
  'exec-and-forget borders active_color=0xffFF4488 inactive_color=0xff444444 width=5.0'
]

Further I added some other visual improvements, like adding some padding in between windows.

inner.horizontal = 10
inner.vertical =   10
outer.left =       10
outer.bottom =     10
outer.top =        10
outer.right =      10

And i disabled backspace key to close all other windows.
I trigger it to often by accident. Which isn't a nice experience ;-)

# backspace = ['close-all-windows-but-current', 'mode main']

Other Keys I disabled are the following. (Using alt-e / alt-u can be usefull to create accented characters (é, ä, .. etc) in OS X)

# alt-e = 'workspace E'
# alt-f = 'workspace F'
# alt-u = 'workspace U'
# alt-v = 'workspace V'

# alt-shift-e = 'move-node-to-workspace E'  
# alt-shift-f = 'move-node-to-workspace F'
# alt-shift-u = 'move-node-to-workspace U' 
# alt-shift-v = 'move-node-to-workspace U' 

FreeBSD 14 pkg upgrade PHP – PHP Warning: PHP Startup: Unable to load dynamic library ‘imagick.so’

Upgrading the PHP packages on FreeBSD 14 results in the following warningf when restarting the php server (php_fpm or mod_php)

PHP Warning:  PHP Startup: Unable to load dynamic library 'imagick.so' (tried: /usr/local/lib/php/20220829/imagick.so (/usr/local/lib/libMagickWand-7.so.10: version VERS_10.0 required by /usr/local/lib/php/20220829/imagick.so not defined), /usr/local/lib/php/20220829/imagick.so.so (Cannot open "/usr/local/lib/php/20220829/imagick.so.so")) in Unknown on line 0

The reason is that the phpXX-pecl-imagick isn't updated automaticly.

Workaround for it is to reïnstall the phpXX-pecl-imagick package. (Check your php version with php --version)

pkg install -f  php82-pecl-imagick

ActiveStorage SVG Analyzer

For My Hero's Journey, we're building a new feature to show/hide layers of an SVG image depending on the progress you make. It's used for pretty large/complex images. (see previous post for a custom svg variant transformer)

The backend of the app, attaches layer names to certain tasks that need to be done. When finishing the task, the layer becomes visible.

The frontend is going to show this SVG, with all layers hidden that aren't available yet. (Pretty easy to do with CSS classes)

To make this performant the layer names need to be extracted from the SVG. A good place to make this happen is via an ActiveStorage Analyzer.

A Custom SVG Analyzer

The ActiveStorage::Blob model contains a metadata field, that receives the metadata. The default image analyzer only extracts width and height from the SVG.

Registering a custom anlyzer can be done with an initializer. During development I noticed ActiveStorage only runs one analyzer. The first analyzer that's valid for a given file, is being used.
That's the reason our custom analyzer is prepended to the analyzers array. (The delete/prepend squence is for development-mode reloading)

config/initializers/active_storage_svg_analyzer.rb

ActiveSupport::Reloader.to_prepare do
  Rails.application.config.active_storage.analyzers.delete ActiveStorage::CustomSvgAnalyzer

  # Important! Needs to be prepended to be placed before the othe analyzers. It seems active storage only runs a single analyzer
  Rails.application.config.active_storage.analyzers.prepend ActiveStorage::CustomSvgAnalyzer
end

The interface for an analyzer is pretty simple, the accept? class method needs to return true if the analyzer supports the given file.

The metadata method should return a hash with all metadata, this metadata is placed in the active storage blob record.

A SVG file is just a XML file, so parsing this is pretty easy with for example Nokogiri. In the example below all id's are extracted. (Affinity Designer exports the layer names as id's).

Because only 1 analyzer is run, I also included the width/height metadata attributes. (Though I don't think these are being used)

app/lib/active_storage/custom_svg_analyzer.rb

class ActiveStorage::CustomSvgAnalyzer < ActiveStorage::Analyzer
  def self.accept?(blob)
    blob.content_type == 'image/svg+xml'
  end

  def metadata
    download_blob_to_tempfile do |file|
      doc = Nokogiri::XML(file)
      width, height = extract_dimensions(doc)
      { width:, height:, layer_names: layer_names(doc) }.compact
    end
  end

  private def layer_names(doc)
    doc.xpath("//*[@id]").map { |e| e[:id] }
  end

  private def extract_dimensions(doc)
    view_box = doc.root.attributes["viewBox"]&.to_s
    return [] unless view_box

    left, top, right, bottom = view_box.split(/\s+/).map(&:to_i) # => 0 0 4167 4167

    [right - left, bottom - top]
  end
end

The result

The active_storage_blobs.metadata record now contains the following data:

{"identified":true,"width":4167,"height":4167,"layer_names":["island","island-plant-3","island-plant-2","island-plant-1","island-color-overlay","house","mask","water-well","drum2","drum1","vase3","vase2","vase1","woman-and-child","tree","racoon-tower","racoon1","racoon2","racoon-top-back","_clip1","racoon-middle-back","racoon-bottom","racoon-middle","racoon-top","single-orange","oranges"],"analyzed":true}

Data that is is pretty easy to acces, via the blob.metadata hash. See the asset model below.

models/asset.rb

class Asset
  has_one_attached :file

  def image_layer_names
    content_type_svg? ? Array(file.blob.metadata['layer_names']) : []
  end
end

That's all, thanks for reading!