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 <shelley@exe.dev>
This commit is contained in:
commit
55a25f6d63
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
build/bin
|
||||||
|
node_modules
|
||||||
|
frontend/dist
|
||||||
19
README.md
Normal file
19
README.md
Normal file
|
|
@ -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`.
|
||||||
321
app.go
Normal file
321
app.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
124
app_test.go
Normal file
124
app_test.go
Normal file
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
35
build/README.md
Normal file
35
build/README.md
Normal file
|
|
@ -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.
|
||||||
BIN
build/appicon.png
Normal file
BIN
build/appicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 130 KiB |
34
commands.go
Normal file
34
commands.go
Normal file
|
|
@ -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: ""},
|
||||||
|
}
|
||||||
|
}
|
||||||
32
frontend/index.html
Normal file
32
frontend/index.html
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8"/>
|
||||||
|
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||||
|
<title>CSV Tool</title>
|
||||||
|
<link rel="stylesheet" href="./src/style.css"/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app">
|
||||||
|
<div id="toolbar">
|
||||||
|
<div id="cell-ref"></div>
|
||||||
|
<input type="text" id="formula-bar" placeholder="Select a cell to edit" />
|
||||||
|
</div>
|
||||||
|
<div id="table-container" style="--wails-drop-target:drop">
|
||||||
|
<table id="csv-table">
|
||||||
|
<thead id="table-head"></thead>
|
||||||
|
<tbody id="table-body"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div id="status-bar">
|
||||||
|
<span id="status-text">Ready</span>
|
||||||
|
</div>
|
||||||
|
<div id="command-palette" class="hidden">
|
||||||
|
<input type="text" id="command-input" placeholder="Type a command..." />
|
||||||
|
<div id="command-list"></div>
|
||||||
|
</div>
|
||||||
|
<div id="overlay" class="hidden"></div>
|
||||||
|
</div>
|
||||||
|
<script src="./src/main.js" type="module"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
617
frontend/package-lock.json
generated
Normal file
617
frontend/package-lock.json
generated
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
frontend/package.json
Normal file
13
frontend/package.json
Normal file
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
973
frontend/src/main.js
Normal file
973
frontend/src/main.js
Normal file
|
|
@ -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 = `<span class="cmd-name">${cmd.Name}</span>` +
|
||||||
|
(cmd.Shortcut ? `<span class="cmd-shortcut">${cmd.Shortcut}</span>` : '');
|
||||||
|
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();
|
||||||
279
frontend/src/style.css
Normal file
279
frontend/src/style.css
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
31
frontend/wailsjs/go/main/App.d.ts
vendored
Executable file
31
frontend/wailsjs/go/main/App.d.ts
vendored
Executable file
|
|
@ -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<string>,arg2:Array<any>):Promise<string>;
|
||||||
|
|
||||||
|
export function FormatAsJira(arg1:Array<string>,arg2:Array<any>):Promise<string>;
|
||||||
|
|
||||||
|
export function FormatAsMarkdown(arg1:Array<string>,arg2:Array<any>):Promise<string>;
|
||||||
|
|
||||||
|
export function FormatAsSingleColumn(arg1:Array<any>):Promise<string>;
|
||||||
|
|
||||||
|
export function FormatRowsAsCSV(arg1:Array<any>):Promise<string>;
|
||||||
|
|
||||||
|
export function GetTableData():Promise<main.CSVData>;
|
||||||
|
|
||||||
|
export function LoadCSV(arg1:string):Promise<main.CSVData>;
|
||||||
|
|
||||||
|
export function OpenFileDialog():Promise<string>;
|
||||||
|
|
||||||
|
export function ParseCSVString(arg1:string):Promise<main.CSVData>;
|
||||||
|
|
||||||
|
export function SaveCSV(arg1:string,arg2:Array<string>,arg3:Array<any>):Promise<void>;
|
||||||
|
|
||||||
|
export function SaveCurrentFile(arg1:Array<string>,arg2:Array<any>):Promise<void>;
|
||||||
|
|
||||||
|
export function SaveFileDialog():Promise<string>;
|
||||||
|
|
||||||
|
export function SetTableData(arg1:Array<string>,arg2:Array<any>):Promise<void>;
|
||||||
|
|
||||||
|
export function SetWindowTitle(arg1:string):Promise<void>;
|
||||||
59
frontend/wailsjs/go/main/App.js
Executable file
59
frontend/wailsjs/go/main/App.js
Executable file
|
|
@ -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);
|
||||||
|
}
|
||||||
5
frontend/wailsjs/go/main/CommandRegistry.d.ts
vendored
Executable file
5
frontend/wailsjs/go/main/CommandRegistry.d.ts
vendored
Executable file
|
|
@ -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<Array<main.Command>>;
|
||||||
7
frontend/wailsjs/go/main/CommandRegistry.js
Executable file
7
frontend/wailsjs/go/main/CommandRegistry.js
Executable file
|
|
@ -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']();
|
||||||
|
}
|
||||||
37
frontend/wailsjs/go/models.ts
Executable file
37
frontend/wailsjs/go/models.ts
Executable file
|
|
@ -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"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
24
frontend/wailsjs/runtime/package.json
Normal file
24
frontend/wailsjs/runtime/package.json
Normal file
|
|
@ -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 <lea.anthony@gmail.com>",
|
||||||
|
"license": "MIT",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/wailsapp/wails/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/wailsapp/wails#readme"
|
||||||
|
}
|
||||||
249
frontend/wailsjs/runtime/runtime.d.ts
vendored
Normal file
249
frontend/wailsjs/runtime/runtime.d.ts
vendored
Normal file
|
|
@ -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<boolean>;
|
||||||
|
|
||||||
|
// [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<Size>;
|
||||||
|
|
||||||
|
// [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<Position>;
|
||||||
|
|
||||||
|
// [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<boolean>;
|
||||||
|
|
||||||
|
// [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<boolean>;
|
||||||
|
|
||||||
|
// [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<boolean>;
|
||||||
|
|
||||||
|
// [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<Screen[]>;
|
||||||
|
|
||||||
|
// [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<EnvironmentInfo>;
|
||||||
|
|
||||||
|
// [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<string>;
|
||||||
|
|
||||||
|
// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext)
|
||||||
|
// Sets a text on the clipboard
|
||||||
|
export function ClipboardSetText(text: string): Promise<boolean>;
|
||||||
|
|
||||||
|
// [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
|
||||||
242
frontend/wailsjs/runtime/runtime.js
Normal file
242
frontend/wailsjs/runtime/runtime.js
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
37
go.mod
Normal file
37
go.mod
Normal file
|
|
@ -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
|
||||||
|
)
|
||||||
81
go.sum
Normal file
81
go.sum
Normal file
|
|
@ -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=
|
||||||
48
main.go
Normal file
48
main.go
Normal file
|
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
13
wails.json
Normal file
13
wails.json
Normal file
|
|
@ -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": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue