From a5a28840b10245eb4de14edcc8812b9327cd0de7 Mon Sep 17 00:00:00 2001 From: Doc Date: Sun, 28 Sep 2025 13:48:56 +0200 Subject: [PATCH] basic tui implementation --- cmd/tui/main.go | 405 ++++++++++++++++++++++++++++++++++++++++++++ go.mod | 30 ++++ go.sum | 55 ++++++ internal/app/app.go | 53 +++--- main.go | 1 + 5 files changed, 520 insertions(+), 24 deletions(-) create mode 100644 cmd/tui/main.go create mode 100644 go.sum diff --git a/cmd/tui/main.go b/cmd/tui/main.go new file mode 100644 index 0000000..254e9cf --- /dev/null +++ b/cmd/tui/main.go @@ -0,0 +1,405 @@ +package main + +import ( + "bytes" + "fmt" + "os" + "strconv" + "strings" + + "github.com/charmbracelet/bubbles/textinput" + "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + + "bspviz/internal/app" + "bspviz/internal/geom" +) + +const ( + inputWadPath = iota + inputMapMarker + inputExtract + inputOutDir + inputDotOut + inputTreePNG + inputOverlay + inputAlpha + inputBeta + inputEps + inputLeaf + inputDepth + inputCands + inputSeed + inputCount +) + +const ( + toggleListOnly = iota + toggleInfo + toggleGeomTest + toggleBuildBSP + toggleCount +) + +var ( + indicatorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("205")).Bold(true) + statusStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("39")) + errorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("9")).Bold(true) + boxStyle = lipgloss.NewStyle().Border(lipgloss.NormalBorder()).Padding(0, 1) +) + +type runResultMsg struct { + output string + err error +} + +type model struct { + inputs []textinput.Model + inputLabels []string + toggles []bool + toggleLabels []string + focusIndex int + status string + output string + errMsg string + running bool +} + +func initialModel() model { + m := model{ + inputs: make([]textinput.Model, inputCount), + inputLabels: []string{ + "WAD", "Map", "Extract", "OutDir", "DotOut", "TreePNG", "Overlay", + "Alpha", "Beta", "Eps", "LeafMax", "MaxDepth", "Cands", "Seed", + }, + toggles: make([]bool, toggleCount), + toggleLabels: []string{"List directory", "Info", "Geom test", "Build BSP"}, + status: "Bereit", + } + + for i := range m.inputs { + ti := textinput.New() + ti.CharLimit = 0 + ti.Prompt = "" + switch i { + case inputWadPath: + ti.Placeholder = "Pfad zur WAD" + case inputMapMarker: + ti.SetValue("MAP01") + case inputExtract: + ti.Placeholder = "z.B. VERTEXES,LINEDEFS" + case inputOutDir: + ti.SetValue(".") + case inputAlpha: + ti.SetValue(fmt.Sprintf("%g", 10.0)) + case inputBeta: + ti.SetValue(fmt.Sprintf("%g", 1.0)) + case inputEps: + ti.SetValue(fmt.Sprintf("%g", geom.EPS)) + case inputLeaf: + ti.SetValue("12") + case inputDepth: + ti.SetValue("32") + case inputCands: + ti.SetValue("16") + case inputSeed: + ti.SetValue("0") + } + m.inputs[i] = ti + } + + m.setFocus(0) + return m +} + +func (m model) Init() tea.Cmd { + return nil +} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "ctrl+c", "esc": + return m, tea.Quit + case "tab", "down": + m.setFocus(m.focusIndex + 1) + case "shift+tab", "up": + m.setFocus(m.focusIndex - 1) + case "enter": + if m.focusIndex < len(m.inputs) { + m.setFocus(m.focusIndex + 1) + } else { + idx := m.focusIndex - len(m.inputs) + if idx >= 0 && idx < len(m.toggles) { + m.toggles[idx] = !m.toggles[idx] + } + } + case " ": + if m.focusIndex >= len(m.inputs) { + idx := m.focusIndex - len(m.inputs) + if idx >= 0 && idx < len(m.toggles) { + m.toggles[idx] = !m.toggles[idx] + } + } + case "ctrl+r": + if !m.running { + var cmd tea.Cmd + m, cmd = m.run() + return m, cmd + } + } + case runResultMsg: + m.running = false + if msg.err != nil { + m.errMsg = msg.err.Error() + m.status = "Fehlgeschlagen" + } else { + m.errMsg = "" + m.status = "Fertig" + } + m.output = strings.TrimSpace(msg.output) + return m, nil + } + + cmds := make([]tea.Cmd, len(m.inputs)) + for i := range m.inputs { + m.inputs[i], cmds[i] = m.inputs[i].Update(msg) + } + + return m, tea.Batch(cmds...) +} + +func (m model) View() string { + var b strings.Builder + + b.WriteString("BSPViz Bubble Tea UI\n") + b.WriteString("====================\n\n") + + labelWidth := 12 + for i := range m.inputs { + indicator := " " + if m.focusIndex == i { + indicator = indicatorStyle.Render(">") + } + label := fmt.Sprintf("%-*s", labelWidth, m.inputLabels[i]+":") + fmt.Fprintf(&b, "%s %s %s\n", indicator, label, m.inputs[i].View()) + } + + b.WriteString("\n") + for i := range m.toggles { + indicator := " " + if m.focusIndex == len(m.inputs)+i { + indicator = indicatorStyle.Render(">") + } + mark := " " + if m.toggles[i] { + mark = "x" + } + fmt.Fprintf(&b, "%s [%s] %s\n", indicator, mark, m.toggleLabels[i]) + } + + b.WriteString("\n") + + statusText := "Bereit" + if m.running { + statusText = "Läuft..." + } else if m.status != "" { + statusText = m.status + } + b.WriteString(statusStyle.Render("Status: " + statusText)) + b.WriteString("\n") + + if m.errMsg != "" { + b.WriteString(errorStyle.Render("Fehler: " + m.errMsg)) + b.WriteString("\n") + } + + if m.output != "" { + b.WriteString("\n") + b.WriteString(boxStyle.Render(m.output)) + b.WriteString("\n") + } + + b.WriteString("\n") + b.WriteString("Steuerung: TAB/Shift+TAB navigieren • Space toggeln • Ctrl+R ausführen • Ctrl+C beenden") + b.WriteString("\n") + + return b.String() +} + +func (m model) run() (model, tea.Cmd) { + opts, err := m.buildOptions() + if err != nil { + m.errMsg = err.Error() + m.status = "Fehler" + return m, nil + } + + m.running = true + m.status = "Läuft..." + m.errMsg = "" + m.output = "" + + return m, runAppCmd(opts) +} + +func (m model) buildOptions() (app.Options, error) { + var opts app.Options + + wadPath := strings.TrimSpace(m.inputs[inputWadPath].Value()) + if wadPath == "" { + return opts, fmt.Errorf("WAD-Pfad darf nicht leer sein") + } + opts.WadPath = wadPath + + mapMarker := strings.TrimSpace(m.inputs[inputMapMarker].Value()) + if mapMarker == "" { + mapMarker = "MAP01" + } + opts.MapMarker = mapMarker + + extractRaw := strings.TrimSpace(m.inputs[inputExtract].Value()) + if extractRaw != "" { + parts := strings.Split(extractRaw, ",") + opts.Extract = make([]string, 0, len(parts)) + for _, p := range parts { + n := strings.ToUpper(strings.TrimSpace(p)) + if n == "" { + continue + } + opts.Extract = append(opts.Extract, n) + } + } + + outDir := strings.TrimSpace(m.inputs[inputOutDir].Value()) + if outDir == "" { + outDir = "." + } + opts.OutDir = outDir + + opts.DotOut = strings.TrimSpace(m.inputs[inputDotOut].Value()) + opts.TreePNG = strings.TrimSpace(m.inputs[inputTreePNG].Value()) + opts.Overlay = strings.TrimSpace(m.inputs[inputOverlay].Value()) + + alphaStr := strings.TrimSpace(m.inputs[inputAlpha].Value()) + if alphaStr == "" { + opts.Alpha = 10.0 + } else { + v, err := strconv.ParseFloat(alphaStr, 64) + if err != nil { + return opts, fmt.Errorf("Alpha ungültig: %w", err) + } + opts.Alpha = v + } + + betaStr := strings.TrimSpace(m.inputs[inputBeta].Value()) + if betaStr == "" { + opts.Beta = 1.0 + } else { + v, err := strconv.ParseFloat(betaStr, 64) + if err != nil { + return opts, fmt.Errorf("Beta ungültig: %w", err) + } + opts.Beta = v + } + + epsStr := strings.TrimSpace(m.inputs[inputEps].Value()) + if epsStr == "" { + opts.Eps = geom.EPS + } else { + v, err := strconv.ParseFloat(epsStr, 64) + if err != nil { + return opts, fmt.Errorf("Eps ungültig: %w", err) + } + opts.Eps = v + } + + leafStr := strings.TrimSpace(m.inputs[inputLeaf].Value()) + if leafStr == "" { + opts.LeafMax = 12 + } else { + v, err := strconv.Atoi(leafStr) + if err != nil { + return opts, fmt.Errorf("LeafMax ungültig: %w", err) + } + opts.LeafMax = v + } + + depthStr := strings.TrimSpace(m.inputs[inputDepth].Value()) + if depthStr == "" { + opts.MaxDepth = 32 + } else { + v, err := strconv.Atoi(depthStr) + if err != nil { + return opts, fmt.Errorf("MaxDepth ungültig: %w", err) + } + opts.MaxDepth = v + } + + candsStr := strings.TrimSpace(m.inputs[inputCands].Value()) + if candsStr == "" { + opts.Cands = 16 + } else { + v, err := strconv.Atoi(candsStr) + if err != nil { + return opts, fmt.Errorf("Cands ungültig: %w", err) + } + opts.Cands = v + } + + seedStr := strings.TrimSpace(m.inputs[inputSeed].Value()) + if seedStr == "" { + opts.Seed = 0 + } else { + v, err := strconv.ParseInt(seedStr, 10, 64) + if err != nil { + return opts, fmt.Errorf("Seed ungültig: %w", err) + } + opts.Seed = v + } + + opts.ListOnly = m.toggles[toggleListOnly] + opts.Info = m.toggles[toggleInfo] + opts.GeomTest = m.toggles[toggleGeomTest] + opts.BuildBSP = m.toggles[toggleBuildBSP] + + return opts, nil +} + +func runAppCmd(opts app.Options) tea.Cmd { + return func() tea.Msg { + var buf bytes.Buffer + opts.Out = &buf + err := app.Run(opts) + return runResultMsg{output: buf.String(), err: err} + } +} + +func (m *model) setFocus(index int) { + total := len(m.inputs) + len(m.toggles) + if total == 0 { + return + } + if index < 0 { + index = total - 1 + } + if index >= total { + index = 0 + } + m.focusIndex = index + for i := range m.inputs { + if i == m.focusIndex { + m.inputs[i].Focus() + } else { + m.inputs[i].Blur() + } + } +} + +func main() { + if _, err := tea.NewProgram(initialModel(), tea.WithAltScreen()).Run(); err != nil { + fmt.Fprintf(os.Stderr, "UI error: %v\n", err) + os.Exit(1) + } +} diff --git a/go.mod b/go.mod index d42b24f..5696909 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,33 @@ module bspviz go 1.25.0 + +require ( + github.com/charmbracelet/bubbles v0.18.0 + github.com/charmbracelet/bubbletea v0.25.0 + github.com/charmbracelet/lipgloss v1.1.0 +) + +require ( + github.com/atotto/clipboard v0.1.4 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect + github.com/charmbracelet/x/ansi v0.8.0 // indirect + github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.16.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + golang.org/x/sync v0.1.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/term v0.6.0 // indirect + golang.org/x/text v0.3.8 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..1030bd8 --- /dev/null +++ b/go.sum @@ -0,0 +1,55 @@ +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0= +github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw= +github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM= +github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= +github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= +github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= +github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34= +github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= diff --git a/internal/app/app.go b/internal/app/app.go index 100ada7..2518f63 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -2,7 +2,7 @@ package app import ( "fmt" - "log" + "io" "os" "path/filepath" "strings" @@ -33,9 +33,14 @@ type Options struct { DotOut string TreePNG string Overlay string + Out io.Writer } func Run(opts Options) error { + if opts.Out == nil { + opts.Out = os.Stdout + } + if strings.TrimSpace(opts.WadPath) == "" { return fmt.Errorf("wad path required") } @@ -47,9 +52,9 @@ func Run(opts Options) error { defer w.Close() if opts.ListOnly { - fmt.Printf("WAD: %s\n", opts.WadPath) + fmt.Fprintf(opts.Out, "WAD: %s\n", opts.WadPath) for i, d := range w.Dir() { - fmt.Printf("%3d: %-8s size=%-7d pos=%-8d\n", i, d.Name(), d.Size, d.FilePos) + fmt.Fprintf(opts.Out, "%3d: %-8s size=%-7d pos=%-8d\n", i, d.Name(), d.Size, d.FilePos) } return nil } @@ -58,7 +63,7 @@ func Run(opts Options) error { if err != nil { return fmt.Errorf("find map: %w", err) } - fmt.Printf("Map %s: Directory [%d, %d)\n", strings.ToUpper(opts.MapMarker), start, end) + fmt.Fprintf(opts.Out, "Map %s: Directory [%d, %d)\n", strings.ToUpper(opts.MapMarker), start, end) if opts.Info { lumps, err := w.LoadMapLumps(opts.MapMarker, "VERTEXES", "LINEDEFS") @@ -74,18 +79,18 @@ func Run(opts Options) error { verts := len(vb) / 4 lines := len(lb) / 14 - fmt.Printf("VERTEXES: bytes=%d count=%d\n", len(vb), verts) - fmt.Printf("LINEDEFS: bytes=%d count=%d\n", len(lb), lines) + fmt.Fprintf(opts.Out, "VERTEXES: bytes=%d count=%d\n", len(vb), verts) + fmt.Fprintf(opts.Out, "LINEDEFS: bytes=%d count=%d\n", len(lb), lines) - fmt.Printf("Map has %d vertices and %d linedefs\n", len(m.Vertices), len(m.Linedefs)) - fmt.Printf("First vertex: %+v\n", m.Vertices[0]) - fmt.Printf("First linedef: %+v\n", m.Linedefs[0]) + fmt.Fprintf(opts.Out, "Map has %d vertices and %d linedefs\n", len(m.Vertices), len(m.Linedefs)) + fmt.Fprintf(opts.Out, "First vertex: %+v\n", m.Vertices[0]) + fmt.Fprintf(opts.Out, "First linedef: %+v\n", m.Linedefs[0]) if len(vb)%4 != 0 { - fmt.Println("WARN: VERTEXES size ist kein Vielfaches von 4 → Format prüfen") + fmt.Fprintln(opts.Out, "WARN: VERTEXES size ist kein Vielfaches von 4 → Format prüfen") } if len(lb)%14 != 0 { - fmt.Println("WARN: LINEDEFS size ist kein Vielfaches von 14 → Format prüfen") + fmt.Fprintln(opts.Out, "WARN: LINEDEFS size ist kein Vielfaches von 14 → Format prüfen") } } @@ -102,7 +107,7 @@ func Run(opts Options) error { if err := os.WriteFile(dst, data, 0o644); err != nil { return fmt.Errorf("write %s: %w", dst, err) } - fmt.Printf("wrote %s (%d bytes)\n", dst, len(data)) + fmt.Fprintf(opts.Out, "wrote %s (%d bytes)\n", dst, len(data)) } } @@ -117,9 +122,9 @@ func Run(opts Options) error { } segs := mapfmt.LinedefsToSegs(m.Vertices, m.Linedefs) - fmt.Printf("GEOM: vertices=%d linedefs=%d segs=%d\n", len(m.Vertices), len(m.Linedefs), len(segs)) + fmt.Fprintf(opts.Out, "GEOM: vertices=%d linedefs=%d segs=%d\n", len(m.Vertices), len(m.Linedefs), len(segs)) if len(segs) == 0 { - fmt.Println("GEOM: keine Segmente gefunden – prüfe LINEDEFS/VERTEXES") + fmt.Fprintln(opts.Out, "GEOM: keine Segmente gefunden – prüfe LINEDEFS/VERTEXES") return nil } @@ -130,7 +135,7 @@ func Run(opts Options) error { bb := geom.Bounds(pts) w := bb.Max.X - bb.Min.X h := bb.Max.Y - bb.Min.Y - fmt.Printf("AABB: min=(%.1f,%.1f) max=(%.1f,%.1f) size=(%.1f×%.1f)\n", + fmt.Fprintf(opts.Out, "AABB: min=(%.1f,%.1f) max=(%.1f,%.1f) size=(%.1f×%.1f)\n", bb.Min.X, bb.Min.Y, bb.Max.X, bb.Max.Y, w, h) O := segs[0].A @@ -152,8 +157,8 @@ func Run(opts Options) error { splits++ } } - fmt.Printf("PROBE-SPLIT: O=(%.1f,%.1f) D=(%.1f,%.1f)\n", O.X, O.Y, D.X, D.Y) - fmt.Printf("PROBE-SPLIT: left=%d right=%d splits=%d degens=%d (EPS=%.1e)\n", + fmt.Fprintf(opts.Out, "PROBE-SPLIT: O=(%.1f,%.1f) D=(%.1f,%.1f)\n", O.X, O.Y, D.X, D.Y) + fmt.Fprintf(opts.Out, "PROBE-SPLIT: left=%d right=%d splits=%d degens=%d (EPS=%.1e)\n", left, right, splits, degens, geom.EPS) return nil @@ -177,23 +182,23 @@ func Run(opts Options) error { root := bsp.Build(segs, p) st := bsp.Measure(root) - fmt.Printf("BSP built.\n") - fmt.Printf(" nodes=%d leaves=%d maxDepth=%d totalLeafSegs=%d\n", + fmt.Fprintln(opts.Out, "BSP built.") + fmt.Fprintf(opts.Out, " nodes=%d leaves=%d maxDepth=%d totalLeafSegs=%d\n", st.Nodes, st.Leaves, st.MaxDepth, st.TotalSegs) - fmt.Printf(" params: alpha=%.2f beta=%.2f eps=%.1e leafMax=%d maxDepth=%d cands=%d seed=%d\n", + fmt.Fprintf(opts.Out, " params: alpha=%.2f beta=%.2f eps=%.1e leafMax=%d maxDepth=%d cands=%d seed=%d\n", p.Alpha, p.Beta, p.Eps, p.LeafMax, p.MaxDepth, p.Cands, p.Seed) if opts.DotOut != "" { if err := viz.EmitDOT(root, opts.DotOut); err != nil { return fmt.Errorf("write DOT: %w", err) } - fmt.Printf("DOT export geschrieben: %s\n", opts.DotOut) + fmt.Fprintf(opts.Out, "DOT export geschrieben: %s\n", opts.DotOut) if opts.TreePNG != "" { if err := viz.RunGraphviz(opts.DotOut, opts.TreePNG); err != nil { - log.Printf("Graphviz fehlgeschlagen: %v", err) + fmt.Fprintf(opts.Out, "Graphviz fehlgeschlagen: %v\n", err) } else { - fmt.Printf("Graphviz PNG gebaut: %s\n", opts.TreePNG) + fmt.Fprintf(opts.Out, "Graphviz PNG gebaut: %s\n", opts.TreePNG) } } } @@ -202,7 +207,7 @@ func Run(opts Options) error { if err := viz.RenderPNG(m, root, opts.Overlay); err != nil { return fmt.Errorf("write overlay PNG: %w", err) } - fmt.Printf("Overlay PNG geschrieben: %s\n", opts.Overlay) + fmt.Fprintf(opts.Out, "Overlay PNG geschrieben: %s\n", opts.Overlay) } return nil } diff --git a/main.go b/main.go index 8ce7c30..e10ee2e 100644 --- a/main.go +++ b/main.go @@ -77,6 +77,7 @@ func main() { DotOut: *dotOut, TreePNG: *treePNG, Overlay: *overlay, + Out: os.Stdout, } if err := app.Run(opts); err != nil {