wez / wezterm

A GPU-accelerated cross-platform terminal emulator and multiplexer written by @wez and implemented in Rust
https://wezfurlong.org/wezterm/
Other
16.61k stars 742 forks source link

Example configuration for windows WSL/Fish shell integrations #1242

Closed chichid closed 2 years ago

chichid commented 2 years ago

Is your feature request related to a problem? Please describe. No

Describe the solution you'd like It'll be great to have a guide to describe how to get Wezterm shell integration to work with windows WSL (or if there are any limitations), especially OSC 7 sequences. I use Wezterm -> WSL -> Fish configuration and I've been struggling to find a way to open a new window within the same working directory. Using the guides I'm able to get that working on windows for cmd.exe only, but as soon as I start WSL instead of CMD.exe, it always start from the home directory. Initially I thought this is a fish problem, but the same happens if I use WSL+bash instead. So I think there is something that need to be done to get WSL supported or I'm missing something I just can't figure out.

Describe alternatives you've considered I don't have any alternatives

Additional context

chichid commented 2 years ago

Ok so this is very weird, and I think OSC 7 are working properly. This is probably a bug is wezterm. I used the following code with the nightly build to create an action and log what get_current_dir returns, and here is the outcome: image

LOGS: image

chichid commented 2 years ago

Ok this wasn't easy, but I found something that worked for me, in case someone is interested. It's probably not the cleanest way and there may be easier ways to do this, but it's the only thing that works for me:

you need to add this to the top of you wezterm config file, which will export an environment variable to your shell: $wezterm_startup_directory. When you create a new tab or window.

wezterm.on("SpawnNewWindowInWorkingDirectory", function(window, pane)
  current_directory = pane:get_current_working_dir():gsub("file://dox", "") <--- Replace this one with your hostname
  startup_command = "export wezterm_startup_directory=" .. current_directory .. "&& fish"

  window:perform_action(wezterm.action{SpawnCommandInNewWindow={
    args={"bash", "-c", startup_command}
  }}, pane)
end)

wezterm.on("SpawnNewTabInWorkingDirectory", function(window, pane)
  current_directory = pane:get_current_working_dir():gsub("file://dox", "") <--- Replace this one with your hostname
  startup_command = "export wezterm_startup_directory=" .. current_directory .. "&& fish"

  window:perform_action(wezterm.action{SpawnCommandInNewTab={
    args={"bash", "-c", startup_command}
  }}, pane)
end)

You can then can use these commands in your keymapping as follow (ALT + N and Alt + t):

{ key="n", mods="ALT", action=wezterm.action{EmitEvent="SpawnNewWindowInWorkingDirectory"}}
{ key="t", mods="ALT", action=wezterm.action{EmitEvent="SpawnNewTabInWorkingDirectory"}}

The last part is to add this to the startup script of your shell (fish.config when using fish)

cd $wezterm_startup_directory
wez commented 2 years ago

Thanks for documenting this!

What makes this tricky is that WSL is essentially a different process environment from the GUI running in the win32 environment. OSC 7 is interpreted in the context of Win32 but the shell integration in WSL is produced in a mis-matched context; for example, /mnt/c/something needs to be C:/something in win32 in order to be correctly recognized when wezterm spawns the win32 process.

The closest thing to an out-of-the-box way to make this work without defining custom event handlers is to use the multiplexer technique described here, which sets up a different domain where newly spawned tabs and processes are spawned directly into WSL rather than being bridged through via the wsl.exe or bash.exe executable wrappers for WSL.

Another option to explore might be to adjust the shell integration running inside WSL: if you know that you're in WSL and the GUI is running in win32 then rather than emitting /mnt/c/something in the OSC 7 escape, it could emit c:/something (forward slashes are fine).

Another other possibility is for wezterm to provide a pre-defined wsl domain that implicitly launches command via wsl.exe and that knows how to accommodate the path differences. I'm not enthusiastic about this as I think there may be a number of weird compatibility and edge cases that could be an ongoing PITA to support. For example, using wsl.exe forces the use of the Windows PTY layer which, while improving all the time, still has a number of weird compatibility issues with unix apps around eg: image protocols and mouse reporting. Using the multiplexer allows bypassing that layer of Windows completely.

jshbrntt commented 2 years ago

Ok so this is very weird, and I think OSC 7 are working properly. This is probably a bug is wezterm. I used the following code with the nightly build to create an action and log what get_current_dir returns, and here is the outcome: image

LOGS: image

No idea why this works on your system but not mine.

While debugging myself it appears that pane:get_current_working_dir() always returns the directory wsl.exe was called from.

So if I cd to C:\Temp then execute wsl.exe, every time I call that function it will return C:\Temp.

This is the case for C:\Windows\System32\bash.exe as well.

🤔

wez commented 2 years ago

In the latest release a bunch of work was done to teach wezterm how to inspect the process tree and extract the current working directory of processes on windows.

If OSC 7 is never used, then wezterm will fill in those details. When you run wsl.exe to launch a linux process, the process tree looks like this:

image

wezterm considers the running program to be wsl.exe because the child bash process isn't part of the process tree and doesn't properly show up to the normal win32 process exploring APIs, and the wslhost and conhost processes are not attached to any PTY, so they are not in the running.

In this situation, if you want wezterm to report the working directory, then you will need to configure your shell to emit OSC 7 sequences to tell wezterm about that path. At the moment we only have examples for bash/zsh: https://github.com/wez/wezterm/tree/main/assets/shell-integration

Note that that path will be in the context of the process inside WSL and won't be meaningful to wezterm, which is why those tricks with passing it down to bash are needed in the examples above.

jshbrntt commented 2 years ago

@wez so there's two separate issues, I need to configure my version of zsh running in wsl.exe to emit OSC 7. Which will be received by WezTerm but the path returned by OSC 7 will be relative to the WSL host and not the Windows host.

Which is why in @chichid is using this event based approach to transform the path back to being relative to the Windows host before passing it to WezTerm to create a new terminal instance?

jshbrntt commented 2 years ago

At the moment we only have examples for bash/zsh: https://github.com/wez/wezterm/tree/main/assets/shell-integration How am I supposed to use that bash script?

Should I execute it as part of my .bashrc/.zshrc file?

jshbrntt commented 2 years ago

Never mind I figured it out I copied wezterm.sh to /etc/profile.d in my Ubuntu WSL2 host.

Now pane:get_current_working_dir() is returning a path relative to the WSL host.

jshbrntt commented 2 years ago

Okay great so here's my solution for WSL2 Ubuntu with zsh and bash.

  1. Download the shell-integration/wezterm.sh script to /etc/profile.d/wezterm.sh.

    This will fix bash so it produces OSC 7 allowing WezTerm to to return a path from pane:get_current_working_dir().

  2. Add the following line to the end of your ~/.zshrc, this will fix zsh.

    # wezterm
    . /etc/profile.d/wezterm.sh
  3. Now at the top of your ~/.wezterm.lua file on your Windows host define the following functions.

    local wezterm = require 'wezterm';
    
    wezterm.on("SpawnCommandInNewWindowInCurrentWorkingDirectory", function(window, pane)
      current_directory = pane:get_current_working_dir():gsub("file://", "")
      window:perform_action(wezterm.action{SpawnCommandInNewWindow={
        args={ "wsl.exe", "--cd", current_directory, "--exec", "zsh" }
      }}, pane)
    end)
    
    wezterm.on("SpawnCommandInNewTabInCurrentWorkingDirectory", function(window, pane)
      current_directory = pane:get_current_working_dir():gsub("file://", "")
      window:perform_action(wezterm.action{SpawnCommandInNewTab={
        args={ "wsl.exe", "--cd", current_directory, "--exec", "zsh" }
      }}, pane)
    end)
    
    wezterm.on("SplitVerticalInCurrentWorkingDirectory", function(window, pane)
      current_directory = pane:get_current_working_dir():gsub("file://", "")
      window:perform_action(wezterm.action{SplitVertical={
        domain="CurrentPaneDomain",
        args={ "wsl.exe", "--cd", current_directory, "--exec", "zsh" }
      }}, pane)
    end)
    
    wezterm.on("SplitHorizontalCurrentWorkingDirectory", function(window, pane)
      current_directory = pane:get_current_working_dir():gsub("file://", "")
      window:perform_action(wezterm.action{SplitHorizontal={
        domain="CurrentPaneDomain",
        args={ "wsl.exe", "--cd", current_directory, "--exec", "zsh" }
      }}, pane)
    end)
  4. Update your key bindings to trigger these functions through event emissions (these are mine).

    lua
    return {
      -- snip
      keys = {
        { key = "n", mods = "CTRL|SHIFT", action=wezterm.action{EmitEvent="SpawnCommandInNewWindowInCurrentWorkingDirectory"} },
        { key = "t", mods = "CTRL|SHIFT", action=wezterm.action{EmitEvent="SpawnCommandInNewTabInCurrentWorkingDirectory"} },
        { key = "_", mods = "ALT|SHIFT",  action=wezterm.action{EmitEvent="SplitVerticalInCurrentWorkingDirectory"} },
        { key = "+", mods = "ALT|SHIFT",  action=wezterm.action{EmitEvent="SplitHorizontalCurrentWorkingDirectory"} },
        -- snip
      }
    }
wez commented 2 years ago

I just pushed: 0e9924e58569778ef6a0672849d607112c31ce63 cdf3fc89f7a5fd95555bab94dc6f137ba897b86c b2ee6793f9039ed37f113b44f9e9cc4912a335b7 and 2f9ca151c2559bde0c6eec537fcca2eb14adff7a which should make life a bit easier here.

The gist of it (still need to write up the full docs) is:

There's a new type of domain called a WslDomain that allows wezterm to understand the concept of spawning a command via wsl.exe and how to map its current directory.

There's a new wsl_domains configuration option that defaults to the list of distributions as returned by wsl -l; this is the same source of information that we previously used to populate the WSL entries in the launcher menu.

There's a lua function called wezterm.default_wsl_domains() that will return that information in case you want to override it in some way. For example, you may wish to override the default_prog to have it run zsh if you haven't made that the default shell inside your WSL distribution (pro tip: use chsh to set it to zsh and simplify!).

There's a new default_domain configuration option that will allow you to make spawning into a specific WSL domain the default when starting wezterm.

Putting this all together:

On my system:

; wsl -l -v
  NAME            STATE           VERSION
* Ubuntu-18.04    Running         1

That causes the default value for wsl_domains to be:

{
  { name: "WSL:Ubuntu-18.04",
    distribution: "Ubuntu-18.04",
  },
}

When I start up wezterm, opening the launcher menu (right click on the + button) shows an entry for: "New Tab (domain 'WSL:Ubuntu-18.04')"

If you have shell integration installed in your WSL instance, then the CWD will be used as the default if you split that tab or spawn another tab while it is active.

I can use:

  default_domain = "WSL:Ubuntu-18.04",

to make that domain the default and make the initial tab open up there when I start wezterm. I can use the "New Tab (domain: 'local')" option in the launcher menu to start cmd.exe or whatever the default_prog is.

If you wanted to edit the default list of wsl domains, then you might have something like this in your config (untested):

local wezterm = require 'wezterm'

local wsl_domains = wezterm.default_wsl_domains()

-- Always use zsh in my WSL.  but really: I recommend running `chsh` inside WSL to make it the default!
for idx, dom in ipairs(wsl_domains) do
   dom.default_prog = {"zsh", "-l"},
end

return {
   wsl_domains = wsl_domains,
   default_domain = "WSL:Ubuntu-18.04",
}

Possible fields in a WslDomain can be seen here: https://github.com/wez/wezterm/blob/2f9ca151c2559bde0c6eec537fcca2eb14adff7a/config/src/wsl.rs#L6-L10

Hopefully this will let you avoid having to write extensive amounts of code, particularly the custom events, to make things workable!

This functionality should show up in a nightly build within an hour of this comment being posted.

wez commented 2 years ago

This same technique is potentially extensible to docker; we could add a DockerDomain and tell wezterm how to run things via docker exec

chichid commented 2 years ago

I'm using WSL1 (I hate WSL2, it's super slow on my system and really not in the mood of digging into it).

wez commented 2 years ago

These are now in the main docs:

https://wezfurlong.org/wezterm/config/lua/config/default_domain.html https://wezfurlong.org/wezterm/config/lua/config/wsl_domains.html https://wezfurlong.org/wezterm/config/lua/wezterm/default_wsl_domains.html https://wezfurlong.org/wezterm/config/lua/WslDomain.html

chichid commented 2 years ago

Geez this is amazin @wez! I'm so dependent on wezterm ! I'll try those and send an update in this thread!

chichid commented 2 years ago

This issue is resolved.

chichid commented 2 years ago

I realized that I didn't comment with the solution. I managed to the get this working with chsh/WSL1 for both fish and zsh. So I got rid of the above "hacky" solution.

@wez the domains were not the problem in my case, it was more that I was calling WSL with "~" to make sure it goes into my home directory. Which prevents Wezterm's ability to set the directory properly when OSC7 are fired. I set chsh but it wasn't enough either, as Wezterm was always going to my home directory for some reason.

TLDR: 0 - Remove anything that sets your home directory other than wezterm and your /etc/passwd 1 - Make sure you set the home directory through the default_cwd command if your home directory is overriden by Wezterm. 2 - Configure your shell to send OSC7 sequences as documented in the Wezterm shell integration section.

0 - You need to make sure that there is nothing other than Wezterm that sets your home direction. That includes (but not limited to) the following:

1 - Wezterm config:

{
... 
-- Set wsl.exe WITH NO ARGUMENTS for the initial path otherwise this will not work
default_prog = {"wsl.exe"},
default_cwd = "C:\\Users\\SOME\\PATH",
-- Or if you prefer environment variables
-- default_cwd = os.getenv("HOME"),
...
}

2 - You need fire OSC7 sequence from your Shell. This depends on your shell obviously. But my mistake was to use WSL paths "/mnt/c/Users..." within the sequence documented in Wezterm. Those don't work and you need to call "wslpath -w " in order to convert them before you call the OSC7 sequence.

For fish you can add this to your config

function osc7_promp --on-event fish_prompt
  if grep -q Microsoft /proc/version
    printf "\033]7;file://%s\033\\" (wslpath -w "$PWD") 
  end
end

For zsh add this to your .zshrc precmd hook, obviously :

precmd () {
#### There could be some stuff here, like setting the prompt ##### You may want to add an echo here to make sure this is called when you access your terminal
#The mistake I've done when I tried this first was to not use wslpath to convert WSL paths
  if grep -q Microsoft /proc/version; then
    printf "\033]7;file://%s\033\\" $(wslpath -w "$PWD") 
  fi
}

Bash or other shells, look for how you could call the following whenever a command is run

if grep -q Microsoft /proc/version; then
    printf "\033]7;file://%s\033\\" $(wslpath -w "$PWD") 
  fi
github-actions[bot] commented 1 year ago

I'm going to lock this issue because it has been closed for 30 days ⏳. This helps our maintainers find and focus on the active issues. If you have found a problem that seems similar to this, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further.