Skip to content

Commit ae4f99b

Browse files
refactor: internalize terminal logic
Co-authored-by: ccoVeille <[email protected]>
1 parent ae774fa commit ae4f99b

File tree

8 files changed

+87
-72
lines changed

8 files changed

+87
-72
lines changed

ansi_windows.go

Lines changed: 4 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,40 +2,10 @@
22

33
package godump
44

5-
import (
6-
"os"
7-
"syscall"
8-
"unsafe"
9-
)
5+
import "github.com/goforj/godump/internal/windowsansi"
106

11-
// init activates ANSI support on Windows terminals by setting the
12-
// ENABLE_VIRTUAL_TERMINAL_PROCESSING flag.
13-
// It fails silently if the output is not a console.
7+
// init activates ANSI support on Windows terminals by calling the Enable
8+
// function from the internal winansi package.
149
func init() {
15-
const enableVirtualTerminalProcessing = 0x0004
16-
17-
// Load kernel32.dll and the necessary procedures dynamically.
18-
// This avoids a hard dependency and allows the program to run on non-Windows
19-
// systems, although this file is guarded by a build tag.
20-
kernel32 := syscall.NewLazyDLL("kernel32.dll")
21-
procGetConsoleMode := kernel32.NewProc("GetConsoleMode")
22-
procSetConsoleMode := kernel32.NewProc("SetConsoleMode")
23-
24-
// Get the handle for standard output.
25-
handle := syscall.Handle(os.Stdout.Fd())
26-
var mode uint32
27-
28-
// GetConsoleMode fails if not in a real console.
29-
ret, _, _ := procGetConsoleMode.Call(uintptr(handle), uintptr(unsafe.Pointer(&mode)))
30-
if ret == 0 {
31-
return
32-
}
33-
34-
// Add the virtual terminal processing flag to the current mode.
35-
newMode := mode | enableVirtualTerminalProcessing
36-
37-
// Try to set the new console mode.
38-
// If this call fails, we also silently continue. The result will be
39-
// that colors are not rendered, which is an acceptable fallback.
40-
procSetConsoleMode.Call(uintptr(handle), uintptr(newMode))
10+
windowsansi.Enable()
4111
}

godump.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import (
1212
"text/tabwriter"
1313
"unicode/utf8"
1414
"unsafe"
15+
16+
"github.com/goforj/godump/internal/terminal"
1517
)
1618

1719
const (
@@ -619,6 +621,6 @@ func detectColor() bool {
619621
if os.Getenv("FORCE_COLOR") != "" {
620622
return true
621623
}
622-
623-
return isTerminal(os.Stdout)
624+
625+
return terminal.IsTerminal(os.Stdout)
624626
}

internal/terminal/terminal.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package terminal
2+
3+
import "testing"
4+
5+
// IsTestEnv is a variable that holds the testing.Testing function.
6+
// It's used to determine if the code is running in a test environment,
7+
// and it can be mocked in tests to control behavior.
8+
var IsTestEnv = testing.Testing
Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,16 @@
11
//go:build !windows
22

3-
package godump
3+
package terminal
44

55
import (
66
"os"
7-
"testing"
87
)
98

10-
var isTestEnv = testing.Testing
11-
12-
// isTerminal checks if the given file is a terminal.
9+
// IsTerminal checks if the given file is a terminal.
1310
// Uses ModeCharDevice on Unix-like systems.
1411
// In test environments, it returns true unless explicitly overridden by environment variables.
15-
func isTerminal(f *os.File) bool {
16-
if isTestEnv() {
12+
func IsTerminal(f *os.File) bool {
13+
if IsTestEnv() {
1714
return true
1815
}
1916

@@ -22,4 +19,4 @@ func isTerminal(f *os.File) bool {
2219
return false
2320
}
2421
return (fileInfo.Mode() & os.ModeCharDevice) != 0
25-
}
22+
}
Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package godump
1+
package terminal
22

33
import (
44
"os"
@@ -8,18 +8,18 @@ import (
88
)
99

1010
func TestIsTerminal_Unix(t *testing.T) {
11-
// Mock isTestEnv to bypass the test environment check
12-
originalIsTestEnv := isTestEnv
13-
isTestEnv = func() bool { return false }
14-
defer func() { isTestEnv = originalIsTestEnv }()
11+
// Mock IsTestEnv to bypass the test environment check
12+
originalIsTestEnv := IsTestEnv
13+
IsTestEnv = func() bool { return false }
14+
defer func() { IsTestEnv = originalIsTestEnv }()
1515

1616
t.Run("should return false for a regular file", func(t *testing.T) {
1717
tmpFile, err := os.CreateTemp("", "test-is-terminal")
1818
assert.NoError(t, err)
1919
defer os.Remove(tmpFile.Name())
2020
defer tmpFile.Close()
2121

22-
assert.False(t, isTerminal(tmpFile))
22+
assert.False(t, IsTerminal(tmpFile))
2323
})
2424

2525
t.Run("should return false when stat fails", func(t *testing.T) {
@@ -28,14 +28,14 @@ func TestIsTerminal_Unix(t *testing.T) {
2828
tmpFile.Close()
2929
os.Remove(tmpFile.Name())
3030

31-
assert.False(t, isTerminal(tmpFile))
31+
assert.False(t, IsTerminal(tmpFile))
3232
})
3333

3434
t.Run("should return true when in test environment", func(t *testing.T) {
35-
isTestEnv = func() bool { return true }
35+
IsTestEnv = func() bool { return true }
3636
defer func() {
37-
isTestEnv = originalIsTestEnv
37+
IsTestEnv = originalIsTestEnv
3838
}()
39-
assert.True(t, isTerminal(os.Stdout))
39+
assert.True(t, IsTerminal(os.Stdout))
4040
})
4141
}
Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,17 @@
11
//go:build windows
22

3-
package godump
3+
package terminal
44

55
import (
66
"os"
77
"syscall"
8-
"testing"
98
)
109

11-
var isTestEnv = testing.Testing
12-
13-
// isTerminal checks if the given file is a terminal.
10+
// IsTerminal checks if the given file is a terminal.
1411
// Uses GetConsoleMode on Windows.
1512
// In test environments, it returns true unless explicitly overridden by environment variables.
16-
func isTerminal(f *os.File) bool {
17-
if isTestEnv() {
13+
func IsTerminal(f *os.File) bool {
14+
if IsTestEnv() {
1815
return true
1916
}
2017

@@ -23,4 +20,4 @@ func isTerminal(f *os.File) bool {
2320
// Fails for redirected/piped output
2421
err := syscall.GetConsoleMode(syscall.Handle(f.Fd()), &mode)
2522
return err == nil
26-
}
23+
}
Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//go:build windows
22

3-
package godump
3+
package terminal
44

55
import (
66
"os"
@@ -10,10 +10,10 @@ import (
1010
)
1111

1212
func TestIsTerminal_Windows(t *testing.T) {
13-
// Mock isTestEnv to bypass the test environment check
14-
originalIsTestEnv := isTestEnv
15-
isTestEnv = func() bool { return false }
16-
defer func() { isTestEnv = originalIsTestEnv }()
13+
// Mock IsTestEnv to bypass the test environment check
14+
originalIsTestEnv := IsTestEnv
15+
IsTestEnv = func() bool { return false }
16+
defer func() { IsTestEnv = originalIsTestEnv }()
1717

1818
t.Run("should return false for a regular file", func(t *testing.T) {
1919
tmpFile, err := os.CreateTemp("", "test-is-terminal-windows")
@@ -22,13 +22,13 @@ func TestIsTerminal_Windows(t *testing.T) {
2222
defer tmpFile.Close()
2323

2424
// On Windows, GetConsoleMode should fail for a regular file, so isTerminal returns false.
25-
assert.False(t, isTerminal(tmpFile))
25+
assert.False(t, IsTerminal(tmpFile))
2626
})
2727

2828
t.Run("should return true when in test environment", func(t *testing.T) {
29-
isTestEnv = func() bool { return true }
30-
defer func() { isTestEnv = originalIsTestEnv }()
31-
32-
assert.True(t, isTerminal(os.Stdout))
29+
IsTestEnv = func() bool { return true }
30+
defer func() { IsTestEnv = originalIsTestEnv }()
31+
32+
assert.True(t, IsTerminal(os.Stdout))
3333
})
3434
}

internal/windowsansi/winansi.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
//go:build windows
2+
3+
package windowsansi
4+
5+
import (
6+
"os"
7+
"syscall"
8+
"unsafe"
9+
)
10+
11+
// Enable activates ANSI support on Windows terminals by setting the
12+
// ENABLE_VIRTUAL_TERMINAL_PROCESSING flag.
13+
// It fails silently if the output is not a console.
14+
func Enable() {
15+
const enableVirtualTerminalProcessing = 0x0004
16+
17+
// Load kernel32.dll and the necessary procedures dynamically.
18+
// This avoids a hard dependency and allows the program to run on non-Windows
19+
// systems, although this file is guarded by a build tag.
20+
kernel32 := syscall.NewLazyDLL("kernel32.dll")
21+
procGetConsoleMode := kernel32.NewProc("GetConsoleMode")
22+
procSetConsoleMode := kernel32.NewProc("SetConsoleMode")
23+
24+
// Get the handle for standard output.
25+
handle := syscall.Handle(os.Stdout.Fd())
26+
var mode uint32
27+
28+
// GetConsoleMode fails if not in a real console.
29+
ret, _, _ := procGetConsoleMode.Call(uintptr(handle), uintptr(unsafe.Pointer(&mode)))
30+
if ret == 0 {
31+
return
32+
}
33+
34+
// Add the virtual terminal processing flag to the current mode.
35+
newMode := mode | enableVirtualTerminalProcessing
36+
37+
// Try to set the new console mode.
38+
// If this call fails, we also silently continue. The result will be
39+
// that colors are not rendered, which is an acceptable fallback.
40+
procSetConsoleMode.Call(uintptr(handle), uintptr(newMode))
41+
}

0 commit comments

Comments
 (0)