One interesting technical decision I made for Terminalwire was to use Ruby for the Terminalwire thin-client that’s installed on people’s workstations.
Terrible idea, right? I’ve been told by other engineers, “Use Go! Use Rust! Use Crystal!” They’re not wrong; those languages and runtimes have several advantages over Ruby for similar applications, but it’s not right for me at this moment for lots of reasons that aren’t technical. It also turns out my users don’t care about it either.
When I’m told to use Go, Rust, Crystal, or $LANGUAGE
, the rationale is almost always some sort of technical reason. Here’s what a Redditor had to say about using Ruby for installed software.
I would strongly recommend picking the right tool for the job. If you want cross-platform self-contained binaries, use Go or Rust. You get a fast, small, statically-linked binary that is already faster than Ruby, but which also incurs no additional overhead from virtualizing a filesystem to hold all its dependencies.
Consider Go–it’s a fantastic choice for command-line tooling. On paper, the list of reasons to use it is extensive and compelling.
Here’s the problem though, none of that matters in a product if it fails to achieve adoption.
When a new, more efficient way of solving a problem is introduced to the world, the risk is rarely technical–rather it’s a gap of understanding between the person who built the solution and those who choose whether or not to deploy it. You’ve probably heard this articulated before as “product market fit.”
For Terminalwire, the hypothesis is that running a command-line parser on the server and streaming standard I/O and other commands is 10β-100β more efficient than the current way of building command-line interfaces. What I’m not certain about is how many people want to build a command-line interface into their SaaS web applications and if Terminalwire is their solution. Companies like TRMNL think so, but there are so many more use cases to explore with other users that it’s impossible to know everything at this very moment.
Optimizing for iterations while maintaining quality is the only way to efficiently figure that out, and it ends up being a very personal question for a solo founder or team depending on their working style and actual technical constraints.
My choice to use Ruby to build the Terminalwire client is simple–I needed to optimize for understanding what people want and applying it as quickly as possible back into the product while maintaining quality.
I tend to paint in broad strokes when I approach solving problems. One of my favorite things about Ruby is how powerful abstractions can be built that push the details out of the way so I don’t have to see them all the time and get lost in them.
I lean heavily on pattern matching, keyword arguments, classes, and methods to get the client and server talking to each other via the Terminalwire protocol. I looked at Go, Rust, Crystal, and a few other languages, but none of them had the syntax features I was looking for that could help me maintain the productivity levels I have in Ruby.
I’m drawn to trying “bad ideas” to see what happens. Usually, something good ends up coming from it. If not, then I feel even better about the extra work I’d have to put into an alternative solution, like switching to Go.
When I set out to solve the Ruby distribution problem, I’d heard of a few projects that tried to make Ruby distributions a reality, which I wrote about a few weeks ago. I’m glad I did the research because I found Tebako, which ended up being a great solution with a great community behind it.
If it absolutely wasn’t possible for me to package up Ruby and distribute it as a binary, I knew I could choose something else, take the productivity hit, and move on.
Did you catch the part in the lede where I said “at this moment”?
As products mature, they eventually achieve product market fit and different concerns start to enter the picture. When I look into my crystal ball for Terminalwire, I could easily see the client being rewritten in Go, Rust, Crystal, or $LANGUAGE
as users need it to run on more platforms and do more things.
Or maybe not. Perhaps Ruby and Tebako will continue meeting the needs of users.
As more people start using Terminalwire, I’m going to be confronted with differences between the version of the client a person is using to connect to a version of the server. My plan to deal with this problem is to package up the client <-> server protocol into packages that live on RubyGems.
That way, if a person upgrades their server from 1.0.0
to 1.1.0
, the users that connect to their server will see that the version has changed, check to see if there’s a protocol handler installed locally for 1.1.0
. If not, it will download the handler from RubyGems and continue connecting to the server. All of this will be transparent to the user, which means if all goes well, it will Just Workβ’ automatically.
When I shared my Tebako posts over the past few weeks, there were some interesting conversations about it. One came up on Reddit that I was hoping would happen so I could write a snappy article entitled, “Use the wrong tool for the job”.
Allow me to hand-pick a few quotes from the thread and present them to you in a manner that makes me look like a genius.
The technology behind it is that they package up the Ruby interpreter along with all the libraries and compiled extensions into a single fat binary and ship that.
There’s no doubt that Go, Rust, or whatever can compile binaries smaller than what could be achieved bundling up a runtime like Ruby with scripts.
The downside of tools like this is that your resulting binary is going to be massive.
My compilations ended up being about 15MB. By today’s standards, I wouldn’t call 15MB massive. The heroku
command-line app weighs in at over 400MB. I’d call that massive.
I’ve never heard my users complain that Terminalwire Tebako binaries were too big.
Yes, yes it is. Distributing any kind of software users must install is fraught. That’s the reason why I’m building Terminalwire in the first place–to take on the burden of distributing software so my users don’t have to.
We know now that writing and distributing end-user programs in Ruby is kind of fraught. Your users have to have the right version of Ruby.
Again, yes! I initially distributed the Terminalwire client as a RubyGem and it failed within the first 5 minutes of beta testing. That wasn’t a language problem though, it was a packaging problem, which I set out to solve and identified Tebako as the solution. Now that the build pipeline is set up, I can get builds out in 10’s of minutes. Is Go’s cross-platform compiler better than that? Absolutely! It’s far superior, but again, that’s not what I’m optimizing for at this moment and my users could care less if it takes me 10 minutes to build my binaries in Tebako vs 10 seconds in Go.
The issue of performance almost always comes up in conversations about Ruby because it technically is much slower than most languages.
start over with something like Go where you’ve got a very good developer experience, you’re back to distributing a single simple, statically-linked binary written in a fast, garbage-collected language that provides good primitives for utilizing your 16 or 32 core desktop processor, and it requires literally nothing from the user, it just works.
The Terminalwire client opens a WebSocket connection to a server and streams IO and other commands between the client & server. It barely registers on a single CPU, so multi-processing isn’t really an issue.
Again, my users have never complained about this. I’m sure they will at some point, but maybe not.
When I put Terminalwire in front of real customers, the only issue that came up from the list of all the reasons I listed above to use Go was distribution.
Fortunately, I was able to solve that problem with Tebako. None of the other issues I enumerated from the list of why Go is a great choice for command-line interfaces came up.
Probably the most famous example of using the wrong tool for the job is when Drew Houston launched Dropbox on Hacker News.
The example is a wildly successful outlier, but it does a great job illustrating the nuance and art in knowing when to ignore when really smart people say to you, “use the right tool for the job”.
If you like what you read and want to see more articles like this, please consider using Terminalwire for your web application’s command-line interface. In under 10 minutes you can build a command-line in your favorite language and web framework, deploy it to your server, then stream it to the Terminalwire thin-client that runs on your users desktops. Terminalwire manages the binaries, installation, and updates, so you can focus on building a great CLI experience.