惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

GbyAI
GbyAI
爱范儿
爱范儿
Y
Y Combinator Blog
T
Tor Project blog
V
Visual Studio Blog
U
Unit 42
B
Blog RSS Feed
博客园 - 叶小钗
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
阮一峰的网络日志
阮一峰的网络日志
T
Tailwind CSS Blog
G
Google Developers Blog
I
InfoQ
Stack Overflow Blog
Stack Overflow Blog
IT之家
IT之家
Microsoft Azure Blog
Microsoft Azure Blog
T
The Blog of Author Tim Ferriss
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
The Cloudflare Blog
Google DeepMind News
Google DeepMind News
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
H
Hackread – Cybersecurity News, Data Breaches, AI and More
F
Fortinet All Blogs
人人都是产品经理
人人都是产品经理
Apple Machine Learning Research
Apple Machine Learning Research
The GitHub Blog
The GitHub Blog
Recorded Future
Recorded Future
博客园_首页
罗磊的独立博客
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
量子位
P
Proofpoint News Feed
Jina AI
Jina AI
博客园 - 【当耐特】
S
Security @ Cisco Blogs
I
Intezer
MyScale Blog
MyScale Blog
Simon Willison's Weblog
Simon Willison's Weblog
P
Privacy & Cybersecurity Law Blog
腾讯CDC
T
Tenable Blog
A
Arctic Wolf
T
Threat Research - Cisco Blogs
S
Securelist
Know Your Adversary
Know Your Adversary
Spread Privacy
Spread Privacy
C
Check Point Blog
NISL@THU
NISL@THU
Microsoft Security Blog
Microsoft Security Blog
V
Vulnerabilities – Threatpost

Lobsters

CIFSwitch: a non-universal Linux local root vulnerability RIPE NCC session fixation: poaching logins with an Atlas probe GNOME 2.20 but its Web Components Agentic Search for Context Engineering – Leonie Monigatti Garnix is shutting down [not OC] akashina.tngl.sh/jjc Concerning Emacs (and Jazz) Nitpicking the shell history scene in ‘Tron: Legacy’ What's cooking on SourceHut? Q2 2026 The tenth OpenPGP email summit Package managers that package package managers Clojure on Fennel part three: parsing WordPress at 23 Finding Miscompiles for Fun, Not Profit GitHub - creusot-rs/creusot: Creusot helps you prove your Rust code is correct. Announcing Rust 1.96.0 | Rust Blog A Love Letter to Neovim sqlite AGENTS.md Am I a Bad Friend? CSS vs. JavaScript • Josh W. Comeau Erlang Ecosystem Foundation - Supporting the BEAM community A brief note about slot access cost in Common Lisp Keyboard latency probe Rethinking the GNOME clipboard issues Back to the Building Blocks’ Building Blocks Tech Notes: Theseus: translating win32 to wasm Fast is better than slow Content-addressed Rust builds (or, what kache actually caches) Intent to Prototype: Embedding API Canada’s Bill C-22 and the security cost of collecting more data 5 PostgreSQL locking behaviors that trip people up okmij.org Stop advertising in your commits! | AksDev GitHub - mplsllc/macsurf: A modern web browser for Classic Mac OS 9 PowerPC. Real CSS3, ES5 JavaScript, native HTTPS — built with CodeWarrior on the Carbon API. Introducing DoomBench - Can Your Data Stack Run DOOM? What are some of your favourite developer tools? Building a Scalable Ingestion Pipeline with Temporal (Part 1) Converting shallow Git bundles into normal repositories Are you a member of any professional associations? What is a harmonic? An interactive comic about additive synthesis How Virtual Tables Work in the Itanium C++ ABI Using SwiftUI to Build a Mac-assed App in 2026 Rust (and Slint) on a jailbroken Kindle. ~jack/lambda-on-lambda - Serverless Haskell on AWS - sourcehut git Human proof for FOSS contributions Extremely simple internet radio controlled via IRC Announcing BABLR Splitting Konsole views from Helix to run tools | AksDev GitHub - yugr/rust-slides Serving files over HTTP three ways: synchronous, epoll, and io_uring update docs with information about building with build.py (#979) · astral-sh/python-build-standalone@c9c40c5 A Simple Makefile Tutorial On C extensions, portability, and alternative compilers Switching to Colemak | Pedro Alves Just How Bad Was The Intel IAPX432? Nix's Substituter List Is Not a Routing Table Accelerating copy_if using SIMD Lambda on Lambda: Serverless Haskell on AWS | Blog Announcing feed-repeat v1.0 Scaling Akvorado BMP RIB with sharding EYG news: A host of CLI improvements, new guides and new effects The social contract of writing JS Crossword C array types are weird; and related topics Flatpak will depend on systemd – OSnews Migrating from Go to Rust | corrode Rust Consulting A portentous reunion Vivado Licensing Options How my minimal, memory-safe Go rsync steers clear of vulnerabilities the entropy layer of a wavelet codec, on its own GitHub - nferhat/fht-compositor: A dynamic tiling Wayland compositor. Debian SE Linux and PinTheft Does bulk memmove speed up std::remove_if? (No.) 声明式部分更新 | Blog | Chrome for Developers Fully in-browser container builds Dianne Skoll's Web Site - Remind The Architecture of Open Source Applications (Volume 1)Berkeley DB Pardon MIE? - ironPeak Blog “Long-Term Support” doesn’t mean what you think Jira IS Turing-Complete May I recommend thinking of Emacs as your Fortress of Solitude hershey Floodgap Gopher-HTTP gateway gopher://thelambdalab.xyz/1cuneiforth/ HP QuickWeb, Singular And Pointless That one time I used Go panics for flow control A new suite of modern tools coming for editing and publishing RFCs From the Tabletop… The Digital Antiquarian Building a Host-Tuned GCC to Make GCC Compile Faster Are we self-sovereign PKI yet? Claw Patrol: an open-source security firewall for agents | Deno Revised^7 Report on Scheme, Large: Procedural Fascicle Draft is now public A Network Allow-List Won't Stop Exfiltration — André Graf From AFSK to Goertzel – µArt.cz Software For My New Home Server Introducing Neptune: Direct3D virtualization for QEMU AI Agent Bankrupted Their Operator While Trying to Scan DN42 - Lan Tian @ Blog mimalloc: A new, high-performance, scalable memory allocator for the modern era Making wl_shm fast The Soul of Maintaining a New Machine - Third Draft | Books in Progress What is Git made of?
Text files as a user interface
ratfactor.co · 2026-06-27 · via Lobsters

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:

  • 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-e sequence).

  • 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 -e to edit the cron table

  • git commit to edit the commit message

  • visudo to edit /etc/sudoers

  • vipw to 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!

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.