Terminader
Why are Finder and Terminal separate apps?
This is an experiment that challenges the status quo set literally decades ago. Finder and Terminal are separate interfaces to the file system and applications, but should they be?
Termina-der, half Terminal and half Finder, is my attempt at answering that question.
Before we proceed
This is a toy, which means I didn’t implement most of the destructive actions like deleting files. However, the command line shell is live and can do whatever you type in it. Terminader is not sandboxed and it’s not a simulation.
For the rest of this article, I’ll refer to Terminader’s command-line interface as the CLI and its graphical interface as the GUI, so that the words “Terminal” and “Finder” can refer to the built-in macOS apps.
The current working directory
The first important realization is that both Finder and Terminal have a strong sense of the current working directory, so in Terminader the two interfaces share it. Simply use the GUI/CLI button to flip between the two interfaces to the same current working directory.
But Finder provides a few other ways to move around:
-
The sidebar takes you to a configurable set of favorites and other locations, and clicking on them simply changes the current working directory.
-
Folders you navigate into are added to a web browser-style history, where you can easily go back and forward with a single click.
-
A picker (a ⌘-click in Finder) lets you navigate up to any of the current working directory’s parent directories.
All of these are replicated in Terminader and also change the current working
directory on the CLI side. In addition, you can issue the enhanced pwd back
or pwd forward
CLI commands to move about the history.
Selection
Both Finder and Terminal support powerful ways to select files to act on.
Terminal is especially powerful if a wildcard (e.g., A*.txt
to select text
files that start with “A”) suits your needs, and Finder is powerful if your
choices are somewhat arbitrary.
Terminader has the best of both by also unifying selections. When you select files on the GUI side and then flip to the CLI side, the paste button indicates how many files have been selected, and would paste the list to your command line with one click. This is analogous to dragging selected files from a Finder window into a Terminal.
But that’s not the end of it. Terminader introduces two new commands, select
and deselect
, that can modify the selection using wildcards. So you might
select *.txt
on the CLI side, and then flip to the GUI side and deselect the
two or three that you didn’t actually want.
What about the Shell?
The astute reader will have pointed out by now that some of what I’ve been calling features of “Terminal” are actually implemented in the Shell, yet another piece of software. That is entirely correct, for our concepts to work we need to break that wall too.
Modern terminal emulators are aware of certain contexts within the shells they spawn, such as the current working directory. But possibly to respect a user’s choice of shells, or perhaps in adherenence to the Unix philosophy of just doing your own thing well, the integrations I’m aware of are relatively minimal and rely on gross escape sequences. As a result, we use little more of the computer’s capabilities than a VT100 terminal that shipped in 1978 did. Let’s take this opportunity to rethink a few things, beyond just GUI-CLI integration.
Why is stderr mixed in together with stdout?
If you’ve ever used a command-line program that actually emits output to
stderr
, it probably just clobbers the regular output to stdout
, because
terminal emulators just put them all on screen.
Terminader puts stderr
output in its own pane called “Error”, and if a
program only output to stderr
, it would be mirrored in the regular “Console”
pane to serve as user feedback.
Why are unrelated commands all just dumped into a massive undifferentiated log?
We actually know what output correlates with which user command. This allows us to render them in a way that is distinct from the output of other commands.
Terminader encloses the output from each user command in a rectangle. A green rectangle indicates that the command executed successfully (termination status 0), while a red rectange indicates an error was encountered, and a cyan rectangle indicates the command is still running.
So what?
It means you can filter the log and exclude the outputs of entire commands. It means we can automatically attach a timestamp to a command.
It means you can pop the output block out to its own window, to easily “pin” it without creating a new terminal pane.
I didn’t continue to flesh out this concept, but you should be able to save the output to a file or send it to a printer after the fact, instead of either attempting a large click-and-drag selection, or re-running the program to redirect its output.
Why are we stuck with plain text?
Sure, man
uses boldface and underline, and ls
uses colors, but we’re still
leaving a lot of untapped potential. Modern terminal emulators have
invented
several
ways to output graphics, but
they all rely on clunky escape sequences, and don’t really solve more general
problems. It takes a valiant
effort to even allow a
hyperlink to be clickable.
Terminader allows an application to output MIME. Output that begin with:
MIME-Version: 1.0
are treated as MIME output, and the following MIME headers are honored:
Content-Type: image/*
The image itself must be Base64-encoded because I couldn’t figure out how to
get the pipe to stop converting LF (line feed) characters into CR (carriage
return) LF pairs and corrupting the image data. But otherwise if NSImage
can
decode the format, it should display on screen.
A new cat
command uses this ability to display an image in the CLI.
Content-Type: text/markdown
Markdown provides not only formatted text output, but also hyperlinks.
Terminader includes a rudimentary new ls
command that outputs hyperlinks
that are used to invoke contextual actions.
The new cat
command treats files with an .md
extension as Markdown
documents.
Content-Type: text/plain
This isn’t really necessary because you could just not output the
MIME-Version
header, but it’s there.
Content-Transfer-Encoding:
Only base64
is supported at this point.
What Next?
MIME enables rich CLI output, and there are some interesting avenues to explore:
-
Piping MIME output among command-line utilities, such as ImageMagick.
-
Avoiding mojibake by using
charset
to render text correctly.
But if you mean whether Terminader is likely to become a real tool, it probably won’t. SwiftUI is great at rapid prototyping (all this took me a bit over a week) but the real work of integrating the full features of Finder, Terminal, various shells, and many Unix tools is massive. Such a venture would also likely need private Apple APIs, and likely will break constantly with new macOS versions.
System Requirements
Intel or Apple Silicon Mac running macOS 14 (Sonoma). If you cannot use
Sonoma, defining the SUPPORT_IME
flag and making your own build should still
mostly work.
Known Issues and Excuses
-
Interactive programs are not going to work. That includes
vi
, but also programs liketop
that fairly randomly addresses the terminal screen. -
Internal commands don’t handle quotes and backslash-escapes.
-
Contextual actions in the CLI are invoked by clicking on the link. I can’t figure out how to make
AttributedText
links require a ⌘-click. -
Contextual action pop-ups in the CLI aren’t anchored to the link you clicked on.
-
The “Get Info” contextual action opens a window and does nothing.
-
If you “Show Tab Bar”, the tabs will have blank titles. SwiftUI doesn’t seem to allow me to hide the window title but populate the tab title.
-
There’s a long delay in the GUI when you select the first file. I can’t quite get all the gestures to work simultaneously.
-
⌘A to select all the files would be nice, but I can’t get SwiftUI keyboard handling to work quite right.
-
The CLI prompt can lose keyboard focus and stop accepting input but still happily blink as if it had focus. You can click on the cursor area to give it back the focus.
Hack
The scripts
directory holds scripts that are searched first, so they can be
used to override real commands. For example, the man
script overrides the
pager to return its entire output, but without losing the nice formatting.
Acknowledgements
-
apple / swift-argument-parser for command-line parsing.
-
eonil / FSEvents to monitor changes in the current directory.
-
gonzalezreal / swift-markdown-ui for Markdown rendering.
-
ksemianov / WrappingHStack for the GUI grid.
-
Last but not least, friends I will not name for reasons have contributed important ideas, pointers, and encouragement.