llakala / nixos

My NixOS config
GNU General Public License v3.0
8 stars 1 forks source link

helix: write a script that grabs the current filename #61

Open llakala opened 1 month ago

llakala commented 1 month ago

I should be doing more productive things right now, so I'll just leave a link for myself, and write this issue description out in detail later. https://quantonganh.com/2023/08/19/turn-helix-into-ide

llakala commented 1 month ago

Been working slowly through this. I started with what I assumed would be easy: running code within helix. Turns out it's not so simple.

Well, running code is easy. There's a great example here for running a quick command via :sh. I could set this up to quickly rebuild, and then source the helix configuration.

The problem is running code on the current file, like doing python myfilename.py. See, Helix is missing a key feature: it doesn't have an easy way to print the name of the file you're currently editing. This will eventually be solved, but for now, it's not simple.

We were able to get around this previously, when working with Yazi to setup a file tree. This is because we never needed to get the helix filename, we just had to set the current Helix file to something else. And we could get that filename from Yazi easily.

I could use a fetchFromGithub and build Helix based on the PR adding this functionality, but compiling sucks. Instead, let's use a workaround! It's possible to get the filename... just not through Helix. Instead, we have to hack into our terminal emulator to grab the filename. The blogpost goes into doing this with Wezterm, but that's not my terminal of choice. However, we have a reference for using Kitty! It's all set up here in this Ruby script.

Do I know Ruby? No! But I'm sure it's possible to figure out. My goal is to see if I can replicate the Ruby script's functionality in Bash.

llakala commented 1 month ago

The part of the script most important is here (mildly edited to remove use of an abstraction not relevant to us):

id = ENV['KITTY_WINDOW_ID']
file_text = `kitty @ get-text -m "id:#{id}"`
status_line = file_text.match(/(?:NOR|INS|SEL)\s+[\u{2800}-\u{28FF}]*\s+(\S*)\s[^│]* (\d+):*.*/)
file_name = status_line[1].strip

{
  status_line:,
  file_name:,
  line_number: status_line[2],
  basedir: `dirname "#{file_name}"`.strip,
  basename: `basename "#{file_name}"`.strip,
  extension: file_name.split('.').last.strip
}

Turns out, Ruby uses #{} for variable interpolation. Ew. So, the file_text line in bash is really:

file_text=$(kitty @ get-text -m "id:${id}")

And, we can run that in the shell! After getting the window id with echo $KITTY_WINDOW_ID and pasting it in, we get... nothing.

Just kidding. Turns out it's only something if we actually have text on the screen. We can run it for another tab, and see the output: image Yay! We can now grab content. Now, to filter it.

(Future me, that's your cue to write a post about filtering it.)

llakala commented 1 month ago

(Future me, that's your cue to write a post about filtering it.)

Hi, it's future me. Let's see here.

First thing that unfortunately needs to be documented: kitty @ get-text -m "id:5" is a DIFFERENT COMMAND from kitty @ ls -m "id:5". I've been very confused when I only get the proper output sometimes. Turns out, I've been using get-text and ls interchangably, thinking I was using the same command. Yeah.

Next, we've moved everything into a nix file via writeShellApplication. Now, we can easily test the output of the whole script. Shellcheck is annoying, but the ability to specify runtime inputs is awesome.

We also put all of this in a branch, which is where it should really live. It's been pushed and can be checked on here. Way better for not having a constant test.nix file in main that can never be committed.

Hmm... what else is there...

Oh yeah, functionality! I've succeeded in my ambitions: this is the post about filtering it.

The regex in the example code was actually fairly simple. It matches something like this:

NOR   helix.nix                                                   1 sel  76:14

It gives you access to each part of this: The mode, the filename, the current position, everything. I'm accessing each of these via awk, via something like this:

file_name=$(echo "$status_line" | awk '{print $2}')

For my own future reference, here's what each of them correspond to: $1 - The mode. Something like NOR or INS $2: The filename (what we actually want!). Something like helix.nix. $3: The number of cursors. Magic to me. Just literally 1 for me. $4: The... word SEL? No clue. $5: The line where the cursor is, and the position in that line. Something like 70:14.

And, here's the regex to grab all of these. It is very slightly modified to use PCRE2, since it was previously using ECMAScript which Ripgrep doesn't really support.

(?:NOR|INS|SEL)\s+[\x{2800}-\x{28FF}]*\s+\S*\s[^│]* \d+:*.*

How does it work? Not my problem. What matters is that it works.

So, we've now actually captured the current file. Now, the world is our oyster.

Hey future me: I don't know what you'll be doing with this. But you'd better impress me.

llakala commented 1 month ago

I hope I've impressed you, past me.

In terms of functionality, we now have the exact path to the current file being edited. Before, we only had the name of the file, not the full filepath. I considered using a Helix setting that would've put the absolute filepath into the statusbar, but I realized there was an easier solution.

See, Yazi automatically sets the title of the current tab. This title looks something like this:

Yazi: /foo/bar/awesome/folder

Since we'll always be launching Helix via Yazi, we can just reuse this title for finding the path of the file we're currently editing.

Now, what I spent most of my time on: fixing a niche Kitty problem. I'm very tired of dealing with this bug, so I actually won't go in depth into it here. Instead, I'll just tell you the fix: you need to set listen_on within kitty config to a Unix socket. I do it like this:

hm.programs.kitty.settings = 
{
    listen_on = "unix:@kitty";
}; 

Now, we've implemented the base functionality. We can now get the full path to the current file, and do anything we want with it. So, are we done?

Well, we could be. But I'm getting big into making languishing PRs, and I have a few things I'd like to get working first.

First of all, the script could easily be multipurpose. Dooming it to a life of running python files feels short-sighted. Our wonderful Ruby reference script passed a string as a positional parameter, to choose what to do. I'd prefer to do something a bit more intelligent.

I was previously planning on keeping this as a base script, which others could reuse. However, I'm thinking about instead keeping it all within one script, and instead using smart detection based on the file extension. The way of interacting with this feature will likely be a keyboard shortcut within Helix that runs the script. If we're pressing that shortcut within a .py file, we probably want to run the current python file. If it's in a .md file, maybe we want to display the markdown. We could implement something for any file extension, and it'd all be done automatically.

I also want a better method for seeing the actual output. What if the output is longer than can fit in the little Helix popup for the output of a shell command? I think a classic Kitty hsplit will do us here. If we can set the split up so the output only takes out about 25% of the screen, that'd be perfect.

Oh, and there is a (minor) bug. Getting the folder path from the kitty title via Yazi only works in most cases. When we do ctrl+b to use the Helazitty file manager, it doesn't update the title. Not sure of the best workaround for this. We could make Helazitty use the existing Yazi instance rather than one in a separate part of .config, and that'd probably fix it. An intelligent solution where we forwarded the new title to Kitty is probably beyond me, but you never know. Sometimes I surprise myself.

Anyways, I think we've gotten around the interactions with Helix. Everything else will be classic Kitty stuff. Good luck with that, future me!

llakala commented 1 month ago

Thinking about it now: I think splitting into multiple scripts is still a good idea. We'll just have two scripts. Script 1 will returns the name of the current file being edited in Helix, using get-text and regex magic. Meanwhile, Script 2 will call script 1 for the name, and use the file extension to choose exactly what to do.

If we want to get real abstract, we could split it into three scripts. Script2 could use script3, which generates a new split window and runs whatever command you pass into it in the new window. Here's a little pseudocode:

current_file=$(script1)
extension=get_file_extension($current_file)
if extension == ".py" then
  script3 "python $current_file"
elif extension == ".md" then
  script3 "pandoc $current_file"
else
  echo "I don't know how to run this kind of file, sorry."
fi
llakala commented 1 month ago

With that in mind, we could get this PR merged pretty soon, since all it's doing is accomplishing step 1. We do need to cleanup some things in terms of choosing where the script goes, properly naming the file, setting runtimeInputs, etc. But this is working functionality for the future!

cor commented 4 weeks ago

Just wanted to note that I love how you're documenting your journey here! Keep it up

llakala commented 3 weeks ago

In the interest of documenting my journey further here, let's set a firm cap on scope for this PR. I want this to exclusively be a script that grabs the current tab name. This makes it more likely for me to get this done and get to implementing some actual functionality.

And, because I'm a glutton for punishment and LOVE a languishing PR, I was also thinking about a better way to handle grabbing the current filename. See, the Yazi method is easily breakable. If we ever start using the Helix integrated buffer management, it will fail us. If we use the Helazitty functionality, it'll fail us. It's a great hack, but I think I have a better solution.

See, I already have a Helix command that toggles a config option, done like this:

space.x = ":toggle whitespace.render all none";

I think displaying the absolute path in the statusline all the time is super ugly. But what if, when grabbing the current name of the file, I do something like this:

:set statusline.left ["file-absolute-path"]
:sh myScript
:config-reload

This would give myScript access to the absolute filepath, while keeping my statusline visually seamless, and not relying on anything from Yazi. We then immediately reset the statusline to what it was previously. It'll will probably flash to the absolute filepath for a frame or two, but I don't think that's a problem.

(Sidenote: it wouldn't have to flash if Helix just had a command that just returned the current filepath, and could've saved me most of the work seen in this issue. It knows the filepath, otherwise it wouldn't be able to put it in the statusbar. It's just very stingy about giving it out.)

Anyways, I think I have a good setup here. When I next get time to work on this, I want to cleanup the script, implement this smarter way of grabbing the file, and then I think we're ready to merge! I can then split additional functionality out into several PRs, for all the ideas I have that utilize the current filename.

mauro3 commented 2 weeks ago

Did you see the trick to get the filename in https://github.com/helix-editor/helix/pull/11164#issuecomment-2455523986 ?

llakala commented 2 weeks ago

Did you see the trick to get the filename in helix-editor/helix#11164 (comment) ?

I did not. That's gorgeous, and I'm already using the macro commit for other things, so that's perfect. Thanks for sharing it!