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

A text editor as a user interface

Page started: 2026-04-27
Page published: 2026-04-29

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!

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.