This is a fancy thing that iTerm can do, somewhat invasively, and Terminal.app can do, somewhat transparently, and if you already know how to do this then just keep doing your thing and don’t worry about it.

This post is going to show you how to do it in tmux, because tmux is great and it’s not coupled to your choice of terminal emulator and it’s a cool superpower to add to your toolkit.

But it’s not trivial to do in tmux. It took me a while to figure it out. Which is why you’re looking at a blog post, and not a code snippet.

I mean, I can show you the code snippet if you want:

bind -n S-M-Up {
  copy-mode
  send -X clear-selection
  send -X start-of-line
  send -X start-of-line
  send -X cursor-up
  send -X start-of-line
  send -X start-of-line

  if -F "#{m:*➜\u00A0*,#{copy_cursor_line}}" {
    send -X search-forward-text "➜\u00A0"
    send -X stop-selection
    send -X -N 2 cursor-right
    send -X begin-selection
    send -X end-of-line
    send -X end-of-line
    if "#{m:*➜\u00A0?*,#{copy_cursor_line}}" {
      send -X cursor-left
    }
  } {
    send -X end-of-line
    send -X end-of-line
    send -X begin-selection
    send -X search-backward-text "➜\u00A0"
    send -X end-of-line
    send -X end-of-line
    send -X cursor-right
    send -X stop-selection
  }
}

But my goodness. That’s a lot. That’s a hot mess. And it’s pretty coupled to my particular prompt, which looks like this:

~/src ➜ echo hi
hi

Your prompt probably doesn’t look like that. It’s probably full of fancy git statuses and branch names and all kinds of things. That’s okay; this approach should work anyway. You just need to modify it to suit your particular prompt.

The most important part is this:

 

Er, right. It doesn’t photograph well.

The most important part is the non-breaking space after the little arrow.

It’s important because, without it, tmux will trim the trailing space from the output line when it puts it in scrollback, which means that if you ever hit enter without typing a command:

~/src ➜ echo hi
hi
~/src ➜
~/src ➜
~/src ➜ echo bye
bye

Your prompt will be one character shorter than a regular prompt. Which will break things, but only in this one unusual case.

Anyway, armed with the non-breaking space, we now have a fairly unique string that we can use to distinguish a prompt line from a non-prompt line: "➜ ". Or, if you prefer, "➜\u00A0". I find that much more readable, because my text editor doesn’t make it easy for me to distinguish between spaces and non-breaking spaces.

Typing it out like that has another advantage: if I ever cat ~/.tmux.conf, I won’t confuse my command by printing out a string that makes it look like a prompt line.

So that’s nice.

Anyway, this was a long preamble to say: if your PS1 ends with a space, you have to change it to end with a non-breaking space, or this command won’t work right, for dumb reasons that will be explained below.

If your prompt doesn’t end with a space, well, that’s just fine. Keep on living your life. You basically just need to be able to write a regular expression that can distinguish “prompt lines” from “not prompt lines.”

Then there’s one more tricky thing: you have to worry about “hard-wrapped” lines and “soft-wrapped” lines. So if you type something like this:

~/src ➜ echo "yes this is a very long line and it's going to wrap over the 
edge of the text and in my blog it looks like i just put newlines in but 
pretend i didn't and this is just one big long line that tmux is soft-wrapping"
yes this is a very long line and it's going to wrap over the edge of the text 
and in my blog it looks like i just put newlines in but pretend i didn't and 
this is just one big long line that tmux is soft-wrapping

There are six “displayed” lines, to borrow vim terminology, but only two “physical” lines.

Most tmux operations act on displayed lines – which is what you want, when you’re using it interactively. But obviously we only care about physical lines.

So the way we find the “physical line” above or below the current one is kind of goofy: we do start-of-line; start-of-line. If you run start-of-line once, it takes you to the beginning of the displayed line. But if your cursor is already at the beginning of the displayed line, it takes you to the beginning of the physical line instead. This is very intuitive when you’re doing it by hand, but it makes the script look a little goofy.

Okay. That’s all the background knowledge we need. Now let’s look at that code snippet again, but this time we’ll talk about it.

bind -n S-M-Up {

I like this without a prefix key, but to each their own. It’ll leave you in copy mode, so you can easily repeat it without having to re-type the prefix, if you set up the bindings right.

  copy-mode
  send -X clear-selection

This just “resets” our state so that we’re ready to copy and so that we have no selection active. Pretty simple.

  send -X start-of-line
  send -X start-of-line

This puts our cursor at the start of the current physical line. We want to do this in case we’re in the middle of typing a very very long command that has soft-wrapped. Note that send -X -N 2 start-of-line does not do the same thing, which makes some sense if you think about it.

  send -X cursor-up
  send -X start-of-line
  send -X start-of-line

And this puts our cursor at the beginning of the physical line just above us.

  if -F "#{m:*➜\u00A0*,#{copy_cursor_line}}" {

This is a little crazy looking, but basically this is checking “is our cursor now on a line that contains a prompt.” m: matches against a glob; you can use m/r: to match against a regex, if you need to.

Okay, so now we’re in the “yes it is a prompt” block.

I couldn’t figure out a way to say “okay skip it and keep going; we only care about output here.” You can’t loop or recurse in a tmux command. So I made it select the text of the command after the prompt instead, and honestly, it’s pretty nice? I’m glad that I did that. So this is how:

    send -X search-forward-text "➜\u00A0"

This positions our cursor at the start of that string – so two characters to the left of the beginning of the actual prompt.

    send -X stop-selection

But it also highlights any occurrence of that string in our buffer. The only way I can find to clear the highlight in tmux is with stop-selection, which has the annoying side-effect of detaching your cursor from the current selection. Which doesn’t matter right now, but it will be annoying later.

    send -X -N 2 cursor-right

Next we skip over the ➜  characters (this is the step where the non-breaking space really matters – otherwise there will only be the arrow, and moving the cursor right twice will move it to the next line, as tmux wraps the cursor around like that).

    send -X begin-selection
    send -X end-of-line
    send -X end-of-line

Then we select from there to the end of the physical line. In Emacs mode, we’re done now. But in vi mode, this will select the newline at the end of the line as well. So if you’re using mode-keys vi, you want to run a final:

    if "#{m:*➜\u00A0?*,#{copy_cursor_line}}" {
        send -X cursor-left
    }

To deselect that newline. But note that because #{copy_cursor_line} is only the current displayed line, this will not deselect the terminal newline if you have a really long command that wraps. So that’s… it’s just bad. But I don’t know how to fix it. You could make it consistently keep the terminal newline selected, if you want, but I don’t know how to consistently deselect it (except to use mode-keys emacs).

But you only want to run it if there is actually something to select. Because on an empty prompt, it would cause you to go back and select the non-breaking space.

And then you’re done. Simple right.

  } {

Phew. Okay. Next is the interesting part.

This is the case where we didn’t find a prompt just above the line our cursor was on when we ran the command. Which means we found a line of output.

So the intuitive thing to do here is to select the current line, then search up to the preceding prompt, and then move the cursor back down one line.

And we’re basically going to do that, except instead of “select the current line” – using tmux’s equivalent of “visual line” mode – we’re going to just use a regular selection. This is because in “visual line” mode, the trick to navigate by physical lines no longer works. I have no idea why.

    send -X end-of-line
    send -X end-of-line
    send -X begin-selection

So. This puts the cursor at the end of the current line. In mode-keys vi, this means that you’re on the newline. In mode-keys emacs, this means you’re to the left of the newline, and you need a send -X cursor-right after the second end-of-line in order to include it.

    send -X search-backward-text "➜\u00A0"
    send -X end-of-line
    send -X end-of-line
    send -X cursor-right

The we extend our selection up to the first character after the end of the preceding prompt’s physical line.

    send -X stop-selection

And then we clear the stupid search highlights, the only way we know how.

This has the very annoying side effect of “detaching” our cursor, so we can’t refine the selection after the fact – if we wanted to skip a header line after a cat csv or something, say. But that’s just the way the world is right now, until some brave soul patches tmux to add a cancel-search command.

  }
}

And that’s it! That’s the end. Hopefully that explains how it works in enough detail that you can adapt it to your own prompt, even if you’re using some fancy multiline thing.

Of course I also want to be able to navigate “down,” in case I overshoot something. But the code for that is very similar. You can see it, in my dotfiles. Look for bind -n S-M-Down. It will also tell you how to make it work with mode-keys emacs – I couldn’t come up with a single implementation that worked for both styles. And it will show you how to just jump to the last prompt – which is a much simpler thing.

wait what about PS2

Sigh, yes, this doesn’t work perfectly if you enter multiline commands. There’s no way to select the multiline command you entered without also selecting the contents of your PS2, which is gross, so… I just made it select each individual line of PS2 output as if they were individual commands. It’s not… perfect. But it works alright.

All I did to support this was set my PS2 to also end with the string ➜ , so for the purposes of this one function PS1 and PS2 are indistinguishable. This works fine. I don’t really use PS2 often enough to want something fancier than this.

wait what about RPS1

Okay look I don’t use RPS1 but yeah you’d want to deal with that if you did. I don’t think it would be hard. Instead of end-of-line; end-of-line you’d want like “search to beginning of RPS1” or something. And somehow exclude all the spaces. Also RPS1 might not actually exist, if you type too much? RPS1 is so weird. Is there a way to make zsh take RPS1 into account when it decides where to soft-wrap lines? I don’t know. Though that would make this totally impossible.

there’s a bug where if output does not end in a trailing newline then it shows up on the same line as your prompt and it’s considered part of the prompt line and even if zsh puts your prompt on a new line and prints that % character it’s still considered a “soft wrap” by tmux and everything is broken

Okay yes look I can’t figure out a way to fix this. I’m talking about this case:

~/src ➜ echo hi
hi
~/src ➜ echo -n hi
hi%
~/src ➜ 

The way this works in zsh is sort of goofy. If it detects a partial line, it prints a % (or a #, if you’re root, for some reason) at the end and then goes to the next line. But it goes to the next line in a “soft” way, by basically printing a bunch of spaces until your line wraps.

In bash, it would just look like this:

~/src ➜ echo hi
hi
~/src ➜ echo -n hi
hi~/src ➜ 

Which is even worse, but it makes the issue more clear: our prompt is on “the same physical line” as the hi output. man zshoptions explains:

PROMPT_SP <D>
       Attempt to preserve a partial line (i.e. a line that did not end
       with  a  newline) that would otherwise be covered up by the com-
       mand prompt due to the PROMPT_CR option.   This  works  by  out-
       putting  some  cursor-control  characters, including a series of
       spaces, that should make the terminal wrap to the next line when
       a  partial line is present (note that this is only successful if
       your terminal has automatic margins, which is typical).

I can’t figure out how to fix this.

In bash, it’s not really an issue: it will still be detected as a prompt line. But in zsh, because #{copy_cursor_line} only gives us the current displayed line, the first displayed line in that physical line is just "hi%". So it doesn’t know that’s a prompt.

I feel like the real solution is to submit a patch to tmux to give me commands that actually distinguish between physical and displayed lines, so we can look at #{copy_cursor_physical_line} instead, and all the navigation will be a lot simpler and nicer.

I tried a lot to make this work, and ultimately could not find something that worked without breaking support for long soft-wrapped prompts. Which I have more often than partial lines. So you could unset PROMPT_SP and unset PROMPT_CR and make it work like bash, but… nobody wants that.

Look, I don’t know. For the record Terminal.app doesn’t get this right either. I didn’t check how iTerm works. So… just… don’t use this? If you have a lot of partial lines?

so how much time did you spend on this dumb tmux thing

Look, it’s still quarantine, okay?


Doing this uncovered a number of tmux things that I wish were easier, which I will now enumerate so I don’t forget them.

  • In select-line mode, the start-of-line; start-of-line trick to navigate by physical lines no longer works, for some reason.

  • I wish there were just separate start-of-displayed-line and start-of-physical-line commands, to take the guesswork out of all of this.

  • I wish there were a separate copy_cursor_physical_line so that prompt detection would work even with partial lines.

  • There appears to be no way to clear the search highlight except by running stop-selection, which is very annoying, as that has the side-effect of detaching your cursor from the selection mode.

  • There is a weird bug where if you’re using mode-keys emacs and you select a line’s trailing newline, it appears as if you’ve selected the first character of the next line, but only if you select “from above” (leaving your cursor on the character after the newline) and not if you select from “below.” It’s not selected! It just renders with the selected colors. (This is only really noticeable if you use stop-selection to “detach” your cursor, or use other-end to relocate it, since otherwise the cursor style obscures the “selected” style.) This is hard to describe, but is very obvious if you run the script in the “opposite direction” – it will look like you selected one character too many. But you didn’t. (Note that if you’re using mode-keys vi, you actually would have selected one character too many – it seems like the highlighting is designed to reflect that, whether you’re using emacs keys or not.)

  • There’s actually the opposite bug in mode-keys vi, where you can select text that does not render as selected. Basically running:

    send -X end-of-line
    send -X end-of-line
    send -X cursor-right
    
    send -X begin-selection
    send -X cursor-up
    send -X start-of-line
    

    Will select the first character of the next line although it will not highlight the first character of the next line. These bugs made debugging my script very fun, since I had to actually copy things to make sure that I was actually selecting the text I thought I was.

Maybe one day I’ll submit patches for that, and everything will be better. But for now… I have spent way too much time on this already.