v0.5.1 8e4df5c

scroll.go RawHistoryFolder

1package ansi
2
3import (
4	"bytes"
5	"strconv"
6)
7
8func findSafeScroll(frame CellSource, damage []Damage) (Damage, bool) {
9	for _, d := range damage {
10		if (d.Kind == DamageScrollUp || d.Kind == DamageScrollDown) && isSafeScroll(frame, d) {
11			return d, true
12		}
13	}
14	return Damage{}, false
15}
16
17func isSafeScroll(frame CellSource, d Damage) bool {
18	return d.X == 0 && d.Width == frame.Columns() && d.Y >= 0 && d.Y <= frame.Rows() && d.Height > 0 && d.Height <= frame.Rows()-d.Y && d.Count > 0 && d.Count < d.Height
19}
20
21func emitScroll(out *bytes.Buffer, d Damage) {
22	out.WriteString("\x1b[0m")
23	// \x1b[%d;%dr — set scroll region
24	out.WriteString("\x1b[")
25	var b [16]byte
26	n := strconv.AppendInt(b[:0], int64(d.Y+1), 10)
27	out.Write(n)
28	out.WriteByte(';')
29	n = strconv.AppendInt(b[:0], int64(d.Y+d.Height), 10)
30	out.Write(n)
31	out.WriteByte('r')
32	// SD scrolls down without moving content outside the chosen region.
33	if d.Kind == DamageScrollDown {
34		out.WriteString("\x1b[")
35		n = strconv.AppendInt(b[:0], int64(d.Count), 10)
36		out.Write(n)
37		out.WriteByte('T')
38		out.WriteString("\x1b[r")
39		return
40	}
41	// Position cursor at bottom of scroll region.
42	writeCursor(out, d.Y+d.Height-1, 0)
43	if d.Count == 1 {
44		out.WriteByte('\n')
45	} else {
46		out.WriteString("\x1b[")
47		n = strconv.AppendInt(b[:0], int64(d.Count), 10)
48		out.Write(n)
49		out.WriteByte('S')
50	}
51	out.WriteString("\x1b[r")
52}
53
54func canApplyScrollAgainst(frame CellSource, scroll Damage, damage []Damage, committed Frame) bool {
55	// Scrolling fills exposed rows with default-style blanks. Every target
56	// cell that differs from that fill must be repainted, in either direction.
57	exposed := scroll.Y + scroll.Height - scroll.Count
58	if scroll.Kind == DamageScrollDown {
59		exposed = scroll.Y
60	}
61	blank := BlankCell()
62	for y := exposed; y < exposed+scroll.Count; y++ {
63		for x := range scroll.Width {
64			column := scroll.X + x
65			if !damageCoversCell(damage, column, y) && !frame.Cell(column, y).Equal(blank) {
66				return false
67			}
68		}
69	}
70	switch frame := frame.(type) {
71	case Frame:
72		return canApplyDenseScrollAgainst(frame, scroll, damage, committed)
73	case *Frame:
74		return canApplyDenseScrollAgainst(*frame, scroll, damage, committed)
75	}
76	start, end, offset := scrollRetainedRows(scroll)
77	for y := start; y < end; y++ {
78		for x := range scroll.Width {
79			column := scroll.X + x
80			committedCell, frameCell := committed.Cell(column, y+offset), frame.Cell(column, y)
81			if committedCell == frameCell || committedCell.Equal(frameCell) {
82				continue
83			}
84			if !damageCoversCell(damage, column, y) {
85				return false
86			}
87		}
88	}
89	return true
90}
91
92func canApplyDenseScrollAgainst(frame Frame, scroll Damage, damage []Damage, committed Frame) bool {
93	start, end, offset := scrollRetainedRows(scroll)
94	for y := start; y < end; y++ {
95		for x := range scroll.Width {
96			column := scroll.X + x
97			committedCell, frameCell := committed.At(column, y+offset), frame.At(column, y)
98			if committedCell == frameCell || committedCell.Equal(frameCell) {
99				continue
100			}
101			if !damageCoversCell(damage, column, y) {
102				return false
103			}
104		}
105	}
106	return true
107}
108
109func scrollRetainedRows(scroll Damage) (start, end, offset int) {
110	if scroll.Kind == DamageScrollDown {
111		return scroll.Y + scroll.Count, scroll.Y + scroll.Height, -scroll.Count
112	}
113	return scroll.Y, scroll.Y + scroll.Height - scroll.Count, scroll.Count
114}
115
116func damageCoversCell(damage []Damage, x, y int) bool {
117	for _, d := range damage {
118		if (d.Kind == DamageText || d.Kind == DamageClear) && x >= d.X && x < d.X+d.Width && y >= d.Y && y < d.Y+d.Height {
119			return true
120		}
121	}
122	return false
123}

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 2

Tags 13

↑↓ moveenter openesc close