Hello folks!
In the shell I spend too much time renaming files to replace spaces with dots or dashes. Without that, I'm stuck quoting filenames or escaping spaces everywhere. Really tedious.
Why not automate it? Zsh has all the machinery to customize the line editor (ZLE). Shell scripting is awfully inconsistent, but after renaming files manually one too many times, I decided to wade into the mess.
My initial thought: when using mv, have the shell parse the second argument and automatically replace all spaces. Type mv 'foo bar' <duplicate><transform> and get 'foo.bar'. Simple, right?
This approach has quite a bit of hidden complexity — I was thinking the move command has two arguments: the original file name and the new name. I can duplicate the first argument with Zsh's `copy-earlier-word`` function (which copies the previous word to cursor position), then I only need a way to parse that duplicated string and replace spaces, but only if the cursor is right after a quote symbol.
To duplicate arguments efficiently, I suggest to the interested reader that they bind copy-earlier-word to a chord — I chose Alt-, because it's near Alt-. and both perfom a similar function1:
autoload -Uz copy-earlier-word
zle -N copy-earlier-word
bindkey '^[,' copy-earlier-word # Alt+,
Usage: mv foo → press Alt+, → mv foo foo
Back to the main argument. This is the result of a couple of hours. When the cursor is right after a quoted string, it will parse the string and replace spaces with dots, handling escaped single quotes in the filename. (I don't check for double quotes — who in their right mind would use double quotes in a filename?)
# Function to replace spaces with dots in quoted string at cursor
replace-spaces-with-dots() {
# Get position right before cursor
local pos=$((CURSOR - 1))
# Check if cursor is right after a quote
if [[ $pos -ge 0 ]]; then
local char=${BUFFER:$pos:1}
# Check if it's a quote character
if [[ $char == "'" || $char == '"' ]]; then
local quote_char=$char
local -a segments
local search_pos=$pos
# Find all concatenated quoted segments (handles '\'' pattern)
while [[ $search_pos -ge 0 ]]; do
local end_pos=$search_pos
local start_pos=-1
# Search backward for matching opening quote
local k=$((search_pos - 1))
while [[ $k -ge 0 ]]; do
if [[ ${BUFFER:$k:1} == $quote_char ]]; then
# Count preceding backslashes
local backslash_count=0
local m=$((k - 1))
while [[ $m -ge 0 && ${BUFFER:$m:1} == '\' ]]; do
((backslash_count++))
((m--))
done
# If even number of backslashes (including 0), quote is not escaped
if [[ $((backslash_count % 2)) -eq 0 ]]; then
start_pos=$k
break
fi
fi
((k--))
done
# If we found a matching quote, record this segment
if [[ $start_pos -ge 0 ]]; then
segments=("$start_pos:$end_pos" "${segments[@]}")
# Check if there's a '\'' pattern before this segment (for single quotes)
# Pattern is: ' \ ' ' (4 characters: close quote, backslash, quote, open quote)
if [[ $quote_char == "'" && $start_pos -ge 3 ]]; then
local pattern=${BUFFER:$((start_pos-3)):4}
if [[ $pattern == "'\\''" ]]; then
# Continue searching before the '\'' pattern (before the closing quote)
search_pos=$((start_pos - 4))
continue
fi
fi
fi
break
done
# Process all segments (from left to right)
if [[ ${#segments[@]} -gt 0 ]]; then
local total_offset=0
local seg
for seg in "${segments[@]}"; do
local seg_start=${seg%:*}
local seg_end=${seg#*:}
# Adjust for previous offsets
seg_start=$((seg_start + total_offset))
seg_end=$((seg_end + total_offset))
# Extract content between quotes
local content_start=$((seg_start + 1))
local content_length=$((seg_end - seg_start - 1))
local quoted_content=${BUFFER:$content_start:$content_length}
# Replace spaces with dots
local replacement=${quoted_content// /.}
local diff=$((${#replacement} - content_length))
# Update buffer
BUFFER="${BUFFER:0:$content_start}${replacement}${BUFFER:$seg_end}"
# Track cumulative offset
total_offset=$((total_offset + diff))
done
# Adjust cursor position
CURSOR=$((CURSOR + total_offset))
fi
fi
fi
}
# Register the function as a ZLE widget
zle -N replace-spaces-with-dots
# Bind it to Alt+/
bindkey '^[/' replace-spaces-with-dots
This monstrosity operates on Zsh's line editor buffer ($BUFFER). When triggered, it checks if the cursor sits right after a quote character. If so, it searches backward through the buffer to find the matching opening quote, carefully counting backslashes to distinguish escaped quotes from real delimiters.
But wait, there's more! It handles filenames like 'foo'\''s bar' — the shell's awkward way of embedding a single quote inside single-quoted strings. The function detects these '\'' patterns and treats them as concatenated segments, collecting all pieces that belong together.
Once it maps out all the segments, it extracts the content between quotes, replaces spaces with dots using Zsh's ${var// /.} substitution, then reconstructs the buffer. It even tracks offset changes to keep the cursor position correct as string lengths change.
All this complexity for what? So I could type mv 'foo bar' <Alt+,><Alt+/> and have the second argument magically transformed to 'foo.bar'.
Awesome!
Only after debugging the horrible mess above did it occur to me that I was approaching it wrong. Why replace the spaces in the argument when I could simply create a function that takes the file name and renames the file without spaces?
To be fair, the first approach has merit — it works during command construction. It could theoretically be used with cp, ln, or any command where one want to transform a duplicated argument on the fly. But honestly? Beyond mv, I can't think of many practical use cases where this inline string manipulation would be needed.
So, here is the sensibly simpler function spc2dot in all its glory:
# Rename files by replacing spaces with dots
spc2dot() {
if [[ $# -eq 0 ]]; then
print "Usage: spc2dot <filename>"
return 1
fi
# Join all arguments (handles unquoted filenames)
local original="$*"
# Replace spaces with dots
local newname="${original// /.}"
if [[ ! -e "$original" ]]; then
print "Error: File '$original' not found"
return 1
fi
if [[ "$original" == "$newname" ]]; then
print "No spaces to replace"
return 0
fi
if [[ -e "$newname" ]]; then
print "Error: '$newname' already exists"
return 1
fi
mv -v "$original" "$newname"
}
The lesson? Sometimes the clever solution is the wrong solution. A simple wrapper function beats 100 lines of buffer manipulation any day. In the process I learned way too much I needed to know about Zsh.
on default Z_sh it's bound to insert-last-word ↩
Greetings terminal dwellers!
Scrcpy lets you display and control Android devices from your desktop over USB or TCP/IP. No root required, minimal latency, works over adb.
Debian ships it, but packages lag behind releases. I build from source and manage with Stow.
Getting the prebuilt server
Scrcpy has two parts: desktop client (what you build) and Android server (prebuilt APK). Download the server binary first:
wget https://github.com/Genymobile/scrcpy/releases/download/v2.2.1/scrcpy-server-v2.2.1
Building with Meson
Extract source, then configure with Meson. Key detail: --prefix=/ not --prefix=/usr/local. Stow handles the prefix via DESTDIR:
meson setup build --buildtype release --strip -Db_lto=true \
-Dprebuilt_server=scrcpy-server-v2.2.1 --prefix=/
Build flags: release mode, stripped binaries, LTO for smaller size. Point to downloaded server binary.
Compile:
ninja -C build
Installing to Stow directory
Install to version-specific Stow directory instead of system paths:
DESTDIR=/usr/local/stow/scrcpy-2.2.1 ninja -C build install
This creates /usr/local/stow/scrcpy-2.2.1/bin/scrcpy, /usr/local/stow/scrcpy-2.2.1/share/scrcpy/*, etc.
Activate with Stow:
cd /usr/local/stow
sudo stow scrcpy-2.2.1
Symlinks created in /usr/local/bin, /usr/local/share, pointing to Stow directory.
Uninstalling
Remove symlinks:
cd /usr/local/stow
sudo stow -D scrcpy-2.2.1
Delete directory when done:
sudo rm -rf /usr/local/stow/scrcpy-2.2.1
If server path isn't found
Sometimes scrcpy can't locate the server binary. Set the path manually:
export SCRCPY_SERVER_PATH=/usr/local/share/scrcpy/scrcpy-server
scrcpy
Or add to shell rc file for persistence.
Why bother
Distro packages work fine. But building from source means latest features and bug fixes. Stow keeps /usr/local clean—multiple versions coexist peacefully. Upgrades are symmetric: build new version, stow it, stow -D the old one.
Is it necessary? No. Is compiling software more satisfying than apt install? Absolutely. Even when it doesn't matter.
Welcome Interweb traveler.
In the last few weeks I decided to tackle the lack of options in Wayland/Weston1 to configure a keyboard's keys as "dual role".
Probably I should do a quick recap to explain why I need this kind of machinery. In the last few years I grew annoyed with standard staggered keyboards based on an ancient design that has no right to exist in this day and age.
What I can't stand is the horizontal stagger itself, as if they were still using mechanical levers from the steam age, but also the lack of easily reachable modifiers, as Ctrl and Alt keys in a default layout are probably in the most inconvenient place, at the side of the uselessly humongous space bar.
When in the past I've been stuck with a non replaceable keyboard I settled on using a little utility called Xcape to configure the space bar to act as a space when tapped and as a Ctrl modifier when pressed with another key, what's called a "dual role" or "tap and hold" key. Pressing two keys at the same time is called chording, like in music.
Well, I also added left paren to left Shift, right paren to right Shift, and Escape to Capslock, as in Emacs complex combinations like Meta-Ctrl-somethingelse could be simplified by tapping Escape and then doing a plain Ctrl chord instead of doing a more awkward three-key chord2.
Anywho, Xcape is a little cumbersome to configure and to use as there's a little delay that's used to discriminate between one of the two roles, also it's more evident when one types fast enough, but the biggest showstopper lies in its underlying operations as it taps directly into X11 and that's a big no-no under Wayland.
So doing a documentation survey on evdev to tackle the problem myself, I stumbled on a little and relatively unknown utility called evdoublebind that allegedly already did what I was looking for.
Well, it needs a proper reading of the documentation and because Gnome lacks support for customized XKB rules it also requires modifying the system XKB file directly — /usr/share/X11/xkb/rules/evdev. On Debian I suggest using dpkg-divert to preserve it in case of upgrade — but at least now I've learned how to build a keyboard description file using XKB.
After a few weeks of usage I could say I'm a happy camper, it's extremely reliable as I've never found the keyboard unable to type space characters after resuming the notebook from sleep — as it sometimes happened previously with Xcape — and I have to say that I haven't mistyped a single space, nor have I changed the way I type like when I was using Xcape.
In conclusion, to the reader with my same requirements who followed along this far, I can give my two thumbs up to evdoublebind wholeheartedly.