Skip to content

Commit 0754e36

Browse files
authored
Handle cursor area with custom writer (#17)
1 parent 00834e0 commit 0754e36

13 files changed

Lines changed: 844 additions & 189 deletions

File tree

README.md

Lines changed: 307 additions & 54 deletions
Large diffs are not rendered by default.

_examples/area/movement/main.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"time"
6+
7+
"atomicgo.dev/cursor"
8+
)
9+
10+
func main() {
11+
fmt.Println("Cursor area movement demo")
12+
fmt.Println("--------------------------")
13+
14+
area := cursor.NewArea()
15+
content := `Start content with some rows
16+
1. Row1
17+
2. Row2
18+
---
19+
`
20+
area.Update(content)
21+
22+
time.Sleep(1 * time.Second)
23+
area.Up(2)
24+
area.Move(3, 0)
25+
fmt.Print("Replaced row 2")
26+
27+
time.Sleep(1 * time.Second)
28+
area.StartOfLine()
29+
area.Move(8, -1)
30+
fmt.Print("3. Appended row")
31+
32+
time.Sleep(1 * time.Second)
33+
area.Update(content + "(restored content after move)")
34+
35+
time.Sleep(1 * time.Second)
36+
area.Up(6)
37+
fmt.Print("<<< AFTER Up(6)")
38+
time.Sleep(1 * time.Second)
39+
area.Update(content + "(restored content after cursor up out of bounds)")
40+
41+
time.Sleep(1 * time.Second)
42+
area.Down(6)
43+
fmt.Print("<<< AFTER Down(6)")
44+
time.Sleep(1 * time.Second)
45+
area.Update(content + "(restored content after cursor down out of bounds)")
46+
47+
time.Sleep(1 * time.Second)
48+
area.Top()
49+
fmt.Print("<<< AFTER Top()")
50+
time.Sleep(1 * time.Second)
51+
area.Update(content + "(restored content after cursor top)")
52+
53+
time.Sleep(1 * time.Second)
54+
area.Bottom()
55+
fmt.Print("<<< AFTER Bottom()")
56+
time.Sleep(1 * time.Second)
57+
area.Update(content + "(restored content after cursor bottom)")
58+
59+
time.Sleep(1 * time.Second)
60+
area.Update("")
61+
time.Sleep(1 * time.Second)
62+
area.Update(content + "(restored content after empty line)")
63+
64+
time.Sleep(1 * time.Second)
65+
fmt.Println("\n--- DONE")
66+
}

_examples/area/multiline/main.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"math/rand"
6+
"time"
7+
8+
"atomicgo.dev/cursor"
9+
)
10+
11+
func main() {
12+
fmt.Println("Multiline cursor area demo")
13+
fmt.Println("--------------------------")
14+
15+
area := cursor.NewArea()
16+
header := "This is a multiline demo\nwith 2 lines:\n"
17+
area.Update(header)
18+
content := header
19+
for i := 1; i < 6; i++ {
20+
if i%2 == 0 {
21+
content += fmt.Sprintf(" + %d\n", i)
22+
} else {
23+
content += fmt.Sprintf(" - line: %d", i)
24+
}
25+
time.Sleep(1 * time.Second)
26+
area.Update(content)
27+
}
28+
29+
time.Sleep(1 * time.Second)
30+
area.Update("Test varying area sizes now")
31+
time.Sleep(500 * time.Millisecond)
32+
area.Update(buildContent(1, 2))
33+
time.Sleep(500 * time.Millisecond)
34+
area.Update(buildContent(2, 9))
35+
time.Sleep(500 * time.Millisecond)
36+
area.Update(buildContent(3, 5))
37+
time.Sleep(500 * time.Millisecond)
38+
area.Update(buildContent(4, 0))
39+
time.Sleep(500 * time.Millisecond)
40+
area.Update(buildContent(5, 6))
41+
time.Sleep(500 * time.Millisecond)
42+
area.Update(buildContent(6, 1))
43+
time.Sleep(500 * time.Millisecond)
44+
area.Update(buildContent(7, 3))
45+
46+
time.Sleep(1 * time.Second)
47+
fmt.Println("\n--- DONE")
48+
}
49+
50+
func buildContent(idx int, n int) string {
51+
content := fmt.Sprintf(">>> START OF CONTENT %d/%d <<<\n", idx, n)
52+
for i := 0; i < n; i++ {
53+
for i := 0; i < 5; i++ {
54+
content += words[rand.Intn(len(words))] + " "
55+
}
56+
content += "\n"
57+
}
58+
59+
return content
60+
}
61+
62+
var words = []string{
63+
"ball", "summer", "hint", "mountain", "island", "onion", "world",
64+
"run", "hit", "fly", "swim", "crawl", "build", "dive", "jump",
65+
"crazy", "funny", "strange", "yellow", "red", "blue", "green", "white",
66+
}

_examples/area/singleline/main.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"time"
6+
7+
"atomicgo.dev/cursor"
8+
)
9+
10+
func main() {
11+
fmt.Println("Single line cursor area demo")
12+
fmt.Println("----------------------------")
13+
14+
area := cursor.NewArea()
15+
16+
header := "This is a singleline without newline"
17+
area.Update(header)
18+
for i := 1; i < 6; i++ {
19+
time.Sleep(1 * time.Second)
20+
area.Update(fmt.Sprintf("%s: %d", header, i))
21+
}
22+
23+
header = "This is a singleline with newline"
24+
area.Update(header + "\n")
25+
for i := 1; i < 6; i++ {
26+
time.Sleep(1 * time.Second)
27+
area.Update(fmt.Sprintf("%s: %d\n", header, i))
28+
}
29+
30+
time.Sleep(1 * time.Second)
31+
fmt.Println("\n--- DONE")
32+
}

area.go

Lines changed: 125 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,164 @@
11
package cursor
22

33
import (
4-
"fmt"
54
"os"
6-
"runtime"
75
"strings"
86
)
97

108
// Area displays content which can be updated on the fly.
119
// You can use this to create live output, charts, dropdowns, etc.
1210
type Area struct {
13-
height int
14-
writer Writer
11+
height int
12+
writer Writer
13+
cursor *Cursor
14+
cursorPosY int
1515
}
1616

1717
// NewArea returns a new Area.
1818
func NewArea() Area {
1919
return Area{
20-
writer: os.Stdout,
21-
height: 0,
20+
height: 0,
21+
writer: os.Stdout,
22+
cursor: cursor,
23+
cursorPosY: 0,
2224
}
2325
}
2426

25-
// WithWriter sets a custom writer for the Area.
27+
// WithWriter sets the custom writer.
2628
func (area Area) WithWriter(writer Writer) Area {
2729
area.writer = writer
30+
area.cursor = area.cursor.WithWriter(writer)
2831

2932
return area
3033
}
3134

3235
// Clear clears the content of the Area.
3336
func (area *Area) Clear() {
34-
Bottom()
37+
// Initialize writer if not done yet
38+
if area.writer == nil {
39+
area.writer = os.Stdout
40+
}
3541

3642
if area.height > 0 {
37-
ClearLinesUp(area.height)
43+
area.Bottom()
44+
area.ClearLinesUp(area.height)
45+
area.StartOfLine()
46+
} else {
47+
area.StartOfLine()
48+
area.cursor.ClearLine()
3849
}
3950
}
4051

41-
// Update overwrites the content of the Area.
52+
// Update overwrites the content of the Area and adjusts its height based on content.
4253
func (area *Area) Update(content string) {
43-
oldWriter := target
44-
45-
SetTarget(area.writer) // Temporary set the target to the Area's writer so we can use the cursor functions
4654
area.Clear()
55+
area.writeArea(content)
56+
area.cursorPosY = 0
57+
area.height = strings.Count(content, "\n")
58+
}
4759

48-
lines := strings.Split(content, "\n")
49-
fmt.Fprintln(area.writer, strings.Repeat("\n", len(lines)-1)) // This appends space if the terminal is at the bottom
50-
Up(len(lines))
51-
SetTarget(oldWriter) // Reset the target to the old writer
52-
53-
// Workaround for buggy behavior on Windows
54-
if runtime.GOOS == "windows" {
55-
for _, line := range lines {
56-
fmt.Fprint(area.writer, line)
57-
StartOfLineDown(1)
60+
// Up moves the cursor of the area up one line.
61+
func (area *Area) Up(n int) {
62+
if n > 0 {
63+
if area.cursorPosY+n > area.height {
64+
n = area.height - area.cursorPosY
5865
}
59-
} else {
60-
for _, line := range lines {
61-
fmt.Fprintln(area.writer, line)
66+
67+
area.cursor.Up(n)
68+
area.cursorPosY += n
69+
}
70+
}
71+
72+
// Down moves the cursor of the area down one line.
73+
func (area *Area) Down(n int) {
74+
if n > 0 {
75+
if area.cursorPosY-n < 0 {
76+
n = area.height - area.cursorPosY
6277
}
78+
79+
area.cursor.Down(n)
80+
area.cursorPosY -= n
81+
}
82+
}
83+
84+
// Bottom moves the cursor to the bottom of the terminal.
85+
// This is done by calculating how many lines were moved by Up and Down.
86+
func (area *Area) Bottom() {
87+
if area.cursorPosY > 0 {
88+
area.Down(area.cursorPosY)
89+
area.cursorPosY = 0
90+
}
91+
}
92+
93+
// Top moves the cursor to the top of the area.
94+
// This is done by calculating how many lines were moved by Up and Down.
95+
func (area *Area) Top() {
96+
if area.cursorPosY < area.height {
97+
area.Up(area.height - area.cursorPosY)
98+
area.cursorPosY = area.height
6399
}
100+
}
101+
102+
// StartOfLine moves the cursor to the start of the current line.
103+
func (area *Area) StartOfLine() {
104+
area.cursor.HorizontalAbsolute(0)
105+
}
64106

65-
height = 0
66-
area.height = len(strings.Split(content, "\n"))
107+
// StartOfLineDown moves the cursor down by n lines, then moves to cursor to the start of the line.
108+
func (area *Area) StartOfLineDown(n int) {
109+
area.Down(n)
110+
area.StartOfLine()
111+
}
112+
113+
// StartOfLineUp moves the cursor up by n lines, then moves to cursor to the start of the line.
114+
func (area *Area) StartOfLineUp(n int) {
115+
area.Up(n)
116+
area.StartOfLine()
117+
}
118+
119+
// UpAndClear moves the cursor up by n lines, then clears the line.
120+
func (area *Area) UpAndClear(n int) {
121+
area.Up(n)
122+
area.cursor.ClearLine()
123+
}
124+
125+
// DownAndClear moves the cursor down by n lines, then clears the line.
126+
func (area *Area) DownAndClear(n int) {
127+
area.Down(n)
128+
area.cursor.ClearLine()
129+
}
130+
131+
// Move moves the cursor relative by x and y.
132+
func (area *Area) Move(x, y int) {
133+
if x > 0 {
134+
area.cursor.Right(x)
135+
} else if x < 0 {
136+
area.cursor.Left(-x)
137+
}
138+
139+
if y > 0 {
140+
area.Up(y)
141+
} else if y < 0 {
142+
area.Down(-y)
143+
}
144+
}
145+
146+
// ClearLinesUp clears n lines upwards from the current position and moves the cursor.
147+
func (area *Area) ClearLinesUp(n int) {
148+
area.StartOfLine()
149+
area.cursor.ClearLine()
150+
151+
for i := 0; i < n; i++ {
152+
area.UpAndClear(1)
153+
}
154+
}
155+
156+
// ClearLinesDown clears n lines downwards from the current position and moves the cursor.
157+
func (area *Area) ClearLinesDown(n int) {
158+
area.StartOfLine()
159+
area.cursor.ClearLine()
160+
161+
for i := 0; i < n; i++ {
162+
area.DownAndClear(1)
163+
}
67164
}

area_other.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
//go:build !windows
2+
// +build !windows
3+
4+
package cursor
5+
6+
import (
7+
"fmt"
8+
)
9+
10+
// Update overwrites the content of the Area and adjusts its height based on content.
11+
func (area *Area) writeArea(content string) {
12+
fmt.Fprint(area.writer, content)
13+
}

area_windows.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
//go:build windows
2+
// +build windows
3+
4+
package cursor
5+
6+
import (
7+
"fmt"
8+
)
9+
10+
// writeArea is a helper for platform dependant output.
11+
// For Windows newlines '\n' in the content are replaced by '\r\n'
12+
func (area *Area) writeArea(content string) {
13+
last := ' '
14+
for _, r := range content {
15+
if r == '\n' && last != '\r' {
16+
fmt.Fprint(area.writer, "\r\n")
17+
continue
18+
}
19+
fmt.Fprint(area.writer, string(r))
20+
}
21+
}

0 commit comments

Comments
 (0)