Skip to content

Commit f57e8a5

Browse files
authored
feat: Windows terminal support (#55)
So they key here is `start "" /WAIT nvim ...`, but getting there is annoying. We write a `.bat` file, because the user's shell might be git-bash which would need translating windows paths and cmd-style arguments. We still have to be aware of powershell and cmd, so that's all plumbed through, so powershell users on Linux should also be supported now. Closes #54 ## Preview https://github.com/user-attachments/assets/908ddee1-ff12-4a89-ba7c-fc35039cb38f
1 parent 2060a06 commit f57e8a5

9 files changed

Lines changed: 289 additions & 120 deletions

File tree

script/test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ WORKING_HOST="$1"
66
fresh-nvrh() {
77
NVRH_CLIENT_DEBUG=false \
88
NVRH_CLIENT_NVIM_CMD="/home/linuxbrew/.linuxbrew/bin/nvim,-u,NONE" \
9+
NVRH_CLIENT_SERVER_ENV=NVRH_TEST=true \
910
env \
1011
-u NVRH_CLIENT_SSH_ARG \
11-
-u NVRH_CLIENT_SERVER_ENV \
1212
-u NVRH_CLIENT_SSH_PATH \
1313
-u NVRH_CLIENT_LOCAL_EDITOR \
1414
-u NVRH_CLIENT_USE_PORTS \

src/bridge_files/lua/init_bridge.lua

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
local session_id, channel_id, socket_path, browser_script_path, should_map_ports, nvrh_server_info =
2-
...
1+
local session_id, channel_id, socket_path, browser_script_path, should_map_ports, nvrh_server_info, windows_launcher_path =
2+
...
33

44
local should_initialize = _G._nvrh == nil
55

src/bridge_files/lua/init_nvrh.lua

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,14 @@ if should_initialize then
3636
local function cleanup()
3737
os.remove(browser_script_path)
3838
os.remove(socket_path)
39+
40+
if
41+
_G._nvrh.server_info.os == 'windows'
42+
and windows_launcher_path
43+
and windows_launcher_path ~= ''
44+
then
45+
os.remove(windows_launcher_path)
46+
end
3947
end
4048

4149
-- Cleanup when exiting Neovim.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
local script_contents, script_path = ...
2+
3+
vim.fn.writefile(vim.fn.split(script_contents, '\n'), script_path)

src/bridge_files/lua/types/nvrh.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,6 @@ socket_path = ''
1515
browser_script_path = ''
1616
should_map_ports = false
1717
nvrh_server_info = ''
18+
windows_launcher_path = ''
1819

1920
should_initialize = false

src/client/main.go

Lines changed: 111 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -139,26 +139,31 @@ var CliClientOpenCommand = cli.Command{
139139
Endpoint: endpoint,
140140
RemoteDirectory: c.Args().Get(1),
141141

142-
RemoteEnv: c.StringSlice("server-env"),
143-
LocalEditor: c.StringSlice("local-editor"),
144-
145-
ShouldUsePorts: c.Bool("use-ports"),
146-
147-
RemoteSocketPath: fmt.Sprintf("/tmp/nvrh-socket-%s", sessionId),
148-
LocalSocketPath: filepath.Join(os.TempDir(), fmt.Sprintf("nvrh-socket-%s", sessionId)),
149-
AutomapPorts: c.Bool("enable-automap-ports"),
142+
AutomapPorts: c.Bool("enable-automap-ports"),
150143

151144
Debug: isDebug,
152145

153146
TunneledPorts: make(map[string]bool),
154147

155148
NvimCmd: c.StringSlice("nvim-cmd"),
156-
157-
SshArgs: c.StringSlice("ssh-arg"),
158149
}
159150

151+
remoteEnv := c.StringSlice("server-env")
152+
localEditor := c.StringSlice("local-editor")
153+
sshArgs := c.StringSlice("ssh-arg")
154+
155+
shouldUsePorts := c.Bool("use-ports")
156+
remoteSocketPath := fmt.Sprintf("/tmp/nvrh-socket-%s", sessionId)
157+
localSocketPath := filepath.Join(os.TempDir(), fmt.Sprintf("nvrh-socket-%s", sessionId))
158+
159+
randomPort := getRandomPort()
160+
localPortNumber := randomPort
161+
remotePortNumber := randomPort
162+
163+
var tunnelInfo *ssh_tunnel_info.SshTunnelInfo
164+
160165
// Setup SSH client
161-
sshClient, sshClientErr := getSshClient(nvrhContext, endpoint, sshPath)
166+
sshClient, sshClientErr := getSshClient(nvrhContext, endpoint, sshPath, sshArgs)
162167
if sshClientErr != nil {
163168
return sshClientErr
164169
}
@@ -172,14 +177,13 @@ var CliClientOpenCommand = cli.Command{
172177
slog.Info("Cleaning up")
173178
closeNvimSocket(nv, didClientFail)
174179
killAllCmds(nvrhContext.CommandsToKill)
175-
os.Remove(nvrhContext.LocalSocketPath)
180+
os.Remove(localSocketPath)
176181
if nvrhContext.SshClient != nil {
177182
nvrhContext.SshClient.Close()
178183
}
179184
}()
180185

181186
siDone := make(chan error, 1)
182-
randomPort := getRandomPort()
183187

184188
siTunnelInfo := &ssh_tunnel_info.SshTunnelInfo{
185189
Mode: "port",
@@ -191,8 +195,9 @@ var CliClientOpenCommand = cli.Command{
191195
// Start server info nvim instance.
192196
slog.Info("Starting server info nvim instance")
193197
go func() {
194-
// Not quoting here because Powershell doesn't like it, and we don't know
195-
// what shell we're using at this point.
198+
// Not quoting here because Powershell doesn't like it without the
199+
// preceding ampersand, and we don't know what shell we're using at this
200+
// point.
196201
nvimCmd := strings.Join(nvrhContext.NvimCmd, " ")
197202

198203
siDone <- nvrhContext.SshClient.Run(
@@ -201,7 +206,7 @@ var CliClientOpenCommand = cli.Command{
201206
)
202207
}()
203208

204-
// Grab server info.
209+
// Grab server info and potentially prepare Windows.
205210
go func() {
206211
siNv, err := nvim_helpers.WaitForNvim(ctx, siTunnelInfo)
207212

@@ -217,6 +222,43 @@ var CliClientOpenCommand = cli.Command{
217222

218223
nvrhContext.ServerInfo = serverInfo
219224

225+
if nvrhContext.ServerInfo.Os == "windows" {
226+
shouldUsePorts = true
227+
228+
nvrhContext.WindowsLauncherPath = fmt.Sprintf(
229+
`%s\nvim-launcher-%s.bat`,
230+
nvrhContext.ServerInfo.Tmpdir,
231+
nvrhContext.SessionId,
232+
)
233+
234+
tunnelInfo = &ssh_tunnel_info.SshTunnelInfo{
235+
Mode: "port",
236+
LocalSocket: fmt.Sprintf("%d", localPortNumber),
237+
RemoteSocket: fmt.Sprintf("%d", remotePortNumber),
238+
Public: false,
239+
}
240+
241+
nvimCmd := nvim_helpers.BuildRemoteCommandString(
242+
nvrhContext.NvimCmd,
243+
"bat",
244+
nvrhContext.RemoteDirectory,
245+
remoteEnv,
246+
tunnelInfo,
247+
)
248+
249+
err := siNv.ExecLua(
250+
bridge_files.ReadFileWithoutError("lua/setup_nvim_launcher.lua"),
251+
nil,
252+
nvimCmd,
253+
nvrhContext.WindowsLauncherPath,
254+
)
255+
256+
if err != nil {
257+
siDone <- err
258+
return
259+
}
260+
}
261+
220262
siNv.ExecLua("vim.cmd('qall!')", nil, nil)
221263
siNv.Close()
222264

@@ -235,46 +277,40 @@ var CliClientOpenCommand = cli.Command{
235277
}
236278
}
237279

238-
// Prep with new server info.
239-
if nvrhContext.ServerInfo.Os == "windows" {
240-
nvrhContext.ShouldUsePorts = true
241-
}
242-
243-
if nvrhContext.ShouldUsePorts {
244-
randomPort := getRandomPort()
245-
246-
nvrhContext.LocalPortNumber = randomPort
247-
nvrhContext.RemotePortNumber = randomPort
248-
}
249-
250-
tunnelInfo := &ssh_tunnel_info.SshTunnelInfo{
251-
Mode: "unix",
252-
LocalSocket: nvrhContext.LocalSocketPath,
253-
RemoteSocket: nvrhContext.RemoteSocketPath,
254-
Public: false,
280+
// Even though this happens in the Windows Server path, we still need a
281+
// check here in case that path isn't hit.
282+
if tunnelInfo == nil {
283+
tunnelInfo = &ssh_tunnel_info.SshTunnelInfo{
284+
Mode: "unix",
285+
LocalSocket: localSocketPath,
286+
RemoteSocket: remoteSocketPath,
287+
Public: false,
288+
}
255289
}
256290

257-
if nvrhContext.ShouldUsePorts {
258-
tunnelInfo.SwitchToPorts(nvrhContext.LocalPortNumber, nvrhContext.RemotePortNumber)
291+
if shouldUsePorts {
292+
tunnelInfo.SwitchToPorts(localPortNumber, remotePortNumber)
259293
}
260294

261295
// Start remote nvim
262296
go func() {
263-
var cmdTemplate string
264-
if nvrhContext.ServerInfo.ShellName == "powershell" {
265-
cmdTemplate = `cd "%s"; %s`
266-
} else if nvrhContext.ServerInfo.ShellName == "cmd" {
267-
cmdTemplate = `cmd /c cd /d "%s" && %s`
297+
var nvimCommandString string
298+
if nvrhContext.ServerInfo.Os == "windows" {
299+
if nvrhContext.ServerInfo.ShellName == "bash" {
300+
nvimCommandString = fmt.Sprintf("/tmp/nvim-launcher-%s.bat", nvrhContext.SessionId)
301+
} else {
302+
nvimCommandString = nvrhContext.WindowsLauncherPath
303+
}
268304
} else {
269-
cmdTemplate = `exec "$SHELL" -i -c 'cd "%s" && %s'`
305+
nvimCommandString = nvim_helpers.BuildRemoteCommandString(
306+
nvrhContext.NvimCmd,
307+
nvrhContext.ServerInfo.ShellName,
308+
nvrhContext.RemoteDirectory,
309+
remoteEnv,
310+
tunnelInfo,
311+
)
270312
}
271313

272-
nvimCommandString := fmt.Sprintf(
273-
cmdTemplate,
274-
nvrhContext.RemoteDirectory,
275-
nvim_helpers.BuildRemoteCommandString(nvrhContext, tunnelInfo),
276-
)
277-
278314
slog.Info("Starting remote nvim", "nvimCommandString", nvimCommandString)
279315
done <- nvrhContext.SshClient.Run(nvimCommandString, tunnelInfo)
280316
// Call stop so WaitForNvim can exit.
@@ -293,7 +329,7 @@ var CliClientOpenCommand = cli.Command{
293329
}
294330

295331
// Start local client
296-
clientCmd := BuildClientNvimCmd(ctx, nvrhContext, tunnelInfo)
332+
clientCmd := BuildClientNvimCmd(ctx, localEditor, tunnelInfo)
297333
if nvrhContext.Debug {
298334
clientCmd.Stdout = os.Stdout
299335
clientCmd.Stderr = os.Stderr
@@ -402,35 +438,35 @@ var CliClientReconnectCommand = cli.Command{
402438
Endpoint: endpoint,
403439
// RemoteDirectory: c.Args().Get(1),
404440

405-
// RemoteEnv: c.StringSlice("server-env"),
406-
LocalEditor: c.StringSlice("local-editor"),
407-
408-
ShouldUsePorts: c.Bool("use-ports"),
409-
410-
RemoteSocketPath: fmt.Sprintf("/tmp/nvrh-socket-%s", sessionId),
411-
LocalSocketPath: filepath.Join(os.TempDir(), fmt.Sprintf("nvrh-socket-%s-%s", sessionId, randomId)),
412441
// TODO Handle mapping ports better with multiple clients.
413-
// AutomapPorts: c.Bool("enable-automap-ports"),
442+
// AutomapPorts: c.Bool("enable-automap-ports"),
414443

415444
Debug: isDebug,
416445

417446
TunneledPorts: make(map[string]bool),
418447

419448
// NvimCmd: c.StringSlice("nvim-cmd"),
420-
421-
SshArgs: c.StringSlice("ssh-arg"),
422449
}
423450

451+
localEditor := c.StringSlice("local-editor")
452+
sshArgs := c.StringSlice("ssh-arg")
453+
454+
shouldUsePorts := c.Bool("use-ports")
455+
remoteSocketPath := fmt.Sprintf("/tmp/nvrh-socket-%s", sessionId)
456+
localSocketPath := filepath.Join(os.TempDir(), fmt.Sprintf("nvrh-socket-%s-%s", sessionId, randomId))
457+
458+
randomPort := getRandomPort()
459+
localPortNumber := randomPort
460+
remotePortNumber := randomPort
461+
424462
// Setup SSH client
425-
sshClient, sshClientErr := getSshClient(nvrhContext, endpoint, sshPath)
463+
sshClient, sshClientErr := getSshClient(nvrhContext, endpoint, sshPath, sshArgs)
426464
if sshClientErr != nil {
427465
return sshClientErr
428466
}
429467
nvrhContext.SshClient = sshClient
430468

431-
if nvrhContext.ShouldUsePorts {
432-
randomPort := getRandomPort()
433-
469+
if shouldUsePorts {
434470
portNumberString := c.Args().Get(2)
435471
portNumber := 0
436472
if portNumberString != "" {
@@ -443,11 +479,8 @@ var CliClientReconnectCommand = cli.Command{
443479
portNumber = converted
444480
}
445481

446-
nvrhContext.LocalPortNumber = randomPort
447482
if portNumber != 0 {
448-
nvrhContext.RemotePortNumber = portNumber
449-
} else {
450-
nvrhContext.RemotePortNumber = randomPort
483+
remotePortNumber = portNumber
451484
}
452485
}
453486

@@ -458,7 +491,7 @@ var CliClientReconnectCommand = cli.Command{
458491
slog.Info("Cleaning up")
459492
closeNvimSocket(nv, false)
460493
killAllCmds(nvrhContext.CommandsToKill)
461-
os.Remove(nvrhContext.LocalSocketPath)
494+
os.Remove(localSocketPath)
462495
if nvrhContext.SshClient != nil {
463496
nvrhContext.SshClient.Close()
464497
}
@@ -467,13 +500,13 @@ var CliClientReconnectCommand = cli.Command{
467500
// Setup SSH tunnel
468501
tunnelInfo := &ssh_tunnel_info.SshTunnelInfo{
469502
Mode: "unix",
470-
LocalSocket: nvrhContext.LocalSocketPath,
471-
RemoteSocket: nvrhContext.RemoteSocketPath,
503+
LocalSocket: localSocketPath,
504+
RemoteSocket: remoteSocketPath,
472505
Public: false,
473506
}
474507

475-
if nvrhContext.ShouldUsePorts {
476-
tunnelInfo.SwitchToPorts(nvrhContext.LocalPortNumber, nvrhContext.RemotePortNumber)
508+
if shouldUsePorts {
509+
tunnelInfo.SwitchToPorts(localPortNumber, remotePortNumber)
477510
}
478511

479512
go func() {
@@ -500,7 +533,7 @@ var CliClientReconnectCommand = cli.Command{
500533
}
501534

502535
// Start local client
503-
clientCmd := BuildClientNvimCmd(ctx, nvrhContext, tunnelInfo)
536+
clientCmd := BuildClientNvimCmd(ctx, localEditor, tunnelInfo)
504537
if nvrhContext.Debug {
505538
clientCmd.Stdout = os.Stdout
506539
clientCmd.Stderr = os.Stderr
@@ -532,11 +565,11 @@ var CliClientReconnectCommand = cli.Command{
532565

533566
func BuildClientNvimCmd(
534567
ctx context.Context,
535-
nvrhContext *nvrh_context.NvrhContext,
568+
cmd []string,
536569
ti *ssh_tunnel_info.SshTunnelInfo,
537570
) *exec.Cmd {
538-
replacedArgs := make([]string, len(nvrhContext.LocalEditor))
539-
for i, arg := range nvrhContext.LocalEditor {
571+
replacedArgs := make([]string, len(cmd))
572+
for i, arg := range cmd {
540573
replacedArgs[i] = strings.ReplaceAll(arg, "{{SOCKET_PATH}}", ti.LocalBoundToIp())
541574
}
542575

@@ -647,6 +680,7 @@ func prepareRemoteNvim(
647680
browserScriptPath,
648681
nvrhContext.AutomapPorts,
649682
string(marshalled),
683+
nvrhContext.WindowsLauncherPath,
650684
)
651685

652686
if err != nil {
@@ -742,6 +776,7 @@ func getSshClient(
742776
nvrhContext *nvrh_context.NvrhContext,
743777
endpoint *ssh_endpoint.SshEndpoint,
744778
sshPath string,
779+
sshArgs []string,
745780
) (nvrh_base_ssh.BaseNvrhSshClient, error) {
746781
if sshPath == "internal" {
747782
sshClient, err := go_ssh_ext.GetSshClientForEndpoint(endpoint)
@@ -758,6 +793,7 @@ func getSshClient(
758793
return nvrh_base_ssh.BaseNvrhSshClient(&nvrh_binary_ssh.NvrhBinarySshClient{
759794
Ctx: nvrhContext,
760795
SshPath: sshPath,
796+
SshArgs: sshArgs,
761797
}), nil
762798
}
763799

0 commit comments

Comments
 (0)