gitstack

grindlemire/go-tui code browser

3.3 KB Go 152 lines 2026-06-03 · d15bb9f raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
package debug

import (
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"time"
)

var (
	logFile *os.File
	mu      sync.Mutex

	overflowOnce      sync.Once
	overflowHighlight bool

	// Resolved once at package init from the DEBUG env var.
	allTopics bool            // DEBUG=1 or DEBUG=*
	anyTopics bool            // true when allTopics or len(topics) > 0
	topics    map[string]bool // DEBUG=keys,dispatch
)

func init() {
	val := strings.TrimSpace(os.Getenv("DEBUG"))
	if val == "" {
		return
	}
	if val == "1" || val == "*" {
		allTopics = true
		anyTopics = true
		return
	}
	topics = make(map[string]bool)
	for t := range strings.SplitSeq(val, ",") {
		t = strings.TrimSpace(t)
		if t != "" {
			topics[t] = true
		}
	}
	anyTopics = len(topics) > 0
}

// OverflowHighlight returns true if the TUI_DEBUG_OVERFLOW environment variable
// is set, indicating that containers with overflowing children should be
// highlighted with a bright red background.
func OverflowHighlight() bool {
	overflowOnce.Do(func() {
		overflowHighlight = os.Getenv("TUI_DEBUG_OVERFLOW") != ""
	})
	return overflowHighlight
}

// Init initializes debug logging to the specified file path.
// If path is empty, uses "debug.log" in the current directory.
func Init(path string) error {
	mu.Lock()
	defer mu.Unlock()
	return initLocked(path)
}

// initLocked does the actual init work. Caller must hold mu.
func initLocked(path string) error {
	if path == "" {
		path = "debug.log"
	}

	// Ensure directory exists
	dir := filepath.Dir(path)
	if dir != "" && dir != "." {
		if err := os.MkdirAll(dir, 0o755); err != nil {
			return fmt.Errorf("failed to create log directory: %w", err)
		}
	}

	f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
	if err != nil {
		return fmt.Errorf("failed to open debug log: %w", err)
	}

	logFile = f
	return nil
}

// Close closes the debug log file.
func Close() error {
	mu.Lock()
	defer mu.Unlock()

	if logFile != nil {
		err := logFile.Close()
		logFile = nil
		return err
	}
	return nil
}

// Log writes a message to the debug log with a timestamp.
// Enabled only when DEBUG=1 or DEBUG=*; specific topic values do not enable Log.
func Log(format string, args ...any) {
	if !allTopics {
		return
	}

	mu.Lock()
	defer mu.Unlock()

	if logFile == nil {
		if err := initLocked(""); err != nil {
			fmt.Fprintf(os.Stderr, "debug: failed to open log: %v\n", err)
			return
		}
	}

	timestamp := time.Now().Format("15:04:05.000")
	msg := fmt.Sprintf(format, args...)
	fmt.Fprintf(logFile, "[%s] %s\n", timestamp, msg)
	logFile.Sync()
}

// Logf is an alias for Log.
func Logf(format string, args ...any) {
	Log(format, args...)
}

// Topic writes a message to the debug log only if the given topic is enabled.
// Topics are enabled via the DEBUG env var: DEBUG=keys,dispatch enables those
// two topics. DEBUG=1 or DEBUG=* enables all topics.
func Topic(topic string, format string, args ...any) {
	if !anyTopics {
		return
	}
	if !allTopics && !topics[topic] {
		return
	}

	mu.Lock()
	defer mu.Unlock()

	if logFile == nil {
		if err := initLocked(""); err != nil {
			fmt.Fprintf(os.Stderr, "debug: failed to open log: %v\n", err)
			return
		}
	}

	timestamp := time.Now().Format("15:04:05.000")
	msg := fmt.Sprintf(format, args...)
	fmt.Fprintf(logFile, "[%s] [%s] %s\n", timestamp, topic, msg)
	logFile.Sync()
}