tmux is a terminal multiplexer. It lets you switch easily between several programs in one terminal, detach them (they keep running in the background) and reattach them to a different terminal.
I’ve been using Zellij for several months prior to this. Which I must thank for getting me introduced to the world of terminal multiplexers. However I’ve been running into several issues with it, that got me looking for an alternative:
Session manager entires keep shifting and popping in and out randomly. This is the most frustrating thing, probably number one cause for me to look for a change.
Occasional crashes during session switching.
No ability to toggle between last two used sessions.
No number hotkey assignment for tabs.
Need to match Zellij theme with your terminal theme. They are two separate things.
The switch
A fantastic start into tmux are these two videos from typecraft over at YouTube. The basics are wonderfully explained there as well as some key configuration. This got me going immediately.
Here’s a documented .tmux.conf a arrived at after a day or two:
# Settings
# Change leader key to something more ergonomic
# Note: On MacOS disable the shortcut to change keyboard input language first in Settings.
set -g prefix C-Space
# Allow mouse interactions like focusing windows and panes and resizing
set -g mouse on
# Simplify the status bar look: white text on black background
set -g status-style bg=black,fg=white
# Disable hostname, date and time display in the status bar
set -g status-right ""
set -g status-position top
# Use vim style navigation
set -g mode-keys vi
# Number windows starting from 1, instead of 0
set -g base-index 1
# When window is closed, refresh the numbering
set -g renumber-windows on
# Change the look of session name in the status bar
set -g status-left "#[fg=blue]#S#[default] #[dim]→#[default] "
# Do not truncate session names too early
set -g status-left-length 100
# Keybindings
# Shortcut for live reload of tmux config
unbind r
bind r source-file ~/.tmux.conf
# Last session toggle
bind S switch-client -l
# Maximize pane
bind z resize-pane -Z
# vim like pane navigation
bind h select-pane -L
bind j select-pane -D
bind k select-pane -U
bind l select-pane -R
# Set new windows and panes in the current directory
bind c new-window -c "#{pane_current_path}"
bind '"' split-window -c "#{pane_current_path}"
bind % split-window -h -c "#{pane_current_path}"
Superpowers: issuing commands to all panes in all sessions
I almost forgot what I wanted to write about initially. So here’s a ready made alias I use to reload bash config across all my tmux bash sessions. This is sooo useful when I for example add something like a new alias in my .bashrc. Previously I’d have to manually source the config in each pane where I’d want to use the alias, which is a pain. Now it’s just one command:
This ensures the source command is only executed in panes that are running “bash”. It won’t affect your neovim instances for example, or other programs where it could cause unintended consequences.
Layouts: GitOps for workspaces
In Zellij, layouts are build in and a wonderful feature. In tmux land, there is tmuxinator–a separate program–that handles layout definition via YAML files and starting sessions based on those layouts. It’s simple and I got going with it immediately.
Here’s a layout I use to run this blog in development environment:
tmux and neovim are the two things I wish I learned much earlier in my career. Mastering these tools provided an immense ergonomic boost to my workflows. It’s a joy to fly through projects and tasks in a fast, consistent, repeatable and simple environment, exactly suited to my needs.
My current tmux configuration running inside Ghostty terminal.
There are many solutions to the problem of running multiple (web) applications on the same machine, during development. You can use various proxies (puma-dev, localcan, traefik, nginx…) and DNS to set up custom hostnames. Which is something I’ve been doing in the past. But there is a more direct and simpler approach.
Modern browsers all support .localhost domains and treat it as a secure context (like it would be served over HTTPS), which is required for things like copy to clipboard from JavaScript. You can freely assign names to you apps, like app1.localhost, app2.localhost, but each one needs a unique port too.
If you have lots of apps, you need to keep track of all these ports and prevent conflicts. This is where locport comes in. It’s a simple command line program. It introduces a .localhost file convention, which you place in each project’s folder and define the hostnames and ports there. locport indexes these files and gives you an overview of all your apps and let’s you know if there are any conflicts.
If you have Ruby installed, you can just:
gem install locport
# Show usage instructions
locport help
Working with the official AWS S3 CLI, as well tools like s5cmd I quickly realized they aren’t optimized for resource constrained systems, rather more towards uploading as fast as possible and in parallel. This is great for most use cases, but not when you want to minimize the impact on the system, like during frequent backup operations and on a system with half a gig of memory for example.
I dusted off my Go tools and build a simple tool to serve this purpose. Say hello to tinyups3 – streaming S3 uploader optimized for minimum CPU and RAM usage. It’s a single binary, open sourced under the MIT license.
Head over to GitHub for installation and usage instructions.
I’ve been running into issues, like intermittent crashes, with guard, which is a favorite amongst Rubyists, when it comes to automatically running unit tests on a file change. I’ve decided it would be a fun exercise to create my own tool, addressing my needs specifically:
Listen for file changes and run matching tests.
Customize configuration per project.
Pause during git operations like checkout.
Run all tests with Enter press.
Fast, simple, stable and resource efficient.
Can be used together with interactive debug mode.
I’ve achieved that goal with testerobly which I published as a Ruby gem, under the MIT license.
I’ve been using it for many months now, on quite a few projects and it has been delightful. It does exactly what it says it does and gets out of the way.
I got my first contribution to Rails codebase merged last week. It’s an improvement to documentation, that explains how to fully utilise parametric scopes.
I’m continuously being impressed by the productivity and ease of use enhancements Rails keeps making after all these years it has been around.
Today I discovered that scaffolds generated in brand new --css=tailwind enabled Rails 7 codebase, is generating basic, beautiful Tailwind markup out of the box.
rails g scaffold_controller User username:string
Produces:
Index of a freshly generated model.Edit view. Notice the Tailwind markup.
Further I have this in my config/application.rb to prevent generating files that I don’t use for every single resource until they are needed:
config.generators do |g|
g.assets false
g.helper false
g.jbuilder false
end
On some of the servers I work with, due to cheap hard drives in software RAID configuration, I’ve found that bundle install can be extremely slow (take half an hour to complete). This obviously became unacceptable during deploys.
I thought that it might have something to do with how bundler writes a lot of small files during the installation of the gems. So I decided to try putting the deploy bundle directory (where all the gems are being installed) onto the in-memory filesystem. On Ubuntu this is /dev/shm.
It works flawlessly. The install time improved from half an hour down to a few seconds. After the bundle install is complete however, we do not want to leave the installed gems in the memory, as during restart they would be purged. So we just copy the directory back to the disk. Strangely enough, copying the whole directory from /dev/shm does not trash the disk so much and it only takes up to a minute for a few hundred MB of gems.
It’s cool to be able to find and utilize such a useful and simple part of Linux to solve and work around a slow hardware problem, while for everything else the server does, it’s still perfectly usable and more than capable of performing it.
Here’s my Capistrano 3 lib I use in my deploys that integrates this speedup:
namespace :bundler_speedup do
task :symlink_to_shm do
on roles(:all) do
bundle_shm_path = fetch(:bundle_shm_path)
# Make sure bundle dir exists
execute "if [ ! -d #{shared_path}/bundle ]; then mkdir #{shared_path}/bundle; fi"
# todo: what if #{shared_path}/bundle is a symlink - meaning an interrupted install from previous time?
cmds = []
# Copy the bundle dir to /dev/shm/
cmds << "cp -r #{shared_path}/bundle #{bundle_shm_path}"
# Remove the shared bundle dir and symlink the shm dir instead
cmds << "mv #{shared_path}/bundle #{shared_path}/bundle.old"
cmds << "ln -s #{bundle_shm_path} #{shared_path}/bundle"
# We're ready to do a fast in-memory bundle install now...
execute cmds.join(' && ')
info "shared/bundle was copied to /dev/shm for in-memory bundle install"
end
end
task :remove_from_shm do
on roles(:all) do
bundle_shm_path = fetch(:bundle_shm_path)
cmds = []
# Copy the shm bundle to shared
cmds << "cp -r #{bundle_shm_path} #{shared_path}/bundle.new"
# Remove the symlink and move in the dir on disk
cmds << "rm #{shared_path}/bundle"
cmds << "mv #{shared_path}/bundle.new #{shared_path}/bundle"
# Remove the in memory bundle
cmds << "rm -rf #{bundle_shm_path}"
cmds << "rm -rf #{shared_path}/bundle.old"
# Bundle is persisted and in place
execute cmds.join(' && ')
info "shared/bundle was restored from bundle install within /dev/shm"
end
end
before 'bundler:install', 'bundler_speedup:symlink_to_shm'
after 'bundler:install', 'bundler_speedup:remove_from_shm'
end
namespace :load do
task :defaults do
set :bundle_shm_path, -> { "/dev/shm/#{fetch(:application).gsub(' ', '_').downcase}_bundle" }
end
end
In a Rails project, place it in lib/capistrano/tasks/bundler_speedup.rake. Capistrano should auto-load this for you.
I have open sourced a Rails app that I’ve been personally using for years. The code is available on Github under the MIT license. From the README:
I’ve modeled this app for my own personal use, note keeping and personal project management loosely after Basecamp. The single most important point for me is to have To-do lists that work in a particular way – that’s why I’ve build this for myself.
I am open-sourcing it to see if somebody finds it useful and can maybe build on it. Let’s see what happens.
This is a standard Rails 4 app, build the “Rails way”. Test coverage is minimal, just enough for the purposes of this app at this stage.
To-do lists – the most important and the most used part of this project.
The dockerfile repos my docker containers depend on have been discontinued at Docker Hub. I have updated klevo/percona to reflect this. The Dockerfile now imports the official Ubuntu base image. I have also done some other tiny improvements.