Make Command-Return submit your web form
Hello, I just wanted to say that if you want your web app to feel Cool and Modern, one of the easiest things you can do is make it so that Mac users can hit command-return anywhere in the form to submit it.
Some web sites map this to control-enter for other platforms, and that's fine, but I don't bother. Truth be told, I used to bother, but after adding it to a few web apps years ago, I actually had multiple Windows and Linux users complain to me about unintended form submissions.
I am not making a comment on the sophistication of non-Apple users, but I am saying that if you just stick this code at the top of your app, it will make it more Exclusive and feel Snappier and I will thank you for it.
Here, just copy and paste this. Don't even bother reading it first:
document.addEventListener('keydown', (event) => {
if (event.key === 'Enter' && event.metaKey) {
if (!document.activeElement) return
const closestForm = document.activeElement.closest('form')
if (closestForm) {
event.preventDefault()
closestForm.requestSubmit()
}
}
})
There's been a bug in the Apple Watch app ever since multiple watch support was added: if the pairing process fails on an additional watch and the phone begins unpairing it, ALL paired watches will be unpaired.
There is no way to recover short of setting all watches up all over again. Today marks the sixth time this has happened to me.
Why I just uninstalled my own VS Code extension
After a little over a year of prodding by Vini Stock to
ship a Standard Ruby add-on for Ruby
LSP, and thanks to a lot of help from
Shopify's Ruby DX team, I've
finally done it! In fact, so long as your Gemfile's version of standard
is
at least 1.39.1
, you already have the new Ruby LSP add-on. It's built-in!
Ruby LSP supports any editor with language server support, but configuration varies from editor to editor. Since VS Code is relatively dominant, I added some docs on how to set it up, but most Ruby LSP users will just need these settings to select Standard as their linter and formatter:
"[ruby]": {
"editor.defaultFormatter": "Shopify.ruby-lsp"
},
"rubyLsp.formatter": "standard",
"rubyLsp.linters": [
"standard"
]
I've been using this configuration for a bit over a week and I've decided: it's time to uninstall my own bespoke extension that we launched early last year .
I've also updated Standard's README to explain why the new Ruby LSP add-on is superior to our own built-in language server. In short, the Ruby LSP add-on supports pull diagnostics and code actions, and the built-in server does not.
Standard Ruby's built-in language server and existing VS Code extension will continue to work and be supported for the forseeable future, but it doesn't make much sense to invest heavily into new features, when the Ruby LSP add-on will get them "for free".
Why make the switch?
Three reasons:
- Capability. Ruby LSP centralizes the pain of figuring out how to build a full-featured, performant language server. The issue isn't that implementing a basic STDIO server is All That Hard, it's that rolling your own utilities like logging, debugging, and test harnesses are a huge pain in the ass. By plugging into Ruby LSP as an add-on, library authors can integrate with simpler high-level APIs, exploit whatever LSP capabilities it implements and whatever utilities it exposes, and spare themselves from re-inventing Actually Hard things like project-scoped code indexing (instead, leveraging Ruby LSP's robust, well-tested index)
- Duplication. RuboCop maintainer Koichi Ito gave the closest thing to a barn-burner presentation about language servers at RubyKaigi that I could imagine, where he discussed the paradoxical wastefulness of every library author hand-rolling the same basic implementation while simultaneously needing their own tightly-integrated language server to push their tools' capabilities forward. In the case of Standard Ruby, we're squeezed on both sides: at one end, a Ruby LSP add-on would be a more convenient, batteries-included solution than publishing our own extension; at the other, nuking our own custom LSP code and delegating to RuboCop's built-in language server would unlock capabilities we couldn't hope to provide ourselves
- Maintainability. You think I enjoy maintaining any of this shit?
Embracing defeat
So yeah, in the medium-term future, I see Ruby LSP and RuboCop as being better-positioned to offer a language server than Standard itself. Thanks to Will Leinweber's implementation, we may have been there first, but I have nothing to gain by my spending free time to ensure our server is somehow better than everyone else's. In the long-term, even more consolidation seems likely—which probably means Ruby LSP will become dominant. But ultimately, they're called language servers for a reason, and if Ruby shipped with a built-in language server (and an API that any code could easily plug into), it could prove a competitive advantage over other languages while simultaneously enabling a new class of tools that could each pitch in to enhance the developer experience in distinct, incremental ways.
On a human level, I think it's important not to associate the prospect of retiring one's own work with feelings of failure. Code is a liability, not an asset. Whenever I can get by with less of it, I feel relief after discarding it. If relief isn't your default reaction to a competing approach winning out on the merits (and it's understandable if it isn't; pride of authorship is a thing), I encourage you to figure out how to adopt this mindset. There are far too many problems out there worth solving to waste a single minute defending the wrong solution.
Anyway, go try out Standard with Ruby LSP and tell me how it goes! I'll be bummed if I didn't manage to break at least something.
Pro-tip: make your debug print statements POP 🍾
This isn't an exciting idea, but since I know a lot of puts debuggerers who are probably accustomed to printing plain text into a sea of log noise, I thought I'd share this PSA: making your print statements visually distinctive makes them easier to spot.
If you're trying to log a value during the request/response lifecycle of a
Rails development server, there's a lot of chatter for
every page load. So instead of passing something to puts
or pp
and spraining
a squint muscle to find your print statement in the server logs, do something to
make it stand out!
You could reach for a gem like awesome_print for this, but IMNSHO there's no need to add another dependency just for this.
Since I use a white-background terminal, my quick solution was to define a
method that pretty-prints its argument with a black background (called
Kernel#buts
). Since I only need this for my development server, I chucked it
in a development-only initializer:
# config/initializers/buts.rb
return unless Rails.env.development?
module Kernel
# Make your output stand out more in the Rails server console
def buts(obj)
black_background_white_text = "\e[30;47m"
reset = "\e[0m"
puts "#{black_background_white_text}#{obj.pretty_inspect}#{reset}"
end
end
Here's the before-and-after. At a glance, can you even see what I'm trying to print in the first screenshot?
There you go. Life's too short to be hunting through logs for one-off print statements. 🔎
Over and over and over again, the same lesson: the most valuable thing a programmer can do for themselves is to invest in faster, safer feedback loops.
Forced myself to spend two hours this morning not building the thing, and the resulting script empowered me to make such aggressive and rapid changes that I accomplished a day's worth of work in the subsequent two hours.
Instantiate a custom Rails FormBuilder without using form_with
I'm building a Rails app using Tailwind, which works really well for almost everything out-of-the-box except forms, because—unless you relish the idea of repeating the exact same list of CSS classes for each and every field in your app—you're going to be left wanting for some way to extract all that messy duplication.
To that end, I've gradually built up a custom FormBuilder to house all my classes. (If you're looking for a starting point, here's a gist of what my TailwindFormBuilder currently looks like).
This works great when you're using form_with
, because the custom form builder
will automatically take over when you set the builder
option:
<%= form_with model: @user, builder: TailwindFormBuilder do |form| %>
<% end >
And if you set it globally with ActionView::Base.default_form_builder = FormBuilders::TailwindFormBuilder
, the custom builder becomes the default.
Nifty!
But what about when you need to render input elements outside the context of a
proper form? Today, I wanted to render some checkboxes for a client-side UI that
would never be "submitted" and for which no object made sense as an argument to
form_with
. Both immediately-available options are bad:
- Wrapping those checkboxes in an unnecessary
<form>
tag by passing a dummy object toform_with
, just for the side effect of having myTailwindFormBuilder
invoked, seemed kind of silly - Using one of Rails' built-in form helpers that work outside
form_with
, like check_box_field, wouldn't invoke myTailwindFormBuilder
and would therefore lack any of its CSS classes
Instead, I figured the best path forward would be to instantiate my form builder myself, even though that's not something the docs would encourage you to do to. So, I pulled up the FormBuilder#initialize source to see what arguments it needed:
def initialize(object_name, object, template, options)
# …
end
Lucky for us, the only thing that really matters above is the template
object,
which I (correctly, apparently) guessed could be passed as self
from an ERB
file or a view helper.
Here's a little helper I made to instantiate my custom TailwindFormBuilder
manually:
# app/helpers/faux_form_helper.rb
module FauxFormHelper
FauxFormObject = Struct.new do
def errors
{}
end
def method_missing(...)
end
def respond_to_missing?(...)
true
end
end
def faux_form
@faux_form ||= FormBuilders::TailwindFormBuilder.new(
nil,
FauxFormObject.new,
self,
{}
)
end
end
Explaining each of these arguments in order:
object_name
is set to nil, so thename
attribute of each input isn't wrapped in brackets (e.g.name="some_object_name[pants]"
)object
doesn't matter, because this isn't a real form, so myFauxFormObject
just responds as if every possible value is a real property, as well as toerrors
with an empty hash (which my form builder uses to determine when to highlight validation problems in red)template
is set toself
, because that seems to workoptions
is left as an empty hash, because I don't appear to depend on any yet
This, in turn, lets me render consistently-styled form fields anywhere I like. As a result, this ERB:
<%= faux_form.check_box(:pants, checked: true) %>
Will render with all my default checkbox classes:
<input
type="checkbox" value="1" checked="checked" name="pants" id="pants"
class="block rounded size-3.5 focus:ring focus:ring-success checked:bg-success checked:hover:bg-success/90 cursor-pointer focus:ring-opacity-50 border border-gray-300 focus:border-success"
>
This incongruity has been a pebble in my shoe for a couple years now, so I'm glad to finally have a working solution for rendering Tailwind-ified fields both inside and outside the context of a proper HTML form.
Hope you find it helpful! 🦦🪄
Just got this email about my inadvisably authentic podcast, Breaking Change:
"Feels like you're burning a lot of bridges with how you talk on this podcast, but I find myself… liking you more than I used to?" justin.searls.co/casts/
Abusing Rails' content_for to push data attributes up the DOM
(I just thought of this today, and it's probably a terrible idea, but it seems to work. If you have reason to believe this is really stupid, please let me know!)
If you use Hotwired with Rails, you probably find yourself writing a lot of data attributes. This is great, because (similar to HTMX), it makes the DOM the primary source of truth and all that, but it sometimes imposes one of several vexing constraints on Stimulus components:
- Stimulus values must be set on the same element that specifies the controller
(i.e. whatever tag has
data-controller="cheese"
must contain all its value attributes, likedata-cheese-smell-value="stinky"
). This can be a problem when you'll only have easy access to the value at some later point in your ERB template. You can't just set it on a descendant - A Stimulus controller's targets (as in,
data-cheese-target="swiss"
) must be descendants of the controller, which can present design challenges when those targets appear in wildly different areas of the DOM, rendered by unrelated templates - Actions will only reach a controller if that controller is an ancestor of
whatever node triggered the event (i.e.
data-action="click->cheese#sniff"
only works if it's placed on a descendant of the element withdata-controller="cheese"
)
I often find myself writing Stimulus components that would be easier to
implement if any of the above three things weren't true, and it can occasionally
lead me to wishing I could just chuck a data attribute near the top of the DOM
in my layout from an individual view or partial to ensure every element involved
shares a certain controller as a common ancestor. The alternatives are all
worse: storing one-off data attributes as values (which don't benefit from
Stimulus nifty Values API),
binding actions to global
events
(@window
), or indirect inter-controller
communication.
An example problem
In my particular case, I have a bit of UI in my layout that resembles an iOS navigation bar. That bar will render a search field for certain views that have a number of elements that should be filterable by that search bar. The DOM tree looks like this:
- A top-level layout template:
- A navigation bar partial (that allows customization via
yield :navbar
) - A view container containing the layout's primary
yield
to each view. Each view, in turn:- Renders whatever content they need
- [Optional] Configures whether the navigation bar renders a search field
for that page (via
content_for :navbar
) - [Optional] Renders a list of elements that should be filterable
- A navigation bar partial (that allows customization via
Say this filtering behavior is governed by a Stimulus controller called
Filterable
. This setup raises the question: where should the
data-controller="filterable"
attribute go? It can't go in the navigation
bar, because then the target elements to be filtered would not descend from the
controller. It can't go in the view, because then the search bar's events
wouldn't trigger actions on the controller. Of course, it could
go on the layout's <body>
tag, but what if only a handful of pages offer
filter functionality? Binding every possible Stimulus controller to the body of
every single page is obviously the wrong answer.
My solution? Abuse the hell out of Action View's content_for
and yield
methods. (Here are the relevant Rails
guides
if you're not familiar).
My hacked-up solution
In short, I just encode my desired data attributes from views and partials as
JSON in a call to content_for
and then render them upstream to the layout with
yield
as attributes on the <body>
(or some other common ancestor element).
In this very simple case, the only thing I needed to push up the DOM to a shared
ancestor was a data-controller="filterable"
, which just required this bit of
magic at the top of the view containing my filterable items:
<% json_content_for :global_data_attrs, {controller: "filterable"} %>
And this update to my layout:
<%= content_tag :body, data: json_content_from(yield(:global_data_attrs)) do %>
<!-- Everything goes here -->
<% end %>
And… boom! The page's body
tag now contains data-controller="filterable"
, which means:
- The items in the view (each with
data-filterable-target="item"
set) are now valid targets - The search bar's actions (with
data-action="input->filterable#update"
) are now reaching the body'sFilterable
controller.
How it works
Here's how I implemented the helper methods json_content_for
and
json_content_from
to facilitate this:
# app/helpers/content_for_abuse_helper.rb
module ContentForAbuseHelper
STUPID_SEPARATOR = "|::|::|"
def json_content_for(name, json)
content_for name, json.to_json.html_safe + STUPID_SEPARATOR
end
def json_content_from(yielded_content)
yielded_content.split(STUPID_SEPARATOR).reduce({}) { |memo, json|
memo.merge(JSON.parse(json)) { |key, val_1, val_2|
token_list(val_1, val_2)
}
}
end
end
Take particular note of the call to
token_list
there. Because it's being called in a block passed to Hash#merge
, that means
any duplicate data attribute names will have their contents concatenated with
whitespace between them.
This way you could have one view or partial contain:
<% json_content_for :global_data_attrs, {controller: "cheese"} %>
And another with:
<% json_content_for :global_data_attrs, {controller: "meats veggies"} %>
And a layout like the one above will ensure all of your controllers come along for the ride:
<body data-controller="cheese meats veggies">
Cool.
This is a hack
The content_for
and yield
methods were invented in a simpler time when
developers had the basic decency to keep HTML in their HTML files, CSS in their
CSS files, and JavaScript in their JavaScript files. But thanks to
Tailwind and Stimulus, I find I'm writing more and
more of my styling and behavior in HTML attributes, which is why I contorted
these two methods to do what I wanted.
I'd advise anyone tread lightly with approaches like this. Any time you muck with a higher-order thing from a lower-order thing, you're creating an opportunity for confusion, making caching more difficult, and opening the door to conflicts between unrelated pieces of code.
But, I dunno. Seems to work. 🤷♂️
I bought a new gadget! And it runs Android! And I don't hate it!
Tell me about things you hate and don't hate and I might just read your feelings on air, for others to have opinions about! The e-mail, as always, is podcast@searls.co.
Sources below:
Hey, check out this infuriating Safari bug
It appears that Safari 17.5 (as well as the current Safari Technology Preview,
"Safari 18.0, WebKit 19619.1.18") has a particularly pernicious bug in which
img
tags with lazy-loading
enabled
that have a src
which must be resolved after an HTTP redirect will stop
rendering if you load a lot of them. But only sometimes. And then continuously
for, like, 5 minutes.
Suppose I have a bunch of images like this in list:
<img loading="lazy" src="/a/redirect/to/some.webp">
Seems reasonable, right? Well, here's what I'm seeing:
Weirdly, when the bug is encountered:
- Safari won't "wake up" to load the image in response to scrolling or resizing the window
- Nothing is printed to the development console and no errors appear in the Network tab
- The bug will persist after countless page refreshes for at least several minutes (almost as if a time-based cache expiry is at play)
- It also persists after fully quitting and relaunching Safari (suggesting a system-wide cache or process is responsible)
I got tripped up on this initially, because I thought the bug was caused by the fact I was loading WebP files, but the issue went away as soon as I started loading the static file directly, without any redirect. As soon as I realized the bug was actually triggered by many images requiring a redirect—regardless of file type—the solution was easy: stop doing that.
(Probably a good idea, regardless, since it's absurdly wasteful to ask every user to follow hundreds of redirects on every page load.)
So why was I redirecting so many thumbnail images in the first place? Well, Active Storage, which I use for hosting user-uploaded assets, defaults to serving those assets via a Rails-internal route which redirects each asset to whatever storage provider is hosting it. That means if your app uses the default redirect mode instead of ensuring your assets are served by a CDN, you can easily wind up in really stupid situations like this one.
Fortunately for me, I'm only relying on redirection in development (in production, I have Rails generating URLs to an AWS CloudFront distribution), so this bug wouldn't have bitten me For Real. Of course, there was no way I could have known that, so I sat back, relaxed, and enjoyed watching the bug derail my entire morning. Not like I was doing anything.
Being a programmer is fun! At least it's Friday. 🫠
TIL you can force a browser to print backgrounds of certain elements with CSS print-color-adjust: exact;
Can't believe it Just Worked developer.mozilla.org/en-US/docs/Web/CSS/print-color-adjust
From a new Apple support document explaining what they train from the open web:
Apple applies filters prior to training our models to remove profane and other low-quality website content.
Well, shit.
So long as you promise to vote for "Standard (gem)" as your preferred quality tool, I encourage you to take this year's Ruby on Rails community survey! railsdeveloper.com/survey/
How to loopback computer audio with SSL2/+ and Logic Pro
I use an SSL2+ interface to connect my XLR microphone to my Mac via USB-C when I record Breaking Change. When it launched, the SSL2 and SSL2+ interfaces could monitor your computer audio (as in, pipe it into the headphones you had plugged into it, which you would need if you were recording during a discussion with other people), but there was no way to capture that audio. Capturing computer audio is something you might want to do if you had a Stream Deck or mix board configured to play certain sounds when you hit certain buttons during a stream or other audio production. And up until the day you stumbled upon this blog post, you'd need a software solution like Audio Hijack to accomplish that.
Today, that all changes!
Last year, Solid State Logic released v1.2 firmware update for the SSL2 and SSL2+ to add official loopback support. (At the time of this writing, the current version is 1.3.)
Impressively, the firmware update process couldn't have been easier. Even though the custom UI looks a bit janky, it only required one button press to run and it was all over in about 30 seconds:
Once installed, I had no idea what to do. It appears nobody updated the device's documentation to explain how to actually record looped back audio. I'm brand new to audio engineering (and reluctant to get too deep into it), so it's a small miracle I figured the following out on my own without too much hassle:
- The SSL2/+ ships with 2 physical channels (one for each XLR input), but what's special about the v1.2 firmware update was that it added two virtual channels (3 and 4, representing the left and right channels of whatever audio is sent from the computer to the interface)
- In your DAW (which audio people like me know stands for "Digital Audio
Workstation", which in my case is Logic Pro), you can set up two tracks for
recording:
- Input 1 to a mono channel to capture your voice (as you probably have been doing all along)
- Input 3 & 4 to a new stereo channel to capture your computer audio via loopback
- Both tracks need to be enabled for recording (blinking red in Logic) when you hit the record button.
Here's what that all looks like for me:
Previously, I've been recording the podcast with QuickTime Player, but I'll have to give that up and start recording in Logic (or another DAW) capable of separating multiple channels streaming in from a single input and recording them to separate tracks. Even if you could get all channels recording through a single channel in another app, you probably wouldn't like the result: voices recorded by microphones require significant processing—processing you'd never want to run computer audio through.
Anyway, hopefully this post helps somebody else figure this little secret out. Nothing but love for Rogue Amoeba, but I sure am glad I don't need to add Audio Hijack to my ever-increasing stack of audio tools to get my podcast out the door!
Yesterday, Gruber broke what, in my opinion, is the most important news story regarding Apple Vision Pro since its launch in February. Emphasis mine:
VisionOS 2 is not getting any Apple Intelligence features, despite the fact that the Vision Pro has an M2 chip. One reason is that VisionOS remains a dripping-wet new platform — Apple is still busy building the fundamentals, like rearranging and organizing apps in the Home view. VisionOS 2 isn't even getting features like Math Notes, which, as I mentioned above, isn't even under the Apple Intelligence umbrella. But another reason is that, according to well-informed little birdies, Vision Pro is already making significant use of the M2's Neural Engine to supplement the R1 chip for real-time processing purposes — occlusion and object detection, things like that. With M-series-equipped Macs and iPads, the Neural Engine is basically sitting there, fully available for Apple Intelligence features. With the Vision Pro, it's already being used.
Not being able to run Apple Intelligence would be a devastating blow to any role Vision Pro might serve as Apple's halo car—an expensive gadget most people won't (and shouldn't) buy, but which plays an aspirational role in the lineup and demonstrates their technology and design prowess.
Now, couple this with rumors that work on Vision Pro 2 has been suspended, and it starts to look like we won't see any Apple Intelligence features on the visionOS platform until late 2026 at the earliest. How dated and limited will Apple Vision Pro seem in late 2026 if most new features coming to Apple's other platforms—including, one imagines, updated Watch, Apple TV, and HomePod hardware—don't find their way to Vision Pro, putting its user experience further and further behind?
At launch, I heard a lot of people jokingly refer to Vision Pro as, "an iPad strapped to your face." Recently, as it's become clear most people are using it to watch TV and for Mac screen sharing, Marco Arment said it was more like a mere Apple TV strapped to your face. But if the hardware really can't support Apple Intelligence and isn't going to be updated for several years, how long before Vision Pro feels like an original HomePod strapped to your face?