Skip to content
This repository was archived by the owner on Aug 30, 2024. It is now read-only.

Commit 386bb22

Browse files
committed
wip xterminal windows support
Change-Id: I93f662b3e8383f881dc53df139dc13e4618beeb2
1 parent 6a5fbc5 commit 386bb22

File tree

6 files changed

+156
-44
lines changed

6 files changed

+156
-44
lines changed

cmd/coder/resize_unix.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,17 @@ import (
1313
"go.coder.com/flog"
1414
)
1515

16-
func resizeEvents(ctx context.Context, termfd int) chan resizeEvent {
16+
func resizeEvents(ctx context.Context, termfd uintptr) chan resizeEvent {
1717
sigs := make(chan os.Signal, 16)
1818
signal.Notify(sigs, unix.SIGWINCH)
1919

2020
events := make(chan resizeEvent)
2121

2222
go func() {
2323
for ctx.Err() == nil {
24-
width, height, err := terminal.GetSize(termfd)
24+
width, height, err := terminal.GetSize(int(termfd))
2525
if err != nil {
26-
flog.Error("get term size: %v", err)
26+
flog.Error("get unix terminal size: %v", err)
2727
break
2828
}
2929
events <- resizeEvent{

cmd/coder/resize_windows.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@ import (
77
"time"
88

99
"golang.org/x/crypto/ssh/terminal"
10+
"golang.org/x/sys/windows"
1011

1112
"go.coder.com/flog"
1213
)
1314

1415
// windows does have a unix.SIGWINCH equivalent, so we poll the terminal size
1516
// and send resize events when needed
16-
func resizeEvents(ctx context.Context, termfd int) chan resizeEvent {
17+
func resizeEvents(ctx context.Context, termfd uintptr) chan resizeEvent {
1718
events := make(chan resizeEvent)
1819
t := time.Tick(time.Millisecond * 100)
1920

@@ -23,12 +24,12 @@ func resizeEvents(ctx context.Context, termfd int) chan resizeEvent {
2324
for {
2425
select {
2526
case <-ctx.Done():
26-
break
27+
return
2728
case <-t:
28-
width, height, err := terminal.GetSize(termfd)
29+
width, height, err := terminal.GetSize(int(windows.Handle(termfd)))
2930
if err != nil {
30-
flog.Error("get term size: %v", err)
31-
break
31+
flog.Error("get terminal size: %v", err)
32+
return
3233
}
3334
event := resizeEvent{
3435
height: uint16(height),

cmd/coder/shell.go

Lines changed: 29 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"go.coder.com/flog"
1818

1919
"cdr.dev/coder-cli/internal/activity"
20+
"cdr.dev/coder-cli/internal/xterminal"
2021
"cdr.dev/wsep"
2122
)
2223

@@ -31,19 +32,6 @@ func (cmd *shellCmd) Spec() cli.CommandSpec {
3132
}
3233
}
3334

34-
func enableTerminal(fd int) (restore func(), err error) {
35-
state, err := terminal.MakeRaw(fd)
36-
if err != nil {
37-
return restore, xerrors.Errorf("make raw term: %w", err)
38-
}
39-
return func() {
40-
err := terminal.Restore(fd, state)
41-
if err != nil {
42-
flog.Error("restore term state: %v", err)
43-
}
44-
}, nil
45-
}
46-
4735
type resizeEvent struct {
4836
height, width uint16
4937
}
@@ -55,25 +43,6 @@ func (s resizeEvent) equal(s2 *resizeEvent) bool {
5543
return s.height == s2.height && s.width == s2.width
5644
}
5745

58-
func sendResizeEvents(ctx context.Context, termfd int, process wsep.Process) {
59-
events := resizeEvents(ctx, termfd)
60-
61-
// Limit the frequency of resizes to prevent a stuttering effect.
62-
resizeLimiter := rate.NewLimiter(rate.Every(time.Millisecond*100), 1)
63-
for {
64-
select {
65-
case newsize := <-events:
66-
err := process.Resize(ctx, newsize.height, newsize.width)
67-
if err != nil {
68-
return
69-
}
70-
_ = resizeLimiter.Wait(ctx)
71-
case <-ctx.Done():
72-
return
73-
}
74-
}
75-
}
76-
7746
func (cmd *shellCmd) Run(fl *pflag.FlagSet) {
7847
if len(fl.Args()) < 1 {
7948
exitUsage(fl)
@@ -101,21 +70,45 @@ func (cmd *shellCmd) Run(fl *pflag.FlagSet) {
10170
}
10271
}
10372

73+
func sendResizeEvents(ctx context.Context, termfd uintptr, process wsep.Process) {
74+
events := resizeEvents(ctx, termfd)
75+
76+
// Limit the frequency of resizes to prevent a stuttering effect.
77+
resizeLimiter := rate.NewLimiter(rate.Every(time.Millisecond*100), 1)
78+
for {
79+
select {
80+
case newsize := <-events:
81+
err := process.Resize(ctx, newsize.height, newsize.width)
82+
if err != nil {
83+
return
84+
}
85+
_ = resizeLimiter.Wait(ctx)
86+
case <-ctx.Done():
87+
return
88+
}
89+
}
90+
}
91+
10492
func runCommand(ctx context.Context, envName string, command string, args []string) error {
10593
var (
10694
entClient = requireAuth()
10795
env = findEnv(entClient, envName)
10896
)
10997

110-
termfd := int(os.Stdin.Fd())
98+
termfd := os.Stdin.Fd()
11199

112-
tty := terminal.IsTerminal(termfd)
100+
tty := terminal.IsTerminal(int(termfd))
113101
if tty {
114-
restore, err := enableTerminal(termfd)
102+
state, err := xterminal.MakeRaw(termfd)
115103
if err != nil {
116104
return err
117105
}
118-
defer restore()
106+
defer func() {
107+
err := xterminal.Restore(termfd, state)
108+
if err != nil {
109+
flog.Error("restore term state: %v", err)
110+
}
111+
}()
119112
}
120113

121114
ctx, cancel := context.WithCancel(ctx)

internal/xterminal/doc.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Package xterminal provides functions to change termios or console attributes
2+
// and restore them later on. It supports Unix and Windows.
3+
//
4+
// This does the same thing as x/crypto/ssh/terminal on Linux. On Windows, it
5+
// sets the same console modes as the terminal package but also sets
6+
// `ENABLE_VIRTUAL_TERMINAL_INPUT` and `ENABLE_VIRTUAL_TERMINAL_PROCESSING` to
7+
// allow for VT100 sequences in the console. This is important, otherwise Linux
8+
// apps (with colors or ncurses) that are run through SSH or wsep get
9+
// garbled in a Windows console.
10+
//
11+
// More details can be found out about Windows console modes here:
12+
// https://docs.microsoft.com/en-us/windows/console/setconsolemode
13+
package xterminal

internal/xterminal/terminal.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// +build !windows
2+
3+
package xterminal
4+
5+
import (
6+
"golang.org/x/crypto/ssh/terminal"
7+
)
8+
9+
// State differs per-platform.
10+
type State struct {
11+
s *terminal.State
12+
}
13+
14+
// MakeRaw sets the terminal to raw.
15+
func MakeRaw(fd uintptr) (*State, error) {
16+
s, err := terminal.MakeRaw(int(fd))
17+
return &State{s}, err
18+
}
19+
20+
// MakeOutputRaw does nothing on non-Windows platforms.
21+
func MakeOutputRaw(fd uintptr) (*State, error) {
22+
return nil, nil
23+
}
24+
25+
// Restore terminal back to original state.
26+
func Restore(fd uintptr, state *State) error {
27+
if state == nil {
28+
return nil
29+
}
30+
31+
return terminal.Restore(int(fd), state.s)
32+
}
33+
34+
// ColorEnabled returns true on Linux if handle is a terminal.
35+
func ColorEnabled(fd uintptr) (bool, error) {
36+
return terminal.IsTerminal(int(fd)), nil
37+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// +build windows
2+
3+
package xterminal
4+
5+
import (
6+
"golang.org/x/sys/windows"
7+
)
8+
9+
// State differs per-platform.
10+
type State struct {
11+
mode uint32
12+
}
13+
14+
func makeRaw(handle windows.Handle, input bool) (uint32, error) {
15+
var st uint32
16+
if err := windows.GetConsoleMode(handle, &st); err != nil {
17+
return 0, err
18+
}
19+
20+
var raw uint32
21+
if input {
22+
raw = st &^ (windows.ENABLE_ECHO_INPUT | windows.ENABLE_PROCESSED_INPUT | windows.ENABLE_LINE_INPUT | windows.ENABLE_PROCESSED_OUTPUT)
23+
raw |= windows.ENABLE_VIRTUAL_TERMINAL_INPUT
24+
} else {
25+
raw = st | windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING
26+
}
27+
28+
if err := windows.SetConsoleMode(handle, raw); err != nil {
29+
return 0, err
30+
}
31+
return st, nil
32+
}
33+
34+
// MakeRaw sets an input terminal to raw and enables VT100 processing.
35+
func MakeRaw(handle uintptr) (*State, error) {
36+
inSt, err := makeRaw(windows.Handle(handle), true)
37+
if err != nil {
38+
return nil, err
39+
}
40+
41+
return &State{inSt}, nil
42+
}
43+
44+
// MakeOutputRaw sets an output terminal to raw and enables VT100 processing.
45+
func MakeOutputRaw(handle uintptr) (*State, error) {
46+
outSt, err := makeRaw(windows.Handle(handle), false)
47+
if err != nil {
48+
return nil, err
49+
}
50+
51+
return &State{outSt}, nil
52+
}
53+
54+
// Restore terminal back to original state.
55+
func Restore(handle uintptr, state *State) error {
56+
return windows.SetConsoleMode(windows.Handle(handle), state.mode)
57+
}
58+
59+
// ColorEnabled returns true if VT100 processing is enabled on the output
60+
// console.
61+
func ColorEnabled(handle uintptr) (bool, error) {
62+
var st uint32
63+
if err := windows.GetConsoleMode(windows.Handle(handle), &st); err != nil {
64+
return false, err
65+
}
66+
67+
return st&windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING != 0, nil
68+
}

0 commit comments

Comments
 (0)