diff --git a/.github/settings.yml b/.github/settings.yml index 0ed80b6..1ee791d 100644 --- a/.github/settings.yml +++ b/.github/settings.yml @@ -4,10 +4,10 @@ repository: # See https://developer.github.com/v3/repos/#edit for all available settings. # A short description of the repository that will show up on GitHub - description: 🔹 Golang module + description: 🔹 Golang module to move the terminal cursor in any direction on every operating system. # A comma-separated list of topics to set on the repository - topics: atomicgo, go, golang, golang-library + topics: atomicgo, go, golang, golang-library, terminal, cursor, tui # Either `true` to make the repository private, or `false` to make it public. # private: false diff --git a/.gitignore b/.gitignore index e30b64b..99e2741 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ gen ### macOS # General .DS_Store +experimenting diff --git a/README.md b/README.md index 3be0dfc..ed70d2a 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,25 @@ -
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
@@ -33,7 +33,7 @@
Get The Module
|
-Documentation
+Documentation
|
Contributing
|
@@ -48,33 +48,140 @@
## Description
-Package template is used to generate new AtomicGo repositories.
+Package cursor contains cross-platform methods to move the terminal cursor in
+different directions. This package can be used to create interactive CLI tools
+and games, live charts, algorithm visualizations and other updatable output of
+any kind.
-Write the description of the module here. You can use **markdown**! This
-description should clearly explain what the package does.
-
-Example description: https://golang.org/src/encoding/gob/doc.go
+Special thanks to github.com/k0kubun/go-ansi which this project is based on.
## Install
```console
# Execute this command inside your project
-go get -u github.com/atomicgo/template
+go get -u github.com/atomicgo/cursor
```
```go
// Add this to your imports
-import "github.com/atomicgo/template"
+import "github.com/atomicgo/cursor"
```
## Usage
-#### func HelloWorld
+#### func Bottom
+
+```go
+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 ClearLine
+
+```go
+func ClearLine()
+```
+ClearLine clears the current line and moves the cursor to it's start position.
+
+#### func Down
+
+```go
+func Down(n int)
+```
+Down moves the cursor n lines down relative to the current position.
+
+#### func DownAndClear
+
+```go
+func DownAndClear(n int)
+```
+DownAndClear moves the cursor down by n lines, then clears the line.
+
+#### func Hide
+
+```go
+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 he reopens the terminal.
+
+#### func HorizontalAbsolute
+
+```go
+func HorizontalAbsolute(n int)
+```
+HorizontalAbsolute moves the cursor to n horizontally. The position n is
+absolute to the start of the line.
+
+#### func Left
+
+```go
+func Left(n int)
+```
+Left moves the cursor n characters to the left relative to the current position.
+
+#### func Move
+
+```go
+func Move(x, y int)
+```
+Move moves the cursor relative by x and y.
+
+#### func Right
+
+```go
+func Right(n int)
+```
+Right moves the cursor n characters to the right relative to the current
+position.
+
+#### func Show
+
+```go
+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 he reopens the terminal.
+
+#### func StartOfLine
+
+```go
+func StartOfLine()
+```
+StartOfLine moves the cursor to the start of the current line.
+
+#### func StartOfLineDown
+
+```go
+func StartOfLineDown(n int)
+```
+StartOfLineDown moves the cursor down by n lines, then moves to cursor to the
+start of the line.
+
+#### func StartOfLineUp
+
+```go
+func StartOfLineUp(n int)
+```
+StartOfLineUp moves the cursor up by n lines, then moves to cursor to the start
+of the line.
+
+#### func Up
+
+```go
+func Up(n int)
+```
+Up moves the cursor n lines up relative to the current position.
+
+#### func UpAndClear
```go
-func HelloWorld() string
+func UpAndClear(n int)
```
-HelloWorld returns `Hello, World!`.
+UpAndClear moves the cursor up by n lines, then clears the line.
---
diff --git a/cursor.go b/cursor.go
new file mode 100644
index 0000000..ef66ecf
--- /dev/null
+++ b/cursor.go
@@ -0,0 +1,59 @@
+// +build !windows
+
+package cursor
+
+import (
+ "fmt"
+)
+
+// Up moves the cursor n lines up relative to the current position.
+func Up(n int) {
+ fmt.Printf("\x1b[%dA", n)
+ height += n
+}
+
+// Down moves the cursor n lines down relative to the current position.
+func Down(n int) {
+ fmt.Printf("\x1b[%dB", n)
+ if height-n < 0 {
+ height = 0
+ } else {
+ height -= n
+ }
+}
+
+// Right moves the cursor n characters to the right relative to the current position.
+func Right(n int) {
+ fmt.Printf("\x1b[%dC", n)
+}
+
+// Left moves the cursor n characters to the left relative to the current position.
+func Left(n int) {
+ fmt.Printf("\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 += 1 // Moves the line to the character after n
+ fmt.Printf("\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 he reopens the terminal.
+func Show() {
+ fmt.Print("\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 he reopens the terminal.
+func Hide() {
+ fmt.Print("\x1b[?25l")
+}
+
+// ClearLine clears the current line and moves the cursor to it's start position.
+func ClearLine() {
+ fmt.Print("\x1b[2K")
+}
diff --git a/cursor_test.go b/cursor_test.go
new file mode 100644
index 0000000..b74902c
--- /dev/null
+++ b/cursor_test.go
@@ -0,0 +1,27 @@
+package cursor
+
+import (
+ "fmt"
+ "testing"
+)
+
+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)
+ }
+ Down(3)
+ if height != 0 {
+ t.Errorf("height should be 0 but is %d", height)
+ }
+}
+
+func TestHeightCannotBeNegative(t *testing.T) {
+ Down(10)
+ if height < 0 {
+ t.Errorf("height is negative: %d", height)
+ }
+}
diff --git a/cursor_windows.go b/cursor_windows.go
new file mode 100644
index 0000000..232882b
--- /dev/null
+++ b/cursor_windows.go
@@ -0,0 +1,105 @@
+package cursor
+
+import (
+ "os"
+ "syscall"
+ "unsafe"
+)
+
+// Up moves the cursor n lines up relative to the current position.
+func Up(n int) {
+ move(0, -n)
+ height += 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
+ }
+}
+
+// Right moves the cursor n characters to the right relative to the current position.
+func Right(n int) {
+ 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 move(x int, y int) {
+ handle := syscall.Handle(os.Stdout.Fd())
+
+ var csbi consoleScreenBufferInfo
+ _, _, _ = procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
+
+ var cursor coord
+ cursor.x = csbi.cursorPosition.x + short(x)
+ cursor.y = csbi.cursorPosition.y + short(y)
+
+ _, _, _ = procSetConsoleCursorPosition.Call(uintptr(handle), uintptr(*(*int32)(unsafe.Pointer(&cursor))))
+}
+
+// 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(os.Stdout.Fd())
+
+ var csbi consoleScreenBufferInfo
+ _, _, _ = procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
+
+ var cursor coord
+ cursor.x = short(n)
+ cursor.y = csbi.cursorPosition.y
+
+ if csbi.size.x < cursor.x {
+ cursor.x = csbi.size.x
+ }
+
+ _, _, _ = procSetConsoleCursorPosition.Call(uintptr(handle), uintptr(*(*int32)(unsafe.Pointer(&cursor))))
+}
+
+// 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(os.Stdout.Fd())
+
+ var cci consoleCursorInfo
+ _, _, _ = procGetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci)))
+ cci.visible = 1
+
+ _, _, _ = procSetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci)))
+}
+
+// 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(os.Stdout.Fd())
+
+ var cci consoleCursorInfo
+ _, _, _ = procGetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci)))
+ cci.visible = 0
+
+ _, _, _ = procSetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci)))
+}
+
+// ClearLine clears the current line and moves the cursor to it's start position.
+func ClearLine() {
+ handle := syscall.Handle(os.Stdout.Fd())
+
+ var csbi consoleScreenBufferInfo
+ _, _, _ = procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
+
+ var w uint32
+ var x short
+ cursor := csbi.cursorPosition
+ x = csbi.size.x
+ _, _, _ = procFillConsoleOutputCharacter.Call(uintptr(handle), uintptr(' '), uintptr(x), uintptr(*(*int32)(unsafe.Pointer(&cursor))), uintptr(unsafe.Pointer(&w)))
+}
diff --git a/doc.go b/doc.go
index 0731a16..444326d 100644
--- a/doc.go
+++ b/doc.go
@@ -1,9 +1,7 @@
/*
-Package template is used to generate new AtomicGo repositories.
+Package cursor contains cross-platform methods to move the terminal cursor in different directions.
+This package can be used to create interactive CLI tools and games, live charts, algorithm visualizations and other updatable output of any kind.
-Write the description of the module here. You can use **markdown**!
-This description should clearly explain what the package does.
-
-Example description: https://golang.org/src/encoding/gob/doc.go
+Special thanks to github.com/k0kubun/go-ansi which this project is based on.
*/
-package template
+package cursor
diff --git a/go.mod b/go.mod
index eb6e377..8d34575 100644
--- a/go.mod
+++ b/go.mod
@@ -1,3 +1,3 @@
-module github.com/atomicgo/template
+module github.com/atomicgo/cursor
go 1.15
diff --git a/syscall_windows.go b/syscall_windows.go
new file mode 100644
index 0000000..d4bcdf7
--- /dev/null
+++ b/syscall_windows.go
@@ -0,0 +1,43 @@
+package cursor
+
+import (
+ "syscall"
+)
+
+var (
+ kernel32 = syscall.NewLazyDLL("kernel32.dll")
+ procFillConsoleOutputCharacter = kernel32.NewProc("FillConsoleOutputCharacterW")
+ procGetConsoleCursorInfo = kernel32.NewProc("GetConsoleCursorInfo")
+ procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo")
+ procSetConsoleCursorInfo = kernel32.NewProc("SetConsoleCursorInfo")
+ procSetConsoleCursorPosition = kernel32.NewProc("SetConsoleCursorPosition")
+)
+
+type short int16
+type dword uint32
+type word uint16
+
+type coord struct {
+ x short
+ y short
+}
+
+type smallRect struct {
+ bottom short
+ left short
+ right short
+ top short
+}
+
+type consoleScreenBufferInfo struct {
+ size coord
+ cursorPosition coord
+ attributes word
+ window smallRect
+ maximumWindowSize coord
+}
+
+type consoleCursorInfo struct {
+ size dword
+ visible int32
+}
diff --git a/template.go b/template.go
deleted file mode 100644
index f74b2c0..0000000
--- a/template.go
+++ /dev/null
@@ -1,6 +0,0 @@
-package template
-
-// HelloWorld returns `Hello, World!`.
-func HelloWorld() string {
- return "Hello, World!"
-}
diff --git a/template_test.go b/template_test.go
deleted file mode 100644
index b2fd824..0000000
--- a/template_test.go
+++ /dev/null
@@ -1,9 +0,0 @@
-package template
-
-import "testing"
-
-func TestHelloWorld(t *testing.T) {
- if HelloWorld() != "Hello, World!" {
- t.Fatal("Not equal")
- }
-}
diff --git a/utils.go b/utils.go
new file mode 100644
index 0000000..85fd05a
--- /dev/null
+++ b/utils.go
@@ -0,0 +1,57 @@
+package cursor
+
+var height int
+
+// 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() {
+ Down(height)
+ StartOfLine()
+ height = 0
+}
+
+// StartOfLine moves the cursor to the start of the current line.
+func StartOfLine() {
+ HorizontalAbsolute(0)
+}
+
+// StartOfLineDown moves the cursor down by n lines, then moves to cursor to the start of the line.
+func StartOfLineDown(n int) {
+ Down(n)
+ StartOfLine()
+}
+
+// StartOfLineUp moves the cursor up by n lines, then moves to cursor to the start of the line.
+func StartOfLineUp(n int) {
+ Up(n)
+ StartOfLine()
+}
+
+// UpAndClear moves the cursor up by n lines, then clears the line.
+func UpAndClear(n int) {
+ Up(n)
+ ClearLine()
+}
+
+// DownAndClear moves the cursor down by n lines, then clears the line.
+func DownAndClear(n int) {
+ Down(n)
+ ClearLine()
+}
+
+// Move moves the cursor relative by x and y.
+func Move(x, y int) {
+ if x > 0 {
+ Right(x)
+ } else if x < 0 {
+ x *= -1
+ Left(x)
+ }
+
+ if y > 0 {
+ Up(y)
+ } else if y < 0 {
+ y *= -1
+ Down(y)
+ }
+}