v0.38.1 969e52a

resize_windows.go RawHistoryFolder

1//go:build windows
2
3package tui
4
5import (
6	"encoding/binary"
7	"os"
8	"time"
9	"unsafe"
10
11	"golang.org/x/sys/windows"
12)
13
14// Windows doesn't have SIGWINCH; resize events are signaled via input
15// records. For v1 we just ignore resize and rely on the next render
16// cycle to pick up the new size via term.GetSize.
17func (p *ProcTerm) installResizeHandler() {}
18
19func (p *ProcTerm) SetNonblock(enable bool) error { return nil }
20
21// peekStdin waits for one byte on a supported Windows stdin handle without
22// consuming anything unless a byte is available. Console input handles can be
23// waited on directly. Pipes are checked with PeekNamedPipe, which does not
24// consume data; polling that non-consuming check keeps the wait bounded.
25//
26// Disk files and other character handles are deliberately unsupported. A
27// speculative os.File.Read on either can block (or consume a byte that cannot
28// be put back safely), so those handles report no data instead of corrupting
29// the escape parser. In particular, do not replace this with a blocking Read
30// fallback: an isolated Esc must remain observable within d.
31func peekStdin(in *os.File, d time.Duration) (byte, bool, error) {
32	handle := windows.Handle(in.Fd())
33	fileType, err := windows.GetFileType(handle)
34	if err != nil {
35		return 0, false, err
36	}
37
38	switch fileType {
39	case windows.FILE_TYPE_CHAR:
40		// FILE_TYPE_CHAR also includes devices such as NUL. GetConsoleMode
41		// distinguishes an actual console input handle from those devices.
42		var mode uint32
43		if err := windows.GetConsoleMode(handle, &mode); err != nil {
44			return 0, false, windows.ERROR_INVALID_FUNCTION
45		}
46		return peekConsole(in, handle, d)
47	case windows.FILE_TYPE_PIPE:
48		return peekPipe(in, handle, d)
49	case windows.FILE_TYPE_DISK:
50		return 0, false, nil
51	default:
52		return 0, false, nil
53	}
54}
55
56const consoleKeyEvent = 0x0001
57
58type consoleInputRecord struct {
59	eventType uint16
60	_         uint16
61	event     [16]byte
62}
63
64var (
65	peekConsoleInputProc = windows.NewLazySystemDLL("kernel32.dll").NewProc("PeekConsoleInputW")
66	readConsoleInputProc = windows.NewLazySystemDLL("kernel32.dll").NewProc("ReadConsoleInputW")
67)
68
69func consoleInputRecordCall(proc *windows.LazyProc, handle windows.Handle, record *consoleInputRecord) (uint32, error) {
70	if err := proc.Find(); err != nil {
71		return 0, err
72	}
73	var read uint32
74	r1, _, callErr := proc.Call(
75		uintptr(handle),
76		uintptr(unsafe.Pointer(record)),
77		1,
78		uintptr(unsafe.Pointer(&read)),
79	)
80	if r1 == 0 {
81		if callErr != nil && callErr != windows.ERROR_SUCCESS {
82			return 0, callErr
83		}
84		return 0, windows.ERROR_INVALID_FUNCTION
85	}
86	return read, nil
87}
88
89func peekConsole(in *os.File, handle windows.Handle, d time.Duration) (byte, bool, error) {
90	deadline := time.Now().Add(d)
91	for {
92		remaining := d
93		if d > 0 {
94			remaining = time.Until(deadline)
95			if remaining <= 0 {
96				return 0, false, nil
97			}
98		}
99		event, err := windows.WaitForSingleObject(handle, waitMilliseconds(remaining))
100		if err != nil {
101			return 0, false, err
102		}
103		switch event {
104		case uint32(windows.WAIT_TIMEOUT):
105			return 0, false, nil
106		case windows.WAIT_OBJECT_0:
107			var record consoleInputRecord
108			count, err := consoleInputRecordCall(peekConsoleInputProc, handle, &record)
109			if err != nil {
110				return 0, false, err
111			}
112			if count == 0 {
113				if d <= 0 {
114					return 0, false, nil
115				}
116				continue
117			}
118			if record.eventType == consoleKeyEvent {
119				keyDown := binary.LittleEndian.Uint32(record.event[0:4]) != 0
120				char := binary.LittleEndian.Uint16(record.event[10:12])
121				if keyDown && char != 0 {
122					// The callback returns one byte, so leave non-ASCII
123					// characters queued for the normal reader rather than
124					// consuming them and truncating their encoding.
125					if char > 0xff {
126						return 0, false, nil
127					}
128					count, err := consoleInputRecordCall(readConsoleInputProc, handle, &record)
129					if err != nil {
130						return 0, false, err
131					}
132					if count != 0 {
133						return byte(char), true, nil
134					}
135					continue
136				}
137			}
138			// Consume non-character, key-up, and non-printing records so
139			// a ready console handle cannot make the next wait spin.
140			if _, err := consoleInputRecordCall(readConsoleInputProc, handle, &record); err != nil {
141				return 0, false, err
142			}
143			if d <= 0 || time.Until(deadline) <= 0 {
144				return 0, false, nil
145			}
146			continue
147		default:
148			return 0, false, windows.ERROR_INVALID_FUNCTION
149		}
150	}
151}
152
153func waitMilliseconds(d time.Duration) uint32 {
154	if d <= 0 {
155		return 0
156	}
157	milliseconds := uint64(d / time.Millisecond)
158	if d%time.Millisecond != 0 {
159		milliseconds++
160	}
161	const maxWaitMilliseconds = uint64(^uint32(0) - 1) // reserve INFINITE
162	if milliseconds > maxWaitMilliseconds {
163		return uint32(maxWaitMilliseconds)
164	}
165	return uint32(milliseconds)
166}
167
168var peekNamedPipe = windows.NewLazySystemDLL("kernel32.dll").NewProc("PeekNamedPipe")
169
170func peekPipe(in *os.File, handle windows.Handle, d time.Duration) (byte, bool, error) {
171	if err := peekNamedPipe.Find(); err != nil {
172		return 0, false, err
173	}
174	deadline := time.Now().Add(d)
175	for {
176		var available uint32
177		ok, _, callErr := peekNamedPipe.Call(
178			uintptr(handle),
179			0, 0, 0,
180			uintptr(unsafe.Pointer(&available)),
181			0,
182		)
183		if ok == 0 {
184			if callErr != nil && callErr != windows.ERROR_SUCCESS {
185				return 0, false, callErr
186			}
187			return 0, false, windows.ERROR_INVALID_FUNCTION
188		}
189		if available != 0 {
190			var b [1]byte
191			n, err := in.Read(b[:])
192			if err != nil {
193				return 0, false, err
194			}
195			if n != len(b) {
196				return 0, false, windows.ERROR_INVALID_FUNCTION
197			}
198			return b[0], true, nil
199		}
200		if d <= 0 {
201			return 0, false, nil
202		}
203		remaining := time.Until(deadline)
204		if remaining <= 0 {
205			return 0, false, nil
206		}
207		// PeekNamedPipe is a non-consuming availability check. A short
208		// sleep avoids a busy loop while the deadline remains authoritative.
209		if remaining > time.Millisecond {
210			remaining = time.Millisecond
211		}
212		time.Sleep(remaining)
213	}
214}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close