Text files as a user interface
Update: After some feedback, I’m realizing that I need to do a better job of explaining what I get out of this method versus other methods. I’m finding it’s slightly nuanced and difficult to get across, so perhaps in the form of a list:
-
First, this card was originally titled A text editor as a user interface which was wrong and gave entirely the wrong impression. That was entirely my fault. Sorry!
-
This is not really about editing long commands. Shells have built-in ways of doing that (e.g. Bash has the
Ctrl-x Ctrl-esequence). -
I don’t want to create an in-editor interface controlled by the text editor as in an ELisp extension in Emacs.
-
I don’t want to write any more Vim script than I have to, ever. I’ve done it, thanks.
-
I don’t want to limit this to any particular editor at all.
-
This is usually part of a collection of command-line tools as a personal ecosystem.
-
One of the biggest surprise advantages of this method is keeping the text files around so that previous input is still there.
-
As a bonus, persistant editor undo history can even give you recently used values.
-
I added a new middle section, My image gallery tool example, which I think will get the point across much better.
Okay, on with the card:
A huge difference in effort lies between making a utility that takes a few command-line arguments versus a full-blown text-based user interface (TUI). Good user interfaces are hard work to make.
And yet, sometimes I want something in between. Something I’ve been experimenting with lately is using a text editor as a user interface for my programs.
Having a program launch your text editor (as specified in environment variable
$EDITOR) for you is a well-known idea. Here are three existing examples
right off the top of my head:
-
crontab -eto edit the cron table -
git committo edit the commit message -
visudoto edit /etc/sudoers -
vipwto edit /etc/passwd
All four open temporary files and allow you to edit them to your heart’s content from the warm comfort of your favorite text editor and then take that input and use it.
vipw makes a good example, since its entire purpose is to
ensure your edits are okay before applying them. From the man
page:
"vipw edits the password file after setting the appropriate locks, and does any necessary processing after the password file is unlocked. vipw performs a number of consistency checks on the password entries, and will not allow a password file with a "mangled" entry to be installed."
You, too, can use the power of text editing to control programs of arbitrary complexity.
Tiny example
As a contrived example, let’s imagine I want the ability to write some
text and have it reversed with the rev utility.
This three-line shell script will open a temporary file with your
favorite text editor and then run its contents through rev, printing
the reversed result.
FILE=$(mktemp) $EDITOR $FILE rev < $FILE
I told you it would be contrived. But you can see how simple the concept is. I’ve given myself the full expressive power of my text editor with just a single line. It would take me years to write my own ad-hoc TUI of comparable editing power. And unlike an ad-hoc interface, I don’t have to explain how this one works!
My image gallery tool example
Over the years, I’ve really struggled with managing our digital family photo "album". It needs to stand the test of time, so proprietary software is right out.
I’ve slowly created a collection of Ruby scripts (and a PHP website served from the home intranet web server) that create and manage a gallery based on:
-
Directories (broken up by year and event).
-
Text files.
-
Thumbnails generated on ingestion.
The "ingestion" phase is a script called gallery, and this is the piece I’d
like to focus on. Here’s the help text when you run it with no arguments:
Usage: gallery <dir> [dry]
Options:
dry do a dry run to see what would happen
Input:
<dir>/
inbox/ <-- incoming images
inbox.txt <-- description, first line will be <out> in output
Output:
<dir>/
<out>/
set<#>/
thumbs/ (all thumbs with md5 names)
description.txt (copy of inbox.txt)
thumb_map.txt
(all images moved here)
Verbose, right? But I almost never see this. The script is usually called from a shortcut. My gallery directory almost never changes and so when it does, I want this help message to be thorough!
The "Input" section is what this card is about:
-
The images to be added are sitting in and
inboxdirectory. -
Outside of that directory is a file named
inbox.txtdescribing the images as a whole.
The format of inbox.txt is the real user interface.
Each line of the text file gives me different functionality such as
naming the set, tagging it, etc. The exact details aren’t important
so much as the fact that editing the text file has become incredibly
intuitive and fast.
The gallery script takes care of making thumbnails, creating directories,
moving files, etc.
Have you ever wished a traditional "UI" would just get out of your way and let you edit the dang text?
Using this looks like this:
-
On one side, I’ve got a graphical directory (usually just the Thunar file manager, which shows thumbnails) open to the
inboxdirectory. -
On the other side, I have Vim open with
inbox.txt. -
I transfer images to the inbox directory from the camera or a file share somewhere.
-
I edit
inbox.txt. -
A keystroke sequence in Vim runs the
galleryscript. -
galleryprints little dots…as it makes thumbnails. -
The images are moved out of the
inboxdirectory. -
When it’s done, I’m dropped back into Vim and the inbox is empty and ready for the next set.
Simply put, the lack of traditional UI makes this the best image cataloging interface I’ve ever used. It’s as fast as editing text.
(Note that I do call this script from within Vim, but in no way is this an in-editor plugin. The script could call the text editor rather than vice-versa. The next example is more like that.)
Oh, and here’s another awesome thing about text files. If you had looked real
carefully at the "Output" section of the help text, you might have discerned
that I don’t throw away the contents of inbox.txt after each use. The
gallery script copies it verbatim to the final gallery as description.txt
for the set so my other tools can still use it! Cool, right?
Full example - controlling a complicated CLI interface
Command line interfaces for tools like find, ffmpeg, and rsync can
be incredibly complex. Imagine controlling them with text files with
comments and explanations as verbose as you like.
It’s like having your own step-by-step wizard, but with zero UI programming and no new interface to learn.
For this example, I’ll show how I’ve created a text interface for the
popular Python video downloader, yt-dlp.
This could be a shell script, but I’m practicing self-care by doing it in Ruby instead (and I think Ruby is better for "shell" scripting anyway).
First, the script, then an explanation:
#!/usr/bin/env ruby
$destination="/media/video"
$settings_file = "#{$destination}/download.txt"
if !Dir.exist?($destination)
puts "Error: #{$destination} doesn't exist."
exit 1
end
# Read sub-directories from destination
# Do this first so we can populate the default
dirs = Dir.glob "#{$destination}/*/"
subdirs = dirs.map{|d| d.split('/').last }
# Populate settings with defaults
$settings = {
subdir: subdirs[0],
fname: 'foo',
url: 'https://example.com/foo/',
};
def read_settings
File.readlines($settings_file).each_with_index do |line, idx|
if(idx==1) then $settings[:subdir] = line.chomp end
if(idx==2) then $settings[:fname] = line.chomp end
if(idx==3) then $settings[:url] = line.chomp end
end
end
# If it already exists, use existing values
if File.exist? $settings_file
read_settings()
end
# Now write current findings to file for editing pleasure
File.open($settings_file, 'w') do |f|
f.write '# '
f.write subdirs.join(' ')
f.write "\n"
f.puts $settings[:subdir]
f.puts $settings[:fname]
f.puts $settings[:url]
end
# Now use vim to edit new settings
system("vim #{$settings_file}")
# Make sure it exists
if !File.exist? $settings_file
puts "Error: #{$settings_file} was not written!"
exit 1
end
# Read it back in
read_settings()
outpath = "#{$destination}/#{$settings[:subdir]}"
if !Dir.exist?(outpath)
puts "ERROR: #{outpath} doesn't exist!"
exit 1
end
# Timestamp the filename to avoid having to sequentially number!
timestamp = Time.now.to_i
outfname = "#{outpath}/#{$settings[:fname]}_#{timestamp}"
# Call executable
# %(ext)s = Auto-selected file extension as a string (s)
cmd = "yt-dlp_linux -o '#{outfname}.%(ext)s' '#{$settings[:url]}'"
puts "Running: #{cmd}..."
system(cmd)
This is a pretty long example, but it’s a whole application with error checking and everything and it has proven quite robust so far. (A version without error checking, etc. could be a fraction of this size.)
The text file specifies a destination sub-directory, a base filename, and the page URL hosting the video.
One of the features I think is pretty neat is that the script populates the current list of available sub-directories for downloading as a comment in the text file so I don’t have to remember them.
An example:
# cs_lectures film howto sketchbooks watercolor howto fix_toilet https://example.com/how-to/fix-a-toilet.html
This will save the video as something like
/media/video/howto/fix_toilet_1777324491.mp4.
The base filename is appended with a timestamp so I can grab serial videos like lecture series and keep them sequential for sorting without having to add logic to figure out which number comes next (1,2,3, etc.).
I do not use a randomly named temporary text file because I want to re-use my last settings (if there were any). But if the file doesn’t exist, I populate it with example defaults.
If I had more parameters than this, I might be tempted to add more comments or some sort of simple .ini, .conf, or .toml style input "fields" in the text file format. Keep it simple and it can be dirt simple to read those in even without a library.
I’ve used a variation of this trick in at least half a dozen different utilities, some of which I use daily. The technique has really grown on me.
Hopefully this gives you some ideas for similar uses.