package main import ( "bytes" "context" "encoding/csv" "fmt" "os" "path/filepath" "strings" "github.com/wailsapp/wails/v2/pkg/runtime" ) // CSVData represents the CSV data passed between Go and the frontend. type CSVData struct { Headers []string `json:"Headers"` Rows [][]string `json:"Rows"` FilePath string `json:"FilePath"` } // App struct holds the application state. type App struct { ctx context.Context headers []string rows [][]string currentFile string } // NewApp creates a new App application struct. func NewApp() *App { return &App{} } // startup is called when the app starts. The context is saved // so we can call the runtime methods. func (a *App) startup(ctx context.Context) { a.ctx = ctx // Set up file drop handler runtime.OnFileDrop(ctx, func(x, y int, paths []string) { if len(paths) > 0 { runtime.EventsEmit(ctx, "file-dropped", paths[0]) } }) } // domReady is called after the front-end DOM is ready. func (a *App) domReady(ctx context.Context) { // placeholder for any post-DOM-ready initialisation } // LoadCSV reads a CSV file and returns its data. The first row is treated as headers. func (a *App) LoadCSV(filePath string) (*CSVData, error) { data, err := os.ReadFile(filePath) if err != nil { return nil, fmt.Errorf("failed to read file: %w", err) } reader := csv.NewReader(bytes.NewReader(data)) reader.FieldsPerRecord = -1 // allow variable field counts records, err := reader.ReadAll() if err != nil { return nil, fmt.Errorf("failed to parse CSV: %w", err) } if len(records) == 0 { a.headers = []string{} a.rows = [][]string{} a.currentFile = filePath a.updateWindowTitle() return &CSVData{Headers: a.headers, Rows: a.rows, FilePath: filePath}, nil } a.headers = records[0] if len(records) > 1 { a.rows = records[1:] } else { a.rows = [][]string{} } a.currentFile = filePath a.updateWindowTitle() return &CSVData{Headers: a.headers, Rows: a.rows, FilePath: filePath}, nil } // SaveCSV writes headers and rows to the given file path as CSV. func (a *App) SaveCSV(filePath string, headers []string, rows [][]string) error { f, err := os.Create(filePath) if err != nil { return fmt.Errorf("failed to create file: %w", err) } defer f.Close() writer := csv.NewWriter(f) defer writer.Flush() if err := writer.Write(headers); err != nil { return fmt.Errorf("failed to write headers: %w", err) } for _, row := range rows { if err := writer.Write(row); err != nil { return fmt.Errorf("failed to write row: %w", err) } } a.headers = headers a.rows = rows a.currentFile = filePath a.updateWindowTitle() return nil } // SaveCurrentFile saves to the currently open file. func (a *App) SaveCurrentFile(headers []string, rows [][]string) error { if a.currentFile == "" { return fmt.Errorf("no file is currently open") } return a.SaveCSV(a.currentFile, headers, rows) } // GetTableData returns the current CSV state. func (a *App) GetTableData() *CSVData { return &CSVData{ Headers: a.headers, Rows: a.rows, FilePath: a.currentFile, } } // SetTableData updates the in-memory state from the frontend. func (a *App) SetTableData(headers []string, rows [][]string) { a.headers = headers a.rows = rows } // OpenFileDialog shows a native open dialog filtered for CSV files. func (a *App) OpenFileDialog() (string, error) { return runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{ Title: "Open CSV File", Filters: []runtime.FileFilter{ { DisplayName: "CSV Files (*.csv)", Pattern: "*.csv", }, { DisplayName: "All Files (*.*)", Pattern: "*.*", }, }, }) } // SaveFileDialog shows a native save dialog. func (a *App) SaveFileDialog() (string, error) { return runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{ Title: "Save CSV File", DefaultFilename: "untitled.csv", Filters: []runtime.FileFilter{ { DisplayName: "CSV Files (*.csv)", Pattern: "*.csv", }, { DisplayName: "All Files (*.*)", Pattern: "*.*", }, }, }) } // ParseCSVString parses CSV text content and returns structured data. func (a *App) ParseCSVString(content string) (*CSVData, error) { reader := csv.NewReader(strings.NewReader(content)) reader.FieldsPerRecord = -1 records, err := reader.ReadAll() if err != nil { return nil, fmt.Errorf("failed to parse CSV string: %w", err) } if len(records) == 0 { return &CSVData{Headers: []string{}, Rows: [][]string{}}, nil } headers := records[0] var rows [][]string if len(records) > 1 { rows = records[1:] } else { rows = [][]string{} } return &CSVData{Headers: headers, Rows: rows}, nil } // FormatAsCSV formats headers and rows as a CSV string. func (a *App) FormatAsCSV(headers []string, rows [][]string) string { var buf bytes.Buffer writer := csv.NewWriter(&buf) _ = writer.Write(headers) for _, row := range rows { _ = writer.Write(row) } writer.Flush() return buf.String() } // FormatRowsAsCSV formats just the rows (no headers) as CSV text. func (a *App) FormatRowsAsCSV(rows [][]string) string { var buf bytes.Buffer writer := csv.NewWriter(&buf) for _, row := range rows { _ = writer.Write(row) } writer.Flush() return buf.String() } // FormatAsMarkdown formats headers and rows as a Markdown table. func (a *App) FormatAsMarkdown(headers []string, rows [][]string) string { if len(headers) == 0 { return "" } var sb strings.Builder // Header row sb.WriteString("| ") sb.WriteString(strings.Join(headers, " | ")) sb.WriteString(" |\n") // Separator row separators := make([]string, len(headers)) for i := range separators { separators[i] = "---" } sb.WriteString("| ") sb.WriteString(strings.Join(separators, " | ")) sb.WriteString(" |\n") // Data rows for _, row := range rows { padded := padRow(row, len(headers)) sb.WriteString("| ") sb.WriteString(strings.Join(padded, " | ")) sb.WriteString(" |\n") } return sb.String() } // FormatAsJira formats headers and rows as Jira table markup. func (a *App) FormatAsJira(headers []string, rows [][]string) string { if len(headers) == 0 { return "" } var sb strings.Builder // Header row: || Header1 || Header2 || sb.WriteString("|| ") sb.WriteString(strings.Join(headers, " || ")) sb.WriteString(" ||\n") // Data rows: | val1 | val2 | for _, row := range rows { padded := padRow(row, len(headers)) sb.WriteString("| ") sb.WriteString(strings.Join(padded, " | ")) sb.WriteString(" |\n") } return sb.String() } // FormatAsSingleColumn formats a single column of data as newline-separated lines. func (a *App) FormatAsSingleColumn(rows [][]string) string { var lines []string for _, row := range rows { if len(row) > 0 { lines = append(lines, row[0]) } else { lines = append(lines, "") } } return strings.Join(lines, "\n") } // SetWindowTitle sets the window title. If title is empty, resets to default. func (a *App) SetWindowTitle(title string) { if title == "" { runtime.WindowSetTitle(a.ctx, "CSV Tool") } else { runtime.WindowSetTitle(a.ctx, title+" - CSV Tool") } } // updateWindowTitle sets the window title based on the current file. func (a *App) updateWindowTitle() { if a.currentFile == "" { runtime.WindowSetTitle(a.ctx, "CSV Tool") } else { name := filepath.Base(a.currentFile) runtime.WindowSetTitle(a.ctx, name+" - CSV Tool") } } // padRow ensures a row has exactly n columns, padding with empty strings if needed. func padRow(row []string, n int) []string { if len(row) >= n { return row[:n] } padded := make([]string, n) copy(padded, row) return padded }