Shell Widgets, Fish and some fun

decorator
Tinkering around Fish, learning about shell widgets and having a lot of fun!
Published on 30 Sep 2024
# fish # shell # cli

So, the other day I was trying to find some projects which I can contribute to. And, I wrote a small readme for one project and checked out some issues in listmonk but I couldn’t find any issue there which I could contribute to. I got to know about Fex 2-3 months ago and I checked the repo again, after scrolling down the readme, I saw that Alan had added configuration for zsh (from what the code said) to bind/invoke fex with Keybindings.

Shell Widget? What?

Now the biggest question was “What the heck is a shell widget”, in the last 4 years, I had never read about Shell widgets or had never even heard about it from anyone in my connections or Telegram Group Chats. In the past, I was someone who used to embrace the KISS Principle or Minimalism where I at some point started using Mksh (MirBSD Korn Shell) inspired from one of my friends - siduck. And hence, I was never interested in Fish or ZSH.

I tried searching a lot about what a Shell Widget was and there was no proper information on the Internet (google, DDG, etc) and turns out there’s really nothing like a shell widget in the Shell Ecosystem. But after making a post in r/commandline. I stumbled upon Mastering-zsh from a comment and there was a detailed document about what Widgets are. So, maybe it exists in the ZSH Ecosystem :D.

A ZSH Widget is basically just a custom function which is made to do everything including executing commands, tab completions or playing tetris game and everything you do in your terminal just like all the other shells. I do not understand what makes them call it a “Widget” honestly, do let me know if you have more broader knowledge on this and I’d really like to know why they had to called it a “Widget”.

So, after reading the README from that repository. I understood what I had to do in order to contribute to fex. I read the .fex.zsh on how the maintainer had implemented his widget. And it looked pretty simple (but it isn’t). Here’s what he did in his script:

  • Storing all the default ZSH options in a variable to reset it later.
  • A widget(function) to run fex and save its stdout into a variable: Here the stdout is the action/command entered by the user in fex’s CLI interface.
    • if its empty, then ZLE (ZSH Line Editor) is invoked in order to execute whats in the Buffer and reset the shell prompt.
  • And every time after all of this is done, the default options are put back in place.

I don’t know why, but I felt that it would’ve been very complicated to implement this in bash (perhaps its not). So, I thought I’ll do it in fish, installed fish and changed my default shell to fish (because I ended up opening many instances of fish while writing the widget).

The initial chunk looked something like this which worked fine but it wasn’t handling the most important in fish which was “executing commands”. Honestly, I did not even know that there was this feature until the developer pointed it out. Then I started to figure out how I wanted to implement that.

After a bit of looking here and there, doing some chatpgt (because I was needlessly reading how fex managed to do the operation selection). I somehow got the function to also handle the commands. It was basically again just getting the stdout and executing it. And I committed the changes.

Scratch my Itch

accept-line

Another issue pointed by the maintainer which came into sight was that the widget(function) wasn’t printing what command it was going to print like the above screenshot.

ZSH was handling it very well because of the accept-line widget in ZSH. Perhaps, if I’m not wrong accept-line is invoked whenever a user presses enter in their zsh shell. But, wouldn’t pass this as a concrete comment because

bindkey -L | awk '{ print $2 }' | grep return

(does nothing for '\x0a' either) returns nil.

Now, it was time for me to scratch my itch. Because, I wasn’t able to implement something like zle accept-line because accept-line doesn’t only execute the previous BUFFER (command) but also prints it on a new prompt line. And I had no idea how to do that in Fish.

commandline is great!

Fish has this in built tool called commandline which executes whats in the buffer. It basically replaces the BUFFER with what command is inside the buffer, but that’s not how I wanted it to be.

Hence, I modified the function something like this

function fex-launch-widget -d "Launch fex on ^f"

	# setting global variable for fex

	set -g FEX_COMMAND fex

	# execute and capture output of $FEX_COMMAND in exec_cmd
	set exec_cmd (eval $FEX_COMMAND)
	if test -z "$exec_cmd"
		commandline -f repaint
		return
	end
	commandline $exec_cmd
	eval $exec_cmd
	# cleaning after execution
	commandline -f repaint
end

I thought, I could make the command appear on the prompt before execution by just organizing the repaint and eval. But that wasn’t going to work at all. I thought of debugging this and someone shared bash set -x/+x and I googled up for Fish. And turns on out Fish Debugger is way better than what Bash has.

You can turn on the debugger by set fish_trace 1. And it is very good. After debugging I came to understand that eval is getting executed before commandline $exec_cmd. Check the stack trace in the below image.

Fish Trace

Fish is literally full of features. Do you see that cd and /path/to/backup is again on different lines ? After searching I found that you can also manipulate strings with string. String can do all sorts of operations including join, match find length, join and etc. And, this was getting funnier.

string join " " foo bar

But, in no way. I was able to find a way to rearrange the execution in the stack like I wanted to. Because, that’s how commandline would do. Whatsoever you do the order of the execution won’t change at all. Maybe because commandline just replaces the variable with whats in the buffer. And eval actually executes it ? Not sure at all!

Perhaps, Alan (the maintainer) was ready to merge the PR as it is if there was going to be a lot of pain and time investment needed. But, I wanted to do it the same way zsh (pitch perfect!) does it but I had already invested 5-6 hours straight in order to debug this (with Arun helping here and there). So, I came back to my plan B - which was just echoing the command before execution. And so, I did.

Fish is great, just chsh already

I am amazed by how folks working on fish are doing their work. They have put some really great features in it.

Here are some features I like:

  1. Of course the debugger!
  2. the default functions for the user including repaint
  3. fzf like interface for tab completion.
  4. And some more features, which julia has already covered in her blog.

And, the documentation is amazing and contains a lot of examples for better understanding. It was really fun doing this contribution to fex. Check it out, the code is written very cleanly!

My PR was merged but I am still interested in understanding and maybe implementing it the perfect way! Please reach out to me if you how I can do that.

And with that my Hacking ends!

Edit (01/10/2024): With a reddit and some help from Fish Shell Matrix channel, I was able to do the implemention perfectly like ZSH was doing. The change was putting the command in buffer and using commandline -f execute to execute the command in buffer which got my work done. And, I sent another short fix PR.

References:

  1. https://fishshell.com/docs/
  2. https://github.com/rothgar/mastering-zsh/
  3. https://gist.github.com/18alantom/d9f0565c0f42d6a71311d4a3093a1331
  4. https://zsh.sourceforge.io/Doc/Release/Zsh-Line-Editor.html