Hugo Sales

Full stack developer / FOSS contributor

Demos

Space builder

Title
Space Builder · Add Tool
Medium
Vue, Three.js
Year
Notes
Draw a rectangle on the floor and it fills with chairs. Sliders set the spacing, the aisles and the seat count.

Space Builder · Add Tool

Drawing No. 1 / 5

Place Area

Space Builder · Add Tool

Drawing No. 2 / 5

Edit Parameters

Space Builder · Add Tool

Drawing No. 3 / 5

Layout Styles

Space Builder · Add Tool

Drawing No. 4 / 5
Tag offset
φ=θ−α r = min ⁡ ( h 2 | cos ⁡ φ | , w 2 | sin ⁡ φ | ) offset=r+0.95

φ is the camera's angle relative to the area; r is the distance from center to the area's silhouette edge at that angle.

Tag offset
φ=θ−α r = min ⁡ ( h 2 | cos ⁡ φ | , w 2 | sin ⁡ φ | ) offset=r+0.95

φ is the camera's angle relative to the area; r is the distance from center to the area's silhouette edge at that angle.

Capacity Badge

Space Builder · Add Tool

Drawing No. 5 / 5

Marker editor

Title
Interactive Map Marker Editor
Medium
React, Leaflet, Redux
Year
Notes
A pin editor for venue maps. Shape, border, fill and decoration are separate parts, and a saved pin rebuilds itself from them.

Interactive Map Marker Editor

Drawing No. 1 / 6
Create marker Each marker gets theletter or numbermatching its ordering Create marker Each marker gets theletter or numbermatching its ordering

Marker Selector

Interactive Map Marker Editor

Drawing No. 2 / 6
React Dragging outside the render loop

The whole interactive map is a React app, so the editor started as a regular component. That held until I dragged a control point: every pointermove re-rendered a large subtree (solve constraints, preview SVG, update indicators, update sidebar) and the frame rate fell apart.

On drag, the handlers now mutate MarkerPart.controlPoints directly, then:

  1. Circles and crosses move by CSS translate through DOM refs.
  2. Each editor step is a class with its own imperativeReactComponent, a createRoot plus an .update() method, so only the part that changed is redrawn.
  3. Widgets that still need React (colours, text, icons) read an external reactiveState through useSyncExternalStore, so React re-renders only once that store is notified.
  • Circles: the handle you drag
  • Crosses: where the constraint put it
  • Sidebar: the parts a marker is made of

Marker Editor

Interactive Map Marker Editor

Drawing No. 3 / 6
Preview chrome Crosshair background
Failed Overlapping linear-gradients

H + V stripe layers stack into a grid, but each axis rounds its stops alone, so the columns drift.

Shipped SVG tile · repeat × 17

One tiny SVG draws a centred + from two short strokes in a viewBox="-1 -1 2 2". background-repeat tiles that cell so every cross stays coherent.

H stripes

Rows, no columns

V stripes

Columns, no rows

Overlap = grid

Crosses where lines meet

SVG cell

Short +, then tiled

Subpixel jitter Stable crosses Subpixel jitter Stable crosses

Preview Background

Interactive Map Marker Editor

Drawing No. 4 / 6
MarkerPart Composable stack
Shape Border Fill Decoration Composed
Serialisation

Save writes typed data-* attrs (Point: / Number: / String:). Load rebuilds the parts.

In editor
  • PinMarkerShape
  • center → Point(0, -0.25)
  • IconMarkerDecoration
On the SVG
<path data-kind="PinMarkerShape"
  data-state-center="Point:0.000;-0.250" />
<g data-kind="IconMarkerDecoration" …>

Composable Parts

Interactive Map Marker Editor

Drawing No. 5 / 6
redux-undo History + batch
Past
  • markers′ · spaces′
  • markers″ · spaces″
Present
  • markers[] editor SVGs
  • spaces[] pin → markerId
Future
  • ∅ after edit
groupedUndo.batch
addMarker setSpaceMarker 1 undo

One shared group id, so Ctrl+Z or Cmd+Z undoes the whole batch.

Undoable Store

Interactive Map Marker Editor

Drawing No. 6 / 6

Drag and drop

Title
Space Builder · Drag and Drop
Medium
Vue, Three.js
Year
Notes
Drag a card off the catalogue and the object lands where it is dropped. Double-click one instead and the next click on the floor places it.

Space Builder · Drag & Drop

Drawing No. 1 / 2
Picture over the catalog, model over the floor

Four moments of one drag: the card's picture follows the pointer, the model takes over the moment the pointer crosses onto the floor, and letting go places it.

The pointer dragging the chair card out of the catalog
drag the object to the viewport
The chair picture riding the pointer over the catalog
over the catalog it is still a picture
The chair model under the pointer at the edge of the floor
over the floor it becomes the model
The chair placed on the floor with its selection highlight
let go and it is placed

Picture to Model

Space Builder · Drag & Drop

Drawing No. 2 / 2

Font picker

Title
Interactive Map Font Picker
Medium
React
Year
Notes
The list of faces comes from document.fonts, each face's Unicode subsets folded back into one entry. Nothing in it is hardcoded, so a font that loads later shows up on its own.

Interactive Map Font Picker

Drawing No. 1 / 4

Type a URL

Embed URL rust-lang.org

A page the visitor names in the Embed URL subform

Fetch the page

iframe x-frame-options: deny /api/font-proxy one origin, so the HTML can be read

rust-lang.org refuses to be framed, so a worker on this origin fetches the HTML

Read the stylesheets

<base href="https://rust-lang.org/"> <link href="/static/styles.css"> @import url("fonts.css") @font-face { font-family: "Alfa Slab One"; src: url(/api/font-proxy?url=https%3A%2F%2Frust-lang.org%2F…%2FAlfaSlabOne.woff2)

The browser follows every link and @import. It makes each src absolute and routes it through the proxy, since webfont loads are CORS-checked too

new FontFace().load()
document.fonts.add()

Show the new face

Poppins 400 Alfa Slab One 400

The dropdown re-reads document.fonts on loadingdone. The dot marks what the last source brought

A Google font skips the first two steps. fonts.googleapis.com/css2?family=Lobster returns the same kind of stylesheet, which joins the flow here

Font Extraction

Interactive Map Font Picker

Drawing No. 2 / 4
The font family field, as the request goes

Enter a name of a font that can be found on Google Fonts

The field's own border reports the fetch. A quarter turn orbits while the request is out, then the ring closes from wherever the orbit had got to, green for a font that exists and red for one that does not

1 turn 0 1s 750ms: go linear
cubic-bezier(0.75, 0.25, 0.2, 0.2) gives the orbit a push and lets it settle. Kept for every turn it stutters at each restart, so at 750ms the script swaps the timing function for linear at the point the curve has already reached. The seam goes, the first turn keeps its kick
conic-gradient, on the border box the field's own colour, on the padding box 2px of transparent border, which lets it through
One element, two backgrounds. The gradient is painted on the border box and the field's own colour covers the padding box over it, so the only part left showing is the border. Three custom properties move it: where the arc starts, how much of the turn is coloured, and the colour. @property types them as angles and a colour, which is what makes them animatable at all

Loading Indicator

Interactive Map Font Picker

Drawing No. 3 / 4

Font Size sets the column's own text size. Drag it and the row grows, so the slider walks out from under the pointer. FixedElement pins the row where the drag started. Both columns get the same drag, with the pointer held in one place

Held Controls

Interactive Map Font Picker

Drawing No. 4 / 4

Theme picker

Title
Theme Picker
Medium
Astro, CSS, View Transitions
Year
Notes
The control in the corner that picks one of this page's four themes. A pick grows the icon it was picked with from the corner until it covers the page, and the new page is cut out of the old along it. The first sheet plays it on a sample page with its own theme. The sheets behind it draw the wipe, its code, the tokens and who decides.

stage data-demo-theme= unset, so the OS scheme decides

Theme picker Sample pagewith its own theme Theme picker

Theme Picker

Drawing No. 1 / 5
The visible icon drives the animation by creating a starting point from its placement and an end point with that same icon sized to cover the whole viewport
  • × 2.4
  • × 1.2
  • × 2
  • × 1.4
1First frame
The checked icon's <path>, mapped from its viewBox onto its box in the corner.
2Last frame
The same path, grown to a square the viewport's diagonal × the theme's factor wide.
3Between
clip-path from the one to the other, 500 ms ease-in, over a picture of the old page. Off under reduced motion.

The Wipe

Theme Picker

Drawing No. 2 / 5
The wipe's script in five steps, and the geometry of its last frame
  1. 1Start the transition with startViewTransition, the switch as its callback
    const viewTransition = document.startViewTransition(() => { /**/ });
  2. 2Once ready, measure the icon into rect and read its path
    viewTransition.ready.then(async () => {
      const rect = svg.getBoundingClientRect();
      const pathData = path.getPathData({ normalize: true });
  3. 3For the first frame, map the path from its viewBox onto the icon's box
      const startPathData = transformPath(pathData, viewBox,
        { x: rect.x, y: rect.y, width: rect.width, height: rect.height });
  4. 4For the last frame, map it onto a square 2 × size wide, centred on cx, cy
      const size = Math.hypot(window.innerWidth, window.innerHeight)
        * scaleFactor;
      const cx = rect.x + rect.width / 2;
      const cy = rect.y + rect.height / 2;
      const endPathData = transformPath(pathData, viewBox,
        { x: cx - size, y: cy - size, width: size * 2, height: size * 2 });
    Inside transformPath, for both frames
    const mapWidthAbsolute = (x: number) => ((x - fromViewBox.x) / fromViewBox.width) * toViewBox.width + toViewBox.x;
    Scales x from the viewBox width to the box's width and adds the box's left. y likewise.
    return [mapWidthAbsolute(width), mapHeightAbsolute(height)];
    Each command's values go through in x, y pairs.
    values: (transformers[type] ?? thrower(type))(values),
    Keeps each command's letter and maps its values into viewport pixels, the unit clip-path: path() takes.
  5. 5Clip the new page to the first frame with clip-path: path() and grow it to the last
      document.documentElement.animate(
        [{ clipPath: `path("${pathDataToString(startPathData)}")` },
         { clipPath: `path("${pathDataToString(endPathData)}")` }],
        { duration: 500, easing: 'ease-in',
          pseudoElement: '::view-transition-new(theme-transition)' });

The Wipe in Code

Theme Picker

Drawing No. 3 / 5
ThemePicker.astro · _colors.scss Four seeds per theme, three ramps mixed from them, and the names every sheet reads
seeds
  • canvas
  • ink
  • accent
  • blueprint
neutral
  1. 100
  2. 200
  3. 300
  4. 400
  5. 500
  6. 600
  7. 650
  8. 700
  9. 800
  10. 850
  11. 875
  12. 900
accent
  1. 100
  2. 200
  3. 300
  4. 400
  5. 500
  6. 600
  7. 650
  8. 700
  9. 800
  10. 850
  11. 875
  12. 900
blueprint
  1. 100
  2. 200
  3. 300
  4. 400
  5. 500
  6. 600
  7. 650
  8. 700
  9. 800
  10. 850
  11. 875
  12. 900
names
  • --surface-page
  • --ink
  • --line
  • --accent
  • --blueprint
Seeds
Four oklch colours on :root per theme. A dark theme's ink is light, so its ramps run the other way by themselves.
Ramps
A Sass loop mixes canvas into ink in eleven steps for neutral. accent and blueprint tint to 700, take the seed at 800 and shade to 900.
Names
What a sheet reads, never a step. A blueprint sheet like this one remaps them from its own paper.

Token Layers

Theme Picker

Drawing No. 4 / 5
ThemePicker.astro · Layout.astro The OS decides the theme until a pick, and from then on the stamp decides, before the first paint
  1. OS scheme painting @media (prefers-color-scheme: dark) { :root:not([data-theme]) } the OS only knows light and dark
  2. stamp painting :root[data-theme="dark"] written by the pick, kept as cv-theme
  3. radio painting :root:has(#theme-dark:checked) works without script
  1. localStorage
  2. <head> script
  3. data-theme on <html>
  4. first paint
On a reload, eight lines in the <head> stamp the kept pick before the first paint
Nothing picked
Only the OS rule matches, so the page follows the system and changes with it.
A pick
Writes the stamp, checks the radio, keeps cv-theme. color-scheme: only on <body> takes scrollbars and controls along.
Radios by id
An attribute inside :has() put every input on the page in :root's invalidation set; an id names one.

Default and Override

Theme Picker

Drawing No. 5 / 5

Object variants

Title
Space Builder · Object Variants
Medium
Vue, Sass
Year
Notes
One card covers a whole group of library objects. The carousel steps through one cell's finishes, the dropdowns move between cells, and a pair the catalogue never made falls back to the nearest one it did.

Space Builder · Object Variants

Drawing No. 1 / 3
CatalogObjectField.vue · CatalogObjectThumbnailOverlay.vue · HoverSelect.vue One card stands for every library object in its group
Arrows step the finishes One pipper style 4 objects, one card Seats pick a cell Size, other axis Next finish Pip per style 4 objects Seats: a cell Other axis
Group
Every library object under one catalog entry: 5 chairs, 4 banquet sets
Cells
pax and size cut the group, 3 seat counts by 2 sizes here; a cell's objects are its styles
Preview
An option previews on the card before you pick it

Variant Groups

Space Builder · Object Variants

Drawing No. 2 / 3
CatalogObjectFooter.vue · isAvailablePax What happens when the combination does not exist

Every seat count stays on the list. The 8 and 4 seat sets have no object at the size held, so they go grey. Grey is not disabled.

  1. Try the pair Same seat count, and the size already showing
  2. Else take the seat count The first object with that many seats, whatever its size
  3. Both readings move The size under the list changes to match what was found
Never disabled
A greyed option is still clickable. Grey says the other axis will move, not that the pick is refused
Sorted
Every value sorts in one descending run. Missing ones go grey and keep their place
Nothing selected
With no object picked yet, every value counts as available

Missing Variants

Space Builder · Object Variants

Drawing No. 3 / 3

Tagging tool

Title
Library Tagging Tool
Medium
CSS, JavaScript
Year
Notes
One input tags a base object and every variant under it. Variants that already disagree keep their own value, and an object leaves the page once nothing about it is missing. Pick another property and the values already stored for it come back.

Library Tagging Tool

Drawing No. 1 / 4
One "Shared value" input for the base object and every variant
Banquet Set · shared value
Ivory
keyup · focus · click · blur
  • 12 seats · base Ivory --caret: 5
  • 11 seats Ivory --caret: 5
  • 10 seats Ivory --caret: 5
  • 0 seats Ivory --caret: 5
Mirror
Every keystroke copies the value into each variant card's own input, scrolled to its end so the tail stays in view
Save
Finishing the field submits with update_styles: one row per object, titleized, and a cleared value destroys the rows
Warn
Variants that already disagree keep the input open but empty; the placeholder lists the overrides and the tick becomes a warning sign

Shared Value

Library Tagging Tool

Drawing No. 2 / 4
A caret for inputs that never have focus
selectionStart = 9 --caret: 9 left: min(var(--caret) * 1ch, var(--input-width) - 1ch) + 0.5ch
  1. The shared input's keyup, focus, click, blur and input read selectionStart
  2. Each variant form gets --caret and its input's --input-width; the value is copied in and scrolled to its end
  3. The form is monospace, so 1ch is one character and a ::before holding a literal | lands where the real caret would
  4. It only paints while the shared input :focus holds, blinks with opacity keyframes, and stands still under prefers-reduced-motion

Simulated Caret

Library Tagging Tool

Drawing No. 3 / 4

Fill the last gap, and the object leaves the list

Only objects still missing a value One row per object, on finishing the field Last gap filled: on its way out Only objects still missing a value One row per object, on finishing the field Last gap filled: on its way out

A save is find_or_initialize_by then save!; an emptied field destroys the row.

Completed Objects

Library Tagging Tool

Drawing No. 4 / 4

Paper stack

Title
Paper Stack
Medium
CSS, TypeScript
Year
Notes
Every demo on this page is a stack of drawing sheets, and the dog-ear turns them. These three sheets take the stack apart: the pile and its front page, the fold's three layers and the release that turns a page, and the row the stack becomes without JavaScript.
the clip, on the pin front page, --page-index 1 dog-ear, 2cm by 1cm pages to come, fanned about the pin

Paper Stack

Drawing No. 1 / 3

How a fold is drawn

  1. the page, cut on the crease
  2. the shade, on the clip wrapper
  3. the flap, the back mirrored over
T, the pointer C, the corner the crease, halfway commit point, ¾ in

The Fold

Paper Stack

Drawing No. 2 / 3

The stack without JavaScript

snaps one sheet at a time next sheet, out of view arrow, half over the edge a dot per sheet, the current one lit the pile, two shadows

One rule carries all of it: @media (scripting: none).

Without JavaScript

Paper Stack

Drawing No. 3 / 3

GNU social event bus

Title
GNU social · Event Dispatch
Medium
PHP, Symfony
Year
Notes
GNU social is a federated social network server. Its plugins never call each other: a plugin listens for an event by naming a method after it, and the first one to return stop has the job. Switch ImageEncoder off and no other plugin claims the JPEG, so the template draws a plain link. ImageEncoder shows a GIF too, then asks for its thumbnail with a second event, FileResizerAvailable. That one has two takers at the same priority. VideoEncoder asked for image/gif by name, so it goes first and keeps the GIF moving. Switch it off and ImageEncoder makes a still.

Each listener is [Module, 'onViewAttachment'], in the order the modules were found ImageEncoder's view asks for the GIF's thumbnail, and getOrCreate raises this. Each listener is [Module, 'onFileResizerAvailable']

  1. AudioEncoder prio 0
    next
  2. ImageEncoder prio 0
    stop
  3. VideoEncoder prio 0
    skipped
Not loaded
{% for block in handle_event('ViewAttachment', …) %}
    {{ block | raw }}
{% endfor %}
{% if not handled %}
    <a href="{{ attachment.getShowUrl(note) }}">{{ title }}</a>
{% endif %}
no block came back, so the template writes its own link a block came back, so the template's link is not written
then getOrCreate tries the resizers in this order array_merge($event_map['image/gif'], $event_map['image'])
  1. 'image/gif' VideoEncoder::resizeVideoPath true
  2. 'image' ImageEncoder::resizeImagePath not called
Every module handler registers at the same priority, so GNU social orders these by how specific the mimetype is.
$res$event_map
'image/gif' => [VideoEncoder]'image' => [ImageEncoder] <img class="u-photo"> ImageEncoder <video controls> VideoEncoder <audio controls> AudioEncoder harbour.jpg attachment.html.twig

GNU social · Event Dispatch

Drawing No. 1 / 4

One handler returns Event::stop, and propagation ends there AudioEncoder hears the JPEG first and answers next. ImageEncoder claims it and appends its view to $res. VideoEncoder never hears the event.

enum EventResult
case next chain goes on
Not this handler's job, or its part is done and others may add theirs. Every encoder answers next to a mimetype that is not its own
case stop chain halts
Claimed the job. Nothing after it hears the event, and $res holds what it appended
case unhandled chain goes on
Declared in the enum, returned by no handler in the tree: a handler with nothing to add says next

EventResult

GNU social · Event Dispatch

Drawing No. 2 / 4

No module registers a listener Each directory's main class in components/*/ and plugins/*/ is a module. Its public on<EventName> methods listen for that event.

components/*/
  • Attachment
  • Avatar
  • Conversation
  • Feed
  • Posting
  • Tag
  • +13
plugins/*/
  • ActivityPub
  • AudioEncoder
  • Embed
  • ImageEncoder
  • VideoEncoder
  • +29
plugins/ImageEncoder/ImageEncoder.php becomes listener table
public static function version (): string not a listener
public static function shouldHandle (string $mimetype): bool not a listener
public function onFileMetaAvailable (array &$event_map, string $mimetype): EventResult becomes FileMetaAvailable [ImageEncoder, 'onFileMetaAvailable'] prio 0
public function onFileSanitizerAvailable (array &$event_map, string $mimetype): EventResult becomes FileSanitizerAvailable [ImageEncoder, 'onFileSanitizerAvailable'] prio 0
public function onFileResizerAvailable (array &$event_map, string $mimetype): EventResult becomes FileResizerAvailable [ImageEncoder, 'onFileResizerAvailable'] prio 0
public function onViewAttachment (array $vars, array &$res): EventResult becomes ViewAttachment [ImageEncoder, 'onViewAttachment'] prio 0
public function resizeImagePath (string $source, …): bool not a listener
public function onPluginVersion (array &$versions): EventResult becomes PluginVersion [ImageEncoder, 'onPluginVersion'] prio 0

Found by reflection in a compiler pass, once, when the container is built. The table is written into the compiled container, so a request pays nothing for the wiring.

Module Discovery

GNU social · Event Dispatch

Drawing No. 3 / 4

The wrong return type fails the build, not a request The same compiler pass that finds a handler reads its declared return type, and anything but EventResult throws BugFoundException before the container is written.

plugins/ImageEncoder/ImageEncoder.php:183
public function onViewAttachment(
    array $vars,
    array &$res,
): bool {
    if (!self::shouldHandle($vars['attachment']->getMimetype())) {
        return Event::next;
    }
    …
    return Event::stop;
}
shell, the container build
$ bin/console cache:clear

 // Clearing the cache for the dev environment with debug true

In ModuleManager.php line 93:

  [App\Util\Exception\BugFoundException]

$ tail -n 1 var/log/dev.log
app.CRITICAL: Return type of Plugin\ImageEncoder\ImageEncoder::onViewAttachment is not the required EventResult in src/Core/ModuleManager.php:93

A handler that declares no return type gets past the pass. The dispatch catches it the first time it returns nothing (src/Core/Event.php:93): Events must return an \EventResult, which {file} for {event} does not.

Build-Time Guard

GNU social · Event Dispatch

Drawing No. 4 / 4

Fediverse playground

Title
Fediverse Playground
Medium
TypeScript, Docker
Year
Notes
A command-line tool that runs a small federated network on one machine, so you can test how fediverse servers talk to each other before a release. Each server gets its own hostname, database and branch. One YAML file in, a Docker Compose stack out.

Fediverse Playground runs a small federated network on one machine, to test how fediverse servers talk to each other. Switch servers on and off to redraw the docker-compose.yml it writes.

playground.config.yaml defaults.local_tld: fediverse recipes: social-v3, social-v2, mastodon planned: lemmy, friendica, gnu-social-v1

instances

13 services · 28 depends_on, all defined

3 servers behind nginx, on db, redis, search and mediagnusocial-alice uses db, redis and media. gnusocial-bob uses db, redis and media. mastodon-carol uses db, redis, search and media.completedcompletedcompleteddbpostgres:alpineredisredis:alpinemediadarthsim/imgproxymariadbnot emittedsearchelasticsearch:7.17.4nginxnginx:alpine8080 8443gnusocial-alicesocial-v3alice.localhostgnusocial-alice-installone-shotrestart: nognusocial-bobsocial-v3bob.localhostgnusocial-bob-installone-shotrestart: nognusocial-v2not emittedgnusocial-v2-installnot emittedmastodon-carolmastodoncarol.localhost3 processesmastodon-carol-installone-shotrestart: no3 servers behind nginx, on db, redis, search and mediagnusocial-alice uses db, redis and media. gnusocial-bob uses db, redis and media. mastodon-carol uses db, redis, search and media.completedcompletedcompleteddbpostgres:alpineredisredis:alpinemediadarthsim/imgproxymariadbnot emittedsearchelasticsearch:7.17.4nginxnginx:alpine8080 8443gnusocial-alicesocial-v3alice.localhostgnusocial-alice-installone-shotrestart: nognusocial-bobsocial-v3bob.localhostgnusocial-bob-installone-shotrestart: nognusocial-v2not emittedgnusocial-v2-installnot emittedmastodon-carolmastodoncarol.localhost3 processesmastodon-carol-installone-shotrestart: no
docker-compose.yml 284 lines, 96 shown
# This file was generated by Fediverse Playground (GPL3+)
version: "3"
name: fediverse-playground
services:
  nginx:
    image: nginx:alpine
    ports:
      - 8080:80
      - 8443:443
    depends_on:
      - gnusocial-alice
      - gnusocial-bob
      - mastodon-carol
      - mastodon-carol-streaming
  db:
    image: postgres:alpine
  redis:
    image: redis:alpine
  media:
    image: darthsim/imgproxy
  gnusocial-alice-install:
    build: instances/gnusocial-alice/docker/php
    depends_on:
      db:
        condition: service_healthy
  gnusocial-alice:
    build: instances/gnusocial-alice/docker/php
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
      media:
        condition: service_started
      gnusocial-alice-install:
        condition: service_completed_successfully
  gnusocial-bob-install:
    build: instances/gnusocial-bob/docker/php
    depends_on:
      db:
        condition: service_healthy
  gnusocial-bob:
    build: instances/gnusocial-bob/docker/php
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
      media:
        condition: service_started
      gnusocial-bob-install:
        condition: service_completed_successfully
  search:
    image: elasticsearch:7.17.4
  mastodon-carol-install:
    build: instances/mastodon-carol
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
  mastodon-carol:
    build: instances/mastodon-carol
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
      search:
        condition: service_started
      mastodon-carol-install:
        condition: service_completed_successfully
  mastodon-carol-streaming:
    build:
      context: instances/mastodon-carol
      dockerfile: streaming/Dockerfile
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
      mastodon-carol-install:
        condition: service_completed_successfully
  mastodon-carol-sidekiq:
    build: instances/mastodon-carol
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
      search:
        condition: service_started
      media:
        condition: service_started
      mastodon-carol-install:
        condition: service_completed_successfullyLeft out here: volumes, environment, restart, tty, command and healthcheck.

Fediverse Playground

Drawing No. 1 / 4
One bare mirror per software, one worktree per instance An example: the shipped config puts Bob on main. Here he asks for v3, which Alice's worktree already holds, so the branch is renamed after him.
mirrors/social-v3 git clone --bare · fetch --all on every sync
  • instances/gnusocial-alice v3
  • instances/gnusocial-bob v3 held by gnusocial-alice v3-gnusocial-bob
mirrors/social-v2 git clone --bare · fetch --all on every sync
  • instances/gnusocial-v2 nightly
mirrors/mastodon git clone --bare · fetch --all on every sync
  • instances/mastodon-carol main
gnusocial-bob: the first rung that matches runs
  1. local branch already exists git worktree add instances/gnusocial-bob v3-gnusocial-bob
  2. branch off the local head git worktree add -b v3-gnusocial-bob instances/gnusocial-bob v3
  3. branch off origin git worktree add -b v3-gnusocial-bob instances/gnusocial-bob origin/v3
  4. start a new branch git worktree add -b v3-gnusocial-bob instances/gnusocial-bob

A Branch for Every Server

Fediverse Playground

Drawing No. 2 / 4
A one-shot installer brings each server up gnusocial-alice's containers, in the order the compose file starts them
  1. db: pg_isready every 3s
  2. db to gnusocial-alice-install: service_healthy
  3. gnusocial-alice-install: composer install
  4. gnusocial-alice-install to db: doctrine:schema:create
  5. gnusocial-alice-install: app:populate_initial_values
  6. gnusocial-alice-install to db: seed-v3-admin.php
  7. gnusocial-alice-install: exit 0
  8. gnusocial-alice-install to gnusocial-alice: service_completed_successfully
  9. gnusocial-alice: entrypoint.sh
  10. gnusocial-alice to nginx: nginx.depends_on
  11. nginx: server_name alice.localhost alice.fediverse
One shot
The installer has restart: no. Its lane stops at exit 0, and only then does the app start.
Run again
If the language table has rows, the script only seeds the admin again and exits 0, so up is safe to repeat.
Its name
CONFIG_DOMAIN=alice.localhost tells the server who it is on the network. Its admin is admin@alice.localhost.
v2
The v2 server has the same pair, gated on mariadb instead of Postgres.

Bringing a Server Up

Fediverse Playground

Drawing No. 3 / 4
Every server answers to two names Its hostname, and a .fediverse twin from defaults.local_tld, each written everywhere it has to resolve or be served
name hosts.snippetsnippetapply-hosts → /etc/hostshosts.nix.nixNixOS networking.hostsserver_namenginxthe server's nginx blocksubjectAltNameSANthe server's certificate
gnusocial-alice social-v3
alice.localhost yesyesyes
alice.fediverse yesyesyesyes
gnusocial-bob social-v3
bob.localhost yesyesyes
bob.fediverse yesyesyesyes
gnusocial-v2 social-v2
v2.localhost yesyesyes
v2.fediverse yesyesyesyes
mastodon-carol mastodon
carol.localhost yesyesyes
carol.fediverse yesyesyesyes

snippet hosts.snippet.nix hosts.nixnginx server_nameSAN subjectAltName

Why two
*.localhost already resolves to 127.0.0.1, so hosts.nix leaves it out. *.fediverse needs an entry.
apply-hosts
Merges the marked block from files/hosts.snippet into /etc/hosts.
NixOS
The flake exports nixosModules.hosts, which reads files/hosts.nix.
Dev shell
Entering it checks every *.fediverse name with getent and prints one hint if one does not resolve.

Names on the Network

Fediverse Playground

Drawing No. 4 / 4

Synthetic properties

Title
Synthetic Properties
Medium
Rails, MariaDB
Year
Notes
A library object stores its size once, in centimetres. On each save, database triggers wrote that size out in centimetres, metres, inches and feet, so a search for 5ft3in or 1.6m found the same table. The triggers lived five weeks on a branch before Ruby callbacks replaced them.
One table's size, split, converted and searched

The table's size is stored as 160x80x74, in centimetres. When the row is saved, triggers split it on x, write each piece in centimetres, metres, inches and feet, and store every form in the text that search reads.

The walkthrough types in these fields. They don't take your own typing.

  1. 1 Stored library_objects
    split on x, one row per dimension
    1. 160
    2. 80
    3. 74
  2. 2 Converted library_object_synthetic_properties
    • cm 160cm by 80cm by 74cm
    • m 1.6m by 0.8m by 0.74m
    • in" 63" by 31" by 29"
    • in 63in by 31in by 29in
    • ft'in" 5'3" by 2'7" by 2'5"
    • ft in 5ft3in by 2ft7in by 2ft5in
    • Pax 8
    • Seats 8

Synthetic Properties

Drawing No. 1 / 4
Each dimension in six units Each row of split_sizes goes through six SELECTs, one per unit, and GROUP_CONCAT joins a unit's pieces with ' by '. Centimetres and metres keep two significant figures. Inches round to the whole inch.
Unit 16080182
Size (centimeters) ROUND(cm, 1 - FLOOR(LOG10(cm))) 160cm 80cm 180cm
Size (meters) ROUND(cm / 100, 3 - FLOOR(LOG10(cm))) 1.6m 0.8m 1.8m
Size (inches) ROUND(cm / 2.54) 63" 31" 72"
Size (inches) ROUND(cm / 2.54) 63in 31in 72in
Size (feet-inches) FLOOR(in / 12) + carry, ROUND(in MOD 12) if not 0 5'3" 2'7" 6'
Size (feet-inches) FLOOR(in / 12) + carry, ROUND(in MOD 12) if not 0 5ft3in 2ft7in 6ft

182cm is 71.65in, or 5ft and 11.65in. The 11.65in rounds to 12, so it carries and prints as 6ft, not 5ft12in.

The shipped SQL did not round, so 182.5 printed as 182.5cm and 1.825m. It added a foot from six leftover inches, so 80cm read 3ft7in, and it wrote whole feet as 6ft0in. The rules above fix all three. When rounding changes a size, the exact centimetres and metres are kept too, so a search for 182cm still finds it.

Unit Conversions

Synthetic Properties

Drawing No. 2 / 4
The same SQL in the backfill and three triggers The backfill and the insert and update triggers on library_objects call one Ruby method, each with its own where. The delete trigger only deletes the rows. The hairtrigger gem writes the triggers into a migration.
app/models/library_object.rb
def self.size_synthetic_property_select_sql(where = "1=1")
  <<~SQL.chomp
    WITH RECURSIVE split_sizes AS (…)  -- one row per dimension
    SELECT id AS library_object_id, 'Size (centimeters)' AS name,
      GROUP_CONCAT(CONCAT(ROUND(size, 1 - FLOOR(LOG10(size))), 'cm') SEPARATOR ' by ') AS value
    FROM split_sizes WHERE #{where} AND size <> '' GROUP BY id
    UNION ALL  -- five more, and the exact cm and m where they round
  SQL
end
  1. Backfill rake data_migrations:initialize_…, once where = "1=1"
    INSERT IGNORE INTO library_object_synthetic_properties  (library_object_id, name, value)#{size_synthetic_property_select_sql}UNION ALL#{pax_synthetic_property_select_sql};
  2. trigger.after(:insert) where = "id = NEW.id"
    INSERT INTO library_object_synthetic_properties  (library_object_id, name, value)#{size_synthetic_property_select_sql("id = NEW.id")}UNION ALL#{pax_synthetic_property_select_sql("id = NEW.id")};
  3. trigger.after(:update).of(:size) where = "id = NEW.id"
    UPDATE library_object_synthetic_properties AS lopsJOIN (  #{size_synthetic_property_select_sql("id = NEW.id")}) AS tempON lops.library_object_id = temp.library_object_id  AND lops.name = temp.nameSET lops.value = temp.valueWHERE lops.library_object_id = NEW.id;
  4. trigger.after(:delete)
    DELETE FROM library_object_synthetic_propertiesWHERE library_object_synthetic_properties.library_object_id = OLD.id;

Database Triggers

Synthetic Properties

Drawing No. 3 / 4
An object's rows joined into one string for FULLTEXT search Triggers on library_object_synthetic_properties rebuild the string whenever one of the object's rows changes.
library_object_synthetic_properties
objectnamevalue
1024 Size (centimeters) 160cm by 80cm by 74cm
1024 Size (meters) 1.6m by 0.8m by 0.74m
1024 Size (inches) 63" by 31" by 29"
1024 Size (inches) 63in by 31in by 29in
1024 Size (feet-inches) 5'3" by 2'7" by 2'5"
1024 Size (feet-inches) 5ft3in by 2ft7in by 2ft5in
1024 Pax 8
1024 Seats 8
app/models/library_object_synthetic_property.rb · AFTER INSERT and UPDATE, each row
INSERT INTO library_object_properties_concatenated
  (`library_object_id`, `values`)
SELECT
    lops.library_object_id,
    GROUP_CONCAT(CONCAT(lops.`value`, ' ', lops.`name`)
      SEPARATOR ' ')
  FROM library_object_synthetic_properties lops
  WHERE lops.library_object_id = NEW.library_object_id
    AND `lops`.`value` IS NOT NULL
  GROUP BY lops.library_object_id
ON DUPLICATE KEY UPDATE `values` = VALUES(`values`);
library_object_properties_concatenated.values · FULLTEXT
160cm by 80cm by 74cm Size (centimeters) 1.6m by 0.8m by 0.74m Size (meters) 63" by 31" by 29" Size (inches) 63in by 31in by 29in Size (inches) 5'3" by 2'7" by 2'5" Size (feet-inches) 5ft3in by 2ft7in by 2ft5in Size (feet-inches) 8 Pax 8 Seats

Searchable Text

Synthetic Properties

Drawing No. 4 / 4

Schema driver

Title
schemaDef → Doctrine Metadata
Medium
PHP, Symfony, Doctrine
Year
Notes
GNU social is a federated social network server, written in PHP. Each of its tables is one PHP array on the entity class, and a Doctrine driver turns that array into the ORM mapping, the database schema and the types PHPStan checks.

GNU social is a federated social network server. Each table is a PHP array on its entity class, and this driver turns that array into Doctrine's mapping, printed here as the SQL it creates.

Actor::schemaDef() src/Entity/Actor.php
public static function schemaDef(): array{return ['name' => 'actor','description' => 'local and remote users, groups and bots are actors, for instance','fields' => ['id' => ['type' => 'serial', 'not null' => true, 'description' => 'unique identifier'],'nickname' => ['type' => 'varchar', 'length' => 64, 'not null' => true, 'description' => 'nickname or username'],'fullname' => ['type' => 'text', 'description' => 'display name', 'default' => null],'roles' => ['type' => 'int', 'not null' => true, 'description' => 'Bitmap of permissions this actor has'],'type' => ['type' => 'int', 'not null' => true, 'description' => 'The type of actor (person, group, bot, etc)'],'homepage' => ['type' => 'text', 'description' => 'identifying URL', 'default' => null],'bio' => ['type' => 'text', 'description' => 'descriptive biography', 'default' => null],'location' => ['type' => 'text', 'description' => 'physical location', 'default' => null],'lat' => ['type' => 'numeric', 'precision' => 10, 'scale' => 7, 'description' => 'latitude', 'default' => null],'lon' => ['type' => 'numeric', 'precision' => 10, 'scale' => 7, 'description' => 'longitude', 'default' => null],'location_id' => ['type' => 'int', 'description' => 'location id if possible', 'default' => null],'location_service' => ['type' => 'int', 'description' => 'service used to obtain location id', 'default' => null],'is_local' => ['type' => 'bool', 'not null' => true, 'description' => 'Does this actor have a LocalUser associated'],'created' => ['type' => 'datetime', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was created'],'modified' => ['type' => 'timestamp', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'],],'primary key' => ['id'],'indexes' => ['actor_nickname_idx' => ['nickname'],],'fulltext indexes' => ['actor_fulltext_idx' => ['nickname', 'fullname', 'location', 'bio', 'homepage'],],];}
MySQL SchemaDefDriver, printed as DBAL would
CREATE TABLE actor (id INT AUTO_INCREMENT NOT NULL COMMENT 'unique identifier',nickname VARCHAR(64) NOT NULL COMMENT 'nickname or username',fullname LONGTEXT DEFAULT NULL COMMENT 'display name',roles INT NOT NULL COMMENT 'Bitmap of permissions this actor has',type INT NOT NULL COMMENT 'The type of actor (person, group, bot, etc)',homepage LONGTEXT DEFAULT NULL COMMENT 'identifying URL',bio LONGTEXT DEFAULT NULL COMMENT 'descriptive biography',location LONGTEXT DEFAULT NULL COMMENT 'physical location',lat NUMERIC(10, 7) DEFAULT NULL COMMENT 'latitude',lon NUMERIC(10, 7) DEFAULT NULL COMMENT 'longitude',location_id INT DEFAULT NULL COMMENT 'location id if possible',location_service INT DEFAULT NULL COMMENT 'service used to obtain location id',is_local TINYINT(1) NOT NULL COMMENT 'Does this actor have a LocalUser associated',created DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL COMMENT 'date this record was created',modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL COMMENT 'date this record was modified',INDEX actor_nickname_idx (nickname),-- actor_fulltext_idx: not read by SchemaDefDriverPRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDBCOMMENT = 'local and remote users, groups and bots are actors, for instance';
  • Rule Hover, tap or focus a line in the PHP or the SQL to see what the driver did to it

schemaDef → Doctrine Metadata

Drawing No. 1 / 4
schemaDef Doctrine MySQL e.g.
'varchar' string VARCHAR(64) actor.nickname
'char' string CHAR(88) rememberme_token.series
'serial' integer INT AUTO_INCREMENT actor.id
'int' integer INT actor.roles
'tiny' + 'int' smallint SMALLINT local_user.subscription_policy
'big' + 'int' bigint BIGINT foreign_user.id
'bool' boolean TINYINT(1) actor.is_local
'numeric' decimal NUMERIC(10, 7) actor.lat
'text' text LONGTEXT actor.bio
'datetime' datetime DATETIME actor.created
'timestamp' datetime DATETIME actor.modified
'phone_number' phone_number bundle type local_user.phone_number

Mapped ahead of need, unused by any v2 schema: date, time, datetimez, object, array, simplearray, json_array, float, guid, blob

Size
Glued onto the name before the lookup: 'tiny' on an int looks up tinyint, which has no portable form, so smallint
Null
'not null' => true is inverted into 'nullable' => false; a field that says nothing is nullable
Width
char is the same string type as varchar, with 'fixed' => true
Serial
An integer plus GENERATOR_TYPE_AUTO on the class
Docs
Each 'description' becomes a column comment: the documentation lands in the database
Keys
['idx' => [cols]] is reshaped into [['name' => 'idx', 'columns' => [cols]]]

Type Vocabulary

schemaDef → Doctrine Metadata

Drawing No. 2 / 4
  • src/Entity/ App\Entity 17 · core
  • components/*/Entity/ Component\<Name>\Entity 29 · 12 components
  • plugins/*/Entity/ Plugin\<Name>\Entity 28 · 16 plugins

addDriver(app.schemadef_driver, $namespace)

doctrine.orm.default_metadata_driver One driver chain, one SchemaDefDriver, 74 entities as of October 2022 isTransient($class) = !method_exists($class, 'schemaDef')
src/Core/ModuleManager.php · process()
foreach ($module_paths as $path) {// … $type, $module and $dir from the pathif (!\is_null($container) && file_exists($entity_dir = $dir . '/Entity') && is_dir($entity_dir)) {$entity_paths[] = $entity_dir;$container->findDefinition('doctrine.orm.default_metadata_driver')->addMethodCall('addDriver',[new Reference('app.schemadef_driver'), "{$type}\\{$module}\\Entity"],);}}

plugins/Favourite/Entity/NoteFavourite.php is a whole plugin table: drop it in and note_favourite exists. Its 'foreign key' => true is still read by nobody, since the driver's association branch sits behind if (false && …), so note_id maps as a plain INT.

Plugin Entities

schemaDef → Doctrine Metadata

Drawing No. 3 / 4
components/Link/Entity/NoteToLink.php
$note = DB::find('note', ['id' => $args['note_id']]); // PHPStan: App\Entity\Note|null
  1. 1 Boot flag PHPSTAN_BOOT_KERNEL=1vendor/bin/phpstan analyse The analyser runs with the real kernel behind it, not a stub phpstan.sh
  2. 2 Real kernel $kernel->boot();return $kernel; GNUsocialProvider requires the real kernel, then starts every module as a request would config/phpstan-bootstrap.php
  3. 3 Table map DB::initTableMap();'note' => 'App\Entity\Note' Built from getAllMetadata(), so from the same schemaDefs the driver reads DB.php
  4. 4 Return type new UnionType([ Note, new NullType(),]) For find, getReference, getPartialReference and getRepository with a literal first argument ClassFromTableName….php

Without the flag the extension steps aside and PHPStan types the call the ordinary way, so a plain run still works; with it, whatever is called on $note next is checked against Note like any other typed value.

Static Analysis

schemaDef → Doctrine Metadata

Drawing No. 4 / 4

web-ts-mode

Title
web-ts-mode
Medium
Emacs Lisp, tree-sitter
Year
Notes
An Emacs major mode for web templates such as Astro and Vue, where one file mixes markup, TypeScript and styles. Each language gets its own tree-sitter parser over its part of the file, and highlighting and indenting work across the whole file.
Notes
  1. web-ts-mode is an Emacs major mode for files that mix languages. This Astro component from the CV has TypeScript in its frontmatter, markup with expressions in it, and a SCSS style block.
  2. The astro parser reads the whole file, markup included. The TypeScript and the SCSS each get a parser of their own that sees only its range. Each dashed box is one range, drawn inside the range that holds it.
  3. The site's build colours the code with a small lexer that follows the mode's faces. The ranges are the ones Emacs 31.1 hands each parser for this file.
  1. astro, lines 1 to 34
  2. typescript, lines 2 to 19
  3. html, lines 22 to 34
  4. tsx, line 22
  5. tsx, line 24
  6. tsx, line 25
  7. scss, lines 30 to 33
---
/**
* The sheet number in the desk's left margin: a stencil numeral with that sheet's own title
* under it, riding along while the sheet scrolls past. It is decoration, so assistive tech
* never sees it; the sheet carries the same words as its heading.
*
* It is a child of #main rather than of the sheet, because the margin it stands in belongs to
* the desk and not to the paper. Absolutely positioned in the left margin column of the grid
* row its sheet sits in, it takes that row as its containing block, which is what gives the
* numeral the sheet's own height to stick along. The row is the number: there is one folio
* per sheet and they run in page order. Both ends of the row are named, since a grid line an
* absolutely positioned child leaves at auto resolves to the grid's own padding edge.
*/
type Props = {
number: number;
label: string;
};
const { number, label } = Astro.props;
---
<div class="folio-rail" style={`--folio-row: ${number} / ${number + 1}`} aria-hidden="true">
<p class="folio">
<span class="folio-number stencil">{String(number).padStart(2, '0')}</span>
<span class="folio-label typewriter">{label}</span>
</p>
</div>
<style lang="scss">
.folio-label {
font-size: 0.75rem;
letter-spacing: 0.06em;
}
</style>

Point at or tab to the code to see which parser owns itastro: the host parser, over the whole buffertypescript: frontmatter (frontmatter_js_block), :local ttsx: attribute_interpolation (attribute_js_expr), :local ttsx: element (html_interpolation), :offset '(1 . -1), :local ttsx: element (html_interpolation), :offset '(1 . -1), :local tscss: style_element (raw_text), picked by lang="scss", :local t

web-ts-mode

Drawing No. 1 / 4

One interpolation, one TSX program

PostIt.astro, line 3
buffer Astro.props.title && <h3>{Astro.props.title}</h3>} astro
html_interpolation
permissible_text element
tsx
brace left out one range, :offset '(1 . -1) brace left out
skip
nested, left out
Whole program › binary_expression member_expression && jsx_element
Text only "Astro.props.title && " ERROR
Range rules
frontmatter_js_block typescript
attribute_js_expr tsx unless inside an interpolation
script_element raw_text tsx
element > html_interpolation tsx top level only, :offset '(1 . -1)
style_element raw_text scss | css by lang="scss"
Each rule is :local t, so every capture gets a parser of its own.

Parser Ranges

web-ts-mode

Drawing No. 2 / 4

Each level moves the hue toward green

section {
.aside:not(:has(.note-card > *)) .note-fold {
display: none;
}
&:not(:has(.note-card > *)) {
.content {
grid-column: 1 / -1;
}
}
}
Depth, and the share of the way to hue 0.33
  1. 0 base face
  2. 1 0.22
  3. 2 0.44
  4. 3 0.66
  5. 4 0.88
  6. 5 1.00 end hue
.aside:not( 0 :has( 1 .note-card 2 > 1 * 3 ) 1 ) 0 .note-fold 4
Notes
  1. In a > b the right side starts one level past the deepest on its left.
  2. The arguments of :not() and :has() sit one level deeper than the pseudo-class.
  3. The branches of a, b share a depth. A nested rule starts again at 0.

Selector Depth Hue

web-ts-mode

Drawing No. 3 / 4

One command says what is installed and what is not

Notes
  1. Each language needs a tree-sitter grammar, and completion and error checks need a language server on PATH. M-x web-ts-doctor lists both.
  2. A missing grammar shows up as a line here. When a file opens, the mode leaves that language unhighlighted and carries on.
  3. This is the report from the machine that builds this CV. Every grammar is installed, and one server, astro-ls.
web-ts-mode doctor================= Emacs: 31.1System type: gnu/linux Grammars--------  astro: ok  css: ok  tsx: ok  typescript: ok  javascript: ok  html: ok  scss: ok  xml: ok  jsdoc: ok  vue: ok LSP---  client: eglot  auto-enable: t Language servers on PATH------------------------  astro-ls: found (node_modules/.bin/astro-ls --stdio)  vue: not found (tried vue-language-server, vls)  html: not found (tried vscode-html-language-server, html-language-server, vscode-html-languageserver, html-languageserver)  xml: not found (tried lemminx, xml-language-server)  typescript: not found (tried typescript-language-server)  css: not found (tried vscode-css-language-server, css-languageserver, vscode-css-languageserver) CLI---  bin/web-ts grammars  — install pinned tree-sitter grammars  bin/web-ts doctor    — this report (batch)  bin/web-ts test      — ERT suite  bin/web-ts compile   — byte-compile package .el files  bin/web-ts bench     — soft perf timings

Doctor Report

web-ts-mode

Drawing No. 4 / 4

Career

Experience

  1. to present

    Backend web developer at Runtime Revolution

    Backend development in Ruby on Rails at Runtime Revolution, a Lisbon company that builds software for its clients

  2. to

    Full-stack web developer at Visrez, Ireland, remote

    • A Vue and Three.js app that lays an event space out in 3D. Blocks of tables and chairs fill a drawn area and guests get seats.
    • A React map of a resort with custom pins, a virtual tour and step-by-step wayfinding.
    • The Ruby backend behind both. 3D model orders, subscriptions, Google and Apple sign-in and a library of about 11,000 3D objects.

    See Demos

  3. to

    European Innovation Academy finalist at ProtoSyn

    A startup enabling researchers to design and run protein folding simulations without requiring coding expertise. A node-based editor in Vue and TypeScript wires up inputs, algorithms, outputs and plots, and the app writes the Julia that runs them

  4. to

    Core maintainer at GNU social

    The v3 rewrite of the federated social network, in PHP. An event-driven modular architecture, low system requirements, and a deployment a sysadmin can run without much time for it

    See GNU social v3

  5. to

    Software engineering intern at Leb Mobility

    Brought an acquired ride-sharing platform's PHP backend up to date and its iOS and Android apps onto the versions then current, then built a driver and vehicle management platform with a React Native app

  6. to

    Table service and catering

    Summer jobs waiting tables and working catering

Education
BSc Computer Science Minor in Mathematics Faculdade de Ciências, Universidade do Porto, Coding since age 13, always learning
Languages
  • Portuguese Native
  • English C2
  • Spanish C1
  • Japanese A1
Location
Porto, Portugal
Interests
Open Source, Rubik's cubes, languages, motorcycles, the gym, cooking and bread
See also

Skills

Filter by category

65 skills 50 software 15 hardware & CAD

Open Source

Merged
26
Open
43
Closed
31

Checked

Contributor

GCC 16 wrong-code fix

gcc.gnu.org
Period
2026

FreeCAD on NixOS crashed on a boolean subtract of a cube and an inscribed cylinder. The crash led through OpenCascade 7.9.3's point classifier to a wrong-code bug in GCC 16, where interprocedural constant propagation redirected a recursive call to a clone specialised for a different dynamic type, so the program ran the wrong virtual method. Bisected GCC to two January 2026 changes, reduced the miscompile to a C++ testcase of a few dozen lines, wrote the fix and a regression test, and bootstrapped and regtested GCC. The IPA maintainer reviewed it and pushed it to master the same day, with a GCC 16 backport planned. Around it, a nixpkgs workaround builds OpenCascade with `-fno-ipa-cp-clone` and fails the build if the miscompile returns, an OpenCascade issue asks for a 7.9 backport of its fix, and a FreeCAD PR backports OpenCascade 8 support to the 1.1 branch

Maintainer

GNU social v3

GNUsocial/gnu-social
Commits
~1200
Period
2021–2022

Led the GNU social v3 rewrite from 2021 to 2022 with ~1200 commits, funded by NLnet through the NGI0 Discovery programme. Ported the federated microblogging stack to Symfony 5 with an event driven and modular architecture. Rewrote the controllers and posting form layer, expanded PHPUnit coverage, and integrated PHPStan and tooling for dependency hygiene

GSoC student

Blender GSoC 2019

blender/blender
Period
2019

Proposed replacing Blender's Python OBJ, STL, and PLY importers/exporters with a shared C/C++ IO framework using memory-mapped files and optional parallelization. A prototype patch made OBJ and STL import and export about 3× faster, and the proposal was accepted into the GSoC programme

Merged 26

Added the ELAN 04f3:0c6e press sensor of the ASUS ROG Flow X13 to libfprint's new elanpress driver. On master it sits under the swipe-only elan driver, where enrolment fails with a protocol error and verify never matches. Two follow-up fixes keep a background frame captured with the finger down from becoming a touch, and keep more distinct probe frames per touch. Scored offline on a recorded session, 15 held-out touches of the enrolled finger matched at 0.848 to 0.954 and 47 touches from other fingers and another person stayed at 0.294 to 0.761, so no false accepts. inauman took all three commits into the branch behind libfprint!654 on 2026-10-20 and closed this one, so they now wait on upstream review there

· +56 −16 · 4 files

Biome's HTML lexer read the ? before > as part of a name, so <?xml version="1.0"?> never closed and astro check rejected an SVG component that starts with an XML declaration (withastro/compiler-rs#199). Opened #12159 with the fix and fixtures for the plain, Astro, Vue and Angular lexers. The maintainer landed a reworked version with me as co-author

· +2074 −8 · 24 files

FreeCAD booleans crashed on NixOS because OpenCascade's bounding box tree walk ran the line selector's Accept on the point selector. GCC 16's IPA-CP had redirected a recursive call to a clone made for a different polymorphic context. Bisected it with prebuilt snapshots to two January changes, reduced it to two short C++ testcases, wrote the fix and a regression test, and bootstrapped and regtested on x86_64. The IPA maintainer reviewed it and pushed it to master the same day, with a GCC 16 backport to follow (bug 127917). A nixpkgs PR builds OCCT with -fno-ipa-cp-clone and adds an install check for the miscompile, an OpenCascade issue asks for the OCCT 8 fix to be backported to 7.9, and a FreeCAD PR backports its OpenCascade 8 support to the 1.1 branch

· +85 −1 · 2 files

Reworked the scope_css pass in Astro's compiler_rs so combinators inside :global() hoist correctly, dropping the broken ParserOptions::css_modules path

· +786 −215 · 6 files

Fixed JSX-in-object-literal transforms in the Astro Rust compiler so object literals can contain JSX expressions (astro#17198)

· +86 −0 · 5 files

Added a close-on-hide prop to BPopover so popovers dismiss when clipping middleware hides them from view

· +92 −7 · 4 files

Added React support for transitionrun, transitionstart, and transitioncancel events, with matching test coverage

· +78 −1 · 6 files

Fixed chained Redux observers so each run sees the latest store state after upstream observers dispatch updates

· +52 −3 · 2 files

Forwarded Leaflet's feature property generic through @types/leaflet so callers can type GeoJSON features precisely

· +28 −13 · 3 files

Enabled strictNullChecks on the Leaflet typings and fixed the resulting type errors

· +22 −17 · 3 files

Added an inf-ruby variable to wrap the console command, making it easy to launch Ruby inside Docker

· +24 −5 · 2 files

Brought @types/leaflet up to Leaflet 1.9.1

· +17 −2 · 1 file

Added an n format specifier for std::tuple and std::pair in {fmt}, following the tuple-formatting design from #4090

· +11 −1 · 2 files

Added a width prop to BOffcanvas so sidebar sizing matches bootstrap-vue migration expectations

· +7 −0 · 2 files

Aliased BFormInput as BInput for bootstrap-vue compatibility

· +2 −0 · 2 files

Named the Debian package that carries the BlueZ headers in the PyBluez build requirements, so the README says what to install

· +1 −1 · 1 file

Filled in missing Leaflet methods in the DefinitelyTyped declarations for v1.8

Added entries and from_entries helpers to functional-php, mirroring JavaScript's Object.entries / Object.fromEntries

Added an i3blocks memory option to hide the percentage label while keeping the usage bar

Bumped Android Gradle tooling in masked-view to unblock react-native-gesture-handler builds

Bumped Android Gradle tooling in Reanimated to unblock react-native-gesture-handler builds

Fixed a spurious Assimp warning tracked in assimp#3222

Extended Symfony's Translator so trans() accepts ICU-formatted messages directly (symfony#37228)

Added ary, a helper that trims excess arguments before invoking a callable

Repaired functional-php's PHPUnit suite after upstream dependency and PHP version drift

Bumped Android Gradle tooling in react-native-safe-area-context to unblock a react-native-gesture-handler build

Open 43

Every FP_ELANPRESS_* setting comes from the environment, and some bad values broke the driver instead of being refused: a nan quality gate rejected nothing, a huge FP_ELANPRESS_NORM_WIN hit an undefined int cast, and a tiny FP_ELANPRESS_ROT_STEP overflowed the count of rotated probes. Five commits make the shared parser ignore values that are not finite, bound the integer, rotation and overlap settings, and log what it ignored or clamped. Each has a unit test that fails without it. Stacked on !3, awaiting review

· +575 −70 · 6 files

The quality gate drops frames under half the best contrast of a touch, but it took that best from every frame, including ones the absolute gates reject. A frame from a finger landing or lifting is mostly ridge edges and can have twice the contrast of a full press, and on 0c6e one such frame put the bar above all eight full-contact frames of its touch. The best now comes only from frames that pass the absolute gates, with a unit test that feeds that touch. Replaying 116 touches from daily use, six keep more probe frames and the rest are unchanged. Awaiting review on the branch behind libfprint!654

· +230 −14 · 6 files

Drivers that take a setting from an environment variable parsed it by hand with g_ascii_strtoll(), so 5x read as 5, abc as 0, and an out-of-range value was dropped without a message. fpi_env_get_int64() and fpi_env_get_double() take the accepted range and a default, and log anything malformed or out of range along with the default they return instead. The virtual devices' HOT_SECONDS settings and mafpmoc's MAFP_ENROLL_SAMPLES move to them, and the elanpress driver in libfprint!654 can follow. Awaiting review

· +322 −28 · 11 files

Sashiko runs claude with --tools "", and when a review stage asked for tool calls, Opus 5.5 sometimes wrote them out as plain text, then kept inventing calls and results until the 600 s timeout killed it. Requests with tools now also pass --json-schema, so the turn ends on one structured reply, and the model still has no files, commands or network. A drm/client kernel series that ran for about 7 hours and failed now gets a full review in 13 minutes. Fixes #737, awaiting review

· +158 −9 · 4 files

On 0c6e a rejected press against 80 enrolled images took about a second on 16 threads, nearly all of it in the NCC inner loop. Five commits rework that loop: 16-bit layers so every sum is one multiply-add, summed-area tables for the single-layer sums, a half-resolution search kept to the best quarter-resolution peaks, SSE2 intrinsics, and AVX2 and AVX-512 copies picked at run time. Matching 3 probe frames against 80 images on one core went from 2.1 s to 0.17 s. Every commit but the peak search gives the same scores on 240 genuine and 3136 impostor matches. Awaiting review

· +569 −101 · 3 files

On an ASUS ROG Flow X13 booted with video=eDP-1:1920x1200@60, an HDMI hotplug made fbdev reprobe every connector with its 1920x1200 framebuffer as the limit. The 3840x2400 eDP panel lost its native mode from connector->modes, and after suspend/resume amdgpu drove it at 1920x1200 with the image squeezed into the top-left corner. Patch 1 makes drm_client_modeset_probe() probe with the device's mode size limits, as fbdev's first probe already does, and moves the framebuffer size check into the fbdev mode pickers. Patch 2 adds KUnit tests. v2 also checks the size of the CRTC's current mode before the firmware config falls back to it, which Sashiko found. With only this series applied, 255 of 255 resumes kept the native timing, against 28 of 29 wrong without it. Ville Syrjälä said on 2026-10-16 the approach should be fine but gave no tag, so it is still under review

· +550 −20 · 2 files

Two patches on amd-gfx. After an HDMI hotplug and a suspend/resume, a 3840x2400 eDP panel booted with a video= mode came back squeezed into the top-left corner. The fbdev reprobe had dropped the native mode and amdgpu scaled onto the first one left. On eDP and LVDS the fix falls back to a listed mode of the native size, or else to the encoder's native mode, with KUnit tests that fail without it. On 7.2.8, 103 of 103 resumes were wrong before and 1363 of 1363 right after. v2 is rebased onto amd-staging-drm-next, as Tom Chung at AMD asked. A second series on dri-devel, drm/client: Keep larger modes in connector->modes on fbdev hotplug, fixes the same bug for every driver. Both are under review

· +271 −2 · 2 files

When some tiles of a tiled display are missing, drm_client_firmware_config() swaps in drm_connector_fallback_non_tiled_mode(), which can return NULL, and the debug message after it still read modes[i]->name. It now prints (none) there, and drops the stray colon in [CONNECTOR::%d:%s] that broke grepping the log for a connector id. Sashiko found it while reviewing the fbdev hotplug series. Thomas Zimmermann suggested the v2 form and gave his Reviewed-by on 2026-10-23

· +2 −2 · 1 file

The evdev listener slept 5 ms between passes over its devices, so an idle daemon woke about 200 times a second to find nothing to read. It now blocks in poll() on the keyboard fds and the inotify watch, with a 100 ms timeout so the stop signal and the device checks still run, and handles key events as they arrive. On my laptop an idle daemon went from about 229 wakeups a second to about 42. A draft, waiting on CI approval and review

· +20 −2 · 1 file

The action picks the panel's ABM level from the profile and the battery charge, with power-saver at 0 above 50% and balanced at 1 below 30%, and nobody could change it: one user wanted the saving at any charge (#185), others found level 3 too strong (#159). Adds a load_settings vfunc so an action reads its own group from power-profiles-daemon.conf, with PowerSaverLevel, BalancedLevel and PerformanceLevel keys that apply on battery at any charge; unset keys keep today's steps, and unknown groups, keys or values out of 0 to 4 get a warning. A first commit fixes a leak already on main, where both error returns in set_panel_power() skipped freeing the drm device list. Measured on an ASUS ROG Flow X13: level 4 saves 0.4 to 0.6 W of about 10.4 W. Stacked on !241

· +895 −35 · 6 files

power-profiles-daemon saved an action as off whenever it was inactive for one run, so a --block-action debug flag or a failed probe switched it off for good in state.ini, and a configure-action choice was lost if the daemon stopped before the next profile change. The first commit keeps the state file to the user's own choices instead of what is active right now; the second writes it as soon as configure-action or configure-battery-aware runs. Seven new integration tests cover the blocked, opt-in and failed-probe cases and the restarts; all 152 pass

· +162 −10 · 2 files

GCC 16 at -O3 miscompiles OCCT 7.9.3. Classifying a point on an edge or vertex of a solid segfaults, and so do booleans where faces touch along a line, which is how FreeCAD crashed on a boolean subtract of a cube and an inscribed cylinder. Builds OCCT with -fno-ipa-cp-clone when the compiler is GCC 16 or newer and adds an install check that runs the failing classification, so the build fails if the miscompile comes back

· +36 −1 · 1 file

Blocking a driver or an action meant overriding ExecStart in a systemd drop-in, which breaks when a distribution installs the daemon elsewhere or changes the unit. The daemon now looks for power-profiles-daemon.conf and a .conf.d/ directory in /etc, /run, /usr/local/lib and /usr/lib, in systemd's order and with systemd's rules: a later key replaces an earlier one, a drop-in of the same name higher up masks the lower one, a link to /dev/null masks a file, and files are opened without blocking and read rather than mapped so a FIFO or a truncated file cannot stop startup. The [Daemon] group takes BlockDrivers and BlockActions, and unknown keys, groups and names get a warning. A second commit makes SetActionEnabled refuse to turn on a blocked action instead of reporting success

· +620 −27 · 3 files

Tapping a tab on a touchscreen did not switch to it, and the tab bar could disappear until a new window opened, because the click handler only listened to mouse buttons, which Hyprland never raises for touch. Moves the lookup into Hy3Layout::tabAt() and calls it from an input.touch.down listener too; a tap on a tab cancels its down, motion and up events so the client underneath never sees the touch, and the listener repeats Hyprland's own checks for the locked session, kill mode, workspace swipes and covering surfaces. Tested in a VM with a virtual touchscreen and earlier on a real one

· +129 −8 · 4 files

Unloading hy3 with a tab group open crashed Hyprland on the next animation tick, which any reload that changes the plugin path triggers, such as a NixOS switch that rebuilds hy3. Hyprland calls PLUGIN_EXIT first and removes the layouts after, and removing them put fading tab groups back on a list whose tick listener was already gone. Removes the layout at the start of PLUGIN_EXIT so the clears free what is left, and drops the tab pass element from the render pass as hyprbars does. Tested in a VM through an unload, a reload and six swaps between builds

· +6 −0 · 1 file

When the last monitor disconnects, Hyprland leaves workspaces pointing at the monitor it is destroying, and hy3 dereferenced that pointer on the next window map or unmap and segfaulted. Checks the workspace's monitor in insertNode, recalcGeometry, moveNodeToWorkspace and the tab group constructor, and the focused monitor in the dispatchers and cursor warp, which is null while no monitor is connected; each place skips its monitor work. Fixes #332 and #336

· +23 −9 · 2 files

Letting go of push-to-talk a moment early cut the last word, and Whisper dropped it or guessed. With audio.stop_tail_ms set, the daemon keeps capturing after the stop until the last 160 ms are quiet, for at most that long; a clean release stops at once. Quiet is three times the recording's noise floor, taken as its 10th-percentile frame and capped where quiet speech starts, so the bar follows the mic and the room. Off by default, with a schema entry, a field in the configure TUI, docs and a unit test

· +155 −1 · 7 files

The modifier guard opened every /dev/input/event* node on each paste and closed them all afterwards, and each close waits for a kernel RCU grace period of 10 to 35 ms, so the guard alone added a few hundred milliseconds before every output (#543). Keyboards now stay open in a process-wide cache; each call still lists /dev/input and opens only nodes that are new or re-created, told apart by inode, so hotplug keeps working, and any close runs on a background thread off the output path

· +99 −9 · 1 file

With context window optimisation on, Whisper sometimes reaches the end of the speech and transcribes the clip again from the start, returning the transcript twice or with its opening sentence repeated at the end; the loop check from #827 needs three copies, so these got through. If the first pass used a reduced audio_ctx and the text ends with a copy of its first six or more words, the text before the copy is kept, with no second pass. Replaying 20 archived dictations on large-v3-turbo, the three that came back doubled now match the full-context output at no extra cost. Stacked on #827

· +139 −1 · 1 file

Kept the microphone open while a push-to-talk dictation transcribes in Voxtype. A press in that window is queued and recorded on the open capture, so the next sentence is no longer lost

· +128 −1 · 1 file

Added hotkey.cancel_on_other_key for a modifier used as the hotkey. Pressing any other key while it is held cancels the recording, so Right Ctrl+C no longer dictates

· +97 −0 · 5 files

Added hotkey.double_tap_latch. A double tap keeps the recording running after the key is released, until the next press, and holding the key works as before. A small TapLatch state machine in the evdev listener does the timing

· +188 −1 · 5 files

Added [hotkey.language_modifiers], which maps modifier keys to Whisper language codes. Holding one during a recording transcribes that recording in that language, and the next goes back to the configured one

· +119 −1 · 5 files

Added a loop check to the Whisper transcriber. When a phrase of three or more words repeats three or more times in a row, it retries with beam search, and keeps one copy of each phrase if the retry loops too

· +79 −1 · 1 file

Fixed picking in the 3D view on fractional-scale screens. toDevicePixelPosition() flipped the Y axis in logical pixels and only then scaled to device pixels, so on a 2.67× panel a click landed 1 to 4 device pixels below the cursor

· +7 −2 · 1 file

Added WISPR_CAPTURE_IGNORE_VK, a list of virtual-key codes the Linux helper leaves out of its global key monitor, so a push-to-talk bind on Right Ctrl stops firing on Left Ctrl as well. Skipped keys do not take a sequence index, so the app never sees a gap, and paste injection is untouched

· +114 −0 · 2 files

Added on-enter and on-leave command hooks to every Waybar module, requested since 2023. Ignored GTK's inferior crossings and latched on a hover flag so each command ran once per hover, and mirrored the on-click guard that keeps built-in actions from also running as shell commands

· +312 −1 · 36 files

Packaged claude-auto-retry as a Nix flake with NixOS and home-manager modules that build from source and manage the shell wrapper, the reconcile timer, and the StopFailure hook through real options instead of npm i -g. Also fixed the wrapper clobbering each tmux pane's TERM

· +404 −3 · 10 files

Modernized XML_XRD with PSR-4 autoloading under an XRD namespace, replacing legacy PEAR layout

· +346 −314 · 25 files

Added setInteractive on Leaflet layers so interactivity can be toggled after creation

· +465 −20 · 10 files

Made SVGO's optimize return the AST and exported parseSvg in the browser bundle to avoid double parsing

· +49 −1 · 3 files

Updated XML_XRD tests to PHPUnit 9.5 as part of broader PEAR modernization

· +90 −108 · 10 files

Let Leaflet controls use role="button" instead of requiring anchor elements in control markup

· +29 −13 · 1 file

Added a GNU Stow option to emit absolute symlinks when packages must resolve outside the stow tree

· +28 −5 · 2 files

Let capistrano-db-tasks pass extra mysqldump flags, needed for schemas where a non-unique key follows the primary key

· +24 −5 · 5 files

Added missing return types on XML_XRD to clear PHP 8 deprecation warnings

· +7 −7 · 2 files

Clamped negative whiptail menu heights that broke item rendering from v0.52.22 onward

· +1 −1 · 1 file

Clamped negative whiptail menu heights that broke item rendering from v0.52.22 onward

· +1 −1 · 1 file

Added cartesian_product and InvalidArgumentException::assertStringable to functional-php

Added copy/paste shortcuts to the Rete.js keyboard plugin for moving node selections between editor states

Opened the Rete.js context menu at the pointer and clamped it to the viewport edges

Switched Rete.js shortcuts from event.code to event.key so remapped and non-QWERTY layouts work

Added a redshift i3blocks module that shows the current screen colour temperature

Closed 31

With the Biome lexer fix, <?xml version="1.0"?> parses as an HtmlProcessingInstruction in Astro files, and astro2tsx copied it into the TSX verbatim, where TypeScript cannot parse <?. It now prints each instruction as a JSX comment through the path HTML comments use, at the root before the doctype and inside elements. New snapshot fixtures and a type-check test cover it, and astro check on the repro went from 7 errors to 0. Closes #199. Closed in favour of #205, which the maintainer merged with the same astro2tsx change

· +186 −16 · 14 files

Backported the OpenCascade 8 support from main to the 1.1 release branch, 15 commits plus the -Werror guards and the TechDraw fix, so distros shipping FreeCAD 1.1 could build against OCCT 8 and sidestep the GCC 16 miscompile of 7.9.3. Tested with FreeCADCmd -t 0 on OCCT 8.0.1 and 7.9.3, 1666 tests with the same results as 1.1.4, and the cube minus cylinder recomputes instead of crashing. The maintainers closed it, since the 1.1 branch gets no further releases and 26.3 is the first version to support OCCT 8

· +654 −328 · 130 files

In paste mode the output chain held only the paste method. When a modifier was still down past modifier_release_timeout_ms, the guard skipped the paste and nothing else ran, so the dictation was lost although the notification said it was copied to the clipboard. Hit with Right Ctrl as the hotkey. Added wl-copy after the paste, as type mode already has, with a test that the chain keeps a method that survives the keystroke filter. Closed as a duplicate of #588 and #654

· +18 −1 · 1 file

Added git rebase --continue --no-edit and rebase.noEdit to skip opening $EDITOR after resolving conflicts, with --edit to override the config for a single continue. Junio Hamano and Phillip Wood worried that rebase.noEdit would also skip the editor after an edit or reword stop, and asked for tests beyond the conflict case. Dropped without a v2

· +126 −7 · 5 files

Updated react-scoped-css for Webpack 5 and CRACO 7 without deprecated loader APIs. Closed as implemented by the maintainer

· +10329 −11380 · 52 files

Wrapped every Intl stub class in a class_exists guard, so loading the polyfill next to a real intl extension stops fataling with "Cannot declare class Normalizer, because the name is already in use"

· +190 −156 · 17 files

Let React Leaflet's LayersControl instantiate a custom L.Control subclass, needed for image-based layer toggles

· +310 −26 · 4 files

Added max-height and max-width props to BDropdown so a menu can override the size @floating-ui/vue works out for it, with an example in the docs

· +85 −2 · 4 files

Passed drag-and-drop context into dnd-kit sorting strategies so custom sorters can read sensors and collisions

· +33 −15 · 3 files

Added initial and delta pointer coordinates to dnd-kit collision detection for frame-to-frame movement

· +23 −3 · 4 files

Allowed optional altitude in Leaflet's LatLngTuple typing

· +16 −4 · 2 files

Bumped gtk4 to 4.22.5 for the upstream munmap() fix in the Wayland dmabuf format table, which took down any GTK4 client on Hyprland whenever a monitor was plugged in. Traced from a coredump and first sent as a backported patch; the reviewer asked for the bump once 4.22.5 was tagged. Closed as a duplicate of #559510, the GNOME 51 update on staging, which takes gtk4 to 4.24.0

· +2 −16 · 1 file

Fixed a malformed Markdown table in the Seastar README

· +6 −6 · 1 file

Synced Leaflet marker interactive when the React Leaflet prop changes after mount

· +10 −0 · 1 file

Let BootstrapVueNextResolver take a map of component aliases, so a project moving off bootstrap-vue can keep auto-importing the names it already uses

· +7 −3 · 1 file

Bound the missing clear event when the Rete.js history plugin installs, so clearing the history no longer throws (retejs/history-plugin#16)

· +1 −0 · 1 file

Added explicit return types on Symfony overrides in Codeception to silence Symfony 5.4 deprecations

Updated getopt-php type signatures for PHP 8.1 and PHPStan cleanliness

Upgraded cte-builder to Doctrine 3 and beberlei/assert 3.3. Closed after the maintainer folded parts of it into 3.1.0, which supports DBAL 2 and 3 side by side

Bumped Android Gradle tooling in react-native-screens to unblock gesture-handler builds

Bumped Android Gradle tooling in the TypeScript RN template to fix Groovy/Java7 build failures

Documented Symfony Translator ICU message support alongside symfony#37371

Added an aur-update2 i3blocks module that surfaces pending AUR upgrades via pikaur

Packaged php-align for MELPA with a recipe covering its PHP alignment rules. Closed once the thread led to php-align.el being merged into php-mode instead

Added a PHP CS Fixer option to keep single-line autogenerated getters and setters

Fixed IncludeOS build-system issues blocking a clean CI build

Repaired a php-cs-fixer wrapper for Emacs, swapping its broken patch step for a direct file fix and a buffer revert

Lets git mv create missing leading directories in the destination instead of failing: git mv -p foo bar/quux/ creates bar/ and bar/quux/ first, then moves foo into place

Argued for restore.defaultDestination on the Git mailing list (mirrored as git/git#1467) so users control where restored files land during recovery

Fixed bbswitch kernel-module packaging and build issues for NVIDIA GPU power switching in a personal fork, never submitted upstream

Fixed and extended GNU FM (Libre.fm) PHP code in a fork of the Savannah-hosted music platform, never submitted upstream