diff options
| author | Paul Buetow <paul@buetow.org> | 2026-02-26 23:33:55 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-02-26 23:33:55 +0200 |
| commit | 4ca34f040203c8e31603bbb39fd38632b68067d8 (patch) | |
| tree | eed81b39e169eb6d0cd7d2eca6b338c7c0914ba4 /internal/tui/eventstream/export.go | |
| parent | e5cb5db2292ae84680935767d455a777125e0fe9 (diff) | |
tui: add paused stream CSV export and foreground editor open
Diffstat (limited to 'internal/tui/eventstream/export.go')
| -rw-r--r-- | internal/tui/eventstream/export.go | 107 |
1 files changed, 107 insertions, 0 deletions
diff --git a/internal/tui/eventstream/export.go b/internal/tui/eventstream/export.go new file mode 100644 index 0000000..679fddb --- /dev/null +++ b/internal/tui/eventstream/export.go @@ -0,0 +1,107 @@ +package eventstream + +import ( + "encoding/csv" + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "time" +) + +func defaultStreamExportFilename() string { + return fmt.Sprintf("ior-stream-%s.csv", time.Now().Format("20060102-150405")) +} + +func ensureCSVFilename(name string) (string, error) { + clean := strings.TrimSpace(name) + if clean == "" { + return "", errors.New("filename cannot be empty") + } + if strings.HasSuffix(strings.ToLower(clean), ".csv") { + return clean, nil + } + return clean + ".csv", nil +} + +func (m *Model) exportFilteredToCSV(filename string) (string, error) { + name, err := ensureCSVFilename(filename) + if err != nil { + return "", err + } + path := name + if m.exportDir != "" { + path = filepath.Join(m.exportDir, name) + } + + f, err := os.Create(path) + if err != nil { + return "", err + } + defer f.Close() + + w := csv.NewWriter(f) + defer w.Flush() + + header := []string{"seq", "time_ns", "gap_ns", "latency_ns", "comm", "pid", "tid", "syscall", "fd", "ret", "bytes", "file", "error"} + if err := w.Write(header); err != nil { + return "", err + } + for i := range m.filtered { + ev := m.filtered[i] + record := []string{ + fmt.Sprintf("%d", ev.Seq), + fmt.Sprintf("%d", ev.TimeNs), + fmt.Sprintf("%d", ev.GapNs), + fmt.Sprintf("%d", ev.DurationNs), + ev.Comm, + fmt.Sprintf("%d", ev.PID), + fmt.Sprintf("%d", ev.TID), + ev.Syscall, + fmt.Sprintf("%d", ev.FD), + fmt.Sprintf("%d", ev.RetVal), + fmt.Sprintf("%d", ev.Bytes), + ev.FileName, + fmt.Sprintf("%t", ev.IsError), + } + if err := w.Write(record); err != nil { + return "", err + } + } + if err := w.Error(); err != nil { + return "", err + } + absPath, err := filepath.Abs(path) + if err != nil { + return path, nil + } + return absPath, nil +} + +// EditorCommandForPath builds an editor command for the given path. +func EditorCommandForPath(path string) (*exec.Cmd, error) { + parts, _, err := resolveEditorCommand() + if err != nil { + return nil, err + } + args := append(parts[1:], path) + return exec.Command(parts[0], args...), nil +} + +func resolveEditorCommand() ([]string, string, error) { + candidates := []string{"SUDO_EDITOR", "VISUAL", "EDITOR"} + for _, key := range candidates { + value := strings.TrimSpace(os.Getenv(key)) + if value == "" { + continue + } + parts := strings.Fields(value) + if len(parts) == 0 { + continue + } + return parts, key, nil + } + return []string{"vi"}, "fallback", nil +} |
