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 -}