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:
exe.dev user 2026-03-03 21:34:09 +00:00
commit 55a25f6d63
24 changed files with 3283 additions and 0 deletions

32
frontend/index.html Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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);
}

View 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>>;

View 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
View 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"];
}
}
}

View 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
View 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

View 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);
}