reddit / emacs

always show the appropriate latest prompt in shell mode: feedback for my solution

andyjda · 2022-11-28 23:32
I was working on a large project and using lots of `make`, `make clean`, `configure` and other commands that triggered large streams of output in my shell. It was getting hard to keep track of which output came from which input. So I came up with this solution: it makes a 'sticky' header that always shows the prompt corresponding to the top-visible output-line (hard to explain but see screenshots for reference) (defun comint-get-prompt-above () "Get the prompt above the top visible line in the current window." (interactive) (save-excursion (goto-char (window-start)) (comint-previous-prompt 1) (let ((prompt (thing-at-point 'line))) (aset prompt (- (length prompt) 1) 0) ; remove the newline ending char prompt))) (define-minor-mode comint-sticky-mode "Minor mode to show the previous prompt as a sticky header." :group 'comint :global nil :lighter nil (if comint-sticky-mode (setq-local header-line-format (list '(:eval (comint-get-prompt-above)))) (setq-local header-line-format nil))) Sharing here because: 1. I'd like some feedback on my solution. Code-wise I'm wondering if there is a more efficient solution, maybe without calling `comint-next-prompt` 2. I figured other people might find this feature useful: feel free to paste it in your config and let me know your experience. 3. If people are interested, I'm wondering if I should create a package for this. On the one hand I'd like the feature to be available to whoever wants it. But also it's such a small piece of code that maybe just sharing it as a snippet is enough. Unless there are possible enhancements, or additional related features that might go well with this? ​ https://preview.redd.it/ppqae0rh5s2a1.png?width=600&format=png&auto=webp&s=12370bc6d01def11e35f4667f971564341d93cc2 https://preview.redd.it/dhdbwbjg5s2a1.png?width=603&format=png&auto=webp&s=fbf5113b3cd24216815f0bb5ac6607dec3271aea
gopar · 2022-11-29 04:57
Yes, please create package 📦. As a side note, I am a regular user of eshell and would love someone like this for that too :)
andyjda · 2022-11-29 05:11
Oh I didn't realize `eshell-mode` didn't inherit from `comint`. Luckily they're still very similar, so you can do it by just replacing every `comint` with `eshell`: (defun eshell-get-prompt-above () "Get the prompt above the top visible line in the current window." (interactive) (save-excursion (goto-char (window-start)) (eshell-previous-prompt 1) (let ((prompt (thing-at-point 'line))) (aset prompt (- (length prompt) 1) 0) ; remove the newline ending char prompt))) (define-minor-mode eshell-sticky-mode "Minor mode to show the previous prompt as a sticky header." :group 'eshell :global nil :lighter nil (if eshell-sticky-mode (setq-local header-line-format (list '(:eval (eshell-get-prompt-above)))) (setq-local header-line-format nil))) I'll check if there are any other types of shells that might need this, and put all the modes in a package
gopar · 2022-11-29 05:14
Oh, TIL about \`header-line-format\`. Thought it was special to comint when I read your code. thx! Much appreciated :)
igorepst · 2022-11-29 08:22
[https://imgur.com/a/f65ek2j](https://imgur.com/a/f65ek2j) Seems not to work properly on Emacs 29 built from master yesterday. 1. On multiline prompts it shows the first line that is not helpful 2. The header does NOT change when I disable/enable the minor mode or run command in \`eshell\` 3. I think it would be better to show not the prompt, but the running command only So, as it seems, there is a work to do and this justifies releasing a package :)
andyjda · 2022-11-29 14:18
Thanks! I'll look into fixing the issues and making it more configurable, and release a package then. A couple points: >I think it would be better to show not the prompt, but the running command only This was the original idea, but then I realized the prompt might hold useful information, such as the directory that the command was run in. I might make it configurable, so that users can decide how much of the prompt-command line to keep in the header. I didn't realize there were configurations like yours: am I right that the prompt takes in a whole line, and then the next line is the command? I'll have to tinker with the current solution to make this work. Is this in `eshell` or `shell`? Can you share a quick way to set it up this way?
igorepst · 2022-11-29 15:18
This is Eshell. [Gist of the config](https://gist.github.com/igorepst/db9ee3c62664ea06f011d5671715ac33). From [packages config](https://github.com/igorepst/dotfiles-reboot/blob/master/packages/emacs/config/emacs/lisp/ig-packages.el) and [Eshell config](https://github.com/igorepst/dotfiles-reboot/blob/master/packages/emacs/config/emacs/lisp/ig-eshell.el). The prompt takes the first line fully and until either '❯' or '#' on the second line. I agree that current directory may be useful in the header. However, for configs like mine, it would be better to customize the header completely separately and not to try to extract parts of the prompt. BTW, in another package I am messing with the Eshell prompt, so you may take some ideas [from there](https://github.com/igorepst/gitstatus-el/blob/c3e30341d0add9728010e566b9eb031c76414b47/gitstatus-eshell.el#L102)
deaddyfreddy · 2022-11-29 21:19
I just use [Detached.el](https://lists.sr.ht/~niklaseklund/detached.el) and `M-&` shortcut (rebound to `detached-shell-command`) instead. It stores session command, so it's pretty easy to tell which one we ran in a current buffer.
andyjda · 2022-11-29 21:34
I'm not familiar with detached so having a hard time understand.. It looks like a way to launch a shell as a separate process? But how does it store the session commands? A regular shell also stores commands, you could easily run `comint-previous-prompt` to jump to it. My code just makes it easier to keep track: as you scroll, you'll always see the command that triggered the top visible line
deaddyfreddy · 2022-11-29 22:12
> It looks like a way to launch a shell as a separate process? yes, in background, the output is pretty fast and the best thing is it creates a new output buffer on per-command basis, so no need to jump here and there, and the issue you are trying to solve just doesn't exist by design. > But how does it store the session commands? in a structure, you can view (and select) sessions using something like `M-x detached-view-session`
andyjda · 2022-11-29 22:21
I see. Yeah I can see how that could be very useful for huge compilation commands. I like that my code is more generic and should “just work” when you’re running a bunch of commands in the same shell, but I’ll take another look and maybe install detached when I have some time. Thanks for the tip
deaddyfreddy · 2022-11-29 22:25
and, btw, It has even more integration stuff, like `detached-compile` (so all highlighting stuff and line hyperlinks are there!), eshell, dired, vterm, you name it!
andyjda · 2022-11-30 01:06
Hey u/igorepst I loaded your config and can't reproduce the error. Are you using `eshell-sticky-mode` as pasted above or `comint-sticky-mode`? I'm running on a fairly recent version of 29 built from master, though not the latest one.
igorepst · 2022-11-30 10:20
Not only can I reproduce it with clean config (`emacs -q`) in Eshell - I didn't try the `comint`. It is actually even worse without `(delete 'eshell-banner eshell-modules-list)` as in my full config, as I see "Welcome to the Emacs shell" stick to the header line. Steps to reproduce 1. `emacs -q` 2. Paste the Gist + `eshell-get-prompt-above` \+ `eshell-sticky-mode` \+ `(delete 'eshell-banner eshell-modules-list)` into scratch buffer. Run `eval-buffer` 3. `M-x eshell`, `M-x eshell-sticky-mode` 4. Observe that the first line is stuck forever in the header line. Just press `Enter` a couple of times 5. In such circumstances, `eshell-sticky-mode` indeed toggles the header. Not sure why I saw that disabling the minor mode doesn't erase it - probably my mistake. However, after disabling and re-enabling the mode, I see the **first** prompt there, even not the prompt created after toggling the mode on. But according to the code it cannot work other way, as it jumps to the start of the window and tries to get the **previous** prompt that just returns nil if there is no previous prompt there and is stuck on the first line until there will be some output that will cause scrolling of the window
andyjda · 2022-11-30 13:37
>Observe that the first line is stuck forever in the header line. Just press `Enter` a couple of times This is what happens. But as you point out later, this gets adjusted if I scroll down far enough so that the top line I see is the output of a command (even if it's just `Enter`). This is the expected behavior. Some clarification on how the code behaves: the intent is *not* to show the *latest* command in the header. See my first screenshot: the latest command is `make`, but the header shows `make clean`. To me this makes intuitive sense, because I can already see the `make` command and (part of) its output. What wouldn't be clear without the header is what command triggered the top lines. That's why the code jumps to the top of the visible window before looking for the previous prompt. Of course, this could also be customizable. You can comment out the `(goto-char (window-start))` line to see if that's the behavior you'd like. Then the header becomes the prompt before wherever your cursor is. If you'd want the header to always show the latest-run command, then put: (goto-char (point-max)) (forward-line -1) at the beginning of the `save-excursion` block. I'll make all of these available options in the package
gopar · 2022-12-11 03:04
Just a friendly check to see if this was ever made into a package? :)
andyjda · 2022-12-11 16:18
It was! I haven't had time to add it to (M)ELPA yet, but you can clone it from the github repo: [https://github.com/andyjda/sticky-shell](https://github.com/andyjda/sticky-shell) ​ The README and the docstrings should have enough info to get you started, but do let me know if you have any questions or you encounter any issues! Thanks