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

Commit 12f601a

Browse files
committed
wip xterminal windows support
Change-Id: I93f662b3e8383f881dc53df139dc13e4618beeb2
1 parent f98739c commit 12f601a

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
@@ -16,6 +16,7 @@ import (
1616
"go.coder.com/flog"
1717

1818
"cdr.dev/coder-cli/internal/activity"
19+
"cdr.dev/coder-cli/internal/xterminal"
1920
"cdr.dev/wsep"
2021
)
2122

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

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

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

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

112-
termfd := int(os.Stdin.Fd())
100+
termfd := os.Stdin.Fd()
113101

114-
tty := terminal.IsTerminal(termfd)
102+
tty := terminal.IsTerminal(int(termfd))
115103
if tty {
116-
restore, err := enableTerminal(termfd)
104+
state, err := xterminal.MakeRaw(termfd)
117105
if err != nil {
118106
return err
119107
}
120-
defer restore()
108+
defer func() {
109+
err := xterminal.Restore(termfd, state)
110+
if err != nil {
111+
flog.Error("restore term state: %v", err)
112+
}
113+
}()
121114
}
122115

123116
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)