diff --git a/README.md b/README.md
index 9760fef..8edb167 100644
--- a/README.md
+++ b/README.md
@@ -77,35 +77,62 @@ Special thanks to github.com/k0kubun/go\-ansi which this project is based on.
## Index
-- [func Bottom\(\)](<#Bottom>)
-- [func ClearLine\(\)](<#ClearLine>)
-- [func ClearLinesDown\(n int\)](<#ClearLinesDown>)
-- [func ClearLinesUp\(n int\)](<#ClearLinesUp>)
-- [func Down\(n int\)](<#Down>)
-- [func DownAndClear\(n int\)](<#DownAndClear>)
-- [func Hide\(\)](<#Hide>)
-- [func HorizontalAbsolute\(n int\)](<#HorizontalAbsolute>)
-- [func Left\(n int\)](<#Left>)
-- [func Move\(x, y int\)](<#Move>)
-- [func Right\(n int\)](<#Right>)
-- [func SetTarget\(w Writer\)](<#SetTarget>)
-- [func Show\(\)](<#Show>)
-- [func StartOfLine\(\)](<#StartOfLine>)
-- [func StartOfLineDown\(n int\)](<#StartOfLineDown>)
-- [func StartOfLineUp\(n int\)](<#StartOfLineUp>)
-- [func TestCustomIOWriter\(t \*testing.T\)](<#TestCustomIOWriter>)
-- [func Up\(n int\)](<#Up>)
-- [func UpAndClear\(n int\)](<#UpAndClear>)
-- [type Area](<#Area>)
- - [func NewArea\(\) Area](<#NewArea>)
- - [func \(area \*Area\) Clear\(\)](<#Area.Clear>)
- - [func \(area \*Area\) Update\(content string\)](<#Area.Update>)
- - [func \(area Area\) WithWriter\(writer Writer\) Area](<#Area.WithWriter>)
-- [type Writer](<#Writer>)
+- [cursor](#cursor)
+ - [Index](#index)
+ - [func Bottom](#func-bottom)
+ - [func Clear](#func-clear)
+ - [func ClearLine](#func-clearline)
+ - [func ClearLinesDown](#func-clearlinesdown)
+ - [func ClearLinesUp](#func-clearlinesup)
+ - [func Down](#func-down)
+ - [func DownAndClear](#func-downandclear)
+ - [func Hide](#func-hide)
+ - [func HorizontalAbsolute](#func-horizontalabsolute)
+ - [func Left](#func-left)
+ - [func Move](#func-move)
+ - [func Right](#func-right)
+ - [func SetTarget](#func-settarget)
+ - [func Show](#func-show)
+ - [func StartOfLine](#func-startofline)
+ - [func StartOfLineDown](#func-startoflinedown)
+ - [func StartOfLineUp](#func-startoflineup)
+ - [func TestCustomIOWriter](#func-testcustomiowriter)
+ - [func Up](#func-up)
+ - [func UpAndClear](#func-upandclear)
+ - [type Area](#type-area)
+ - [func NewArea](#func-newarea)
+ - [func (\*Area) Bottom](#func-area-bottom)
+ - [func (\*Area) Clear](#func-area-clear)
+ - [func (\*Area) ClearLinesDown](#func-area-clearlinesdown)
+ - [func (\*Area) ClearLinesUp](#func-area-clearlinesup)
+ - [func (\*Area) Down](#func-area-down)
+ - [func (\*Area) DownAndClear](#func-area-downandclear)
+ - [func (\*Area) Move](#func-area-move)
+ - [func (\*Area) StartOfLine](#func-area-startofline)
+ - [func (\*Area) StartOfLineDown](#func-area-startoflinedown)
+ - [func (\*Area) StartOfLineUp](#func-area-startoflineup)
+ - [func (\*Area) Top](#func-area-top)
+ - [func (\*Area) Up](#func-area-up)
+ - [func (\*Area) UpAndClear](#func-area-upandclear)
+ - [func (\*Area) Update](#func-area-update)
+ - [func (\*Area) WithWriter](#func-area-withwriter)
+ - [type Cursor](#type-cursor)
+ - [func NewCursor](#func-newcursor)
+ - [func (\*Cursor) Clear](#func-cursor-clear)
+ - [func (\*Cursor) ClearLine](#func-cursor-clearline)
+ - [func (\*Cursor) Down](#func-cursor-down)
+ - [func (\*Cursor) Hide](#func-cursor-hide)
+ - [func (\*Cursor) HorizontalAbsolute](#func-cursor-horizontalabsolute)
+ - [func (\*Cursor) Left](#func-cursor-left)
+ - [func (\*Cursor) Right](#func-cursor-right)
+ - [func (\*Cursor) Show](#func-cursor-show)
+ - [func (\*Cursor) Up](#func-cursor-up)
+ - [func (\*Cursor) WithWriter](#func-cursor-withwriter)
+ - [type Writer](#type-writer)
-## func [Bottom]()
+## func [Bottom]()
```go
func Bottom()
@@ -113,8 +140,17 @@ func Bottom()
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 [Clear]()
+
+```go
+func Clear()
+```
+
+Clear clears the current position and moves the cursor to the left.
+
-## func [ClearLine]()
+## func [ClearLine]()
```go
func ClearLine()
@@ -123,7 +159,7 @@ func ClearLine()
ClearLine clears the current line and moves the cursor to it's start position.
-## func [ClearLinesDown]()
+## func [ClearLinesDown]()
```go
func ClearLinesDown(n int)
@@ -132,7 +168,7 @@ func ClearLinesDown(n int)
ClearLinesDown clears n lines downwards from the current position and moves the cursor.
-## func [ClearLinesUp]()
+## func [ClearLinesUp]()
```go
func ClearLinesUp(n int)
@@ -141,7 +177,7 @@ func ClearLinesUp(n int)
ClearLinesUp clears n lines upwards from the current position and moves the cursor.
-## func [Down]()
+## func [Down]()
```go
func Down(n int)
@@ -150,7 +186,7 @@ func Down(n int)
Down moves the cursor n lines down relative to the current position.
-## func [DownAndClear]()
+## func [DownAndClear]()
```go
func DownAndClear(n int)
@@ -159,7 +195,7 @@ func DownAndClear(n int)
DownAndClear moves the cursor down by n lines, then clears the line.
-## func [Hide]()
+## func [Hide]()
```go
func Hide()
@@ -168,7 +204,7 @@ func Hide()
Hide the cursor. Don't forget to show the cursor at least at the end of your application with Show. Otherwise the user might have a terminal with a permanently hidden cursor, until they reopen the terminal.
-## func [HorizontalAbsolute]()
+## func [HorizontalAbsolute]()
```go
func HorizontalAbsolute(n int)
@@ -177,7 +213,7 @@ func HorizontalAbsolute(n int)
HorizontalAbsolute moves the cursor to n horizontally. The position n is absolute to the start of the line.
-## func [Left]()
+## func [Left]()
```go
func Left(n int)
@@ -186,7 +222,7 @@ func Left(n int)
Left moves the cursor n characters to the left relative to the current position.
-## func [Move]()
+## func [Move]()
```go
func Move(x, y int)
@@ -195,7 +231,7 @@ func Move(x, y int)
Move moves the cursor relative by x and y.
-## func [Right]()
+## func [Right]()
```go
func Right(n int)
@@ -204,16 +240,16 @@ func Right(n int)
Right moves the cursor n characters to the right relative to the current position.
-## func [SetTarget]()
+## func [SetTarget]()
```go
func SetTarget(w Writer)
```
-SetTarget allows for any arbitrary io.Writer to be used for cursor movement \(will not work on Windows\).
+
-## func [Show]()
+## func [Show]()
```go
func Show()
@@ -222,7 +258,7 @@ func Show()
Show the cursor if it was hidden previously. Don't forget to show the cursor at least at the end of your application. Otherwise the user might have a terminal with a permanently hidden cursor, until they reopen the terminal.
-## func [StartOfLine]()
+## func [StartOfLine]()
```go
func StartOfLine()
@@ -231,7 +267,7 @@ func StartOfLine()
StartOfLine moves the cursor to the start of the current line.
-## func [StartOfLineDown]()
+## func [StartOfLineDown]()
```go
func StartOfLineDown(n int)
@@ -240,7 +276,7 @@ func StartOfLineDown(n int)
StartOfLineDown moves the cursor down by n lines, then moves to cursor to the start of the line.
-## func [StartOfLineUp]()
+## func [StartOfLineUp]()
```go
func StartOfLineUp(n int)
@@ -258,7 +294,7 @@ func TestCustomIOWriter(t *testing.T)
TestCustomIOWriter tests the cursor functions with a custom Writer.
-## func [Up]()
+## func [Up]()
```go
func Up(n int)
@@ -267,7 +303,7 @@ func Up(n int)
Up moves the cursor n lines up relative to the current position.
-## func [UpAndClear]()
+## func [UpAndClear]()
```go
func UpAndClear(n int)
@@ -276,7 +312,7 @@ func UpAndClear(n int)
UpAndClear moves the cursor up by n lines, then clears the line.
-## type [Area]()
+## type [Area]()
Area displays content which can be updated on the fly. You can use this to create live output, charts, dropdowns, etc.
@@ -287,16 +323,25 @@ type Area struct {
```
-### func [NewArea]()
+### func [NewArea]()
```go
-func NewArea() Area
+func NewArea() *Area
```
NewArea returns a new Area.
+
+### func \(\*Area\) [Bottom]()
+
+```go
+func (area *Area) Bottom()
+```
+
+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\) [Clear]()
+### func \(\*Area\) [Clear]()
```go
func (area *Area) Clear()
@@ -304,8 +349,107 @@ func (area *Area) Clear()
Clear clears the content of the Area.
+
+### func \(\*Area\) [ClearLinesDown]()
+
+```go
+func (area *Area) ClearLinesDown(n int)
+```
+
+ClearLinesDown clears n lines downwards from the current position and moves the cursor.
+
+
+### func \(\*Area\) [ClearLinesUp]()
+
+```go
+func (area *Area) ClearLinesUp(n int)
+```
+
+ClearLinesUp clears n lines upwards from the current position and moves the cursor.
+
+
+### func \(\*Area\) [Down]()
+
+```go
+func (area *Area) Down(n int)
+```
+
+Down moves the cursor of the area down one line.
+
+
+### func \(\*Area\) [DownAndClear]()
+
+```go
+func (area *Area) DownAndClear(n int)
+```
+
+DownAndClear moves the cursor down by n lines, then clears the line.
+
+
+### func \(\*Area\) [Move]()
+
+```go
+func (area *Area) Move(x, y int)
+```
+
+Move moves the cursor relative by x and y.
+
+
+### func \(\*Area\) [StartOfLine]()
+
+```go
+func (area *Area) StartOfLine()
+```
+
+StartOfLine moves the cursor to the start of the current line.
+
+
+### func \(\*Area\) [StartOfLineDown]()
+
+```go
+func (area *Area) StartOfLineDown(n int)
+```
+
+StartOfLineDown moves the cursor down by n lines, then moves to cursor to the start of the line.
+
+
+### func \(\*Area\) [StartOfLineUp]()
+
+```go
+func (area *Area) StartOfLineUp(n int)
+```
+
+StartOfLineUp moves the cursor up by n lines, then moves to cursor to the start of the line.
+
+
+### func \(\*Area\) [Top]()
+
+```go
+func (area *Area) Top()
+```
+
+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\) [Up]()
+
+```go
+func (area *Area) Up(n int)
+```
+
+Up moves the cursor of the area up one line.
+
+
+### func \(\*Area\) [UpAndClear]()
+
+```go
+func (area *Area) UpAndClear(n int)
+```
+
+UpAndClear moves the cursor up by n lines, then clears the line.
+
-### func \(\*Area\) [Update]()
+### func \(\*Area\) [Update]()
```go
func (area *Area) Update(content string)
@@ -314,16 +458,126 @@ func (area *Area) Update(content string)
Update overwrites the content of the Area.
-### func \(Area\) [WithWriter]()
+### func \(\*Area\) [WithWriter]()
+
+```go
+func (area *Area) WithWriter(writer Writer) *Area
+```
+
+WithWriter sets the custom writer
+
+
+## type [Cursor]()
+
+Cursor displays content which can be updated on the fly. You can use this to create live output, charts, dropdowns, etc.
+
+```go
+type Cursor struct {
+ // contains filtered or unexported fields
+}
+```
+
+
+### func [NewCursor]()
+
+```go
+func NewCursor() *Cursor
+```
+
+
+
+
+### func \(\*Cursor\) [Clear]()
```go
-func (area Area) WithWriter(writer Writer) Area
+func (c *Cursor) Clear()
```
-WithWriter sets a custom writer for the Area.
+Clear clears the current position and moves the cursor to the left.
+
+
+### func \(\*Cursor\) [ClearLine]()
+
+```go
+func (c *Cursor) ClearLine()
+```
+
+ClearLine clears the current line and moves the cursor to it's start position.
+
+
+### func \(\*Cursor\) [Down]()
+
+```go
+func (c *Cursor) Down(n int)
+```
+
+Down moves the cursor n lines down relative to the current position.
+
+
+### func \(\*Cursor\) [Hide]()
+
+```go
+func (c *Cursor) Hide()
+```
+
+Hide the cursor. Don't forget to show the cursor at least at the end of your application with Show. Otherwise the user might have a terminal with a permanently hidden cursor, until they reopen the terminal.
+
+
+### func \(\*Cursor\) [HorizontalAbsolute]()
+
+```go
+func (c *Cursor) HorizontalAbsolute(n int)
+```
+
+HorizontalAbsolute moves the cursor to n horizontally. The position n is absolute to the start of the line.
+
+
+### func \(\*Cursor\) [Left]()
+
+```go
+func (c *Cursor) Left(n int)
+```
+
+Left moves the cursor n characters to the left relative to the current position.
+
+
+### func \(\*Cursor\) [Right]()
+
+```go
+func (c *Cursor) Right(n int)
+```
+
+Right moves the cursor n characters to the right relative to the current position.
+
+
+### func \(\*Cursor\) [Show]()
+
+```go
+func (c *Cursor) Show()
+```
+
+Show the cursor if it was hidden previously. Don't forget to show the cursor at least at the end of your application. Otherwise the user might have a terminal with a permanently hidden cursor, until they reopen the terminal.
+
+
+### func \(\*Cursor\) [Up]()
+
+```go
+func (c *Cursor) Up(n int)
+```
+
+Up moves the cursor n lines up relative to the current position.
+
+
+### func \(\*Cursor\) [WithWriter]()
+
+```go
+func (c *Cursor) WithWriter(w Writer) *Cursor
+```
+
+WithWriter allows for any arbitrary Writer to be used for cursor movement abstracted.
-## type [Writer]()
+## type [Writer]()
Writer is an expanded io.Writer interface with a file descriptor.
@@ -336,7 +590,6 @@ type Writer interface {
Generated by [gomarkdoc]()
-
---
diff --git a/_examples/area/movement/main.go b/_examples/area/movement/main.go
new file mode 100644
index 0000000..f0aaf77
--- /dev/null
+++ b/_examples/area/movement/main.go
@@ -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")
+}
diff --git a/_examples/area/multiline/main.go b/_examples/area/multiline/main.go
new file mode 100644
index 0000000..5134d5a
--- /dev/null
+++ b/_examples/area/multiline/main.go
@@ -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",
+}
diff --git a/_examples/area/singleline/main.go b/_examples/area/singleline/main.go
new file mode 100644
index 0000000..9c4e062
--- /dev/null
+++ b/_examples/area/singleline/main.go
@@ -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")
+}
diff --git a/area.go b/area.go
index b090576..5b26b5d 100644
--- a/area.go
+++ b/area.go
@@ -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)
+ }
}
diff --git a/area_other.go b/area_other.go
new file mode 100644
index 0000000..b92390d
--- /dev/null
+++ b/area_other.go
@@ -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)
+}
diff --git a/area_windows.go b/area_windows.go
new file mode 100644
index 0000000..de7dd29
--- /dev/null
+++ b/area_windows.go
@@ -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))
+ }
+}
diff --git a/cursor.go b/cursor.go
index e59e968..89a3efc 100644
--- a/cursor.go
+++ b/cursor.go
@@ -1,70 +1,26 @@
-//go:build !windows
-// +build !windows
-
package cursor
import (
- "fmt"
"os"
)
-var target Writer = os.Stdout
-
-// SetTarget allows for any arbitrary io.Writer to be used
-// for cursor movement (will not work on Windows).
-func SetTarget(w Writer) {
- target = w
+// Cursor displays content which can be updated on the fly.
+// You can use this to create live output, charts, dropdowns, etc.
+type Cursor struct {
+ writer Writer
}
-// Up moves the cursor n lines up relative to the current position.
-func Up(n int) {
- fmt.Fprintf(target, "\x1b[%dA", n)
- height += n
+// NewCursor creates a new Cursor instance writing to os.Stdout.
+func NewCursor() *Cursor {
+ return &Cursor{writer: os.Stdout}
}
-// Down moves the cursor n lines down relative to the current position.
-func Down(n int) {
- fmt.Fprintf(target, "\x1b[%dB", n)
-
- if height-n <= 0 {
- height = 0
- } else {
- height -= n
+// WithWriter allows for any arbitrary Writer to be used
+// for cursor movement abstracted.
+func (c *Cursor) WithWriter(w Writer) *Cursor {
+ if w != nil {
+ c.writer = w
}
-}
-
-// Right moves the cursor n characters to the right relative to the current position.
-func Right(n int) {
- fmt.Fprintf(target, "\x1b[%dC", n)
-}
-
-// Left moves the cursor n characters to the left relative to the current position.
-func Left(n int) {
- fmt.Fprintf(target, "\x1b[%dD", n)
-}
-
-// HorizontalAbsolute moves the cursor to n horizontally.
-// The position n is absolute to the start of the line.
-func HorizontalAbsolute(n int) {
- n++ // Moves the line to the character after n
- fmt.Fprintf(target, "\x1b[%dG", n)
-}
-
-// Show the cursor if it was hidden previously.
-// Don't forget to show the cursor at least at the end of your application.
-// Otherwise the user might have a terminal with a permanently hidden cursor, until they reopen the terminal.
-func Show() {
- fmt.Fprint(target, "\x1b[?25h")
-}
-
-// Hide the cursor.
-// Don't forget to show the cursor at least at the end of your application with Show.
-// Otherwise the user might have a terminal with a permanently hidden cursor, until they reopen the terminal.
-func Hide() {
- fmt.Fprintf(target, "\x1b[?25l")
-}
-// ClearLine clears the current line and moves the cursor to it's start position.
-func ClearLine() {
- fmt.Fprintf(target, "\x1b[2K")
+ return c
}
diff --git a/cursor_other.go b/cursor_other.go
new file mode 100644
index 0000000..7c18557
--- /dev/null
+++ b/cursor_other.go
@@ -0,0 +1,67 @@
+//go:build !windows
+// +build !windows
+
+package cursor
+
+import (
+ "fmt"
+)
+
+// Up moves the cursor n lines up relative to the current position.
+func (c *Cursor) Up(n int) {
+ if n > 0 {
+ fmt.Fprintf(c.writer, "\x1b[%dA", n)
+ }
+}
+
+// Down moves the cursor n lines down relative to the current position.
+func (c *Cursor) Down(n int) {
+ if n > 0 {
+ fmt.Fprintf(c.writer, "\x1b[%dB", n)
+ }
+}
+
+// Right moves the cursor n characters to the right relative to the current position.
+func (c *Cursor) Right(n int) {
+ if n > 0 {
+ fmt.Fprintf(c.writer, "\x1b[%dC", n)
+ }
+}
+
+// Left moves the cursor n characters to the left relative to the current position.
+func (c *Cursor) Left(n int) {
+ if n > 0 {
+ fmt.Fprintf(c.writer, "\x1b[%dD", n)
+ }
+}
+
+// HorizontalAbsolute moves the cursor to n horizontally.
+// The position n is absolute to the start of the line.
+func (c *Cursor) HorizontalAbsolute(n int) {
+ n++ // Moves the line to the character after n
+ fmt.Fprintf(c.writer, "\x1b[%dG", n)
+}
+
+// Show the cursor if it was hidden previously.
+// Don't forget to show the cursor at least at the end of your application.
+// Otherwise the user might have a terminal with a permanently hidden cursor, until they reopen the terminal.
+func (c *Cursor) Show() {
+ fmt.Fprint(c.writer, "\x1b[?25h")
+}
+
+// Hide the cursor.
+// Don't forget to show the cursor at least at the end of your application with Show.
+// Otherwise the user might have a terminal with a permanently hidden cursor, until they reopen the terminal.
+func (c *Cursor) Hide() {
+ fmt.Fprintf(c.writer, "\x1b[?25l")
+}
+
+// ClearLine clears the current line and moves the cursor to it's start position.
+func (c *Cursor) ClearLine() {
+ fmt.Fprintf(c.writer, "\x1b[2K")
+}
+
+// Clear clears the current position and moves the cursor to the left.
+func (c *Cursor) Clear() {
+ fmt.Fprintf(c.writer, "\x1b[K")
+}
diff --git a/cursor_test.go b/cursor_test.go
index 23fdeed..3b88a87 100644
--- a/cursor_test.go
+++ b/cursor_test.go
@@ -9,24 +9,23 @@ func TestHeightChanges(t *testing.T) {
for i := 0; i < 4; i++ {
fmt.Println()
}
-
Up(3)
- if height != 3 {
- t.Errorf("height should be 3 but is %d", height)
+ if autoheight != 3 {
+ t.Errorf("height should be 3 but is %d", autoheight)
}
Down(3)
- if height != 0 {
- t.Errorf("height should be 0 but is %d", height)
+ if autoheight != 0 {
+ t.Errorf("height should be 0 but is %d", autoheight)
}
}
func TestHeightCannotBeNegative(t *testing.T) {
Down(10)
- if height < 0 {
- t.Errorf("height is negative: %d", height)
+ if autoheight < 0 {
+ t.Errorf("height is negative: %d", autoheight)
}
}
diff --git a/cursor_windows.go b/cursor_windows.go
index 9a6173b..ebe2bc5 100644
--- a/cursor_windows.go
+++ b/cursor_windows.go
@@ -1,46 +1,35 @@
+//go:build windows
+// +build windows
+
package cursor
import (
- "os"
"syscall"
"unsafe"
)
-var target Writer = os.Stdout
-
-// SetTarget allows for any arbitrary Writer to be used
-func SetTarget(w Writer) {
- target = w
-}
-
// Up moves the cursor n lines up relative to the current position.
-func Up(n int) {
- move(0, -n)
- height += n
+func (c *Cursor) Up(n int) {
+ c.move(0, -n)
}
// Down moves the cursor n lines down relative to the current position.
-func Down(n int) {
- move(0, n)
- if height-n <= 0 {
- height = 0
- } else {
- height -= n
- }
+func (c *Cursor) Down(n int) {
+ c.move(0, n)
}
// Right moves the cursor n characters to the right relative to the current position.
-func Right(n int) {
- move(n, 0)
+func (c *Cursor) Right(n int) {
+ c.move(n, 0)
}
// Left moves the cursor n characters to the left relative to the current position.
-func Left(n int) {
- move(-n, 0)
+func (c *Cursor) Left(n int) {
+ c.move(-n, 0)
}
-func move(x int, y int) {
- handle := syscall.Handle(target.Fd())
+func (c *Cursor) move(x int, y int) {
+ handle := syscall.Handle(c.writer.Fd())
var csbi consoleScreenBufferInfo
_, _, _ = procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
@@ -54,8 +43,8 @@ func move(x int, y int) {
// HorizontalAbsolute moves the cursor to n horizontally.
// The position n is absolute to the start of the line.
-func HorizontalAbsolute(n int) {
- handle := syscall.Handle(target.Fd())
+func (c *Cursor) HorizontalAbsolute(n int) {
+ handle := syscall.Handle(c.writer.Fd())
var csbi consoleScreenBufferInfo
_, _, _ = procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
@@ -74,8 +63,8 @@ func HorizontalAbsolute(n int) {
// Show the cursor if it was hidden previously.
// Don't forget to show the cursor at least at the end of your application.
// Otherwise the user might have a terminal with a permanently hidden cursor, until he reopens the terminal.
-func Show() {
- handle := syscall.Handle(target.Fd())
+func (c *Cursor) Show() {
+ handle := syscall.Handle(c.writer.Fd())
var cci consoleCursorInfo
_, _, _ = procGetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci)))
@@ -87,8 +76,8 @@ func Show() {
// Hide the cursor.
// Don't forget to show the cursor at least at the end of your application with Show.
// Otherwise the user might have a terminal with a permanently hidden cursor, until he reopens the terminal.
-func Hide() {
- handle := syscall.Handle(target.Fd())
+func (c *Cursor) Hide() {
+ handle := syscall.Handle(c.writer.Fd())
var cci consoleCursorInfo
_, _, _ = procGetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci)))
@@ -98,8 +87,8 @@ func Hide() {
}
// ClearLine clears the current line and moves the cursor to its start position.
-func ClearLine() {
- handle := syscall.Handle(target.Fd())
+func (c *Cursor) ClearLine() {
+ handle := syscall.Handle(c.writer.Fd())
var csbi consoleScreenBufferInfo
_, _, _ = procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
@@ -110,3 +99,20 @@ func ClearLine() {
x = csbi.size.x
_, _, _ = procFillConsoleOutputCharacter.Call(uintptr(handle), uintptr(' '), uintptr(x), uintptr(*(*int32)(unsafe.Pointer(&cursor))), uintptr(unsafe.Pointer(&w)))
}
+
+// Clear clears the current position and moves the cursor to the left.
+func (c *Cursor) Clear() {
+ handle := syscall.Handle(c.writer.Fd())
+
+ var csbi consoleScreenBufferInfo
+ _, _, _ = procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
+
+ var w uint32
+ cursor := csbi.cursorPosition
+ _, _, _ = procFillConsoleOutputCharacter.Call(uintptr(handle), uintptr(' '), uintptr(1), uintptr(*(*int32)(unsafe.Pointer(&cursor))), uintptr(unsafe.Pointer(&w)))
+
+ if cursor.x > 0 {
+ cursor.x--
+ }
+ _, _, _ = procSetConsoleCursorPosition.Call(uintptr(handle), uintptr(*(*int32)(unsafe.Pointer(&cursor))))
+}
diff --git a/go.work b/go.work
new file mode 100644
index 0000000..c71ff3b
--- /dev/null
+++ b/go.work
@@ -0,0 +1,10 @@
+go 1.18
+
+use .
+
+// replace git.neotel.at/go/c5rest => /home/rl/work/c5rest
+// replace git.neotel.at/go/c5db => /home/rl/work/c5db
+
+// replace github.com/pterm/pterm => H:/work/github.com/pterm/pterm
+
+// replace atomicgo.dev/cursor => H:/github.com/atomicgo.dev/cursor
diff --git a/utils.go b/utils.go
index 6bf619b..4b75f09 100644
--- a/utils.go
+++ b/utils.go
@@ -1,17 +1,92 @@
package cursor
-import "io"
+import (
+ "io"
+ "os"
+)
-var height int
+//
+// Helpers for global cursor handling on os.Stdout
+//
+
+var autoheight int
+var cursor = &Cursor{writer: os.Stdout}
+
+// Writer is an expanded io.Writer interface with a file descriptor.
+type Writer interface {
+ io.Writer
+ Fd() uintptr
+}
+
+// SetTarget sets to output target of the default curser to the
+// provided cursor.Writer (wrapping io.Writer).
+func SetTarget(w Writer) {
+ cursor = cursor.WithWriter(w)
+}
+
+// Up moves the cursor n lines up relative to the current position.
+func Up(n int) {
+ cursor.Up(n)
+ autoheight += n
+}
+
+// Down moves the cursor n lines down relative to the current position.
+func Down(n int) {
+ cursor.Down(n)
+
+ if autoheight > 0 {
+ autoheight -= n
+ }
+}
+
+// Right moves the cursor n characters to the right relative to the current position.
+func Right(n int) {
+ cursor.Right(n)
+}
+
+// Left moves the cursor n characters to the left relative to the current position.
+func Left(n int) {
+ cursor.Left(n)
+}
+
+// HorizontalAbsolute moves the cursor to n horizontally.
+// The position n is absolute to the start of the line.
+func HorizontalAbsolute(n int) {
+ cursor.HorizontalAbsolute(n)
+}
+
+// Show the cursor if it was hidden previously.
+// Don't forget to show the cursor at least at the end of your application.
+// Otherwise the user might have a terminal with a permanently hidden cursor, until they reopen the terminal.
+func Show() {
+ cursor.Show()
+}
+
+// Hide the cursor.
+// Don't forget to show the cursor at least at the end of your application with Show.
+// Otherwise the user might have a terminal with a permanently hidden cursor, until they reopen the terminal.
+func Hide() {
+ cursor.Hide()
+}
+
+// ClearLine clears the current line and moves the cursor to it's start position.
+func ClearLine() {
+ cursor.ClearLine()
+}
+
+// Clear clears the current position and moves the cursor to the left.
+func Clear() {
+ cursor.Clear()
+}
// 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 Bottom() {
- if height > 0 {
- Down(height)
+ if autoheight > 0 {
+ Down(autoheight)
StartOfLine()
- height = 0
+ autoheight = 0
}
}
@@ -74,9 +149,3 @@ func ClearLinesDown(n int) {
DownAndClear(1)
}
}
-
-// Writer is an expanded io.Writer interface with a file descriptor.
-type Writer interface {
- io.Writer
- Fd() uintptr
-}