Andrey Listopadov

You don't need a terminal emulator

@random-thoughts @emacs ~15 minutes read

…if you’re an Emacs user, that is.

You know, it’s funny, because people have opinions on why you don’t need a terminal on entirely different ends of a spectrum. It’s like that IQ chart meme:

Figure 1: *That’s Visual Studio on the left, not VS Code

Figure 1: *That’s Visual Studio on the left, not VS Code

And yes, I know, there are many people who will disagree with me, probably on everything that I’m going to say next. But hey, it’s OK to agree to disagree. You don’t have to believe me, instead, you should try it for yourself.

I often see how young programmers walk this kind of path:

  • Terminals are scary.
  • I don’t need a terminal - my IDE does all things for me with a click of a button.
  • Well, I needed a terminal for some kind of task in my latest project and it wasn’t so bad.
  • GOSH, terminals are so powerful with all these commands available.
  • installs Vim
  • OMG, Vim is so powerful and it is a Terminal-base program!
  • I don’t need a fancy IDE - I’m a power user now!

And many often stop there, which is fine, but a bit sad. I also been there, but somehow moved on. Well, not all - some stay within the comfort zone of their IDE which is fine too, some ditch Vim for something more, or even something less, and really become power users. Mine were:

  • Emacs? Eww, what’s that ugly white theme
  • Well, Emacs seems interesting, but it starts so slow compared to my 100+ plugins NeoVim setup.
  • EVIL mode? Second-floor basement1? MAGIT?
  • Emacs has wrappings for the tools I use?
  • Emacs can run any shell command?
  • Even remote ones?
  • I don’t need a terminal?
  • I don’t need a terminal.

Well, I know some people who think alike, so it’s not just me, but I do believe that it’s not a majority of Emacs users. The Vterm package for Emacs has 1.6k stars on GitHub at the point of this article, and Emacs itself has several terminal implementations in its core. So, where I’m coming from with this?

Basically, a few months ago I decided to stop using a terminal emulator. At all. I deleted my Vterm configuration from Emacs, and since I didn’t use terminal outside of Emacs it was pretty much it. I did keep the EAT package still, as it is sometimes useful for things not so related to terminals directly, but more on that later.

Now, today I’m going to describe why terminal emulators aren’t really needed in Emacs specifically, but nothing actually prevents other software from following suit. It just doesn’t happen that often, I guess because most of the time people freak out of non-interactive shells, even though they come with lots of benefits.

To be clear, yes, my use cases may not cover all of the workflows that involve terminals, however, I think I’m covering most of them. For instance:

  • I often SSH to remote servers to browse and edit files;
    • More often I’m SSH’ing to remote servers to run some commands;
      • Sometimes even via multiple hops;
  • I update existing and install new system packages;
  • I run commands on my local machine. Things like:
    • project compilation;
    • grepping stuff;
    • finding files;
    • using things like AWK, Perl, Sed, etc.;
    • running servers and services in the background.

So, combined, I do both local and remote work, and I don’t need a terminal emulator for that. How?

async-shell-command

Emacs has this command, bound by default to Alt+& (M-&), and what it does is really simple. It runs a given command asynchronously in the background, feeding its output to the *Async Shell Command* buffer. That’s it. Well, in my case, I’m redirecting stderr to the *Shell Command Errors* buffer mostly for cases when stdout is somehow structured. We’ll get to that.

What’s cool about async-shell-command is that it basically is a terminal emulator. Well, not quite, but very close to being that. Thing is, let’s just think about what a terminal emulator is - it’s a prompt reading a line, and text spitted between each prompt. Well, async-shell-command is basically that, except it doesn’t have the prompt in the buffer itself - instead, it’s part of Emacs’ minibuffer.

And, unlike a terminal emulator, the result of each command is put into the dedicated buffer that you can do all of your usual Emacs operations on. Ever wondered why things like Tmux implement things like copy-mode? Because the terminal is interactive by nature and when you need to do non-interactive (by interactive I mean text input and command running) stuff you have to switch to a separate mode. I don’t like separate modes that much.

So async-shell-command gives you all of the properties of a regular terminal - i.e. it displays text output from a command, and you get a prompt with history and stuff like command/file completions. If the command requires some input from the user, Emacs will properly accept it in the said buffer - it is interactive. However, it is not a real terminal emulator still - it can’t do TUI stuff. So if you’ve got anything in your workflow that uses TUI async-shell-command is not the right tool for the job.

I’ll get back to the interactive TUIs in a bit, but I should say that I don’t have any such programs in my workflow. Honestly, if I’m already using Emacs, why do I need something TUI-based, if Emacs probably already has a better graphical interface to it? We’ll get to that.

compile & project-compile

Now let’s get to the non-interactive interactive stuff. Earlier I mentioned that non-interactive shells (again, by interactive I mean text input and command running) have some benefits to them but it’s often hard to describe what are those to a non-Emacs user.

So, compile is very similar to async-shell-command except it doesn’t try to process any user input. Therefore, if you ran something like scp in a compilation buffer, and forgot to place your SSH key to the machine you’re going to copy the file to, you’ll be asked for a password, but you won’t have any ability to insert it. It’s better to use async-shell-command for stuff like that.

project-compile is exactly the same thing as compile, except it automatically detects the current project’s root directory, and runs from there. It’s very handy to run things like make because Makefile is usually located at the project’s root. But, why is it better than async-shell-command?

Well, compilation buffers are pretty specific - they display the log of the compilation process. That’s why we can treat them specially too. And Emacs does it by default, creating file links to every message in the compilation buffer. For example, if I run the M-x compile RET main.c RET with the following main.c:

int main() {
    printf("works\n");
}

We get the following buffer:

As can be seen here, each warning is highlighted with a respectful color, like notes use a teal color, and warnings are yellow. But that can be done by terminals too, you might say. Note, however, that when I hover my mouse over such colored text it shows up as a link that I can click. Upon clicking it, Emacs will open that file and highlight the error.

Well, it might not be as impressive as it sounds, but this is an open interface. You can add support for any kind of language you want, and it will work. I have a post, describing how I’ve added support for Clojure, and went as far as searching for warnings inside dependencies stored as .jar archives in my .m2 cache. It’s nuts how flexible this system is.

Similarly, if you run M-x project-compile RET grep -Rn "something" RET it will create a compilation buffer with the results from grep, and each result will be a clickable link:

You don’t need to click on links either, you can use n or p to go to the next link, and automatically open the file at the link’s specified position. But running grep and other tools through the compilation buffer is a bit cumbersome, as you do have to follow a specific line format that this buffer understands. Instead, you can use Xref.

Xref

Another thing provided by Emacs, mainly for reference navigation purposes, but extensible enough that you can make it do whatever you want. For instance, you can run such things like grep right from Emacs, and get results in the *xref* buffer:

Figure 2: result of calling project-find-regexp which internally uses grep

Figure 2: result of calling project-find-regexp which internally uses grep

Similarly to the compilation buffer, you get the same set of shortcuts to move around, but a more readable and categorized output. And this works for many other things, like results from your language servers, tag files, etc. Even more than that, you can do stuff like xref-query-replace-in-results from the *xref* buffer - so it can act like Sed.

And that’s my point - these tools are great, but using them from the terminal is not fun. Emacs acknowledges that these tools are great, and provides a better interface for them without requiring you to run a terminal emulator inside Emacs. To be fair, Emacs isn’t the only one who does this - but I find it to be the most consistent and most flexible in this regard.

But what if you need to do stuff with remote machines?

TRAMP

So, we’ve covered basic tasks involving running various commands on your local machine. The async-shell-command can be used to run arbitrary commands and those which require some level of interactivity. The compile command is for project compilation stuff, and Xref is for navigating references provided by some other tools or Emacs itself. Now imagine we need to run grep on the remote - how would we do that?

TRAMP got you covered. When opening a file in Emacs you can start the path with a forward slash / followed by a protocol, like ssh and Emacs will open a remote file for you. For example /ssh:user@host:/home/user/.bashrc will open the .bashrc file on the remote machine if you have access to that. If not, Emacs will ask for the password, and it is basically the same as the ssh in the terminal.

But now, when you’ve opened a file via the TRAMP, suddenly, you can use all these things I’ve been talking about before - async-shell-command, compile, project-find-regexp and it will do its work on the remote. It’s really transparent.

More than that - if you have a remote machine, and it has something like a language server installed, Emacs will not try to run your local machine’s language server (if it even has one), instead, it will run it on the remote. This means that you can run Emacs on your machine, but have all of the development stuff needed on the remote. And you don’t need to run Emacs remotely, or do X over ssh, or use things like sshfs - just use TRAMP.

And TRAMP isn’t limited to just ssh. You can enter containers as well, e.g. opening a file with /podman:container-name:/path/to/file will give you access to files and programs in a container. And there are many more methods that TRAMP supports, so you’re probably well covered for your particular remote accessing needs. As for me, instead of SSH’ing to a machine I just open it in DIRED via TRAMP, and run commands using async-shell-commands most of the time.

Yes, TRAMP can be slow, but I haven’t noticed that in my practice. It depends on the connection speed, and when I do my work, all remote machines are usually in the same network. Your case may vary.

But I can’t stress this enough - you don’t need a terminal emulator for the task of editing remote files or running commands.

Imagine this - I have to work with remote machines created by other people. They’re set up from an arbitrary Linux image, not all of them even have Vim - only stripped down vi. So if I were to ssh to them and run vi I would probably be bald now, because vi is terrible, and if your terminfo isn’t supported by the remote you get gibberish symbols here and there when interacting with vi or even the shell some times. And believe me, I know how to use vi and Vim, I used them for many years prior to Emacs - I just don’t want to anymore.

So every time I hear things like: “Yeah, Emacs is cool, but I have to SSH to headless remotes and edit files, so Vim is a more fitting choice for me”, I say: “No. No it isn’t”. And even if that’s the case - by SSH’ing to a remote, you lose your Vim configuration - you either get a pure Vim or sometimes even just a plain vi. When I was using Vim, not having my config around was annoying, because I had many handy keybindings, helpful plugins for navigation, and so on. So instead of SSH’ing, I used sshfs to mount the remote. This can work, until you need to execute something on the remote - then you still need to use SSH.

With Emacs - I don’t.

TUIs

Now, let’s get back to the elephant in the room. Running grep and other stuff is fun and all but we can’t leave out programs that use a TUI - Terminal User Interface.

I often see people running mc (Midnight Commander), some use TUI Git clients, and many find fzf very useful. And yeah, neither async-shell-command nor compile can run those. If I try to run mc from the former, I get this message:

Your terminal lacks the ability to clear the screen or position the cursor.

And that’s reasonable, it’s just a text buffer, not a terminal emulator, as I’ve mentioned.

However, why would you run mc if you have DIRED in Emacs? Same with the TUI Git clients - MAGIT blows those out of the water. FZF? Emacs has multiple completion narrowing and filtering frameworks that integrate with the rest of Emacs, so I don’t see why would I need to use fzf specifically. And if you want - you can, there’s a package that integrates with it.

So, my point is - most of the time, there is either a package providing a superior experience to the one that the TUI app provides. And it’s still text - you can use all of your Emacs operations on it, like on a regular buffer, same as in a terminal, just less clunky.

Also, remember how I said that I install system packages from Emacs? Well, package managers usually don’t have a TUI, so I do it from async-shell-command which allows me to interactively use my system’s package manager. But, if I were a GNU Guix user, I could use Emacs to get an interface similar to the one you get with package.el:

So yeah, Emacs is able to give you a better interface to your tools than a terminal emulator. And if not, which I’m yet to see, you can run a shell in Emacs.

Wait, wait, I said you can run a shell, not a terminal emulator. That doesn’t count, right?

Eshell

Emacs has its own shell, written in Emacs lisp, that works across multiple operating systems. It is called eshell and it is great. You can extend it with Emacs Lisp, you can write scripts in Emacs Lisp, and it still has access to all of the other tools you use regularly. But again, it is not a terminal emulator, it is a shell. Like bash.

So if we try to run a program that requires terminal capabilities specifically, Eshell will automatically create a separate buffer that runs an ANSI terminal emulator, also written in Emacs Lisp. Which means that your interactive TUI programs should run fine. In practice, it isn’t always great.

However, recently I’ve found this package: EAT. It is a terminal emulator that can run inside an Eshell buffer - meaning that you don’t have to deal with separate buffers and such if you happen to run a command that requires a proper terminal. Of course, you can use EAT as a terminal emulator by itself, but I don’t find this that useful. Instead, I use it from Eshell, although very rarely too. It’s just a nice backup.

What are the tasks you believe you need a terminal for?

I’m genuinely interested, feel free to contact me and share your thoughts. Or if you tried the ones I’ve provided and it wasn’t better, or if you just think that they won’t work for you - we can chat about that too.

I found myself on this weird path just a few months ago, but these were very busy months. Meaning, I think I have enough experience with this way of doing things to say that it is good. Again, I work with remote machines all the time, and I have to transfer files to them, edit remote files, watch logs live as the services are running, do remote development and more. And I do it from TRAMP with the help of async-shell-command and nothing else. For local tasks, I use project-compile and other project-related functions.

At first, it wasn’t as seamless, but I got adjusted quickly. Yes, I felt the urge to run Vterm, and SSH from there at first - but as time went by I noticed that opening TRAMP is much easier, as it reuses the already familiar Emacs’ file prompt. And the same goes for every other command I do.

It’s a shame that most other software I know doesn’t do this kind of integration. Visual Studio Code just gives you an integrated terminal - which is no different from an ordinary terminal. And in the case of Linux, probably isn’t the greatest terminal either. IntelliJ IDEA does that too, and while their terminal tries to do some smart things, I find it inconsistent.

The same goes for other code editors, like Vim which also now has an integrated terminal. Though running a terminal from a terminal-based editor is weird - you’re one ^Z away from your shell anyway. And for multiplexing needs, you can always use Tmux, which again, is probably better than the integrated terminal.

Kakoune is an interesting case here, they have a similar idea to Emacs - you can run commands and pipe their output to a buffer to later process it. And because there’s no extension language in Kakoune, it’s the primary way of adding features to the editor. E.g. you can’t implement a fuzzy file picker in Kakscript - you gotta use fzf for that. And while that’s a great way of handling this, I would say it’s better than how Vim handles this, I still think that it’s not as good as what Emacs does. But Emacs is an old project, it had time to develop all of that, while Kakoune is still young.

Well, that’s it. Feel free to share your thoughts, disagree with me, or whatnot. I think I’m now a firm believer that terminals need to die off at this point. A superior integration is possible, and already available. Thanks for reading!


  1. arbitrary Metal Gear Solid reference ↩︎