Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
83eb5c3
Support fix multiline text input area cursor
lammel Jun 19, 2023
da0dea7
Fix compile error due to merge
lammel Jun 20, 2023
935a25a
Use wrapped Writer interface, refactor global cursor helpers to utils
lammel Jun 20, 2023
e4735ab
Update README with gomarkdoc
lammel Jun 20, 2023
993e54d
Refactor cursor for windows
lammel Jun 21, 2023
392d539
Fix handling of cursors internal Writer interface
lammel Jun 21, 2023
f37bfde
Consistently use cursor.Writer interface
lammel Jun 27, 2023
b82f3e7
Change area update to not add unnecessary newline
lammel Jun 27, 2023
8418eaf
Fix using cursor.Writer for Windows
lammel Jun 27, 2023
05fedf2
Fix bad import causing compile error on windows
lammel Jul 17, 2023
4f8caac
Support fix multiline text input area cursor
lammel Jun 19, 2023
33f5e35
Use wrapped Writer interface, refactor global cursor helpers to utils
lammel Jun 20, 2023
beabaa7
Update README with gomarkdoc
lammel Jun 20, 2023
4adbf51
Refactor cursor for windows
lammel Jun 21, 2023
2e40bf2
Fix handling of cursors internal Writer interface
lammel Jun 21, 2023
5541a3e
Consistently use cursor.Writer interface
lammel Jun 27, 2023
17e874e
Change area update to not add unnecessary newline
lammel Jun 27, 2023
3115497
Fix using cursor.Writer for Windows
lammel Jun 27, 2023
32f5b00
Fix bad import causing compile error on windows
lammel Jul 17, 2023
f1da00a
Merge branch 'feature/custom-writer' of github.com:lammel/cursor into…
lammel Jul 19, 2023
3ffca3f
Fix golint warnings
lammel Jul 19, 2023
e8fa94d
Fix more linter warnings
lammel Jul 19, 2023
25fb942
Add examples for cursor and area
lammel Jul 27, 2023
ca3b9a0
Slightly improve examples
lammel Jul 27, 2023
6ede95c
Fix single line clear not moving back to start of line
lammel Jul 27, 2023
8b6acf0
Fix newline handling for windows, refactor platform support for curso…
lammel Jul 27, 2023
7738fd3
Add example to test varying content sizes
lammel Jul 27, 2023
d5f4f59
Fix jumpy content in area caused by wrong cursorPosY
lammel Jul 27, 2023
c886465
Fix jumpy content in area caused by wrong cursorPosY
lammel Jul 27, 2023
a864b73
Merge branch 'feature/custom-writer' of https://github.com/lammel/cur…
lammel Jul 28, 2023
a1df9ff
Fix compile error on windows
lammel Jul 28, 2023
ec37172
Fix area up/down to respect bounds
lammel Jul 28, 2023
009272c
Final cleanup for examples
lammel Jul 28, 2023
1286808
Make linter happy again
lammel Jul 28, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
361 changes: 307 additions & 54 deletions README.md

Large diffs are not rendered by default.

66 changes: 66 additions & 0 deletions _examples/area/movement/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package main

import (
"fmt"
"time"

"atomicgo.dev/cursor"
)

func main() {
fmt.Println("Cursor area movement demo")
fmt.Println("--------------------------")

area := cursor.NewArea()
content := `Start content with some rows
1. Row1
2. Row2
---
`
area.Update(content)

time.Sleep(1 * time.Second)
area.Up(2)
area.Move(3, 0)
fmt.Print("Replaced row 2")

time.Sleep(1 * time.Second)
area.StartOfLine()
area.Move(8, -1)
fmt.Print("3. Appended row")

time.Sleep(1 * time.Second)
area.Update(content + "(restored content after move)")

time.Sleep(1 * time.Second)
area.Up(6)
fmt.Print("<<< AFTER Up(6)")
time.Sleep(1 * time.Second)
area.Update(content + "(restored content after cursor up out of bounds)")

time.Sleep(1 * time.Second)
area.Down(6)
fmt.Print("<<< AFTER Down(6)")
time.Sleep(1 * time.Second)
area.Update(content + "(restored content after cursor down out of bounds)")

time.Sleep(1 * time.Second)
area.Top()
fmt.Print("<<< AFTER Top()")
time.Sleep(1 * time.Second)
area.Update(content + "(restored content after cursor top)")

time.Sleep(1 * time.Second)
area.Bottom()
fmt.Print("<<< AFTER Bottom()")
time.Sleep(1 * time.Second)
area.Update(content + "(restored content after cursor bottom)")

time.Sleep(1 * time.Second)
area.Update("")
time.Sleep(1 * time.Second)
area.Update(content + "(restored content after empty line)")

time.Sleep(1 * time.Second)
fmt.Println("\n--- DONE")
}
66 changes: 66 additions & 0 deletions _examples/area/multiline/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package main

import (
"fmt"
"math/rand"
"time"

"atomicgo.dev/cursor"
)

func main() {
fmt.Println("Multiline cursor area demo")
fmt.Println("--------------------------")

area := cursor.NewArea()
header := "This is a multiline demo\nwith 2 lines:\n"
area.Update(header)
content := header
for i := 1; i < 6; i++ {
if i%2 == 0 {
content += fmt.Sprintf(" + %d\n", i)
} else {
content += fmt.Sprintf(" - line: %d", i)
}
time.Sleep(1 * time.Second)
area.Update(content)
}

time.Sleep(1 * time.Second)
area.Update("Test varying area sizes now")
time.Sleep(500 * time.Millisecond)
area.Update(buildContent(1, 2))
time.Sleep(500 * time.Millisecond)
area.Update(buildContent(2, 9))
time.Sleep(500 * time.Millisecond)
area.Update(buildContent(3, 5))
time.Sleep(500 * time.Millisecond)
area.Update(buildContent(4, 0))
time.Sleep(500 * time.Millisecond)
area.Update(buildContent(5, 6))
time.Sleep(500 * time.Millisecond)
area.Update(buildContent(6, 1))
time.Sleep(500 * time.Millisecond)
area.Update(buildContent(7, 3))

time.Sleep(1 * time.Second)
fmt.Println("\n--- DONE")
}

func buildContent(idx int, n int) string {
content := fmt.Sprintf(">>> START OF CONTENT %d/%d <<<\n", idx, n)
for i := 0; i < n; i++ {
for i := 0; i < 5; i++ {
content += words[rand.Intn(len(words))] + " "
}
content += "\n"
}

return content
}

var words = []string{
"ball", "summer", "hint", "mountain", "island", "onion", "world",
"run", "hit", "fly", "swim", "crawl", "build", "dive", "jump",
"crazy", "funny", "strange", "yellow", "red", "blue", "green", "white",
}
32 changes: 32 additions & 0 deletions _examples/area/singleline/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package main

import (
"fmt"
"time"

"atomicgo.dev/cursor"
)

func main() {
fmt.Println("Single line cursor area demo")
fmt.Println("----------------------------")

area := cursor.NewArea()

header := "This is a singleline without newline"
area.Update(header)
for i := 1; i < 6; i++ {
time.Sleep(1 * time.Second)
area.Update(fmt.Sprintf("%s: %d", header, i))
}

header = "This is a singleline with newline"
area.Update(header + "\n")
for i := 1; i < 6; i++ {
time.Sleep(1 * time.Second)
area.Update(fmt.Sprintf("%s: %d\n", header, i))
}

time.Sleep(1 * time.Second)
fmt.Println("\n--- DONE")
}
153 changes: 125 additions & 28 deletions area.go
Original file line number Diff line number Diff line change
@@ -1,67 +1,164 @@
package cursor

import (
"fmt"
"os"
"runtime"
"strings"
)

// Area displays content which can be updated on the fly.
// You can use this to create live output, charts, dropdowns, etc.
type Area struct {
height int
writer Writer
height int
writer Writer
cursor *Cursor
cursorPosY int
}

// NewArea returns a new Area.
func NewArea() Area {
return Area{
writer: os.Stdout,
height: 0,
height: 0,
writer: os.Stdout,
cursor: cursor,
cursorPosY: 0,
}
}

// WithWriter sets a custom writer for the Area.
// WithWriter sets the custom writer.
func (area Area) WithWriter(writer Writer) Area {
area.writer = writer
area.cursor = area.cursor.WithWriter(writer)

return area
}

// Clear clears the content of the Area.
func (area *Area) Clear() {
Bottom()
// Initialize writer if not done yet
if area.writer == nil {
area.writer = os.Stdout
}

if area.height > 0 {
ClearLinesUp(area.height)
area.Bottom()
area.ClearLinesUp(area.height)
area.StartOfLine()
} else {
area.StartOfLine()
area.cursor.ClearLine()
}
}

// Update overwrites the content of the Area.
// Update overwrites the content of the Area and adjusts its height based on content.
func (area *Area) Update(content string) {
oldWriter := target

SetTarget(area.writer) // Temporary set the target to the Area's writer so we can use the cursor functions
area.Clear()
area.writeArea(content)
area.cursorPosY = 0
area.height = strings.Count(content, "\n")
}

lines := strings.Split(content, "\n")
fmt.Fprintln(area.writer, strings.Repeat("\n", len(lines)-1)) // This appends space if the terminal is at the bottom
Up(len(lines))
SetTarget(oldWriter) // Reset the target to the old writer

// Workaround for buggy behavior on Windows
if runtime.GOOS == "windows" {
for _, line := range lines {
fmt.Fprint(area.writer, line)
StartOfLineDown(1)
// Up moves the cursor of the area up one line.
func (area *Area) Up(n int) {
if n > 0 {
if area.cursorPosY+n > area.height {
n = area.height - area.cursorPosY
}
} else {
for _, line := range lines {
fmt.Fprintln(area.writer, line)

area.cursor.Up(n)
area.cursorPosY += n
}
}

// Down moves the cursor of the area down one line.
func (area *Area) Down(n int) {
if n > 0 {
if area.cursorPosY-n < 0 {
n = area.height - area.cursorPosY
}

area.cursor.Down(n)
area.cursorPosY -= n
}
}

// Bottom moves the cursor to the bottom of the terminal.
// This is done by calculating how many lines were moved by Up and Down.
func (area *Area) Bottom() {
if area.cursorPosY > 0 {
area.Down(area.cursorPosY)
area.cursorPosY = 0
}
}

// Top moves the cursor to the top of the area.
// This is done by calculating how many lines were moved by Up and Down.
func (area *Area) Top() {
if area.cursorPosY < area.height {
area.Up(area.height - area.cursorPosY)
area.cursorPosY = area.height
}
}

// StartOfLine moves the cursor to the start of the current line.
func (area *Area) StartOfLine() {
area.cursor.HorizontalAbsolute(0)
}

height = 0
area.height = len(strings.Split(content, "\n"))
// StartOfLineDown moves the cursor down by n lines, then moves to cursor to the start of the line.
func (area *Area) StartOfLineDown(n int) {
area.Down(n)
area.StartOfLine()
}

// StartOfLineUp moves the cursor up by n lines, then moves to cursor to the start of the line.
func (area *Area) StartOfLineUp(n int) {
area.Up(n)
area.StartOfLine()
}

// UpAndClear moves the cursor up by n lines, then clears the line.
func (area *Area) UpAndClear(n int) {
area.Up(n)
area.cursor.ClearLine()
}

// DownAndClear moves the cursor down by n lines, then clears the line.
func (area *Area) DownAndClear(n int) {
area.Down(n)
area.cursor.ClearLine()
}

// Move moves the cursor relative by x and y.
func (area *Area) Move(x, y int) {
if x > 0 {
area.cursor.Right(x)
} else if x < 0 {
area.cursor.Left(-x)
}

if y > 0 {
area.Up(y)
} else if y < 0 {
area.Down(-y)
}
}

// ClearLinesUp clears n lines upwards from the current position and moves the cursor.
func (area *Area) ClearLinesUp(n int) {
area.StartOfLine()
area.cursor.ClearLine()

for i := 0; i < n; i++ {
area.UpAndClear(1)
}
}

// ClearLinesDown clears n lines downwards from the current position and moves the cursor.
func (area *Area) ClearLinesDown(n int) {
area.StartOfLine()
area.cursor.ClearLine()

for i := 0; i < n; i++ {
area.DownAndClear(1)
}
}
13 changes: 13 additions & 0 deletions area_other.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//go:build !windows
// +build !windows

package cursor

import (
"fmt"
)

// Update overwrites the content of the Area and adjusts its height based on content.
func (area *Area) writeArea(content string) {
fmt.Fprint(area.writer, content)
}
21 changes: 21 additions & 0 deletions area_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//go:build windows
// +build windows

package cursor

import (
"fmt"
)

// writeArea is a helper for platform dependant output.
// For Windows newlines '\n' in the content are replaced by '\r\n'
func (area *Area) writeArea(content string) {
last := ' '
for _, r := range content {
if r == '\n' && last != '\r' {
fmt.Fprint(area.writer, "\r\n")
continue
}
fmt.Fprint(area.writer, string(r))
}
}
Loading