This is a card in Dave's Virtual Box of Cards.

Text files as a user interface

Page started: 2026-04-27
Page published: 2026-04-29
Updated: 2026-05-02

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:

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:

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!

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 inbox directory.

  • Outside of that directory is a file named inbox.txt describing 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 inbox directory.

  • 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 gallery script.

  • gallery prints little dots …​ as it makes thumbnails.

  • The images are moved out of the inbox directory.

  • 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.