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

AtomicGo | template

+

AtomicGo | cursor

- -Latest Release + +Latest Release - -Tests + +Tests - -Coverage + +Coverage - -Unit test count + +Unit test count - -Issues + +Issues @@ -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) + } +}