commit 55a25f6d63f3dd2d7caa06ed478db707bd9bea97 Author: exe.dev user Date: Tue Mar 3 21:34:09 2026 +0000 CSV Tool - Wails-based CSV editor with spreadsheet UI Features: - Spreadsheet-like table with cell navigation (arrow keys) - Formula bar for editing cell values - Click and drag cell selection with Shift+Arrow extend - Column resize by dragging header borders, double-click for best fit - Editable headers via double-click - Command palette (Cmd+P) with 12 commands - Copy/Cut/Paste with CSV, Markdown, and Jira formats - Insert rows/columns above/below/left/right - File drag-and-drop to open CSV files - Native Open/Save dialogs - Go backend for CSV parsing, formatting, and file I/O - Vanilla JS frontend, no frameworks Co-authored-by: Shelley diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..129d522 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +build/bin +node_modules +frontend/dist diff --git a/README.md b/README.md new file mode 100644 index 0000000..397b08b --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +# README + +## About + +This is the official Wails Vanilla template. + +You can configure the project by editing `wails.json`. More information about the project settings can be found +here: https://wails.io/docs/reference/project-config + +## Live Development + +To run in live development mode, run `wails dev` in the project directory. This will run a Vite development +server that will provide very fast hot reload of your frontend changes. If you want to develop in a browser +and have access to your Go methods, there is also a dev server that runs on http://localhost:34115. Connect +to this in your browser, and you can call your Go code from devtools. + +## Building + +To build a redistributable, production mode package, use `wails build`. diff --git a/app.go b/app.go new file mode 100644 index 0000000..cf6b52c --- /dev/null +++ b/app.go @@ -0,0 +1,321 @@ +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 +} diff --git a/app_test.go b/app_test.go new file mode 100644 index 0000000..c157820 --- /dev/null +++ b/app_test.go @@ -0,0 +1,124 @@ +package main + +import ( + "strings" + "testing" +) + +func TestFormatAsMarkdown(t *testing.T) { + app := NewApp() + headers := []string{"Name", "Age", "City"} + rows := [][]string{ + {"Alice", "30", "New York"}, + {"Bob", "25", "SF"}, + } + result := app.FormatAsMarkdown(headers, rows) + expected := `| Name | Age | City | +| --- | --- | --- | +| Alice | 30 | New York | +| Bob | 25 | SF | +` + if result != expected { + t.Errorf("FormatAsMarkdown:\ngot:\n%s\nwant:\n%s", result, expected) + } +} + +func TestFormatAsJira(t *testing.T) { + app := NewApp() + headers := []string{"Name", "Age"} + rows := [][]string{ + {"Alice", "30"}, + } + result := app.FormatAsJira(headers, rows) + expected := `|| Name || Age || +| Alice | 30 | +` + if result != expected { + t.Errorf("FormatAsJira:\ngot:\n%s\nwant:\n%s", result, expected) + } +} + +func TestFormatAsSingleColumn(t *testing.T) { + app := NewApp() + rows := [][]string{{"Alice"}, {"Bob"}, {"Charlie"}} + result := app.FormatAsSingleColumn(rows) + if result != "Alice\nBob\nCharlie" { + t.Errorf("FormatAsSingleColumn: got %q", result) + } +} + +func TestFormatRowsAsCSV(t *testing.T) { + app := NewApp() + rows := [][]string{ + {"Alice", "30", "New York"}, + {"Bob", "25", "San Francisco"}, + } + result := app.FormatRowsAsCSV(rows) + lines := strings.Split(strings.TrimSpace(result), "\n") + if len(lines) != 2 { + t.Fatalf("expected 2 lines, got %d: %v", len(lines), lines) + } + if lines[0] != "Alice,30,New York" { + t.Errorf("line 0: got %q", lines[0]) + } +} + +func TestFormatAsCSV(t *testing.T) { + app := NewApp() + headers := []string{"Name", "Age"} + rows := [][]string{{"Alice", "30"}} + result := app.FormatAsCSV(headers, rows) + lines := strings.Split(strings.TrimSpace(result), "\n") + if len(lines) != 2 { + t.Fatalf("expected 2 lines, got %d", len(lines)) + } + if lines[0] != "Name,Age" { + t.Errorf("header line: got %q", lines[0]) + } +} + +func TestParseCSVString(t *testing.T) { + app := NewApp() + input := "Name,Age\nAlice,30\nBob,25" + data, err := app.ParseCSVString(input) + if err != nil { + t.Fatal(err) + } + if len(data.Headers) != 2 { + t.Errorf("headers: got %d", len(data.Headers)) + } + if len(data.Rows) != 2 { + t.Errorf("rows: got %d", len(data.Rows)) + } + if data.Headers[0] != "Name" { + t.Errorf("header[0]: got %q", data.Headers[0]) + } +} + +func TestGetCommands(t *testing.T) { + reg := NewCommandRegistry() + cmds := reg.GetCommands() + if len(cmds) != 12 { + t.Errorf("expected 12 commands, got %d", len(cmds)) + } + // Check that all have IDs + for _, cmd := range cmds { + if cmd.ID == "" { + t.Error("found command with empty ID") + } + if cmd.Name == "" { + t.Error("found command with empty Name") + } + } +} + +func TestPadRow(t *testing.T) { + row := []string{"a", "b"} + padded := padRow(row, 4) + if len(padded) != 4 { + t.Errorf("expected 4 cols, got %d", len(padded)) + } + if padded[2] != "" || padded[3] != "" { + t.Error("expected empty padding") + } +} diff --git a/build/README.md b/build/README.md new file mode 100644 index 0000000..1ae2f67 --- /dev/null +++ b/build/README.md @@ -0,0 +1,35 @@ +# Build Directory + +The build directory is used to house all the build files and assets for your application. + +The structure is: + +* bin - Output directory +* darwin - macOS specific files +* windows - Windows specific files + +## Mac + +The `darwin` directory holds files specific to Mac builds. +These may be customised and used as part of the build. To return these files to the default state, simply delete them +and +build with `wails build`. + +The directory contains the following files: + +- `Info.plist` - the main plist file used for Mac builds. It is used when building using `wails build`. +- `Info.dev.plist` - same as the main plist file but used when building using `wails dev`. + +## Windows + +The `windows` directory contains the manifest and rc files used when building with `wails build`. +These may be customised for your application. To return these files to the default state, simply delete them and +build with `wails build`. + +- `icon.ico` - The icon used for the application. This is used when building using `wails build`. If you wish to + use a different icon, simply replace this file with your own. If it is missing, a new `icon.ico` file + will be created using the `appicon.png` file in the build directory. +- `installer/*` - The files used to create the Windows installer. These are used when building using `wails build`. +- `info.json` - Application details used for Windows builds. The data here will be used by the Windows installer, + as well as the application itself (right click the exe -> properties -> details) +- `wails.exe.manifest` - The main application manifest file. \ No newline at end of file diff --git a/build/appicon.png b/build/appicon.png new file mode 100644 index 0000000..63617fe Binary files /dev/null and b/build/appicon.png differ diff --git a/commands.go b/commands.go new file mode 100644 index 0000000..32609ef --- /dev/null +++ b/commands.go @@ -0,0 +1,34 @@ +package main + +// Command represents a command that can be executed in the application. +type Command struct { + ID string `json:"ID"` + Name string `json:"Name"` + Shortcut string `json:"Shortcut"` +} + +// CommandRegistry provides command metadata to the frontend. +type CommandRegistry struct{} + +// NewCommandRegistry creates a new CommandRegistry. +func NewCommandRegistry() *CommandRegistry { + return &CommandRegistry{} +} + +// GetCommands returns the list of all available commands. +func (c *CommandRegistry) GetCommands() []Command { + return []Command{ + {ID: "copy", Name: "Copy", Shortcut: "Cmd+C"}, + {ID: "cut", Name: "Cut", Shortcut: "Cmd+X"}, + {ID: "copy-markdown", Name: "Copy as Markdown", Shortcut: ""}, + {ID: "copy-jira", Name: "Copy as Jira", Shortcut: ""}, + {ID: "paste", Name: "Paste", Shortcut: "Cmd+V"}, + {ID: "resize-all", Name: "Resize All Columns", Shortcut: ""}, + {ID: "open", Name: "Open File", Shortcut: "Cmd+O"}, + {ID: "save", Name: "Save File", Shortcut: "Cmd+S"}, + {ID: "open-up", Name: "Insert Row Above", Shortcut: ""}, + {ID: "open-down", Name: "Insert Row Below", Shortcut: ""}, + {ID: "open-left", Name: "Insert Column Left", Shortcut: ""}, + {ID: "open-right", Name: "Insert Column Right", Shortcut: ""}, + } +} diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..c4d8580 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,32 @@ + + + + + + CSV Tool + + + +
+
+
+ +
+
+ + + +
+
+
+ Ready +
+ + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..6d14864 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,617 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "devDependencies": { + "vite": "^3.0.7" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.18.tgz", + "integrity": "sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.18.tgz", + "integrity": "sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.18.tgz", + "integrity": "sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.15.18", + "@esbuild/linux-loong64": "0.15.18", + "esbuild-android-64": "0.15.18", + "esbuild-android-arm64": "0.15.18", + "esbuild-darwin-64": "0.15.18", + "esbuild-darwin-arm64": "0.15.18", + "esbuild-freebsd-64": "0.15.18", + "esbuild-freebsd-arm64": "0.15.18", + "esbuild-linux-32": "0.15.18", + "esbuild-linux-64": "0.15.18", + "esbuild-linux-arm": "0.15.18", + "esbuild-linux-arm64": "0.15.18", + "esbuild-linux-mips64le": "0.15.18", + "esbuild-linux-ppc64le": "0.15.18", + "esbuild-linux-riscv64": "0.15.18", + "esbuild-linux-s390x": "0.15.18", + "esbuild-netbsd-64": "0.15.18", + "esbuild-openbsd-64": "0.15.18", + "esbuild-sunos-64": "0.15.18", + "esbuild-windows-32": "0.15.18", + "esbuild-windows-64": "0.15.18", + "esbuild-windows-arm64": "0.15.18" + } + }, + "node_modules/esbuild-android-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.18.tgz", + "integrity": "sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-android-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.18.tgz", + "integrity": "sha512-G4xu89B8FCzav9XU8EjsXacCKSG2FT7wW9J6hOc18soEHJdtWu03L3TQDGf0geNxfLTtxENKBzMSq9LlbjS8OQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.18.tgz", + "integrity": "sha512-2WAvs95uPnVJPuYKP0Eqx+Dl/jaYseZEUUT1sjg97TJa4oBtbAKnPnl3b5M9l51/nbx7+QAEtuummJZW0sBEmg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.18.tgz", + "integrity": "sha512-tKPSxcTJ5OmNb1btVikATJ8NftlyNlc8BVNtyT/UAr62JFOhwHlnoPrhYWz09akBLHI9nElFVfWSTSRsrZiDUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.18.tgz", + "integrity": "sha512-TT3uBUxkteAjR1QbsmvSsjpKjOX6UkCstr8nMr+q7zi3NuZ1oIpa8U41Y8I8dJH2fJgdC3Dj3CXO5biLQpfdZA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.18.tgz", + "integrity": "sha512-R/oVr+X3Tkh+S0+tL41wRMbdWtpWB8hEAMsOXDumSSa6qJR89U0S/PpLXrGF7Wk/JykfpWNokERUpCeHDl47wA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-32": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.18.tgz", + "integrity": "sha512-lphF3HiCSYtaa9p1DtXndiQEeQDKPl9eN/XNoBf2amEghugNuqXNZA/ZovthNE2aa4EN43WroO0B85xVSjYkbg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.18.tgz", + "integrity": "sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.18.tgz", + "integrity": "sha512-UH779gstRblS4aoS2qpMl3wjg7U0j+ygu3GjIeTonCcN79ZvpPee12Qun3vcdxX+37O5LFxz39XeW2I9bybMVA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.18.tgz", + "integrity": "sha512-54qr8kg/6ilcxd+0V3h9rjT4qmjc0CccMVWrjOEM/pEcUzt8X62HfBSeZfT2ECpM7104mk4yfQXkosY8Quptug==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-mips64le": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.18.tgz", + "integrity": "sha512-Mk6Ppwzzz3YbMl/ZZL2P0q1tnYqh/trYZ1VfNP47C31yT0K8t9s7Z077QrDA/guU60tGNp2GOwCQnp+DYv7bxQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-ppc64le": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.18.tgz", + "integrity": "sha512-b0XkN4pL9WUulPTa/VKHx2wLCgvIAbgwABGnKMY19WhKZPT+8BxhZdqz6EgkqCLld7X5qiCY2F/bfpUUlnFZ9w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-riscv64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.18.tgz", + "integrity": "sha512-ba2COaoF5wL6VLZWn04k+ACZjZ6NYniMSQStodFKH/Pu6RxzQqzsmjR1t9QC89VYJxBeyVPTaHuBMCejl3O/xg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-s390x": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.18.tgz", + "integrity": "sha512-VbpGuXEl5FCs1wDVp93O8UIzl3ZrglgnSQ+Hu79g7hZu6te6/YHgVJxCM2SqfIila0J3k0csfnf8VD2W7u2kzQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-netbsd-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.18.tgz", + "integrity": "sha512-98ukeCdvdX7wr1vUYQzKo4kQ0N2p27H7I11maINv73fVEXt2kyh4K4m9f35U1K43Xc2QGXlzAw0K9yoU7JUjOg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-openbsd-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.18.tgz", + "integrity": "sha512-yK5NCcH31Uae076AyQAXeJzt/vxIo9+omZRKj1pauhk3ITuADzuOx5N2fdHrAKPxN+zH3w96uFKlY7yIn490xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-sunos-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.18.tgz", + "integrity": "sha512-On22LLFlBeLNj/YF3FT+cXcyKPEI263nflYlAhz5crxtp3yRG1Ugfr7ITyxmCmjm4vbN/dGrb/B7w7U8yJR9yw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-32": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.18.tgz", + "integrity": "sha512-o+eyLu2MjVny/nt+E0uPnBxYuJHBvho8vWsC2lV61A7wwTWC3jkN2w36jtA+yv1UgYkHRihPuQsL23hsCYGcOQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.18.tgz", + "integrity": "sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.18.tgz", + "integrity": "sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rollup": { + "version": "2.80.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.80.0.tgz", + "integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/vite": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.11.tgz", + "integrity": "sha512-K/jGKL/PgbIgKCiJo5QbASQhFiV02X9Jh+Qq0AKCRCRKZtOTVi4t6wh75FDpGf2N9rYOnzH87OEFQNaFy6pdxQ==", + "dev": true, + "dependencies": { + "esbuild": "^0.15.9", + "postcss": "^8.4.18", + "resolve": "^1.22.1", + "rollup": "^2.79.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..a1b6f8e --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,13 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "devDependencies": { + "vite": "^3.0.7" + } +} \ No newline at end of file diff --git a/frontend/src/main.js b/frontend/src/main.js new file mode 100644 index 0000000..642c27f --- /dev/null +++ b/frontend/src/main.js @@ -0,0 +1,973 @@ +import './style.css'; + +// ===== State ===== +const state = { + headers: [], + rows: [], + filePath: '', + cursor: { row: 0, col: 0 }, + selection: null, // { startRow, startCol, endRow, endCol } + colWidths: [], // pixel widths per column + isSelecting: false, + editingHeader: -1, + formulaBarFocused: false, + formulaBarFromTable: false, +}; + +// ===== DOM refs ===== +const $ = (sel) => document.querySelector(sel); +const tableHead = $('#table-head'); +const tableBody = $('#table-body'); +const formulaBar = $('#formula-bar'); +const cellRef = $('#cell-ref'); +const statusText = $('#status-text'); +const commandPalette = $('#command-palette'); +const commandInput = $('#command-input'); +const commandList = $('#command-list'); +const overlay = $('#overlay'); +const tableContainer = $('#table-container'); + +// Default column width +const DEFAULT_COL_WIDTH = 120; +const MIN_COL_WIDTH = 40; +const MAX_BEST_FIT = 200; + +// ===== Helpers ===== +function colLabel(i) { + // A, B, C ... Z, AA, AB ... + let s = ''; + let n = i; + do { + s = String.fromCharCode(65 + (n % 26)) + s; + n = Math.floor(n / 26) - 1; + } while (n >= 0); + return s; +} + +function cellRefStr(r, c) { + return colLabel(c) + (r + 1); +} + +function normalizeSelection() { + if (!state.selection) return null; + const { startRow, startCol, endRow, endCol } = state.selection; + return { + r1: Math.min(startRow, endRow), + c1: Math.min(startCol, endCol), + r2: Math.max(startRow, endRow), + c2: Math.max(startCol, endCol), + }; +} + +function isCellSelected(r, c) { + const sel = normalizeSelection(); + if (!sel) return r === state.cursor.row && c === state.cursor.col; + return r >= sel.r1 && r <= sel.r2 && c >= sel.c1 && c <= sel.c2; +} + +function getCellValue(r, c) { + if (r < 0 || r >= state.rows.length) return ''; + if (c < 0 || !state.rows[r] || c >= state.rows[r].length) return ''; + return state.rows[r][c] || ''; +} + +function setCellValue(r, c, val) { + // Ensure row exists + while (state.rows.length <= r) state.rows.push([]); + // Ensure columns exist + while (state.rows[r].length <= c) state.rows[r].push(''); + state.rows[r][c] = val; +} + +function ensureGridSize() { + const numCols = state.headers.length; + for (let i = 0; i < state.rows.length; i++) { + while (state.rows[i].length < numCols) state.rows[i].push(''); + } +} + +function setStatus(msg) { + statusText.textContent = msg; +} + +// ===== Rendering ===== +function render() { + renderHead(); + renderBody(); + updateFormulaBar(); + updateCellRef(); +} + +function renderHead() { + tableHead.innerHTML = ''; + const tr = document.createElement('tr'); + + // Row number header + const th0 = document.createElement('th'); + th0.className = 'row-number-header'; + th0.textContent = ''; + tr.appendChild(th0); + + state.headers.forEach((h, i) => { + const th = document.createElement('th'); + th.style.width = (state.colWidths[i] || DEFAULT_COL_WIDTH) + 'px'; + th.style.position = 'relative'; + th.dataset.col = i; + + const content = document.createElement('div'); + content.className = 'header-content'; + + const textSpan = document.createElement('span'); + textSpan.className = 'header-text'; + textSpan.textContent = h; + content.appendChild(textSpan); + + th.appendChild(content); + + // Resize handle + const handle = document.createElement('div'); + handle.className = 'col-resize-handle'; + handle.addEventListener('mousedown', (e) => startColResize(e, i)); + handle.addEventListener('dblclick', (e) => { + e.stopPropagation(); + bestFitColumn(i); + }); + th.appendChild(handle); + + // Double click to edit header + th.addEventListener('dblclick', (e) => { + if (e.target.classList.contains('col-resize-handle')) return; + startHeaderEdit(i); + }); + + tr.appendChild(th); + }); + + tableHead.appendChild(tr); +} + +function renderBody() { + tableBody.innerHTML = ''; + const numCols = state.headers.length; + + state.rows.forEach((row, r) => { + const tr = document.createElement('tr'); + + // Row number + const tdNum = document.createElement('td'); + tdNum.className = 'row-number'; + tdNum.textContent = r + 1; + tr.appendChild(tdNum); + + for (let c = 0; c < numCols; c++) { + const td = document.createElement('td'); + td.style.width = (state.colWidths[c] || DEFAULT_COL_WIDTH) + 'px'; + td.textContent = row[c] || ''; + td.dataset.row = r; + td.dataset.col = c; + + if (isCellSelected(r, c)) { + td.classList.add('selected'); + } + if (r === state.cursor.row && c === state.cursor.col) { + td.classList.add('cursor-cell'); + } + + tr.appendChild(td); + } + + tableBody.appendChild(tr); + }); +} + +function updateSelectionClasses() { + const tds = tableBody.querySelectorAll('td[data-row]'); + tds.forEach(td => { + const r = parseInt(td.dataset.row); + const c = parseInt(td.dataset.col); + td.classList.toggle('selected', isCellSelected(r, c)); + td.classList.toggle('cursor-cell', r === state.cursor.row && c === state.cursor.col); + }); + updateFormulaBar(); + updateCellRef(); +} + +function updateFormulaBar() { + if (!state.formulaBarFocused) { + formulaBar.value = getCellValue(state.cursor.row, state.cursor.col); + } +} + +function updateCellRef() { + if (state.headers.length > 0 && state.rows.length > 0) { + cellRef.textContent = cellRefStr(state.cursor.row, state.cursor.col); + } else { + cellRef.textContent = ''; + } +} + +function scrollCursorIntoView() { + const td = tableBody.querySelector( + `td[data-row="${state.cursor.row}"][data-col="${state.cursor.col}"]` + ); + if (td) { + td.scrollIntoView({ block: 'nearest', inline: 'nearest' }); + } +} + +// ===== Header editing ===== +function startHeaderEdit(colIndex) { + state.editingHeader = colIndex; + const ths = tableHead.querySelectorAll('th'); + const th = ths[colIndex + 1]; // +1 for row number header + if (!th) return; + + const content = th.querySelector('.header-content'); + content.innerHTML = ''; + + const input = document.createElement('input'); + input.type = 'text'; + input.className = 'header-edit-input'; + input.value = state.headers[colIndex]; + content.appendChild(input); + input.focus(); + input.select(); + + const finish = () => { + state.headers[colIndex] = input.value; + state.editingHeader = -1; + renderHead(); + }; + + input.addEventListener('blur', finish); + input.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { input.blur(); } + if (e.key === 'Escape') { + input.value = state.headers[colIndex]; // revert + input.blur(); + } + e.stopPropagation(); + }); +} + +// ===== Column resizing ===== +let resizeState = null; + +function startColResize(e, colIndex) { + e.preventDefault(); + e.stopPropagation(); + const startX = e.clientX; + const startWidth = state.colWidths[colIndex] || DEFAULT_COL_WIDTH; + + resizeState = { colIndex, startX, startWidth }; + + const handle = e.target; + handle.classList.add('active'); + + const onMove = (ev) => { + const diff = ev.clientX - startX; + const newWidth = Math.max(MIN_COL_WIDTH, startWidth + diff); + state.colWidths[colIndex] = newWidth; + + // Update widths live + const ths = tableHead.querySelectorAll('th'); + if (ths[colIndex + 1]) ths[colIndex + 1].style.width = newWidth + 'px'; + + const allTds = tableBody.querySelectorAll(`td[data-col="${colIndex}"]`); + allTds.forEach(td => td.style.width = newWidth + 'px'); + }; + + const onUp = () => { + handle.classList.remove('active'); + document.removeEventListener('mousemove', onMove); + document.removeEventListener('mouseup', onUp); + resizeState = null; + }; + + document.addEventListener('mousemove', onMove); + document.addEventListener('mouseup', onUp); +} + +function bestFitColumn(colIndex) { + // Measure the widest content in this column + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + ctx.font = '13px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'; + + let maxW = ctx.measureText(state.headers[colIndex] || '').width; + + for (const row of state.rows) { + const val = row[colIndex] || ''; + const w = ctx.measureText(val).width; + if (w > maxW) maxW = w; + } + + // Add padding + const fitted = Math.ceil(maxW) + 24; // 12px padding each side + const width = Math.max(fitted, MAX_BEST_FIT); + state.colWidths[colIndex] = width; + render(); +} + +function bestFitAllColumns() { + for (let i = 0; i < state.headers.length; i++) { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + ctx.font = '13px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'; + + let maxW = ctx.measureText(state.headers[i] || '').width; + for (const row of state.rows) { + const w = ctx.measureText(row[i] || '').width; + if (w > maxW) maxW = w; + } + + const fitted = Math.ceil(maxW) + 24; + state.colWidths[i] = Math.max(fitted, MAX_BEST_FIT); + } + render(); +} + +// ===== Cell click & drag selection ===== +tableBody.addEventListener('mousedown', (e) => { + const td = e.target.closest('td[data-row]'); + if (!td) return; + + const r = parseInt(td.dataset.row); + const c = parseInt(td.dataset.col); + + // Commit formula bar if it was focused + if (state.formulaBarFocused) { + commitFormulaBar(); + } + + if (e.shiftKey) { + // Extend selection + state.selection = { + startRow: state.cursor.row, + startCol: state.cursor.col, + endRow: r, + endCol: c + }; + } else { + state.cursor = { row: r, col: c }; + state.selection = { startRow: r, startCol: c, endRow: r, endCol: c }; + } + + state.isSelecting = true; + updateSelectionClasses(); +}); + +document.addEventListener('mousemove', (e) => { + if (!state.isSelecting) return; + const td = e.target.closest('td[data-row]'); + if (!td) return; + + const r = parseInt(td.dataset.row); + const c = parseInt(td.dataset.col); + + if (state.selection) { + state.selection.endRow = r; + state.selection.endCol = c; + } + + updateSelectionClasses(); +}); + +document.addEventListener('mouseup', () => { + state.isSelecting = false; +}); + +// ===== Keyboard navigation ===== +function moveCursor(dr, dc, shift) { + if (shift) { + if (!state.selection) { + state.selection = { + startRow: state.cursor.row, + startCol: state.cursor.col, + endRow: state.cursor.row, + endCol: state.cursor.col + }; + } + const nr = Math.max(0, Math.min(state.rows.length - 1, state.selection.endRow + dr)); + const nc = Math.max(0, Math.min(state.headers.length - 1, state.selection.endCol + dc)); + state.selection.endRow = nr; + state.selection.endCol = nc; + } else { + const nr = Math.max(0, Math.min(state.rows.length - 1, state.cursor.row + dr)); + const nc = Math.max(0, Math.min(state.headers.length - 1, state.cursor.col + dc)); + state.cursor = { row: nr, col: nc }; + state.selection = null; + } + updateSelectionClasses(); + scrollCursorIntoView(); +} + +document.addEventListener('keydown', (e) => { + // Command palette + if ((e.metaKey || e.ctrlKey) && e.key === 'p') { + e.preventDefault(); + openCommandPalette(); + return; + } + + // If command palette is open, don't handle other keys + if (!commandPalette.classList.contains('hidden')) return; + + // If editing a header, don't interfere + if (state.editingHeader >= 0) return; + + // If formula bar is focused, handle Enter/Escape + if (state.formulaBarFocused) { + if (e.key === 'Enter') { + e.preventDefault(); + commitFormulaBar(); + if (state.formulaBarFromTable) { + formulaBar.blur(); + tableContainer.focus(); + } + return; + } + if (e.key === 'Escape') { + e.preventDefault(); + cancelFormulaBar(); + formulaBar.blur(); + tableContainer.focus(); + return; + } + return; // Let normal typing happen in formula bar + } + + // Shortcuts + const meta = e.metaKey || e.ctrlKey; + + if (meta && e.key === 's') { + e.preventDefault(); + executeCommand('save'); + return; + } + if (meta && e.key === 'c') { + e.preventDefault(); + executeCommand('copy'); + return; + } + if (meta && e.key === 'x') { + e.preventDefault(); + executeCommand('cut'); + return; + } + if (meta && e.key === 'v') { + e.preventDefault(); + executeCommand('paste'); + return; + } + if (meta && e.key === 'o') { + e.preventDefault(); + executeCommand('open'); + return; + } + + // Arrow keys + if (e.key === 'ArrowUp') { e.preventDefault(); moveCursor(-1, 0, e.shiftKey); return; } + if (e.key === 'ArrowDown') { e.preventDefault(); moveCursor(1, 0, e.shiftKey); return; } + if (e.key === 'ArrowLeft') { e.preventDefault(); moveCursor(0, -1, e.shiftKey); return; } + if (e.key === 'ArrowRight') { e.preventDefault(); moveCursor(0, 1, e.shiftKey); return; } + + // Tab + if (e.key === 'Tab') { + e.preventDefault(); + moveCursor(0, e.shiftKey ? -1 : 1, false); + return; + } + + // Enter moves down + if (e.key === 'Enter') { + e.preventDefault(); + moveCursor(1, 0, false); + return; + } + + // Delete/Backspace clears selected cells + if (e.key === 'Delete' || e.key === 'Backspace') { + e.preventDefault(); + clearSelectedCells(); + return; + } + + // Escape clears selection + if (e.key === 'Escape') { + state.selection = null; + updateSelectionClasses(); + return; + } + + // Typing a printable character: focus formula bar and start editing + if (e.key.length === 1 && !meta && !e.altKey) { + e.preventDefault(); + formulaBar.value = ''; + formulaBar.focus(); + state.formulaBarFocused = true; + state.formulaBarFromTable = true; + // Insert the typed character + formulaBar.value = e.key; + // Move cursor to end + formulaBar.setSelectionRange(formulaBar.value.length, formulaBar.value.length); + return; + } +}); + +// ===== Formula bar ===== +formulaBar.addEventListener('focus', () => { + state.formulaBarFocused = true; + // If not triggered by keyboard, this was a direct click + if (!state.formulaBarFromTable) { + formulaBar.value = getCellValue(state.cursor.row, state.cursor.col); + } +}); + +formulaBar.addEventListener('blur', () => { + state.formulaBarFocused = false; + state.formulaBarFromTable = false; +}); + +function commitFormulaBar() { + setCellValue(state.cursor.row, state.cursor.col, formulaBar.value); + state.formulaBarFocused = false; + state.formulaBarFromTable = false; + render(); +} + +function cancelFormulaBar() { + formulaBar.value = getCellValue(state.cursor.row, state.cursor.col); + state.formulaBarFocused = false; + state.formulaBarFromTable = false; +} + +// ===== Selection data extraction ===== +function getSelectedData() { + const sel = normalizeSelection(); + if (!sel) { + return { + headers: [state.headers[state.cursor.col]], + rows: [[getCellValue(state.cursor.row, state.cursor.col)]], + isSingleCol: true + }; + } + const headers = []; + for (let c = sel.c1; c <= sel.c2; c++) { + headers.push(state.headers[c] || ''); + } + const rows = []; + for (let r = sel.r1; r <= sel.r2; r++) { + const row = []; + for (let c = sel.c1; c <= sel.c2; c++) { + row.push(getCellValue(r, c)); + } + rows.push(row); + } + return { + headers, + rows, + isSingleCol: sel.c1 === sel.c2 + }; +} + +function clearSelectedCells() { + const sel = normalizeSelection(); + if (!sel) { + setCellValue(state.cursor.row, state.cursor.col, ''); + } else { + for (let r = sel.r1; r <= sel.r2; r++) { + for (let c = sel.c1; c <= sel.c2; c++) { + setCellValue(r, c, ''); + } + } + } + render(); +} + +// ===== Commands ===== +let commands = []; + +async function loadCommands() { + try { + commands = await window.go.main.CommandRegistry.GetCommands(); + } catch (e) { + console.error('Failed to load commands:', e); + // Fallback commands + commands = [ + { ID: 'copy', Name: 'Copy', Shortcut: 'Cmd+C' }, + { ID: 'cut', Name: 'Cut', Shortcut: 'Cmd+X' }, + { ID: 'copy-markdown', Name: 'Copy as Markdown', Shortcut: '' }, + { ID: 'copy-jira', Name: 'Copy as Jira', Shortcut: '' }, + { ID: 'paste', Name: 'Paste', Shortcut: 'Cmd+V' }, + { ID: 'resize-all', Name: 'Resize All Columns', Shortcut: '' }, + { ID: 'open', Name: 'Open File', Shortcut: 'Cmd+O' }, + { ID: 'save', Name: 'Save File', Shortcut: 'Cmd+S' }, + { ID: 'open-up', Name: 'Insert Row Above', Shortcut: '' }, + { ID: 'open-down', Name: 'Insert Row Below', Shortcut: '' }, + { ID: 'open-left', Name: 'Insert Column Left', Shortcut: '' }, + { ID: 'open-right', Name: 'Insert Column Right', Shortcut: '' }, + ]; + } +} + +function openCommandPalette() { + commandPalette.classList.remove('hidden'); + overlay.classList.remove('hidden'); + commandInput.value = ''; + renderCommandList(''); + commandInput.focus(); +} + +function closeCommandPalette() { + commandPalette.classList.add('hidden'); + overlay.classList.add('hidden'); + commandInput.value = ''; +} + +let activeCommandIndex = 0; + +function renderCommandList(filter) { + commandList.innerHTML = ''; + const lower = filter.toLowerCase(); + const filtered = commands.filter(c => c.Name.toLowerCase().includes(lower)); + + activeCommandIndex = 0; + + filtered.forEach((cmd, i) => { + const div = document.createElement('div'); + div.className = 'command-item' + (i === 0 ? ' active' : ''); + div.innerHTML = `${cmd.Name}` + + (cmd.Shortcut ? `${cmd.Shortcut}` : ''); + div.addEventListener('click', () => { + closeCommandPalette(); + executeCommand(cmd.ID); + }); + commandList.appendChild(div); + }); +} + +commandInput.addEventListener('input', () => { + renderCommandList(commandInput.value); +}); + +commandInput.addEventListener('keydown', (e) => { + const items = commandList.querySelectorAll('.command-item'); + if (e.key === 'Escape') { + e.preventDefault(); + closeCommandPalette(); + return; + } + if (e.key === 'ArrowDown') { + e.preventDefault(); + if (items.length === 0) return; + items[activeCommandIndex]?.classList.remove('active'); + activeCommandIndex = (activeCommandIndex + 1) % items.length; + items[activeCommandIndex]?.classList.add('active'); + items[activeCommandIndex]?.scrollIntoView({ block: 'nearest' }); + return; + } + if (e.key === 'ArrowUp') { + e.preventDefault(); + if (items.length === 0) return; + items[activeCommandIndex]?.classList.remove('active'); + activeCommandIndex = (activeCommandIndex - 1 + items.length) % items.length; + items[activeCommandIndex]?.classList.add('active'); + items[activeCommandIndex]?.scrollIntoView({ block: 'nearest' }); + return; + } + if (e.key === 'Enter') { + e.preventDefault(); + const items = commandList.querySelectorAll('.command-item'); + if (items[activeCommandIndex]) { + items[activeCommandIndex].click(); + } + return; + } + e.stopPropagation(); +}); + +overlay.addEventListener('click', closeCommandPalette); + +// ===== Command execution ===== +async function executeCommand(id) { + switch (id) { + case 'copy': await doCopy(); break; + case 'cut': await doCut(); break; + case 'copy-markdown': await doCopyMarkdown(); break; + case 'copy-jira': await doCopyJira(); break; + case 'paste': await doPaste(); break; + case 'resize-all': bestFitAllColumns(); setStatus('All columns resized'); break; + case 'open': await doOpen(); break; + case 'save': await doSave(); break; + case 'open-up': doInsertRowAbove(); break; + case 'open-down': doInsertRowBelow(); break; + case 'open-left': doInsertColLeft(); break; + case 'open-right': doInsertColRight(); break; + } +} + +async function doCopy() { + const data = getSelectedData(); + let text; + if (data.isSingleCol) { + try { + text = await window.go.main.App.FormatAsSingleColumn(data.rows); + } catch { + text = data.rows.map(r => r[0]).join('\n'); + } + } else { + try { + text = await window.go.main.App.FormatRowsAsCSV(data.rows); + } catch { + text = data.rows.map(r => r.join(',')).join('\n'); + } + } + await navigator.clipboard.writeText(text); + setStatus('Copied to clipboard'); +} + +async function doCut() { + await doCopy(); + clearSelectedCells(); + setStatus('Cut to clipboard'); +} + +async function doCopyMarkdown() { + const data = getSelectedData(); + let text; + try { + text = await window.go.main.App.FormatAsMarkdown(data.headers, data.rows); + } catch { + text = '(Markdown format unavailable)'; + } + await navigator.clipboard.writeText(text); + setStatus('Copied as Markdown'); +} + +async function doCopyJira() { + const data = getSelectedData(); + let text; + try { + text = await window.go.main.App.FormatAsJira(data.headers, data.rows); + } catch { + text = '(Jira format unavailable)'; + } + await navigator.clipboard.writeText(text); + setStatus('Copied as Jira'); +} + +async function doPaste() { + let text; + try { + text = await navigator.clipboard.readText(); + } catch { + setStatus('Failed to read clipboard'); + return; + } + + if (!text || !text.trim()) { + setStatus('Clipboard is empty'); + return; + } + + const lines = text.trim().split('\n'); + + // Check if it looks like CSV: multi-line, consistent comma-separated columns + let isCSV = false; + if (lines.length > 1) { + // Count commas outside quotes for each line + const colCounts = lines.map(line => { + let count = 1; + let inQuote = false; + for (const ch of line) { + if (ch === '"') inQuote = !inQuote; + else if (ch === ',' && !inQuote) count++; + } + return count; + }); + // Check if all lines have the same column count and > 1 + const first = colCounts[0]; + if (first > 1 && colCounts.every(c => c === first)) { + // Also check no spaces before commas (heuristic for CSV-like) + isCSV = true; + } + } + + let pasteRows; + if (isCSV) { + try { + const parsed = await window.go.main.App.ParseCSVString(text); + // Combine headers and rows since we're pasting into cells + pasteRows = [parsed.Headers, ...parsed.Rows]; + } catch { + // Fallback: split by comma + pasteRows = lines.map(l => l.split(',')); + } + } else { + // Each line is a single-column row + pasteRows = lines.map(l => [l]); + } + + // Place at cursor position + const startRow = state.cursor.row; + const startCol = state.cursor.col; + + for (let r = 0; r < pasteRows.length; r++) { + for (let c = 0; c < pasteRows[r].length; c++) { + const targetRow = startRow + r; + const targetCol = startCol + c; + // Extend grid if needed + while (state.rows.length <= targetRow) state.rows.push(new Array(state.headers.length).fill('')); + while (state.headers.length <= targetCol) { + state.headers.push(colLabel(state.headers.length)); + state.colWidths.push(DEFAULT_COL_WIDTH); + for (const row of state.rows) row.push(''); + } + setCellValue(targetRow, targetCol, pasteRows[r][c]); + } + } + + render(); + setStatus(`Pasted ${pasteRows.length} rows`); +} + +async function doOpen() { + let filePath; + try { + filePath = await window.go.main.App.OpenFileDialog(); + } catch (e) { + setStatus('Open cancelled'); + return; + } + if (!filePath) { + setStatus('Open cancelled'); + return; + } + await loadFile(filePath); +} + +async function doSave() { + if (state.filePath) { + try { + await window.go.main.App.SaveCurrentFile(state.headers, state.rows); + setStatus('Saved: ' + state.filePath); + } catch (e) { + setStatus('Error saving: ' + e); + } + } else { + let filePath; + try { + filePath = await window.go.main.App.SaveFileDialog(); + } catch { + setStatus('Save cancelled'); + return; + } + if (!filePath) { + setStatus('Save cancelled'); + return; + } + try { + await window.go.main.App.SaveCSV(filePath, state.headers, state.rows); + state.filePath = filePath; + setStatus('Saved: ' + filePath); + } catch (e) { + setStatus('Error saving: ' + e); + } + } +} + +function doInsertRowAbove() { + const r = state.cursor.row; + const newRow = new Array(state.headers.length).fill(''); + state.rows.splice(r, 0, newRow); + render(); + setStatus('Inserted row above'); +} + +function doInsertRowBelow() { + const r = state.cursor.row; + const newRow = new Array(state.headers.length).fill(''); + state.rows.splice(r + 1, 0, newRow); + state.cursor.row = r + 1; + render(); + setStatus('Inserted row below'); +} + +function doInsertColLeft() { + const c = state.cursor.col; + state.headers.splice(c, 0, colLabel(state.headers.length)); + state.colWidths.splice(c, 0, DEFAULT_COL_WIDTH); + for (const row of state.rows) { + row.splice(c, 0, ''); + } + render(); + setStatus('Inserted column left'); +} + +function doInsertColRight() { + const c = state.cursor.col + 1; + state.headers.splice(c, 0, colLabel(state.headers.length)); + state.colWidths.splice(c, 0, DEFAULT_COL_WIDTH); + for (const row of state.rows) { + row.splice(c, 0, ''); + } + state.cursor.col = c; + render(); + setStatus('Inserted column right'); +} + +// ===== File loading ===== +async function loadFile(filePath) { + try { + const data = await window.go.main.App.LoadCSV(filePath); + state.headers = data.Headers || []; + state.rows = data.Rows || []; + state.filePath = data.FilePath || filePath; + state.cursor = { row: 0, col: 0 }; + state.selection = null; + state.colWidths = state.headers.map(() => DEFAULT_COL_WIDTH); + ensureGridSize(); + render(); + setStatus('Loaded: ' + filePath); + } catch (e) { + setStatus('Error loading: ' + e); + } +} + +function loadEmptySheet() { + const cols = 10; + const rows = 30; + state.headers = Array.from({ length: cols }, (_, i) => colLabel(i)); + state.rows = Array.from({ length: rows }, () => new Array(cols).fill('')); + state.colWidths = new Array(cols).fill(DEFAULT_COL_WIDTH); + state.cursor = { row: 0, col: 0 }; + state.selection = null; + state.filePath = ''; + render(); +} + +// ===== File drop ===== +function setupFileDrop() { + try { + window.runtime.EventsOn('file-dropped', (filePath) => { + if (filePath && filePath.toLowerCase().endsWith('.csv')) { + loadFile(filePath); + } else if (filePath) { + // Try to load anyway + loadFile(filePath); + } + }); + } catch (e) { + console.warn('File drop events not available:', e); + } +} + +// ===== Init ===== +async function init() { + await loadCommands(); + setupFileDrop(); + loadEmptySheet(); +} + +init(); diff --git a/frontend/src/style.css b/frontend/src/style.css new file mode 100644 index 0000000..adf1d35 --- /dev/null +++ b/frontend/src/style.css @@ -0,0 +1,279 @@ +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +:root { + --bg: #ffffff; + --header-bg: #f0f0f0; + --border: #d0d0d0; + --selected-bg: #d4e6f9; + --selected-border: #2266cc; + --text: #222; + --row-num-bg: #f5f5f5; + --row-num-width: 50px; + --toolbar-height: 36px; + --status-height: 24px; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + font-size: 13px; +} + +html, body { + height: 100%; + overflow: hidden; + background: var(--bg); + color: var(--text); +} + +#app { + display: flex; + flex-direction: column; + height: 100vh; +} + +/* Toolbar / formula bar */ +#toolbar { + display: flex; + align-items: center; + height: var(--toolbar-height); + border-bottom: 1px solid var(--border); + background: var(--header-bg); + padding: 0 4px; + flex-shrink: 0; +} + +#cell-ref { + width: 80px; + text-align: center; + font-weight: 600; + border-right: 1px solid var(--border); + padding: 0 8px; + line-height: var(--toolbar-height); + flex-shrink: 0; + font-size: 12px; + color: #555; +} + +#formula-bar { + flex: 1; + border: none; + outline: none; + padding: 4px 8px; + font-size: 13px; + font-family: inherit; + background: var(--bg); + height: 100%; +} + +#formula-bar:focus { + background: #fff; + box-shadow: inset 0 0 0 1px var(--selected-border); +} + +/* Table container */ +#table-container { + flex: 1; + overflow: auto; + position: relative; +} + +#csv-table { + border-collapse: collapse; + table-layout: fixed; + min-width: 100%; +} + +/* Header row */ +#table-head th { + position: sticky; + top: 0; + z-index: 2; + background: var(--header-bg); + border: 1px solid var(--border); + border-top: none; + padding: 4px 6px; + font-weight: 600; + text-align: left; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + user-select: none; + cursor: default; + height: 26px; + font-size: 12px; +} + +#table-head th.row-number-header { + width: var(--row-num-width); + min-width: var(--row-num-width); + max-width: var(--row-num-width); + position: sticky; + left: 0; + z-index: 3; + text-align: center; + background: var(--header-bg); +} + +/* Column resize handle */ +.col-resize-handle { + position: absolute; + right: -3px; + top: 0; + bottom: 0; + width: 6px; + cursor: col-resize; + z-index: 4; +} + +.col-resize-handle:hover, +.col-resize-handle.active { + background: var(--selected-border); + opacity: 0.3; +} + +th .header-content { + display: flex; + align-items: center; + position: relative; + overflow: hidden; + text-overflow: ellipsis; +} + +th .header-text { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; +} + +th .header-edit-input { + width: 100%; + border: 1px solid var(--selected-border); + outline: none; + padding: 1px 4px; + font-size: 12px; + font-weight: 600; + font-family: inherit; +} + +/* Data cells */ +#table-body td { + border: 1px solid var(--border); + padding: 2px 6px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + height: 24px; + cursor: cell; + font-size: 13px; +} + +#table-body td.row-number { + position: sticky; + left: 0; + z-index: 1; + background: var(--row-num-bg); + text-align: center; + font-size: 11px; + color: #888; + width: var(--row-num-width); + min-width: var(--row-num-width); + max-width: var(--row-num-width); + cursor: default; + user-select: none; +} + +/* Selection */ +td.selected { + background: var(--selected-bg) !important; +} + +td.cursor-cell { + outline: 2px solid var(--selected-border); + outline-offset: -1px; + z-index: 1; + position: relative; +} + +/* Status bar */ +#status-bar { + height: var(--status-height); + line-height: var(--status-height); + border-top: 1px solid var(--border); + padding: 0 12px; + background: var(--header-bg); + font-size: 11px; + color: #666; + flex-shrink: 0; +} + +/* Command Palette */ +#overlay { + position: fixed; + inset: 0; + background: rgba(0,0,0,0.3); + z-index: 99; +} + +#overlay.hidden, #command-palette.hidden { + display: none; +} + +#command-palette { + position: fixed; + top: 80px; + left: 50%; + transform: translateX(-50%); + width: 460px; + background: #fff; + border: 1px solid var(--border); + border-radius: 8px; + box-shadow: 0 8px 32px rgba(0,0,0,0.2); + z-index: 100; + overflow: hidden; +} + +#command-input { + width: 100%; + border: none; + border-bottom: 1px solid var(--border); + padding: 12px 16px; + font-size: 15px; + outline: none; + font-family: inherit; +} + +#command-list { + max-height: 300px; + overflow-y: auto; +} + +.command-item { + padding: 8px 16px; + cursor: pointer; + display: flex; + justify-content: space-between; + align-items: center; +} + +.command-item:hover, +.command-item.active { + background: var(--selected-bg); +} + +.command-item .cmd-name { + font-size: 14px; +} + +.command-item .cmd-shortcut { + font-size: 11px; + color: #888; + background: #eee; + padding: 2px 6px; + border-radius: 3px; +} + +/* Drag over style */ +#table-container.drag-over { + background: var(--selected-bg); +} diff --git a/frontend/wailsjs/go/main/App.d.ts b/frontend/wailsjs/go/main/App.d.ts new file mode 100755 index 0000000..8e611b7 --- /dev/null +++ b/frontend/wailsjs/go/main/App.d.ts @@ -0,0 +1,31 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT +import {main} from '../models'; + +export function FormatAsCSV(arg1:Array,arg2:Array):Promise; + +export function FormatAsJira(arg1:Array,arg2:Array):Promise; + +export function FormatAsMarkdown(arg1:Array,arg2:Array):Promise; + +export function FormatAsSingleColumn(arg1:Array):Promise; + +export function FormatRowsAsCSV(arg1:Array):Promise; + +export function GetTableData():Promise; + +export function LoadCSV(arg1:string):Promise; + +export function OpenFileDialog():Promise; + +export function ParseCSVString(arg1:string):Promise; + +export function SaveCSV(arg1:string,arg2:Array,arg3:Array):Promise; + +export function SaveCurrentFile(arg1:Array,arg2:Array):Promise; + +export function SaveFileDialog():Promise; + +export function SetTableData(arg1:Array,arg2:Array):Promise; + +export function SetWindowTitle(arg1:string):Promise; diff --git a/frontend/wailsjs/go/main/App.js b/frontend/wailsjs/go/main/App.js new file mode 100755 index 0000000..ee27c26 --- /dev/null +++ b/frontend/wailsjs/go/main/App.js @@ -0,0 +1,59 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export function FormatAsCSV(arg1, arg2) { + return window['go']['main']['App']['FormatAsCSV'](arg1, arg2); +} + +export function FormatAsJira(arg1, arg2) { + return window['go']['main']['App']['FormatAsJira'](arg1, arg2); +} + +export function FormatAsMarkdown(arg1, arg2) { + return window['go']['main']['App']['FormatAsMarkdown'](arg1, arg2); +} + +export function FormatAsSingleColumn(arg1) { + return window['go']['main']['App']['FormatAsSingleColumn'](arg1); +} + +export function FormatRowsAsCSV(arg1) { + return window['go']['main']['App']['FormatRowsAsCSV'](arg1); +} + +export function GetTableData() { + return window['go']['main']['App']['GetTableData'](); +} + +export function LoadCSV(arg1) { + return window['go']['main']['App']['LoadCSV'](arg1); +} + +export function OpenFileDialog() { + return window['go']['main']['App']['OpenFileDialog'](); +} + +export function ParseCSVString(arg1) { + return window['go']['main']['App']['ParseCSVString'](arg1); +} + +export function SaveCSV(arg1, arg2, arg3) { + return window['go']['main']['App']['SaveCSV'](arg1, arg2, arg3); +} + +export function SaveCurrentFile(arg1, arg2) { + return window['go']['main']['App']['SaveCurrentFile'](arg1, arg2); +} + +export function SaveFileDialog() { + return window['go']['main']['App']['SaveFileDialog'](); +} + +export function SetTableData(arg1, arg2) { + return window['go']['main']['App']['SetTableData'](arg1, arg2); +} + +export function SetWindowTitle(arg1) { + return window['go']['main']['App']['SetWindowTitle'](arg1); +} diff --git a/frontend/wailsjs/go/main/CommandRegistry.d.ts b/frontend/wailsjs/go/main/CommandRegistry.d.ts new file mode 100755 index 0000000..f30ae82 --- /dev/null +++ b/frontend/wailsjs/go/main/CommandRegistry.d.ts @@ -0,0 +1,5 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT +import {main} from '../models'; + +export function GetCommands():Promise>; diff --git a/frontend/wailsjs/go/main/CommandRegistry.js b/frontend/wailsjs/go/main/CommandRegistry.js new file mode 100755 index 0000000..a14a993 --- /dev/null +++ b/frontend/wailsjs/go/main/CommandRegistry.js @@ -0,0 +1,7 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export function GetCommands() { + return window['go']['main']['CommandRegistry']['GetCommands'](); +} diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts new file mode 100755 index 0000000..d91414f --- /dev/null +++ b/frontend/wailsjs/go/models.ts @@ -0,0 +1,37 @@ +export namespace main { + + export class CSVData { + Headers: string[]; + Rows: string[][]; + FilePath: string; + + static createFrom(source: any = {}) { + return new CSVData(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.Headers = source["Headers"]; + this.Rows = source["Rows"]; + this.FilePath = source["FilePath"]; + } + } + export class Command { + ID: string; + Name: string; + Shortcut: string; + + static createFrom(source: any = {}) { + return new Command(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.ID = source["ID"]; + this.Name = source["Name"]; + this.Shortcut = source["Shortcut"]; + } + } + +} + diff --git a/frontend/wailsjs/runtime/package.json b/frontend/wailsjs/runtime/package.json new file mode 100644 index 0000000..1e7c8a5 --- /dev/null +++ b/frontend/wailsjs/runtime/package.json @@ -0,0 +1,24 @@ +{ + "name": "@wailsapp/runtime", + "version": "2.0.0", + "description": "Wails Javascript runtime library", + "main": "runtime.js", + "types": "runtime.d.ts", + "scripts": { + }, + "repository": { + "type": "git", + "url": "git+https://github.com/wailsapp/wails.git" + }, + "keywords": [ + "Wails", + "Javascript", + "Go" + ], + "author": "Lea Anthony ", + "license": "MIT", + "bugs": { + "url": "https://github.com/wailsapp/wails/issues" + }, + "homepage": "https://github.com/wailsapp/wails#readme" +} diff --git a/frontend/wailsjs/runtime/runtime.d.ts b/frontend/wailsjs/runtime/runtime.d.ts new file mode 100644 index 0000000..4445dac --- /dev/null +++ b/frontend/wailsjs/runtime/runtime.d.ts @@ -0,0 +1,249 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export interface Position { + x: number; + y: number; +} + +export interface Size { + w: number; + h: number; +} + +export interface Screen { + isCurrent: boolean; + isPrimary: boolean; + width : number + height : number +} + +// Environment information such as platform, buildtype, ... +export interface EnvironmentInfo { + buildType: string; + platform: string; + arch: string; +} + +// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit) +// emits the given event. Optional data may be passed with the event. +// This will trigger any event listeners. +export function EventsEmit(eventName: string, ...data: any): void; + +// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name. +export function EventsOn(eventName: string, callback: (...data: any) => void): () => void; + +// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple) +// sets up a listener for the given event name, but will only trigger a given number times. +export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void; + +// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce) +// sets up a listener for the given event name, but will only trigger once. +export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void; + +// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff) +// unregisters the listener for the given event name. +export function EventsOff(eventName: string, ...additionalEventNames: string[]): void; + +// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall) +// unregisters all listeners. +export function EventsOffAll(): void; + +// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) +// logs the given message as a raw message +export function LogPrint(message: string): void; + +// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace) +// logs the given message at the `trace` log level. +export function LogTrace(message: string): void; + +// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug) +// logs the given message at the `debug` log level. +export function LogDebug(message: string): void; + +// [LogError](https://wails.io/docs/reference/runtime/log#logerror) +// logs the given message at the `error` log level. +export function LogError(message: string): void; + +// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal) +// logs the given message at the `fatal` log level. +// The application will quit after calling this method. +export function LogFatal(message: string): void; + +// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo) +// logs the given message at the `info` log level. +export function LogInfo(message: string): void; + +// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning) +// logs the given message at the `warning` log level. +export function LogWarning(message: string): void; + +// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload) +// Forces a reload by the main application as well as connected browsers. +export function WindowReload(): void; + +// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp) +// Reloads the application frontend. +export function WindowReloadApp(): void; + +// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop) +// Sets the window AlwaysOnTop or not on top. +export function WindowSetAlwaysOnTop(b: boolean): void; + +// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme) +// *Windows only* +// Sets window theme to system default (dark/light). +export function WindowSetSystemDefaultTheme(): void; + +// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme) +// *Windows only* +// Sets window to light theme. +export function WindowSetLightTheme(): void; + +// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme) +// *Windows only* +// Sets window to dark theme. +export function WindowSetDarkTheme(): void; + +// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter) +// Centers the window on the monitor the window is currently on. +export function WindowCenter(): void; + +// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle) +// Sets the text in the window title bar. +export function WindowSetTitle(title: string): void; + +// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen) +// Makes the window full screen. +export function WindowFullscreen(): void; + +// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen) +// Restores the previous window dimensions and position prior to full screen. +export function WindowUnfullscreen(): void; + +// [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen) +// Returns the state of the window, i.e. whether the window is in full screen mode or not. +export function WindowIsFullscreen(): Promise; + +// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize) +// Sets the width and height of the window. +export function WindowSetSize(width: number, height: number): void; + +// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize) +// Gets the width and height of the window. +export function WindowGetSize(): Promise; + +// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize) +// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMaxSize(width: number, height: number): void; + +// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize) +// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMinSize(width: number, height: number): void; + +// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition) +// Sets the window position relative to the monitor the window is currently on. +export function WindowSetPosition(x: number, y: number): void; + +// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition) +// Gets the window position relative to the monitor the window is currently on. +export function WindowGetPosition(): Promise; + +// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide) +// Hides the window. +export function WindowHide(): void; + +// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow) +// Shows the window, if it is currently hidden. +export function WindowShow(): void; + +// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise) +// Maximises the window to fill the screen. +export function WindowMaximise(): void; + +// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise) +// Toggles between Maximised and UnMaximised. +export function WindowToggleMaximise(): void; + +// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise) +// Restores the window to the dimensions and position prior to maximising. +export function WindowUnmaximise(): void; + +// [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised) +// Returns the state of the window, i.e. whether the window is maximised or not. +export function WindowIsMaximised(): Promise; + +// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise) +// Minimises the window. +export function WindowMinimise(): void; + +// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise) +// Restores the window to the dimensions and position prior to minimising. +export function WindowUnminimise(): void; + +// [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised) +// Returns the state of the window, i.e. whether the window is minimised or not. +export function WindowIsMinimised(): Promise; + +// [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal) +// Returns the state of the window, i.e. whether the window is normal or not. +export function WindowIsNormal(): Promise; + +// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour) +// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels. +export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void; + +// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall) +// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system. +export function ScreenGetAll(): Promise; + +// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl) +// Opens the given URL in the system browser. +export function BrowserOpenURL(url: string): void; + +// [Environment](https://wails.io/docs/reference/runtime/intro#environment) +// Returns information about the environment +export function Environment(): Promise; + +// [Quit](https://wails.io/docs/reference/runtime/intro#quit) +// Quits the application. +export function Quit(): void; + +// [Hide](https://wails.io/docs/reference/runtime/intro#hide) +// Hides the application. +export function Hide(): void; + +// [Show](https://wails.io/docs/reference/runtime/intro#show) +// Shows the application. +export function Show(): void; + +// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext) +// Returns the current text stored on clipboard +export function ClipboardGetText(): Promise; + +// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext) +// Sets a text on the clipboard +export function ClipboardSetText(text: string): Promise; + +// [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop) +// OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings. +export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void + +// [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff) +// OnFileDropOff removes the drag and drop listeners and handlers. +export function OnFileDropOff() :void + +// Check if the file path resolver is available +export function CanResolveFilePaths(): boolean; + +// Resolves file paths for an array of files +export function ResolveFilePaths(files: File[]): void \ No newline at end of file diff --git a/frontend/wailsjs/runtime/runtime.js b/frontend/wailsjs/runtime/runtime.js new file mode 100644 index 0000000..7cb89d7 --- /dev/null +++ b/frontend/wailsjs/runtime/runtime.js @@ -0,0 +1,242 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export function LogPrint(message) { + window.runtime.LogPrint(message); +} + +export function LogTrace(message) { + window.runtime.LogTrace(message); +} + +export function LogDebug(message) { + window.runtime.LogDebug(message); +} + +export function LogInfo(message) { + window.runtime.LogInfo(message); +} + +export function LogWarning(message) { + window.runtime.LogWarning(message); +} + +export function LogError(message) { + window.runtime.LogError(message); +} + +export function LogFatal(message) { + window.runtime.LogFatal(message); +} + +export function EventsOnMultiple(eventName, callback, maxCallbacks) { + return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks); +} + +export function EventsOn(eventName, callback) { + return EventsOnMultiple(eventName, callback, -1); +} + +export function EventsOff(eventName, ...additionalEventNames) { + return window.runtime.EventsOff(eventName, ...additionalEventNames); +} + +export function EventsOffAll() { + return window.runtime.EventsOffAll(); +} + +export function EventsOnce(eventName, callback) { + return EventsOnMultiple(eventName, callback, 1); +} + +export function EventsEmit(eventName) { + let args = [eventName].slice.call(arguments); + return window.runtime.EventsEmit.apply(null, args); +} + +export function WindowReload() { + window.runtime.WindowReload(); +} + +export function WindowReloadApp() { + window.runtime.WindowReloadApp(); +} + +export function WindowSetAlwaysOnTop(b) { + window.runtime.WindowSetAlwaysOnTop(b); +} + +export function WindowSetSystemDefaultTheme() { + window.runtime.WindowSetSystemDefaultTheme(); +} + +export function WindowSetLightTheme() { + window.runtime.WindowSetLightTheme(); +} + +export function WindowSetDarkTheme() { + window.runtime.WindowSetDarkTheme(); +} + +export function WindowCenter() { + window.runtime.WindowCenter(); +} + +export function WindowSetTitle(title) { + window.runtime.WindowSetTitle(title); +} + +export function WindowFullscreen() { + window.runtime.WindowFullscreen(); +} + +export function WindowUnfullscreen() { + window.runtime.WindowUnfullscreen(); +} + +export function WindowIsFullscreen() { + return window.runtime.WindowIsFullscreen(); +} + +export function WindowGetSize() { + return window.runtime.WindowGetSize(); +} + +export function WindowSetSize(width, height) { + window.runtime.WindowSetSize(width, height); +} + +export function WindowSetMaxSize(width, height) { + window.runtime.WindowSetMaxSize(width, height); +} + +export function WindowSetMinSize(width, height) { + window.runtime.WindowSetMinSize(width, height); +} + +export function WindowSetPosition(x, y) { + window.runtime.WindowSetPosition(x, y); +} + +export function WindowGetPosition() { + return window.runtime.WindowGetPosition(); +} + +export function WindowHide() { + window.runtime.WindowHide(); +} + +export function WindowShow() { + window.runtime.WindowShow(); +} + +export function WindowMaximise() { + window.runtime.WindowMaximise(); +} + +export function WindowToggleMaximise() { + window.runtime.WindowToggleMaximise(); +} + +export function WindowUnmaximise() { + window.runtime.WindowUnmaximise(); +} + +export function WindowIsMaximised() { + return window.runtime.WindowIsMaximised(); +} + +export function WindowMinimise() { + window.runtime.WindowMinimise(); +} + +export function WindowUnminimise() { + window.runtime.WindowUnminimise(); +} + +export function WindowSetBackgroundColour(R, G, B, A) { + window.runtime.WindowSetBackgroundColour(R, G, B, A); +} + +export function ScreenGetAll() { + return window.runtime.ScreenGetAll(); +} + +export function WindowIsMinimised() { + return window.runtime.WindowIsMinimised(); +} + +export function WindowIsNormal() { + return window.runtime.WindowIsNormal(); +} + +export function BrowserOpenURL(url) { + window.runtime.BrowserOpenURL(url); +} + +export function Environment() { + return window.runtime.Environment(); +} + +export function Quit() { + window.runtime.Quit(); +} + +export function Hide() { + window.runtime.Hide(); +} + +export function Show() { + window.runtime.Show(); +} + +export function ClipboardGetText() { + return window.runtime.ClipboardGetText(); +} + +export function ClipboardSetText(text) { + return window.runtime.ClipboardSetText(text); +} + +/** + * Callback for OnFileDrop returns a slice of file path strings when a drop is finished. + * + * @export + * @callback OnFileDropCallback + * @param {number} x - x coordinate of the drop + * @param {number} y - y coordinate of the drop + * @param {string[]} paths - A list of file paths. + */ + +/** + * OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings. + * + * @export + * @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished. + * @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target) + */ +export function OnFileDrop(callback, useDropTarget) { + return window.runtime.OnFileDrop(callback, useDropTarget); +} + +/** + * OnFileDropOff removes the drag and drop listeners and handlers. + */ +export function OnFileDropOff() { + return window.runtime.OnFileDropOff(); +} + +export function CanResolveFilePaths() { + return window.runtime.CanResolveFilePaths(); +} + +export function ResolveFilePaths(files) { + return window.runtime.ResolveFilePaths(files); +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..03a76a3 --- /dev/null +++ b/go.mod @@ -0,0 +1,37 @@ +module csvtool + +go 1.22.0 + +toolchain go1.22.2 + +require github.com/wailsapp/wails/v2 v2.11.0 + +require ( + github.com/bep/debounce v1.2.1 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect + github.com/labstack/echo/v4 v4.13.3 // indirect + github.com/labstack/gommon v0.4.2 // indirect + github.com/leaanthony/go-ansi-parser v1.6.1 // indirect + github.com/leaanthony/gosod v1.0.4 // indirect + github.com/leaanthony/slicer v1.6.0 // indirect + github.com/leaanthony/u v1.1.1 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/samber/lo v1.49.1 // indirect + github.com/tkrajina/go-reflector v0.5.8 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect + github.com/wailsapp/go-webview2 v1.0.22 // indirect + github.com/wailsapp/mimetype v1.4.1 // indirect + golang.org/x/crypto v0.33.0 // indirect + golang.org/x/net v0.35.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.22.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e3658ec --- /dev/null +++ b/go.sum @@ -0,0 +1,81 @@ +github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= +github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck= +github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= +github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY= +github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g= +github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc= +github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA= +github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A= +github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU= +github.com/leaanthony/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI= +github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw= +github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js= +github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8= +github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M= +github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI= +github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= +github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +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/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +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/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew= +github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ= +github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/wailsapp/go-webview2 v1.0.22 h1:YT61F5lj+GGaat5OB96Aa3b4QA+mybD0Ggq6NZijQ58= +github.com/wailsapp/go-webview2 v1.0.22/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= +github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs= +github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= +github.com/wailsapp/wails/v2 v2.11.0 h1:seLacV8pqupq32IjS4Y7V8ucab0WZwtK6VvUVxSBtqQ= +github.com/wailsapp/wails/v2 v2.11.0/go.mod h1:jrf0ZaM6+GBc1wRmXsM8cIvzlg0karYin3erahI4+0k= +golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +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.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..3dd91b6 --- /dev/null +++ b/main.go @@ -0,0 +1,48 @@ +package main + +import ( + "embed" + + "github.com/wailsapp/wails/v2" + "github.com/wailsapp/wails/v2/pkg/options" + "github.com/wailsapp/wails/v2/pkg/options/assetserver" + "github.com/wailsapp/wails/v2/pkg/options/mac" +) + +//go:embed all:frontend/dist +var assets embed.FS + +func main() { + app := NewApp() + cmdRegistry := NewCommandRegistry() + + err := wails.Run(&options.App{ + Title: "CSV Tool", + Width: 1200, + Height: 800, + AssetServer: &assetserver.Options{ + Assets: assets, + }, + BackgroundColour: &options.RGBA{R: 255, G: 255, B: 255, A: 255}, + OnStartup: app.startup, + OnDomReady: app.domReady, + Bind: []interface{}{ + app, + cmdRegistry, + }, + DragAndDrop: &options.DragAndDrop{ + EnableFileDrop: true, + DisableWebViewDrop: true, + }, + Mac: &mac.Options{ + TitleBar: mac.TitleBarDefault(), + Appearance: mac.NSAppearanceNameAqua, + WebviewIsTransparent: false, + WindowIsTranslucent: false, + }, + }) + + if err != nil { + println("Error:", err.Error()) + } +} diff --git a/wails.json b/wails.json new file mode 100644 index 0000000..0b813ce --- /dev/null +++ b/wails.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://wails.io/schemas/config.v2.json", + "name": "csvtool", + "outputfilename": "csvtool", + "frontend:install": "npm install", + "frontend:build": "npm run build", + "frontend:dev:watcher": "npm run dev", + "frontend:dev:serverUrl": "auto", + "author": { + "name": "", + "email": "" + } +}