Skip to content

Commit 959cad3

Browse files
committed
Terminal utilities
1 parent e9ed7b8 commit 959cad3

5 files changed

Lines changed: 260 additions & 2 deletions

File tree

cmd/spinner/spinner.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"math"
6+
7+
"github.com/FileFormatInfo/fftools/internal"
8+
)
9+
10+
var (
11+
solid = [...]string{"\u2022", "\u23FA", "\u25CF", "\u2B24"}
12+
hollow = [...]string{"\u25E6", "\uFFEE", "\u25CB", "\u25EF", "\u2B58"}
13+
)
14+
15+
type Point struct {
16+
X int
17+
Y int
18+
}
19+
20+
func main() {
21+
for _, s := range solid {
22+
println("Solid spinner character:", s)
23+
}
24+
for _, h := range hollow {
25+
println("Hollow spinner character:", h)
26+
}
27+
oldState := internal.Init()
28+
defer internal.Deinit(oldState)
29+
width, height := internal.ScreenSize()
30+
println("Terminal size:", width, "x", height)
31+
32+
centerX := width / 2
33+
centerY := height / 2
34+
35+
numPoints := 16
36+
points := make([]Point, numPoints)
37+
radius := 3.0
38+
for i := 0; i < numPoints; i++ {
39+
angle := float64(i) * (360.0 / float64(numPoints)) * (3.14159 / 180.0)
40+
x := centerX + 2*int(math.Round(radius*math.Cos(angle)))
41+
y := centerY + int(math.Round(radius*math.Sin(angle)))
42+
points[i] = Point{X: x, Y: y}
43+
}
44+
45+
internal.ScreenClear()
46+
47+
for _, p := range points {
48+
internal.MoveTo(p.X, p.Y)
49+
//fmt.Print(solid[i%len(solid)])
50+
fmt.Print(solid[0])
51+
}
52+
internal.MoveTo(1, height)
53+
54+
}

cmd/wombat/wombat.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"time"
6+
7+
. "github.com/FileFormatInfo/fftools/internal"
8+
)
9+
10+
func main() {
11+
12+
oldState := Init()
13+
defer Deinit(oldState)
14+
15+
ScreenSave()
16+
w, h := ScreenSize()
17+
18+
xMid := float32(w) / 2
19+
yMid := float32(h) / 2
20+
21+
steps := float32(17)
22+
23+
xStep := float32(xMid) / float32(steps)
24+
yStep := float32(yMid) / float32(steps)
25+
26+
CursorSavePosition()
27+
CursorHide()
28+
ScreenClear()
29+
for loop := float32(0); loop < steps; loop++ {
30+
MoveTo(int(xMid+loop*xStep), int(yMid+loop*yStep))
31+
fmt.Printf("*")
32+
MoveTo(int(xMid+loop*xStep), int(yMid-loop*yStep))
33+
fmt.Printf("*")
34+
MoveTo(int(xMid-loop*xStep), int(yMid-loop*yStep))
35+
fmt.Printf("*")
36+
MoveTo(int(xMid-loop*xStep), int(yMid+loop*yStep))
37+
fmt.Printf("*")
38+
39+
time.Sleep(20 * time.Millisecond)
40+
ScreenClear()
41+
}
42+
43+
CursorPositionRestore()
44+
CursorShow()
45+
ScreenRestore()
46+
}

go.mod

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
module github.com/FileFormatInfo/fftools
22

3-
go 1.24
3+
go 1.24.0
4+
5+
toolchain go1.24.2
46

57
require (
68
github.com/anyascii/go v0.3.2
@@ -18,5 +20,6 @@ require (
1820
github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6 // indirect
1921
github.com/olekukonko/ll v0.0.8 // indirect
2022
github.com/rivo/uniseg v0.2.0 // indirect
21-
golang.org/x/sys v0.33.0 // indirect
23+
golang.org/x/sys v0.37.0 // indirect
24+
golang.org/x/term v0.36.0 // indirect
2225
)

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,9 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
2525
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
2626
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
2727
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
28+
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
29+
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
30+
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
31+
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
2832
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
2933
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=

internal/Terminal.go

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
package internal
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"strconv"
7+
"strings"
8+
9+
"golang.org/x/term"
10+
)
11+
12+
func Init() *term.State {
13+
oldState, err := term.MakeRaw(int(os.Stdin.Fd()))
14+
if err != nil {
15+
//?log
16+
return nil
17+
}
18+
return oldState
19+
}
20+
21+
func Deinit(oldState *term.State) {
22+
if oldState != nil {
23+
term.Restore(int(os.Stdin.Fd()), oldState)
24+
}
25+
}
26+
27+
func ClearLine() {
28+
fmt.Printf("\033[2K")
29+
}
30+
31+
func ClearLineEnd() {
32+
fmt.Printf("\033[0K")
33+
}
34+
35+
func ClearLineStart() {
36+
fmt.Printf("\033[1K")
37+
}
38+
39+
func CursorHide() {
40+
fmt.Printf("\033[?25l")
41+
}
42+
43+
func CursorPositionRestore() {
44+
fmt.Printf("\033[u")
45+
}
46+
47+
func CursorSavePosition() {
48+
fmt.Printf("\033[s")
49+
}
50+
51+
func CursorShow() {
52+
fmt.Printf("\033[?25h")
53+
}
54+
55+
func MoveTo(x, y int) {
56+
fmt.Printf("\033[%d;%df", y, x)
57+
}
58+
59+
// MoveDown moves the cursor to the beginning of n lines down.
60+
func MoveDown(n int) {
61+
fmt.Printf("\033[%dE", n)
62+
}
63+
64+
// MoveUp moves the cursor to the beginning of n lines up.
65+
func MoveUp(n int) {
66+
fmt.Printf("\033[%dF", n)
67+
}
68+
69+
func ScreenClear() {
70+
fmt.Printf("\033[2J")
71+
MoveTo(1, 1)
72+
}
73+
74+
func ScreenRestore() {
75+
fmt.Printf("\033[?47l")
76+
}
77+
78+
func ScreenSave() {
79+
fmt.Printf("\033[?47h")
80+
}
81+
82+
func ScreenSize() (w int, h int) {
83+
fmt.Printf("\033[18t") // Report Size
84+
// The response will be in the form ESC [ 8 ; height ; width t
85+
// read from stdin to get the response
86+
buf := ""
87+
err := error(nil)
88+
for {
89+
var charBuf [1]byte
90+
_, err = os.Stdin.Read(charBuf[:])
91+
if err != nil {
92+
break
93+
}
94+
b := charBuf[0]
95+
if b == 't' {
96+
break
97+
}
98+
99+
buf += string(b)
100+
}
101+
if err != nil || len(buf) < 6 || buf[0] != '\033' || buf[1] != '[' || buf[2] != '8' || buf[3] != ';' {
102+
// ?log?
103+
return 80, 25 // default size
104+
}
105+
// parse height and width
106+
hw := strings.Split(buf[4:], ";")
107+
if len(hw) != 2 {
108+
return 80, 25
109+
}
110+
w, err = strconv.Atoi(hw[1])
111+
if err != nil {
112+
return 80, 25
113+
}
114+
115+
h, err = strconv.Atoi(hw[0])
116+
if err != nil {
117+
return 80, 25
118+
}
119+
120+
return w, h
121+
}
122+
123+
/*
124+
ANSI codes for screen management
125+
Save the current screen: ESC[?47h
126+
This saves the content of the current screen buffer to an alternate buffer.
127+
Restore the saved screen: ESC[?47l
128+
This restores the content from the alternate buffer, effectively "capturing" and then "recapturing" the screen.
129+
Clear the entire screen: ESC[2J
130+
This code clears the screen content but does not save it to an alternate buffer.
131+
132+
smcup
133+
\E7 saves the cursor's position
134+
\E[?47h switches to the alternate screen
135+
rmcup
136+
\E[2J clears the screen (assumed to be the alternate screen)
137+
\E[?47l switches back to the normal screen
138+
\E8 restores the cursor's position.
139+
140+
The syntax is ESC[?1049h and ESC[?1049l -- here h activates the alternate buffer and l returns to normal mode. Other similar codes are 1047 and 1048. But apparently they are older and have less functionality.
141+
142+
143+
https://en.wikipedia.org/wiki/ANSI_escape_code
144+
https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797
145+
https://xtermjs.org/docs/api/vtfeatures/
146+
https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
147+
https://github.com/leaanthony/go-ansi-parser
148+
149+
Xterm allows the window title to be set by ESC ]0;this is the window title BEL
150+
151+
*/

0 commit comments

Comments
 (0)