mirror of
https://github.com/taigrr/wails.git
synced 2026-04-06 07:02:42 -07:00
Compare commits
35 Commits
474-Custom
...
feature/v2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4138779c5e | ||
|
|
ae04b4fcc0 | ||
|
|
5eb91dd3fa | ||
|
|
3ad537fdbb | ||
|
|
2c570bb4f6 | ||
|
|
461f3aec0a | ||
|
|
fd47122e39 | ||
|
|
8de013f192 | ||
|
|
1dd3a602d7 | ||
|
|
a84a49a13f | ||
|
|
64a6a69bbd | ||
|
|
d75b9f26f1 | ||
|
|
10cb7f830f | ||
|
|
dd3e6de9b2 | ||
|
|
d42b84abc1 | ||
|
|
b6c649041b | ||
|
|
93ec65be6a | ||
|
|
bfa8929c47 | ||
|
|
360713c803 | ||
|
|
26ce682824 | ||
|
|
65b546c0f9 | ||
|
|
31494bba22 | ||
|
|
f25abb0b26 | ||
|
|
e831bc75c6 | ||
|
|
852bbd148c | ||
|
|
c158fd369a | ||
|
|
a213e8bcd1 | ||
|
|
8a6c44963d | ||
|
|
d31c5522e9 | ||
|
|
6e5b2ce871 | ||
|
|
c2c9c331cd | ||
|
|
9d5280d4d6 | ||
|
|
01a1288364 | ||
|
|
5cd34e07b9 | ||
|
|
2290f36939 |
7
.gitignore
vendored
7
.gitignore
vendored
@@ -16,4 +16,9 @@ examples/**/example*
|
||||
cmd/wails/wails
|
||||
.DS_Store
|
||||
tmp
|
||||
node_modules/
|
||||
node_modules/
|
||||
package.json.md5
|
||||
v2/test/**/frontend/dist
|
||||
v2/test/**/build/
|
||||
v2/test/frameless/icon.png
|
||||
v2/test/hidden/icon.png
|
||||
|
||||
@@ -31,3 +31,4 @@ Wails is what it is because of the time and effort given by these great people.
|
||||
* [Christopher Murphy](https://github.com/Splode)
|
||||
* [Zámbó, Levente](https://github.com/Lyimmi)
|
||||
* [artem](https://github.com/Unix4ever)
|
||||
* [Tim Kipp](https://github.com/timkippdev)
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -37,13 +37,13 @@ func NewSystemHelper() *SystemHelper {
|
||||
// setSystemDirs calculates the system directories it is interested in
|
||||
func (s *SystemHelper) setSystemDirs() {
|
||||
var err error
|
||||
s.homeDir, err = os.UserConfigDir()
|
||||
s.homeDir, err = os.UserHomeDir()
|
||||
if err != nil {
|
||||
log.Fatal("Cannot find home directory! Please file a bug report!")
|
||||
}
|
||||
|
||||
// TODO: A better config system
|
||||
s.wailsSystemDir = filepath.Join(s.homeDir, "wails")
|
||||
s.wailsSystemDir = filepath.Join(s.homeDir, ".wails")
|
||||
s.wailsSystemConfig = filepath.Join(s.wailsSystemDir, s.configFilename)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"core-js": "^3.6.4",
|
||||
"react": "^16.13.0",
|
||||
"react-dom": "^16.13.0",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"wails-react-scripts": "3.0.1-2",
|
||||
"react-modal": "3.11.2",
|
||||
"@wailsapp/runtime": "^1.0.10"
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.1 KiB |
@@ -1,10 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="description" content="Web site created using create-react-app" />
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
@@ -20,8 +23,9 @@
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
</head>
|
||||
<body>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="app"></div>
|
||||
<!--
|
||||
@@ -34,5 +38,6 @@
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
BIN
cmd/templates/create-react-app/frontend/public/logo192.png
Normal file
BIN
cmd/templates/create-react-app/frontend/public/logo192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
BIN
cmd/templates/create-react-app/frontend/public/logo512.png
Normal file
BIN
cmd/templates/create-react-app/frontend/public/logo512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 176 KiB |
@@ -6,6 +6,16 @@
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
@@ -3,11 +3,16 @@
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #282c34;
|
||||
min-height: 100vh;
|
||||
|
||||
@@ -1,48 +1,35 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import Modal from 'react-modal';
|
||||
|
||||
class HelloWorld extends React.Component {
|
||||
constructor(props, context) {
|
||||
super();
|
||||
this.state = {
|
||||
showModal: false
|
||||
};
|
||||
function HelloWorld() {
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [result, setResult] = useState(null);
|
||||
|
||||
this.handleOpenModal = this.handleOpenModal.bind(this);
|
||||
this.handleCloseModal = this.handleCloseModal.bind(this);
|
||||
}
|
||||
const handleOpenModal = () => {
|
||||
setShowModal(true);
|
||||
|
||||
handleOpenModal () {
|
||||
this.setState({ showModal: true });
|
||||
window.backend.basic().then((result) => setResult(result));
|
||||
};
|
||||
|
||||
window.backend.basic().then(result =>
|
||||
this.setState({
|
||||
result
|
||||
})
|
||||
);
|
||||
}
|
||||
const handleCloseModal = () => {
|
||||
setShowModal(false);
|
||||
};
|
||||
|
||||
handleCloseModal () {
|
||||
this.setState({ showModal: false });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { result } = this.state;
|
||||
return (
|
||||
<div className="App">
|
||||
<button onClick={this.handleOpenModal} type="button">
|
||||
Hello
|
||||
</button>
|
||||
<Modal
|
||||
isOpen={this.state.showModal}
|
||||
contentLabel="Minimal Modal Example"
|
||||
>
|
||||
<p>{result}</p>
|
||||
<button onClick={this.handleCloseModal}>Close Modal</button>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="App">
|
||||
<button onClick={() => handleOpenModal()} type="button">
|
||||
Hello
|
||||
</button>
|
||||
<Modal
|
||||
appElement={document.getElementById("app")}
|
||||
isOpen={showModal}
|
||||
contentLabel="Minimal Modal Example"
|
||||
>
|
||||
<p>{result}</p>
|
||||
<button onClick={() => handleCloseModal()}>Close Modal</button>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default HelloWorld;
|
||||
|
||||
@@ -3,9 +3,20 @@ import ReactDOM from 'react-dom';
|
||||
import 'core-js/stable';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
import * as serviceWorker from './serviceWorker';
|
||||
|
||||
import * as Wails from '@wailsapp/runtime';
|
||||
|
||||
Wails.Init(() => {
|
||||
ReactDOM.render(<App />, document.getElementById('app'));
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
document.getElementById("app")
|
||||
);
|
||||
});
|
||||
|
||||
// If you want your app to work offline and load faster, you can change
|
||||
// unregister() to register() below. Note this comes with some pitfalls.
|
||||
// Learn more about service workers: https://bit.ly/CRA-PWA
|
||||
serviceWorker.unregister();
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "React JS",
|
||||
"version": "1.0.0",
|
||||
"shortdescription": "Create React App v3 template",
|
||||
"description": "Create React App v3 standar tooling",
|
||||
"shortdescription": "Create React App v4 template",
|
||||
"description": "Create React App v4 standard tooling",
|
||||
"install": "npm install",
|
||||
"build": "npm run build",
|
||||
"author": "bh90210 <ktc@pm.me>",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
|
||||
// Version - Wails version
|
||||
const Version = "v1.7.2-pre2"
|
||||
const Version = "v1.7.2-pre3"
|
||||
|
||||
9
lib/renderer/bridge/renderer-mewn.go
Normal file
9
lib/renderer/bridge/renderer-mewn.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package renderer
|
||||
|
||||
// Autogenerated by Mewn - Do not alter
|
||||
|
||||
import "github.com/leaanthony/mewn"
|
||||
|
||||
func init() {
|
||||
mewn.AddAsset(".", "../../runtime/assets/wails.js", "1f8b08000000000000ff94586d8f1bb711fe2b2ba255481f8f91dc14a977c318cee582a838df053ebbfda00a01b53b92e85b915b2e758a20f1bf17b3efba97a2050c7bc519728633cf3c33f468b533a9d7d650c38e8fca45208f216917234f1d3bea1585b95b30077ee74c84df02fe28acf365825bacc42579d4b1e3793c9af246181f43489a4d0637a52acfa96df772cbfb6fcfb815b91c4dfab5e0c5561aee452a817b91c9de550edcb1a317163fd9e974b7fc0aa91719acb481df9c2dc0f943a57604b3db8253cb1ce2d184afc1c72eb0c0bd70727875b233f5ee8c8ca43f146057d1fd61bbb4f9785cff2bbcbdf74e9bf567b51e8f5fb3f85c971f1f55be83987cb4d92e0712187f6d33f9fd77281bb576db6852bbebcfae5f25653a86f1981ae9a9618cff6d0c6d864ca257f43b94125b9922b2bd93198ff18fe82df59b30974e36cea50e94076a7679cef0382f1c75afb9ee38c960a576b9274f235edfc204c6df560e95555cfa201bb6b28e56308ab4890cf322a38e5bde5d17d8b103112c82586a93557e71cb588b2f873132f2399a9fdcf67da7d19f2a1adf43fc82b04330fa059c28c281714073f6494a1ac5264485b3dee225c54695777bd306abae02dc8067149210eea917a59cb040e76718f788cb12228c59ea49823900c61b4f66467bc2e9739f6781b12699c7903489abc34a7e86e56efde2ae5d609dd6ccacec8b4af940e99fca194ce54b7aab81deb573d6bda8950db47e515ee52f6a95ed756c7b1d5befb29cdc1560be7cba7971dfa13dbdd1fb456359bda0b86c0d68395ff4dca7da1c34a1c45cc68663fa962a7d98fd1c7b5ea8436e55164348ce9854afe85e9bcceec55ee9bc5c3a9dade1fdf325b1876569d307f0a20484358b1b25f8c383332a17da3cda07a086712d72306bbff971d2d50cc849023fb482042e2e989ec3821a16e8dfefef6e455d6e7a75a08eb1d0dd2dad11ab28c9ed9af0630e8f90c7866fa12cd51a620803e51dde28a524abb16306a2bc11e90a3043c9aa91ec5b940c855923841a1a4351d98856351e0c0b78cf3d2fcefad2639b9bb64c77793e927e3c6e3e4e27eae5847103fbe83767b7ba04da67de715b675527993d6a692ec825b9d85316f61b9d032de67a5193de8f13867aa92cc17fd65bb03b4f8708b2b4423725572acf236f2372612e48e4f516b2c8eebc883ec1bf7750fa68f6731c910bcd5860dcb3044dc8a3af8ffc55992c8738e50e903a62cb1d94367f84d885c4bb03e6096147f811c94f9bf5adda221633e555fc24cfc82b9a8554f97453c5d29ad2e620aa582330c21008eb33ea7aa4a446a720171543757a073caad15a53f293b3fb129ce8ea6f98c2e5ebba750d1a16f6b2c179ea0e85b7435ac6801b8999fba28dffcbdb0fcea9039d76547fb651acc17f5226b3db7f60a341f4cc278b9798fcdd64f2fdf4ddbbb77ffdeefbef26efde4ddf7c547e235cb597b25011c0b663f4c8d00a5fa3675d77444da44de99549b17f01637ee3ec3e42873f1f0ae800618cf511a62d52519aabb28c5419a9a83d90b040fd4697dc30ee11b197538ebfc555433067cdac9dbd842a8afc50b73fc3f8e5742411f6132925f59772ca42e09bb352b96b4b6533370b897f9d4ef345d31fd0e96d75d30425a2d8951bea06b9bcee3be9139ccd17a2cc75da7aa4dc7ab705e34b562fd329b2af3c9a015221248a127804e309f77565df9f39fb80e610f16d5dc33efaa50d03417d7261181f4d7a7cd79aa369e89dbeeab24eafda794695a55e9bd36918d49e45a709fcd0dd60c8a6d5ddbdec64735824ed3687038b67ff53bff7dc311cd67050f6384c876ec0604d042b30f4610c0dce110a6032790c7cd83fe4f0c7e9d44b7bfd2ac75fe5f116f63fd5b4113f9f8fe60b915a932a4f8d288b5c7b4a0461d56797c4f3939118a16d45d3ae15393949dc0fade0729ab80b39adadd40f04dc377aa096b141729b6a21ad139693489711968e8a1e55aeb3e8ab7a5465ea74e1239d81f17aa5c109c258e2e776814c3fb70b790c5519cded22d4ad1c44610bca1aabfabf5ad5ff9f55bd904ff80afbf0e0d1d4c2a6ab910a037d6e938e6f0df7c8b3dd8cd9f799b3f20769705a5cbf20edb921701f280bbc2590e7d94e8ccc20b5197cf934bbb2dbc21a3098780745ae52a0dffeabbcf876cd0961fdd27c72f94e5dae16c7b701457ffed3b80a823b1c1b4a28942b713e694a12dacb9399a943895a51a1ca12326c91ed0015475dfc413483072722fad8cc2011611d34595293ec8efabaa9d7196c38c4c93ac278aaceb895450bb7b6cf4bd20625faa6b3ea38f9a6cab983b52e3d64a3d188b496cedba61e5ac5ee9a8372ed4060c5591f673c831c3c44e8056f4e786f45dddc69b3c0625ca9ba3c0581f4c802bfb55eaf0ef1b39e83dcdcd395af58bca159c6ebc2ab961ae21a561eaea31fd56c8b95cb30757a983a18a42ea3e76943c7a2128cc7cc99ca3d115d238347c8ec91ac0839d8ae6b51cdc663dff287e35316aac6e343e01fb2ecbe2aa92737ac2f95d9b42a8fe6e9799d03fea2a4ae424c8cf0f0879786779a4b9b1d903dc164571b9d67880e188fafab21686630de57f7f72fb0deabb6fc2107c212c042fce0bdd3cb9d074a90d7092768ffdbb42c09e3202addfb0d807f3ffc21d2b2fc5cf919c3996f4f6ca2ceadcda07abcf7c20da8ec74ea7eaec137ce953f1d3eab6af0a30495080e3aeccc427d6bede319867af6dbd50dc2da803b8b80aebb3c4e82fc561e6fec3a76bc19d262cbabe496f1f1ce3cc9d25df368bd331f77b9d7450ef11dbfde6a1f5ff35f4139bf04759ed9fef184f34a728f3818f056554433e3c13daabc7aa3227b399cb5bbd5e1b08db34860950f1fd20763f739646b889fbcbb466866309365dd2ca6fa4dd1cee0b78936ade3035e309c7c831c8707518cd2eff1d7c18c32434386b27075f6c6e3b7ac6dc1d654257efe1f569e57cf8ee1167163d7a2e9456fdebc79135da9dd7ae3a32f6653314956134e8432c2f86b5b7bc2c4d9e835ad2f9f6e50035ed7b8d106a25b8b5afe75ad2b9befb6a6d173afeb5521401dcb02bfc687a0cecb185fcb901116162cf94f000000ffff04075a367c140000")
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
7070
runtime/js/package-lock.json
generated
7070
runtime/js/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -29,15 +29,15 @@
|
||||
},
|
||||
"homepage": "https://github.com/wailsapp/runtime#readme",
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.5.0",
|
||||
"@babel/core": "^7.5.4",
|
||||
"@babel/plugin-transform-object-assign": "^7.2.0",
|
||||
"@babel/preset-env": "^7.5.4",
|
||||
"babel-loader": "^8.0.6",
|
||||
"babel-preset-minify": "^0.5.0",
|
||||
"core-js": "^3.1.4",
|
||||
"eslint": "^6.5.1",
|
||||
"webpack": "^4.35.3",
|
||||
"webpack-cli": "^3.3.5"
|
||||
"@babel/cli": "^7.11.5",
|
||||
"@babel/core": "^7.11.5",
|
||||
"@babel/plugin-transform-object-assign": "^7.10.4",
|
||||
"@babel/preset-env": "^7.11.5",
|
||||
"babel-loader": "^8.1.0",
|
||||
"babel-preset-minify": "^0.5.1",
|
||||
"core-js": "^3.6.5",
|
||||
"eslint": "^7.8.1",
|
||||
"webpack": "^4.44.1",
|
||||
"webpack-cli": "^3.3.12"
|
||||
}
|
||||
}
|
||||
|
||||
7
v2/.vscode/settings.json
vendored
Normal file
7
v2/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files.associations": {
|
||||
"ios": "c",
|
||||
"typeinfo": "c",
|
||||
"sstream": "c"
|
||||
}
|
||||
}
|
||||
40
v2/NOTES.md
Normal file
40
v2/NOTES.md
Normal file
@@ -0,0 +1,40 @@
|
||||
|
||||
# Packing linux
|
||||
|
||||
* create app, app.desktop, app.png (512x512)
|
||||
* chmod +x app!
|
||||
* ./linuxdeploy-x86_64.AppImage --appdir AppDir -i react.png -d react.desktop -e react --output appimage
|
||||
|
||||
|
||||
# Wails Doctor
|
||||
|
||||
Tested on:
|
||||
|
||||
* Debian 8
|
||||
* Ubuntu 20.04
|
||||
* Ubuntu 19.10
|
||||
* Solus 4.1
|
||||
* Centos 8
|
||||
* Gentoo
|
||||
* OpenSUSE/leap
|
||||
* Fedora 31
|
||||
|
||||
### Development
|
||||
|
||||
Add a new package manager processor here: `v2/internal/system/packagemanager/`. IsAvailable should work even if the package is installed.
|
||||
Add your new package manager to the list of package managers in `v2/internal/system/packagemanager/packagemanager.go`:
|
||||
|
||||
```
|
||||
var db = map[string]PackageManager{
|
||||
"eopkg": NewEopkg(),
|
||||
"apt": NewApt(),
|
||||
"yum": NewYum(),
|
||||
"pacman": NewPacman(),
|
||||
"emerge": NewEmerge(),
|
||||
"zypper": NewZypper(),
|
||||
}
|
||||
```
|
||||
|
||||
## Gentoo
|
||||
|
||||
* Setup docker image using: emerge-webrsync -x -v
|
||||
5
v2/README.md
Normal file
5
v2/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Wails v2 ALPHA
|
||||
|
||||
This branch contains WORK IN PROGRESS! There are no guarantees. Use at your peril!
|
||||
|
||||
This document will be updated as progress is made.
|
||||
116
v2/cmd/wails/internal/commands/build/build.go
Normal file
116
v2/cmd/wails/internal/commands/build/build.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package build
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/leaanthony/clir"
|
||||
"github.com/leaanthony/slicer"
|
||||
"github.com/wailsapp/wails/v2/internal/logger"
|
||||
"github.com/wailsapp/wails/v2/pkg/commands/build"
|
||||
)
|
||||
|
||||
// AddBuildSubcommand adds the `build` command for the Wails application
|
||||
func AddBuildSubcommand(app *clir.Cli) {
|
||||
|
||||
outputType := "desktop"
|
||||
|
||||
validTargetTypes := slicer.String([]string{"desktop", "hybrid", "server"})
|
||||
|
||||
command := app.NewSubCommand("build", "Builds the application")
|
||||
|
||||
// Setup target type flag
|
||||
description := "Type of application to build. Valid types: " + validTargetTypes.Join(",")
|
||||
command.StringFlag("t", description, &outputType)
|
||||
|
||||
// Setup production flag
|
||||
production := false
|
||||
command.BoolFlag("production", "Build in production mode", &production)
|
||||
|
||||
// Setup pack flag
|
||||
pack := false
|
||||
command.BoolFlag("pack", "Create a platform specific package", &pack)
|
||||
|
||||
compilerCommand := "go"
|
||||
command.StringFlag("compiler", "Use a different go compiler to build, eg go1.15beta1", &compilerCommand)
|
||||
|
||||
// Setup Platform flag
|
||||
platform := runtime.GOOS
|
||||
command.StringFlag("platform", "Platform to target", &platform)
|
||||
|
||||
// Quiet Build
|
||||
quiet := false
|
||||
command.BoolFlag("q", "Supress output to console", &quiet)
|
||||
|
||||
// ldflags to pass to `go`
|
||||
ldflags := ""
|
||||
command.StringFlag("ldflags", "optional ldflags", &ldflags)
|
||||
|
||||
// Log to file
|
||||
logFile := ""
|
||||
command.StringFlag("l", "Log to file", &logFile)
|
||||
|
||||
command.Action(func() error {
|
||||
|
||||
// Create logger
|
||||
logger := logger.New()
|
||||
|
||||
if !quiet {
|
||||
logger.AddOutput(os.Stdout)
|
||||
}
|
||||
|
||||
// Validate output type
|
||||
if !validTargetTypes.Contains(outputType) {
|
||||
return fmt.Errorf("output type '%s' is not valid", outputType)
|
||||
}
|
||||
|
||||
if !quiet {
|
||||
app.PrintBanner()
|
||||
}
|
||||
|
||||
task := fmt.Sprintf("Building %s Application", strings.Title(outputType))
|
||||
logger.Writeln(task)
|
||||
logger.Writeln(strings.Repeat("-", len(task)))
|
||||
|
||||
// Setup mode
|
||||
mode := build.Debug
|
||||
if production {
|
||||
mode = build.Production
|
||||
}
|
||||
|
||||
// Create BuildOptions
|
||||
buildOptions := &build.Options{
|
||||
Logger: logger,
|
||||
OutputType: outputType,
|
||||
Mode: mode,
|
||||
Pack: pack,
|
||||
Platform: platform,
|
||||
LDFlags: ldflags,
|
||||
Compiler: compilerCommand,
|
||||
}
|
||||
|
||||
return doBuild(buildOptions)
|
||||
})
|
||||
}
|
||||
|
||||
// doBuild is our main build command
|
||||
func doBuild(buildOptions *build.Options) error {
|
||||
|
||||
// Start Time
|
||||
start := time.Now()
|
||||
|
||||
outputFilename, err := build.Build(buildOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Output stats
|
||||
elapsed := time.Since(start)
|
||||
buildOptions.Logger.Writeln("")
|
||||
buildOptions.Logger.Writeln(fmt.Sprintf("Built '%s' in %s.", outputFilename, elapsed.Round(time.Millisecond).String()))
|
||||
buildOptions.Logger.Writeln("")
|
||||
|
||||
return nil
|
||||
}
|
||||
261
v2/cmd/wails/internal/commands/dev/dev.go
Normal file
261
v2/cmd/wails/internal/commands/dev/dev.go
Normal file
@@ -0,0 +1,261 @@
|
||||
package dev
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/leaanthony/clir"
|
||||
"github.com/leaanthony/slicer"
|
||||
"github.com/wailsapp/wails/v2/internal/fs"
|
||||
"github.com/wailsapp/wails/v2/internal/logger"
|
||||
"github.com/wailsapp/wails/v2/internal/process"
|
||||
"github.com/wailsapp/wails/v2/pkg/commands/build"
|
||||
)
|
||||
|
||||
// AddSubcommand adds the `dev` command for the Wails application
|
||||
func AddSubcommand(app *clir.Cli) error {
|
||||
|
||||
command := app.NewSubCommand("dev", "Development mode")
|
||||
|
||||
outputType := "desktop"
|
||||
|
||||
validTargetTypes := slicer.String([]string{"desktop", "hybrid", "server"})
|
||||
|
||||
// Setup target type flag
|
||||
description := "Type of application to develop. Valid types: " + validTargetTypes.Join(",")
|
||||
command.StringFlag("t", description, &outputType)
|
||||
|
||||
// Passthrough ldflags
|
||||
ldflags := ""
|
||||
command.StringFlag("ldflags", "optional ldflags", &ldflags)
|
||||
|
||||
// compiler command
|
||||
compilerCommand := "go"
|
||||
command.StringFlag("compiler", "Use a different go compiler to build, eg go1.15beta1", &compilerCommand)
|
||||
|
||||
// extensions to trigger rebuilds
|
||||
extensions := "go"
|
||||
command.StringFlag("m", "Extensions to trigger rebuilds (comma separated) eg go,js,css,html", &extensions)
|
||||
|
||||
command.Action(func() error {
|
||||
|
||||
// Validate inputs
|
||||
if !validTargetTypes.Contains(outputType) {
|
||||
return fmt.Errorf("output type '%s' is not valid", outputType)
|
||||
}
|
||||
|
||||
// Create logger
|
||||
logger := logger.New()
|
||||
logger.AddOutput(os.Stdout)
|
||||
app.PrintBanner()
|
||||
|
||||
// TODO: Check you are in a project directory
|
||||
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer watcher.Close()
|
||||
|
||||
var debugBinaryProcess *process.Process = nil
|
||||
var buildFrontend bool = true
|
||||
var extensionsThatTriggerARebuild = strings.Split(extensions, ",")
|
||||
|
||||
// Setup signal handler
|
||||
quitChannel := make(chan os.Signal, 1)
|
||||
signal.Notify(quitChannel, os.Interrupt, os.Kill, syscall.SIGTERM)
|
||||
|
||||
debounceQuit := make(chan bool, 1)
|
||||
|
||||
// Do initial build
|
||||
logger.Info("Building application for development...")
|
||||
debugBinaryProcess = restartApp(logger, outputType, ldflags, compilerCommand, buildFrontend, debugBinaryProcess)
|
||||
|
||||
go debounce(100*time.Millisecond, watcher.Events, debounceQuit, func(event fsnotify.Event) {
|
||||
// logger.Info("event: %+v", event)
|
||||
|
||||
// Check for new directories
|
||||
if event.Op&fsnotify.Create == fsnotify.Create {
|
||||
// If this is a folder, add it to our watch list
|
||||
if fs.DirExists(event.Name) {
|
||||
if !strings.Contains(event.Name, "node_modules") {
|
||||
watcher.Add(event.Name)
|
||||
logger.Info("Watching directory: %s", event.Name)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Check for file writes
|
||||
if event.Op&fsnotify.Write == fsnotify.Write {
|
||||
|
||||
// logger.Info("modified file: %s", event.Name)
|
||||
var rebuild bool = false
|
||||
|
||||
// Iterate all file patterns
|
||||
for _, pattern := range extensionsThatTriggerARebuild {
|
||||
rebuild = strings.HasSuffix(event.Name, pattern)
|
||||
if err != nil {
|
||||
logger.Fatal(err.Error())
|
||||
}
|
||||
if rebuild {
|
||||
// Only build frontend when the file isn't a Go file
|
||||
buildFrontend = !strings.HasSuffix(event.Name, "go")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !rebuild {
|
||||
logger.Info("Filename change: %s did not match extension list %s", event.Name, extensions)
|
||||
return
|
||||
}
|
||||
|
||||
if buildFrontend {
|
||||
logger.Info("Full rebuild triggered: %s updated", event.Name)
|
||||
} else {
|
||||
logger.Info("Partial build triggered: %s updated", event.Name)
|
||||
}
|
||||
|
||||
// Do a rebuild
|
||||
|
||||
// Try and build the app
|
||||
newBinaryProcess := restartApp(logger, outputType, ldflags, compilerCommand, buildFrontend, debugBinaryProcess)
|
||||
|
||||
// If we have a new process, save it
|
||||
if newBinaryProcess != nil {
|
||||
debugBinaryProcess = newBinaryProcess
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
// Get project dir
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get all subdirectories
|
||||
dirs, err := fs.GetSubdirectories(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Setup a watcher for non-node_modules directories
|
||||
dirs.Each(func(dir string) {
|
||||
if strings.Contains(dir, "node_modules") {
|
||||
return
|
||||
}
|
||||
logger.Info("Watching directory: %s", dir)
|
||||
err = watcher.Add(dir)
|
||||
if err != nil {
|
||||
logger.Fatal(err.Error())
|
||||
}
|
||||
})
|
||||
|
||||
// Wait until we get a quit signal
|
||||
quit := false
|
||||
for quit == false {
|
||||
select {
|
||||
case <-quitChannel:
|
||||
println()
|
||||
// Notify debouncer to quit
|
||||
debounceQuit <- true
|
||||
quit = true
|
||||
}
|
||||
}
|
||||
|
||||
// Kill the current program if running
|
||||
if debugBinaryProcess != nil {
|
||||
debugBinaryProcess.Kill()
|
||||
}
|
||||
|
||||
logger.Info("Development mode exited")
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Credit: https://drailing.net/2018/01/debounce-function-for-golang/
|
||||
func debounce(interval time.Duration, input chan fsnotify.Event, quitChannel chan bool, cb func(arg fsnotify.Event)) {
|
||||
var item fsnotify.Event
|
||||
timer := time.NewTimer(interval)
|
||||
exit:
|
||||
for {
|
||||
select {
|
||||
case item = <-input:
|
||||
timer.Reset(interval)
|
||||
case <-timer.C:
|
||||
if item.Name != "" {
|
||||
cb(item)
|
||||
}
|
||||
case <-quitChannel:
|
||||
break exit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func restartApp(logger *logger.Logger, outputType string, ldflags string, compilerCommand string, buildFrontend bool, debugBinaryProcess *process.Process) *process.Process {
|
||||
|
||||
appBinary, err := buildApp(logger, outputType, ldflags, compilerCommand, buildFrontend)
|
||||
println()
|
||||
if err != nil {
|
||||
logger.Error("Build Failed: %s", err.Error())
|
||||
return nil
|
||||
}
|
||||
logger.Info("Build new binary: %s", appBinary)
|
||||
|
||||
// Kill existing binary if need be
|
||||
if debugBinaryProcess != nil {
|
||||
killError := debugBinaryProcess.Kill()
|
||||
|
||||
if killError != nil {
|
||||
logger.Fatal("Unable to kill debug binary (PID: %d)!", debugBinaryProcess.PID())
|
||||
}
|
||||
|
||||
debugBinaryProcess = nil
|
||||
}
|
||||
|
||||
// TODO: Generate `backend.js`
|
||||
|
||||
// Start up new binary
|
||||
newProcess := process.NewProcess(logger, appBinary)
|
||||
err = newProcess.Start()
|
||||
if err != nil {
|
||||
// Remove binary
|
||||
fs.DeleteFile(appBinary)
|
||||
logger.Fatal("Unable to start application: %s", err.Error())
|
||||
}
|
||||
|
||||
return newProcess
|
||||
}
|
||||
|
||||
func buildApp(logger *logger.Logger, outputType string, ldflags string, compilerCommand string, buildFrontend bool) (string, error) {
|
||||
|
||||
// Create random output file
|
||||
outputFile := fmt.Sprintf("debug-%d", time.Now().Unix())
|
||||
|
||||
// Create BuildOptions
|
||||
buildOptions := &build.Options{
|
||||
Logger: logger,
|
||||
OutputType: outputType,
|
||||
Mode: build.Debug,
|
||||
Pack: false,
|
||||
Platform: runtime.GOOS,
|
||||
LDFlags: ldflags,
|
||||
Compiler: compilerCommand,
|
||||
OutputFile: outputFile,
|
||||
IgnoreFrontend: !buildFrontend,
|
||||
}
|
||||
|
||||
return build.Build(buildOptions)
|
||||
|
||||
}
|
||||
154
v2/cmd/wails/internal/commands/doctor/doctor.go
Normal file
154
v2/cmd/wails/internal/commands/doctor/doctor.go
Normal file
@@ -0,0 +1,154 @@
|
||||
package doctor
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/leaanthony/clir"
|
||||
"github.com/wailsapp/wails/v2/internal/logger"
|
||||
"github.com/wailsapp/wails/v2/internal/system"
|
||||
"github.com/wailsapp/wails/v2/internal/system/packagemanager"
|
||||
)
|
||||
|
||||
// AddSubcommand adds the `doctor` command for the Wails application
|
||||
func AddSubcommand(app *clir.Cli) error {
|
||||
|
||||
command := app.NewSubCommand("doctor", "Diagnose your environment")
|
||||
|
||||
command.Action(func() error {
|
||||
|
||||
// Create logger
|
||||
logger := logger.New()
|
||||
logger.AddOutput(os.Stdout)
|
||||
|
||||
app.PrintBanner()
|
||||
print("Scanning system - please wait...")
|
||||
|
||||
// Get system info
|
||||
info, err := system.GetInfo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
println("Done.")
|
||||
|
||||
// Start a new tabwriter
|
||||
w := new(tabwriter.Writer)
|
||||
w.Init(os.Stdout, 8, 8, 0, '\t', 0)
|
||||
|
||||
// Write out the system information
|
||||
fmt.Fprintf(w, "\n")
|
||||
fmt.Fprintf(w, "System\n")
|
||||
fmt.Fprintf(w, "------\n")
|
||||
fmt.Fprintf(w, "%s\t%s\n", "OS:", info.OS.Name)
|
||||
fmt.Fprintf(w, "%s\t%s\n", "Version: ", info.OS.Version)
|
||||
fmt.Fprintf(w, "%s\t%s\n", "ID:", info.OS.ID)
|
||||
|
||||
// Exit early if PM not found
|
||||
if info.PM == nil {
|
||||
fmt.Fprintf(w, "\n%s\t%s", "Package Manager:", "Not Found")
|
||||
w.Flush()
|
||||
println()
|
||||
return nil
|
||||
}
|
||||
fmt.Fprintf(w, "%s\t%s\n", "Package Manager: ", info.PM.Name())
|
||||
|
||||
// Output Go Information
|
||||
fmt.Fprintf(w, "%s\t%s\n", "Go Version:", runtime.Version())
|
||||
fmt.Fprintf(w, "%s\t%s\n", "Platform:", runtime.GOOS)
|
||||
fmt.Fprintf(w, "%s\t%s\n", "Architecture:", runtime.GOARCH)
|
||||
|
||||
// Output Dependencies Status
|
||||
var dependenciesMissing = []string{}
|
||||
var externalPackages = []*packagemanager.Dependancy{}
|
||||
var dependenciesAvailableRequired = 0
|
||||
var dependenciesAvailableOptional = 0
|
||||
fmt.Fprintf(w, "\n")
|
||||
fmt.Fprintf(w, "Dependency\tPackage Name\tStatus\tVersion\n")
|
||||
fmt.Fprintf(w, "----------\t------------\t------\t-------\n")
|
||||
|
||||
// Loop over dependencies
|
||||
for _, dependency := range info.Dependencies {
|
||||
|
||||
name := dependency.Name
|
||||
if dependency.Optional {
|
||||
name += "*"
|
||||
}
|
||||
packageName := "Unknown"
|
||||
status := "Not Found"
|
||||
|
||||
// If we found the package
|
||||
if dependency.PackageName != "" {
|
||||
|
||||
packageName = dependency.PackageName
|
||||
|
||||
// If it's installed, update the status
|
||||
if dependency.Installed {
|
||||
status = "Installed"
|
||||
} else {
|
||||
// Generate meaningful status text
|
||||
status = "Available"
|
||||
|
||||
if dependency.Optional {
|
||||
dependenciesAvailableOptional++
|
||||
} else {
|
||||
dependenciesAvailableRequired++
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if !dependency.Optional {
|
||||
dependenciesMissing = append(dependenciesMissing, dependency.Name)
|
||||
}
|
||||
|
||||
if dependency.External {
|
||||
externalPackages = append(externalPackages, dependency)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", name, packageName, status, dependency.Version)
|
||||
}
|
||||
fmt.Fprintf(w, "\n")
|
||||
fmt.Fprintf(w, "* - Optional Dependency\n")
|
||||
w.Flush()
|
||||
println()
|
||||
println("Diagnosis")
|
||||
println("---------\n")
|
||||
|
||||
// Generate an appropriate diagnosis
|
||||
|
||||
if len(dependenciesMissing) == 0 && dependenciesAvailableRequired == 0 {
|
||||
println("Your system is ready for Wails development!")
|
||||
}
|
||||
|
||||
if dependenciesAvailableRequired != 0 {
|
||||
println("Install required packages using: " + info.Dependencies.InstallAllRequiredCommand())
|
||||
}
|
||||
|
||||
if dependenciesAvailableOptional != 0 {
|
||||
println("Install optional packages using: " + info.Dependencies.InstallAllOptionalCommand())
|
||||
}
|
||||
|
||||
if len(externalPackages) > 0 {
|
||||
for _, p := range externalPackages {
|
||||
if p.Optional {
|
||||
print("[Optional] ")
|
||||
}
|
||||
println("Install " + p.Name + ": " + p.InstallCommand)
|
||||
}
|
||||
}
|
||||
|
||||
if len(dependenciesMissing) != 0 {
|
||||
// TODO: Check if apps are available locally and if so, adjust the diagnosis
|
||||
println("Fatal:")
|
||||
println("Required dependencies missing: " + strings.Join(dependenciesMissing, " "))
|
||||
println("Please read this article on how to resolve this: https://wails.app/guides/resolving-missing-packages")
|
||||
}
|
||||
|
||||
println()
|
||||
return nil
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
120
v2/cmd/wails/internal/commands/initialise/initialise.go
Normal file
120
v2/cmd/wails/internal/commands/initialise/initialise.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package initialise
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/leaanthony/clir"
|
||||
"github.com/wailsapp/wails/v2/internal/logger"
|
||||
"github.com/wailsapp/wails/v2/internal/templates"
|
||||
)
|
||||
|
||||
// AddSubcommand adds the `init` command for the Wails application
|
||||
func AddSubcommand(app *clir.Cli) error {
|
||||
|
||||
// Load the template shortnames
|
||||
validShortNames, err := templates.TemplateShortNames()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
command := app.NewSubCommand("init", "Initialise a new Wails project")
|
||||
|
||||
// Setup template name flag
|
||||
templateName := "vanilla"
|
||||
description := "Name of template to use. Valid tempates: " + validShortNames.Join(" ")
|
||||
command.StringFlag("t", description, &templateName)
|
||||
|
||||
// Setup project name
|
||||
projectName := ""
|
||||
command.StringFlag("n", "Name of project", &projectName)
|
||||
|
||||
// Setup project directory
|
||||
projectDirectory := "."
|
||||
command.StringFlag("d", "Project directory", &projectDirectory)
|
||||
|
||||
// Quiet Init
|
||||
quiet := false
|
||||
command.BoolFlag("q", "Supress output to console", &quiet)
|
||||
|
||||
// List templates
|
||||
list := false
|
||||
command.BoolFlag("l", "List templates", &list)
|
||||
|
||||
command.Action(func() error {
|
||||
|
||||
// Create logger
|
||||
logger := logger.New()
|
||||
|
||||
if !quiet {
|
||||
logger.AddOutput(os.Stdout)
|
||||
}
|
||||
|
||||
// Are we listing templates?
|
||||
if list {
|
||||
app.PrintBanner()
|
||||
err := templates.OutputList(logger)
|
||||
logger.Writeln("")
|
||||
return err
|
||||
}
|
||||
|
||||
// Validate output type
|
||||
if !validShortNames.Contains(templateName) {
|
||||
logger.Write(fmt.Sprintf("ERROR: Template '%s' is not valid", templateName))
|
||||
logger.Writeln("")
|
||||
command.PrintHelp()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate name
|
||||
if len(projectName) == 0 {
|
||||
logger.Writeln("ERROR: Project name required")
|
||||
logger.Writeln("")
|
||||
command.PrintHelp()
|
||||
return nil
|
||||
}
|
||||
|
||||
if !quiet {
|
||||
app.PrintBanner()
|
||||
}
|
||||
|
||||
task := fmt.Sprintf("Initialising Project %s", strings.Title(projectName))
|
||||
logger.Writeln(task)
|
||||
logger.Writeln(strings.Repeat("-", len(task)))
|
||||
|
||||
// Create Template Options
|
||||
options := &templates.Options{
|
||||
ProjectName: projectName,
|
||||
TargetDir: projectDirectory,
|
||||
TemplateName: templateName,
|
||||
Logger: logger,
|
||||
}
|
||||
|
||||
return initProject(options)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// initProject is our main init command
|
||||
func initProject(options *templates.Options) error {
|
||||
|
||||
// Start Time
|
||||
start := time.Now()
|
||||
|
||||
// Install the template
|
||||
err := templates.Install(options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Output stats
|
||||
elapsed := time.Since(start)
|
||||
options.Logger.Writeln("")
|
||||
options.Logger.Writeln(fmt.Sprintf("Initialised project '%s' in %s.", options.ProjectName, elapsed.Round(time.Millisecond).String()))
|
||||
options.Logger.Writeln("")
|
||||
|
||||
return nil
|
||||
}
|
||||
44
v2/cmd/wails/main.go
Normal file
44
v2/cmd/wails/main.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/leaanthony/clir"
|
||||
"github.com/wailsapp/wails/v2/cmd/wails/internal/commands/build"
|
||||
"github.com/wailsapp/wails/v2/cmd/wails/internal/commands/dev"
|
||||
"github.com/wailsapp/wails/v2/cmd/wails/internal/commands/doctor"
|
||||
"github.com/wailsapp/wails/v2/cmd/wails/internal/commands/initialise"
|
||||
)
|
||||
|
||||
func fatal(message string) {
|
||||
println(message)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
var err error
|
||||
version := "v2.0.0-alpha"
|
||||
|
||||
app := clir.NewCli("Wails", "Go/HTML Application Framework", version)
|
||||
|
||||
build.AddBuildSubcommand(app)
|
||||
err = initialise.AddSubcommand(app)
|
||||
if err != nil {
|
||||
fatal(err.Error())
|
||||
}
|
||||
err = doctor.AddSubcommand(app)
|
||||
if err != nil {
|
||||
fatal(err.Error())
|
||||
}
|
||||
|
||||
err = dev.AddSubcommand(app)
|
||||
if err != nil {
|
||||
fatal(err.Error())
|
||||
}
|
||||
|
||||
err = app.Run()
|
||||
if err != nil {
|
||||
println("\n\nERROR: " + err.Error())
|
||||
}
|
||||
}
|
||||
18
v2/go.mod
Normal file
18
v2/go.mod
Normal file
@@ -0,0 +1,18 @@
|
||||
module github.com/wailsapp/wails/v2
|
||||
|
||||
go 1.13
|
||||
|
||||
require (
|
||||
github.com/fsnotify/fsnotify v1.4.9
|
||||
github.com/leaanthony/clir v1.0.4
|
||||
github.com/leaanthony/gosod v0.0.4
|
||||
github.com/leaanthony/slicer v1.4.1
|
||||
github.com/matryer/is v1.4.0
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.4
|
||||
github.com/xyproto/xpm v1.2.1
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202
|
||||
golang.org/x/tools v0.0.0-20200902012652-d1954cc86c82
|
||||
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect
|
||||
nhooyr.io/websocket v1.8.6
|
||||
)
|
||||
114
v2/go.sum
Normal file
114
v2/go.sum
Normal file
@@ -0,0 +1,114 @@
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
|
||||
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
|
||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
|
||||
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
|
||||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
||||
github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
|
||||
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
|
||||
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
|
||||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8=
|
||||
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/leaanthony/clir v1.0.4 h1:Dov2y9zWJmZr7CjaCe86lKa4b5CSxskGAt2yBkoDyiU=
|
||||
github.com/leaanthony/clir v1.0.4/go.mod h1:k/RBkdkFl18xkkACMCLt09bhiZnrGORoxmomeMvDpE0=
|
||||
github.com/leaanthony/gosod v0.0.4 h1:v4hepo4IyL8E8c9qzDsvYcA0KGh7Npf8As74K5ibQpI=
|
||||
github.com/leaanthony/gosod v0.0.4/go.mod h1:nGMCb1PJfXwBDbOAike78jEYlpqge+xUKFf0iBKjKxU=
|
||||
github.com/leaanthony/slicer v1.4.1 h1:X/SmRIDhkUAolP79mSTO0jTcVX1k504PJBqvV6TwP0w=
|
||||
github.com/leaanthony/slicer v1.4.1/go.mod h1:FwrApmf8gOrpzEWM2J/9Lh79tyq8KTX5AzRtwV7m4AY=
|
||||
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
|
||||
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54=
|
||||
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8=
|
||||
github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA=
|
||||
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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
github.com/xyproto/xpm v1.2.1 h1:trdvGjjWBsOOKzBBUPT6JvaIQM3acJEEYfbxN7M96wg=
|
||||
github.com/xyproto/xpm v1.2.1/go.mod h1:cMnesLsD0PBXLgjDfTDEaKr8XyTFsnP1QycSqRw7BiY=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9 h1:L2auWcuQIvxz9xSEqzESnV/QN/gNRXNApHi3fYwl2w0=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e h1:FDhOuMEY4JVRztM/gsbk+IKUQ8kj74bxZrgw87eMMVc=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200902012652-d1954cc86c82 h1:shxDsb9Dz27xzk3A0DxP0JuJnZMpKrdg8+E14eiUAX4=
|
||||
golang.org/x/tools v0.0.0-20200902012652-d1954cc86c82/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U=
|
||||
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
nhooyr.io/websocket v1.8.6 h1:s+C3xAMLwGmlI31Nyn/eAehUlZPwfYZu2JXM621Q5/k=
|
||||
nhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
|
||||
10
v2/internal/app/debug.go
Normal file
10
v2/internal/app/debug.go
Normal file
@@ -0,0 +1,10 @@
|
||||
// +build debug
|
||||
|
||||
package app
|
||||
|
||||
// Init initialises the application for a debug environment
|
||||
func (a *App) Init() error {
|
||||
a.debug = true
|
||||
println("Initialising debug options")
|
||||
return nil
|
||||
}
|
||||
40
v2/internal/app/default.go
Normal file
40
v2/internal/app/default.go
Normal file
@@ -0,0 +1,40 @@
|
||||
// +build !desktop,!hybrid,!server
|
||||
|
||||
package app
|
||||
|
||||
// This is the default application that will get run if the user compiles using `go build`.
|
||||
// The reason we want to prevent that is that the `wails build` command does a lot of behind
|
||||
// the scenes work such as asset compilation. If we allow `go build`, the state of these assets
|
||||
// will be unknown and the application will not work as expected.
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
// App defines a Wails application structure
|
||||
type App struct {
|
||||
Title string
|
||||
Width int
|
||||
Height int
|
||||
Resizable bool
|
||||
|
||||
// Indicates if the app is running in debug mode
|
||||
debug bool
|
||||
}
|
||||
|
||||
// CreateApp returns a null application
|
||||
func CreateApp(options *Options) *App {
|
||||
return &App{}
|
||||
}
|
||||
|
||||
// Run the application
|
||||
func (a *App) Run() error {
|
||||
println(`FATAL: This application was built using "go build". This is unsupported. Please compile using "wails build".`)
|
||||
os.Exit(1)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Bind the dummy interface
|
||||
func (a *App) Bind(dummy interface{}) error {
|
||||
return nil
|
||||
}
|
||||
166
v2/internal/app/desktop.go
Normal file
166
v2/internal/app/desktop.go
Normal file
@@ -0,0 +1,166 @@
|
||||
// +build desktop,!server
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/wailsapp/wails/v2/internal/binding"
|
||||
"github.com/wailsapp/wails/v2/internal/ffenestri"
|
||||
"github.com/wailsapp/wails/v2/internal/logger"
|
||||
"github.com/wailsapp/wails/v2/internal/messagedispatcher"
|
||||
"github.com/wailsapp/wails/v2/internal/servicebus"
|
||||
"github.com/wailsapp/wails/v2/internal/signal"
|
||||
"github.com/wailsapp/wails/v2/internal/subsystem"
|
||||
)
|
||||
|
||||
// App defines a Wails application structure
|
||||
type App struct {
|
||||
window *ffenestri.Application
|
||||
servicebus *servicebus.ServiceBus
|
||||
logger *logger.Logger
|
||||
signal *signal.Manager
|
||||
|
||||
// Subsystems
|
||||
log *subsystem.Log
|
||||
runtime *subsystem.Runtime
|
||||
event *subsystem.Event
|
||||
binding *subsystem.Binding
|
||||
call *subsystem.Call
|
||||
dispatcher *messagedispatcher.Dispatcher
|
||||
|
||||
// Indicates if the app is in debug mode
|
||||
debug bool
|
||||
|
||||
// This is our binding DB
|
||||
bindings *binding.Bindings
|
||||
}
|
||||
|
||||
// Create App
|
||||
func CreateApp(options *Options) *App {
|
||||
|
||||
// Merge default options
|
||||
options.mergeDefaults()
|
||||
|
||||
// Set up logger
|
||||
myLogger := logger.New(os.Stdout)
|
||||
myLogger.SetLogLevel(logger.TRACE)
|
||||
|
||||
window := ffenestri.NewApplicationWithConfig(&ffenestri.Config{
|
||||
Title: options.Title,
|
||||
Width: options.Width,
|
||||
Height: options.Height,
|
||||
MinWidth: options.MinWidth,
|
||||
MinHeight: options.MinHeight,
|
||||
MaxWidth: options.MaxWidth,
|
||||
MaxHeight: options.MaxHeight,
|
||||
Frameless: options.Frameless,
|
||||
StartHidden: options.StartHidden,
|
||||
|
||||
// This should be controlled by the compile time flags...
|
||||
DevTools: true,
|
||||
|
||||
Resizable: !options.DisableResize,
|
||||
Fullscreen: options.Fullscreen,
|
||||
}, myLogger)
|
||||
|
||||
result := &App{
|
||||
window: window,
|
||||
servicebus: servicebus.New(myLogger),
|
||||
logger: myLogger,
|
||||
bindings: binding.NewBindings(myLogger),
|
||||
}
|
||||
|
||||
// Initialise the app
|
||||
result.Init()
|
||||
|
||||
return result
|
||||
|
||||
}
|
||||
|
||||
// Run the application
|
||||
func (a *App) Run() error {
|
||||
|
||||
// Setup signal handler
|
||||
signal, err := signal.NewManager(a.servicebus, a.logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.signal = signal
|
||||
a.signal.Start()
|
||||
|
||||
// Start the service bus
|
||||
a.servicebus.Debug()
|
||||
a.servicebus.Start()
|
||||
|
||||
// Start the runtime
|
||||
runtime, err := subsystem.NewRuntime(a.servicebus, a.logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.runtime = runtime
|
||||
a.runtime.Start()
|
||||
|
||||
// Start the binding subsystem
|
||||
binding, err := subsystem.NewBinding(a.servicebus, a.logger, a.bindings, a.runtime.GoRuntime())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.binding = binding
|
||||
a.binding.Start()
|
||||
|
||||
// Start the logging subsystem
|
||||
log, err := subsystem.NewLog(a.servicebus, a.logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.log = log
|
||||
a.log.Start()
|
||||
|
||||
// create the dispatcher
|
||||
dispatcher, err := messagedispatcher.New(a.servicebus, a.logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.dispatcher = dispatcher
|
||||
dispatcher.Start()
|
||||
|
||||
// Start the eventing subsystem
|
||||
event, err := subsystem.NewEvent(a.servicebus, a.logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.event = event
|
||||
a.event.Start()
|
||||
|
||||
// Start the call subsystem
|
||||
call, err := subsystem.NewCall(a.servicebus, a.logger, a.bindings.DB())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.call = call
|
||||
a.call.Start()
|
||||
|
||||
// Dump bindings as a debug
|
||||
bindingDump, err := a.bindings.ToJSON()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result := a.window.Run(dispatcher, bindingDump)
|
||||
a.logger.Trace("Ffenestri.Run() exited")
|
||||
a.servicebus.Stop()
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Bind a struct to the application by passing in
|
||||
// a pointer to it
|
||||
func (a *App) Bind(structPtr interface{}) {
|
||||
|
||||
// Add the struct to the bindings
|
||||
err := a.bindings.Add(structPtr)
|
||||
if err != nil {
|
||||
a.logger.Fatal("Error during binding: " + err.Error())
|
||||
}
|
||||
}
|
||||
208
v2/internal/app/hybrid.go
Normal file
208
v2/internal/app/hybrid.go
Normal file
@@ -0,0 +1,208 @@
|
||||
// +build !server,!desktop,hybrid
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/leaanthony/clir"
|
||||
"github.com/wailsapp/wails/v2/internal/binding"
|
||||
"github.com/wailsapp/wails/v2/internal/ffenestri"
|
||||
"github.com/wailsapp/wails/v2/internal/logger"
|
||||
"github.com/wailsapp/wails/v2/internal/messagedispatcher"
|
||||
"github.com/wailsapp/wails/v2/internal/servicebus"
|
||||
"github.com/wailsapp/wails/v2/internal/subsystem"
|
||||
"github.com/wailsapp/wails/v2/internal/webserver"
|
||||
)
|
||||
|
||||
// Config defines the Application's configuration
|
||||
type Config struct {
|
||||
Title string // Title is the value to be displayed in the title bar
|
||||
Width int // Width is the desired window width
|
||||
Height int // Height is the desired window height
|
||||
DevTools bool // DevTools enables or disables the browser development tools
|
||||
Resizable bool // Resizable when False prevents window resizing
|
||||
ServerEnabled bool // ServerEnabled when True allows remote connections
|
||||
}
|
||||
|
||||
// App defines a Wails application structure
|
||||
type App struct {
|
||||
config Config
|
||||
window *ffenestri.Application
|
||||
webserver *webserver.WebServer
|
||||
binding *subsystem.Binding
|
||||
call *subsystem.Call
|
||||
event *subsystem.Event
|
||||
log *subsystem.Log
|
||||
runtime *subsystem.Runtime
|
||||
|
||||
bindings *binding.Bindings
|
||||
logger *logger.Logger
|
||||
dispatcher *messagedispatcher.Dispatcher
|
||||
servicebus *servicebus.ServiceBus
|
||||
|
||||
debug bool
|
||||
}
|
||||
|
||||
// Create App
|
||||
func CreateApp(options *Options) *App {
|
||||
|
||||
// Merge default options
|
||||
options.mergeDefaults()
|
||||
|
||||
// Set up logger
|
||||
myLogger := logger.New(os.Stdout)
|
||||
myLogger.SetLogLevel(logger.INFO)
|
||||
|
||||
window := ffenestri.NewApplicationWithConfig(&ffenestri.Config{
|
||||
Title: options.Title,
|
||||
Width: options.Width,
|
||||
Height: options.Height,
|
||||
MinWidth: options.MinWidth,
|
||||
MinHeight: options.MinHeight,
|
||||
MaxWidth: options.MaxWidth,
|
||||
MaxHeight: options.MaxHeight,
|
||||
Frameless: options.Frameless,
|
||||
StartHidden: options.StartHidden,
|
||||
|
||||
// This should be controlled by the compile time flags...
|
||||
DevTools: true,
|
||||
|
||||
Resizable: !options.DisableResize,
|
||||
Fullscreen: options.Fullscreen,
|
||||
}, myLogger)
|
||||
|
||||
app := &App{
|
||||
window: window,
|
||||
webserver: webserver.NewWebServer(myLogger),
|
||||
servicebus: servicebus.New(myLogger),
|
||||
logger: myLogger,
|
||||
bindings: binding.NewBindings(myLogger),
|
||||
}
|
||||
|
||||
// Initialise the app
|
||||
app.Init()
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
// Run the application
|
||||
func (a *App) Run() error {
|
||||
|
||||
// Default app options
|
||||
var port = 8080
|
||||
var ip = "localhost"
|
||||
var suppressLogging = false
|
||||
|
||||
// Create CLI
|
||||
cli := clir.NewCli(filepath.Base(os.Args[0]), "Desktop/Server Build", "")
|
||||
|
||||
// Setup flags
|
||||
cli.IntFlag("p", "Port to serve on", &port)
|
||||
cli.StringFlag("i", "IP to serve on", &ip)
|
||||
cli.BoolFlag("q", "Suppress logging", &suppressLogging)
|
||||
|
||||
// Setup main action
|
||||
cli.Action(func() error {
|
||||
|
||||
// Set IP + Port
|
||||
a.webserver.SetPort(port)
|
||||
a.webserver.SetIP(ip)
|
||||
a.webserver.SetBindings(a.bindings)
|
||||
// Log information (if we aren't suppressing it)
|
||||
if !suppressLogging {
|
||||
cli.PrintBanner()
|
||||
a.logger.Info("Running server at %s", a.webserver.URL())
|
||||
}
|
||||
|
||||
a.servicebus.Start()
|
||||
log, err := subsystem.NewLog(a.servicebus, a.logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.log = log
|
||||
a.log.Start()
|
||||
dispatcher, err := messagedispatcher.New(a.servicebus, a.logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.dispatcher = dispatcher
|
||||
a.dispatcher.Start()
|
||||
|
||||
// Start the runtime
|
||||
runtime, err := subsystem.NewRuntime(a.servicebus, a.logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.runtime = runtime
|
||||
a.runtime.Start()
|
||||
|
||||
// Start the binding subsystem
|
||||
binding, err := subsystem.NewBinding(a.servicebus, a.logger, a.bindings, runtime.GoRuntime())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.binding = binding
|
||||
a.binding.Start()
|
||||
|
||||
// Start the eventing subsystem
|
||||
event, err := subsystem.NewEvent(a.servicebus, a.logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.event = event
|
||||
a.event.Start()
|
||||
|
||||
// Start the call subsystem
|
||||
call, err := subsystem.NewCall(a.servicebus, a.logger, a.bindings.DB())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.call = call
|
||||
a.call.Start()
|
||||
|
||||
// Required so that the WailsInit functions are fired!
|
||||
runtime.GoRuntime().Events.Emit("wails:loaded")
|
||||
|
||||
// Set IP + Port
|
||||
a.webserver.SetPort(port)
|
||||
a.webserver.SetIP(ip)
|
||||
|
||||
// Log information (if we aren't suppressing it)
|
||||
if !suppressLogging {
|
||||
cli.PrintBanner()
|
||||
println("Running server at " + a.webserver.URL())
|
||||
}
|
||||
|
||||
// Dump bindings as a debug
|
||||
bindingDump, err := a.bindings.ToJSON()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err := a.webserver.Start(dispatcher, event); err != nil {
|
||||
a.logger.Error("Webserver failed to start %s", err)
|
||||
}
|
||||
}()
|
||||
|
||||
result := a.window.Run(dispatcher, bindingDump)
|
||||
a.servicebus.Stop()
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
return cli.Run()
|
||||
}
|
||||
|
||||
// Bind a struct to the application by passing in
|
||||
// a pointer to it
|
||||
func (a *App) Bind(structPtr interface{}) {
|
||||
|
||||
// Add the struct to the bindings
|
||||
err := a.bindings.Add(structPtr)
|
||||
if err != nil {
|
||||
a.logger.Fatal("Error during binding: " + err.Error())
|
||||
}
|
||||
}
|
||||
35
v2/internal/app/options.go
Normal file
35
v2/internal/app/options.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package app
|
||||
|
||||
// Options for creating the App
|
||||
type Options struct {
|
||||
Title string
|
||||
Width int
|
||||
Height int
|
||||
DisableResize bool
|
||||
Fullscreen bool
|
||||
Frameless bool
|
||||
MinWidth int
|
||||
MinHeight int
|
||||
MaxWidth int
|
||||
MaxHeight int
|
||||
StartHidden bool
|
||||
}
|
||||
|
||||
// mergeDefaults will set the minimum default values for an application
|
||||
func (o *Options) mergeDefaults() {
|
||||
|
||||
// Create a default title
|
||||
if len(o.Title) == 0 {
|
||||
o.Title = "My Wails App"
|
||||
}
|
||||
|
||||
// Default width
|
||||
if o.Width == 0 {
|
||||
o.Width = 1024
|
||||
}
|
||||
|
||||
// Default height
|
||||
if o.Height == 0 {
|
||||
o.Height = 768
|
||||
}
|
||||
}
|
||||
9
v2/internal/app/production.go
Normal file
9
v2/internal/app/production.go
Normal file
@@ -0,0 +1,9 @@
|
||||
// +build !debug
|
||||
|
||||
package app
|
||||
|
||||
// Init initialises the application for a production environment
|
||||
func (a *App) Init() error {
|
||||
println("Processing production cli options")
|
||||
return nil
|
||||
}
|
||||
160
v2/internal/app/server.go
Normal file
160
v2/internal/app/server.go
Normal file
@@ -0,0 +1,160 @@
|
||||
// +build server,!desktop
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/leaanthony/clir"
|
||||
"github.com/wailsapp/wails/v2/internal/binding"
|
||||
"github.com/wailsapp/wails/v2/internal/logger"
|
||||
"github.com/wailsapp/wails/v2/internal/messagedispatcher"
|
||||
"github.com/wailsapp/wails/v2/internal/servicebus"
|
||||
"github.com/wailsapp/wails/v2/internal/subsystem"
|
||||
"github.com/wailsapp/wails/v2/internal/webserver"
|
||||
)
|
||||
|
||||
// App defines a Wails application structure
|
||||
type App struct {
|
||||
binding *subsystem.Binding
|
||||
call *subsystem.Call
|
||||
event *subsystem.Event
|
||||
log *subsystem.Log
|
||||
runtime *subsystem.Runtime
|
||||
|
||||
bindings *binding.Bindings
|
||||
logger *logger.Logger
|
||||
dispatcher *messagedispatcher.Dispatcher
|
||||
servicebus *servicebus.ServiceBus
|
||||
webserver *webserver.WebServer
|
||||
|
||||
debug bool
|
||||
}
|
||||
|
||||
// Create App
|
||||
func CreateApp(options *Options) *App {
|
||||
options.mergeDefaults()
|
||||
// We ignore the inputs (for now)
|
||||
|
||||
// TODO: Allow logger output override on CLI
|
||||
myLogger := logger.New(os.Stdout)
|
||||
myLogger.SetLogLevel(logger.TRACE)
|
||||
|
||||
result := &App{
|
||||
bindings: binding.NewBindings(myLogger),
|
||||
logger: myLogger,
|
||||
servicebus: servicebus.New(myLogger),
|
||||
webserver: webserver.NewWebServer(myLogger),
|
||||
}
|
||||
|
||||
// Initialise app
|
||||
result.Init()
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Run the application
|
||||
func (a *App) Run() error {
|
||||
|
||||
// Default app options
|
||||
var port = 8080
|
||||
var ip = "localhost"
|
||||
var supressLogging = false
|
||||
var debugMode = false
|
||||
|
||||
// Create CLI
|
||||
cli := clir.NewCli(filepath.Base(os.Args[0]), "Server Build", "")
|
||||
|
||||
// Setup flags
|
||||
cli.IntFlag("p", "Port to serve on", &port)
|
||||
cli.StringFlag("i", "IP to serve on", &ip)
|
||||
cli.BoolFlag("d", "Debug mode", &debugMode)
|
||||
cli.BoolFlag("q", "Supress logging", &supressLogging)
|
||||
|
||||
// Setup main action
|
||||
cli.Action(func() error {
|
||||
|
||||
// Set IP + Port
|
||||
a.webserver.SetPort(port)
|
||||
a.webserver.SetIP(ip)
|
||||
a.webserver.SetBindings(a.bindings)
|
||||
// Log information (if we aren't supressing it)
|
||||
if !supressLogging {
|
||||
cli.PrintBanner()
|
||||
a.logger.Info("Running server at %s", a.webserver.URL())
|
||||
}
|
||||
|
||||
if debugMode {
|
||||
a.servicebus.Debug()
|
||||
}
|
||||
a.servicebus.Start()
|
||||
log, err := subsystem.NewLog(a.servicebus, a.logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.log = log
|
||||
a.log.Start()
|
||||
dispatcher, err := messagedispatcher.New(a.servicebus, a.logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.dispatcher = dispatcher
|
||||
a.dispatcher.Start()
|
||||
|
||||
// Start the runtime
|
||||
runtime, err := subsystem.NewRuntime(a.servicebus, a.logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.runtime = runtime
|
||||
a.runtime.Start()
|
||||
|
||||
// Start the binding subsystem
|
||||
binding, err := subsystem.NewBinding(a.servicebus, a.logger, a.bindings, runtime.GoRuntime())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.binding = binding
|
||||
a.binding.Start()
|
||||
|
||||
// Start the eventing subsystem
|
||||
event, err := subsystem.NewEvent(a.servicebus, a.logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.event = event
|
||||
a.event.Start()
|
||||
|
||||
// Start the call subsystem
|
||||
call, err := subsystem.NewCall(a.servicebus, a.logger, a.bindings.DB())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.call = call
|
||||
a.call.Start()
|
||||
|
||||
// Required so that the WailsInit functions are fired!
|
||||
runtime.GoRuntime().Events.Emit("wails:loaded")
|
||||
|
||||
if err := a.webserver.Start(dispatcher, event); err != nil {
|
||||
a.logger.Error("Webserver failed to start %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return cli.Run()
|
||||
}
|
||||
|
||||
// Bind a struct to the application by passing in
|
||||
// a pointer to it
|
||||
func (a *App) Bind(structPtr interface{}) {
|
||||
|
||||
// Add the struct to the bindings
|
||||
err := a.bindings.Add(structPtr)
|
||||
if err != nil {
|
||||
a.logger.Fatal("Error during binding: " + err.Error())
|
||||
}
|
||||
}
|
||||
112
v2/internal/assetdb/assetdb.go
Normal file
112
v2/internal/assetdb/assetdb.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package assetdb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// AssetDB is a database for assets encoded as byte slices
|
||||
type AssetDB struct {
|
||||
db map[string][]byte
|
||||
}
|
||||
|
||||
// NewAssetDB creates a new AssetDB and initialises a blank db
|
||||
func NewAssetDB() *AssetDB {
|
||||
return &AssetDB{
|
||||
db: make(map[string][]byte),
|
||||
}
|
||||
}
|
||||
|
||||
// AddAsset saves the given byte slice under the given name
|
||||
func (a *AssetDB) AddAsset(name string, data []byte) {
|
||||
a.db[name] = data
|
||||
}
|
||||
|
||||
// Remove removes the named asset
|
||||
func (a *AssetDB) Remove(name string) {
|
||||
delete(a.db, name)
|
||||
}
|
||||
|
||||
// Asset retrieves the byte slice for the given name
|
||||
func (a *AssetDB) Read(name string) ([]byte, error) {
|
||||
result := a.db[name]
|
||||
if result == nil {
|
||||
return nil, fmt.Errorf("asset '%s' not found", name)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// AssetAsString returns the asset as a string.
|
||||
// It also returns a boolean indicating whether the asset existed or not.
|
||||
func (a *AssetDB) String(name string) (string, error) {
|
||||
asset, err := a.Read(name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return *(*string)(unsafe.Pointer(&asset)), nil
|
||||
}
|
||||
|
||||
func (a *AssetDB) Dump() {
|
||||
fmt.Printf("Assets:\n")
|
||||
for k, _ := range a.db {
|
||||
fmt.Println(k)
|
||||
}
|
||||
}
|
||||
|
||||
// Serialize converts the entire database to a file that when compiled will
|
||||
// reconstruct the AssetDB during init()
|
||||
// name: name of the asset.AssetDB instance
|
||||
// pkg: package name placed at the top of the file
|
||||
func (a *AssetDB) Serialize(name, pkg string) string {
|
||||
var cdata strings.Builder
|
||||
// Set buffer size to 4k
|
||||
cdata.Grow(4096)
|
||||
|
||||
// Write header
|
||||
header := `// DO NOT EDIT - Generated automatically
|
||||
package %s
|
||||
|
||||
import "github.com/wailsapp/wails/v2/internal/assetdb"
|
||||
|
||||
var (
|
||||
%s *assetdb.AssetDB = assetdb.NewAssetDB()
|
||||
)
|
||||
|
||||
// AssetsDB is a clean interface to the assetdb.AssetDB struct
|
||||
type AssetsDB interface {
|
||||
Read(string) ([]byte, error)
|
||||
String(string) (string, error)
|
||||
}
|
||||
|
||||
// Assets returns the asset database
|
||||
func Assets() AssetsDB {
|
||||
return %s
|
||||
}
|
||||
|
||||
func init() {
|
||||
`
|
||||
cdata.WriteString(fmt.Sprintf(header, pkg, name, name))
|
||||
|
||||
for aname, bytes := range a.db {
|
||||
cdata.WriteString(fmt.Sprintf("\t%s.AddAsset(\"%s\", []byte{",
|
||||
name,
|
||||
aname))
|
||||
|
||||
l := len(bytes)
|
||||
if l == 0 {
|
||||
cdata.WriteString("0x00})\n")
|
||||
continue
|
||||
}
|
||||
|
||||
// Convert each byte to hex
|
||||
for _, b := range bytes[:l-1] {
|
||||
cdata.WriteString(fmt.Sprintf("0x%x, ", b))
|
||||
}
|
||||
cdata.WriteString(fmt.Sprintf("0x%x})\n", bytes[l-1]))
|
||||
}
|
||||
|
||||
cdata.WriteString(`}`)
|
||||
|
||||
return cdata.String()
|
||||
}
|
||||
70
v2/internal/assetdb/assetdb_test.go
Normal file
70
v2/internal/assetdb/assetdb_test.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package assetdb
|
||||
|
||||
import "testing"
|
||||
import "github.com/matryer/is"
|
||||
|
||||
func TestExistsAsBytes(t *testing.T) {
|
||||
|
||||
is := is.New(t)
|
||||
|
||||
var helloworld = []byte{72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33}
|
||||
|
||||
db := NewAssetDB()
|
||||
db.AddAsset("hello", helloworld)
|
||||
|
||||
result, err := db.Read("hello")
|
||||
|
||||
is.True(err == nil)
|
||||
is.Equal(result, helloworld)
|
||||
}
|
||||
|
||||
func TestNotExistsAsBytes(t *testing.T) {
|
||||
|
||||
is := is.New(t)
|
||||
|
||||
var helloworld = []byte{72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33}
|
||||
|
||||
db := NewAssetDB()
|
||||
db.AddAsset("hello4", helloworld)
|
||||
|
||||
result, err := db.Read("hello")
|
||||
|
||||
is.True(err != nil)
|
||||
is.True(result == nil)
|
||||
}
|
||||
|
||||
func TestExistsAsString(t *testing.T) {
|
||||
|
||||
is := is.New(t)
|
||||
|
||||
var helloworld = []byte{72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33}
|
||||
|
||||
db := NewAssetDB()
|
||||
db.AddAsset("hello", helloworld)
|
||||
|
||||
result, err := db.String("hello")
|
||||
|
||||
// Ensure it exists
|
||||
is.True(err == nil)
|
||||
|
||||
// Ensure the string is the same as the byte slice
|
||||
is.Equal(result, "Hello, World!")
|
||||
}
|
||||
|
||||
func TestNotExistsAsString(t *testing.T) {
|
||||
|
||||
is := is.New(t)
|
||||
|
||||
var helloworld = []byte{72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33}
|
||||
|
||||
db := NewAssetDB()
|
||||
db.AddAsset("hello", helloworld)
|
||||
|
||||
result, err := db.String("help")
|
||||
|
||||
// Ensure it doesn't exist
|
||||
is.True(err != nil)
|
||||
|
||||
// Ensure the string is blank
|
||||
is.Equal(result, "")
|
||||
}
|
||||
176
v2/internal/assetdb/filesystem.go
Normal file
176
v2/internal/assetdb/filesystem.go
Normal file
@@ -0,0 +1,176 @@
|
||||
// +build !desktop
|
||||
package assetdb
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var errWhence = errors.New("Seek: invalid whence")
|
||||
var errOffset = errors.New("Seek: invalid offset")
|
||||
|
||||
// Open implements the http.FileSystem interface for the AssetDB
|
||||
func (a *AssetDB) Open(name string) (http.File, error) {
|
||||
if name == "/" || name == "" {
|
||||
return &Entry{name: "/", dir: true, db: a}, nil
|
||||
}
|
||||
|
||||
if data, ok := a.db[name]; ok {
|
||||
return &Entry{name: name, data: data, size: len(data)}, nil
|
||||
} else {
|
||||
for n, _ := range a.db {
|
||||
if strings.HasPrefix(n, name+"/") {
|
||||
return &Entry{name: name, db: a, dir: true}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return &Entry{}, os.ErrNotExist
|
||||
}
|
||||
|
||||
// readdir returns the directory entries for the requested directory
|
||||
func (a *AssetDB) readdir(name string) ([]os.FileInfo, error) {
|
||||
dir := name
|
||||
var ents []string
|
||||
|
||||
fim := make(map[string]os.FileInfo)
|
||||
for fn, data := range a.db {
|
||||
if strings.HasPrefix(fn, dir) {
|
||||
pieces := strings.Split(fn[len(dir)+1:], "/")
|
||||
if len(pieces) == 1 {
|
||||
fim[pieces[0]] = FI{name: pieces[0], dir: false, size: len(data)}
|
||||
ents = append(ents, pieces[0])
|
||||
} else {
|
||||
fim[pieces[0]] = FI{name: pieces[0], dir: true, size: -1}
|
||||
ents = append(ents, pieces[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(ents) == 0 {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
|
||||
sort.Strings(ents)
|
||||
var list []os.FileInfo
|
||||
for _, dir := range ents {
|
||||
list = append(list, fim[dir])
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
// Entry implements the http.File interface to allow for
|
||||
// use in the http.FileSystem implementation of AssetDB
|
||||
type Entry struct {
|
||||
name string
|
||||
data []byte
|
||||
dir bool
|
||||
size int
|
||||
db *AssetDB
|
||||
off int
|
||||
}
|
||||
|
||||
// Close is a noop
|
||||
func (e Entry) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Read fills the slice provided returning how many bytes were written
|
||||
// and any errors encountered
|
||||
func (e *Entry) Read(p []byte) (n int, err error) {
|
||||
if e.off >= e.size {
|
||||
return 0, io.EOF
|
||||
}
|
||||
numout := len(p)
|
||||
if numout > e.size {
|
||||
numout = e.size
|
||||
}
|
||||
for i := 0; i < numout; i++ {
|
||||
p[i] = e.data[e.off+i]
|
||||
}
|
||||
e.off += numout
|
||||
n = int(numout)
|
||||
err = nil
|
||||
return
|
||||
}
|
||||
|
||||
// Seek seeks to the specified offset from the location specified by whence
|
||||
func (e *Entry) Seek(offset int64, whence int) (int64, error) {
|
||||
switch whence {
|
||||
default:
|
||||
return 0, errWhence
|
||||
case io.SeekStart:
|
||||
offset += 0
|
||||
case io.SeekCurrent:
|
||||
offset += int64(e.off)
|
||||
case io.SeekEnd:
|
||||
offset += int64(e.size)
|
||||
}
|
||||
|
||||
if offset < 0 {
|
||||
return 0, errOffset
|
||||
}
|
||||
e.off = int(offset)
|
||||
return offset, nil
|
||||
}
|
||||
|
||||
// Readdir returns the directory entries inside this entry if it is a directory
|
||||
func (e Entry) Readdir(count int) ([]os.FileInfo, error) {
|
||||
ents := []os.FileInfo{}
|
||||
if !e.dir {
|
||||
return ents, errors.New("Not a directory")
|
||||
}
|
||||
return e.db.readdir(e.name)
|
||||
}
|
||||
|
||||
// Stat returns information about this directory entry
|
||||
func (e Entry) Stat() (os.FileInfo, error) {
|
||||
return FI{e.name, e.size, e.dir}, nil
|
||||
}
|
||||
|
||||
// FI is the AssetDB implementation of os.FileInfo.
|
||||
type FI struct {
|
||||
name string
|
||||
size int
|
||||
dir bool
|
||||
}
|
||||
|
||||
// IsDir returns true if this is a directory
|
||||
func (fi FI) IsDir() bool {
|
||||
return fi.dir
|
||||
}
|
||||
|
||||
// ModTime always returns now
|
||||
func (fi FI) ModTime() time.Time {
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
// Mode returns the file as readonly and directories
|
||||
// as world writeable and executable
|
||||
func (fi FI) Mode() os.FileMode {
|
||||
if fi.IsDir() {
|
||||
return 0755 | os.ModeDir
|
||||
}
|
||||
return 0444
|
||||
}
|
||||
|
||||
// Name returns the name of this object without
|
||||
// any leading slashes
|
||||
func (fi FI) Name() string {
|
||||
return path.Base(fi.name)
|
||||
}
|
||||
|
||||
// Size returns the size of this item
|
||||
func (fi FI) Size() int64 {
|
||||
return int64(fi.size)
|
||||
}
|
||||
|
||||
// Sys returns nil
|
||||
func (fi FI) Sys() interface{} {
|
||||
return nil
|
||||
}
|
||||
108
v2/internal/assetdb/filesystem_test.go
Normal file
108
v2/internal/assetdb/filesystem_test.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package assetdb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/matryer/is"
|
||||
)
|
||||
|
||||
func TestOpenLeadingSlash(t *testing.T) {
|
||||
is := is.New(t)
|
||||
|
||||
var helloworld = []byte{72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33}
|
||||
|
||||
db := NewAssetDB()
|
||||
db.AddAsset("/hello", helloworld)
|
||||
|
||||
file, err := db.Open("/hello")
|
||||
// Ensure it does exist
|
||||
is.True(err == nil)
|
||||
|
||||
buff := make([]byte, len(helloworld))
|
||||
n, err := file.Read(buff)
|
||||
fmt.Printf("Error %v\n", err)
|
||||
is.True(err == nil)
|
||||
is.Equal(n, len(helloworld))
|
||||
result := string(buff)
|
||||
|
||||
// Ensure the string is blank
|
||||
is.Equal(result, string(helloworld))
|
||||
}
|
||||
|
||||
func TestOpen(t *testing.T) {
|
||||
is := is.New(t)
|
||||
|
||||
var helloworld = []byte{72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33}
|
||||
|
||||
db := NewAssetDB()
|
||||
db.AddAsset("/hello", helloworld)
|
||||
|
||||
file, err := db.Open("/hello")
|
||||
|
||||
// Ensure it does exist
|
||||
is.True(err == nil)
|
||||
|
||||
buff := make([]byte, len(helloworld))
|
||||
n, err := file.Read(buff)
|
||||
is.True(err == nil)
|
||||
is.Equal(n, len(helloworld))
|
||||
result := string(buff)
|
||||
|
||||
// Ensure the string is blank
|
||||
is.Equal(result, string(helloworld))
|
||||
}
|
||||
|
||||
func TestReaddir(t *testing.T) {
|
||||
is := is.New(t)
|
||||
|
||||
var helloworld = []byte{72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33}
|
||||
|
||||
db := NewAssetDB()
|
||||
db.AddAsset("/hello", helloworld)
|
||||
db.AddAsset("/directory/hello", helloworld)
|
||||
db.AddAsset("/directory/subdirectory/hello", helloworld)
|
||||
|
||||
dir, err := db.Open("/doesntexist")
|
||||
is.True(err == os.ErrNotExist)
|
||||
ents, err := dir.Readdir(-1)
|
||||
is.Equal([]os.FileInfo{}, ents)
|
||||
|
||||
dir, err = db.Open("/")
|
||||
is.True(dir != nil)
|
||||
is.True(err == nil)
|
||||
ents, err = dir.Readdir(-1)
|
||||
is.True(err == nil)
|
||||
is.Equal(3, len(ents))
|
||||
}
|
||||
|
||||
func TestReaddirSubdirectory(t *testing.T) {
|
||||
is := is.New(t)
|
||||
|
||||
var helloworld = []byte{72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33}
|
||||
|
||||
db := NewAssetDB()
|
||||
db.AddAsset("/hello", helloworld)
|
||||
db.AddAsset("/directory/hello", helloworld)
|
||||
db.AddAsset("/directory/subdirectory/hello", helloworld)
|
||||
|
||||
expected := []os.FileInfo{
|
||||
FI{name: "hello", dir: false, size: len(helloworld)},
|
||||
FI{name: "subdirectory", dir: true, size: -1},
|
||||
}
|
||||
|
||||
dir, err := db.Open("/directory")
|
||||
is.True(dir != nil)
|
||||
is.True(err == nil)
|
||||
ents, err := dir.Readdir(-1)
|
||||
is.Equal(expected, ents)
|
||||
|
||||
// Check sub-subdirectory
|
||||
dir, err = db.Open("/directory/subdirectory")
|
||||
is.True(dir != nil)
|
||||
is.True(err == nil)
|
||||
ents, err = dir.Readdir(-1)
|
||||
is.True(err == nil)
|
||||
is.Equal([]os.FileInfo{FI{name: "hello", size: len(helloworld)}}, ents)
|
||||
}
|
||||
9
v2/internal/bind/bind.go
Normal file
9
v2/internal/bind/bind.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package bind
|
||||
|
||||
func IsStructPointer(value interface{}) bool {
|
||||
switch t := value.(type) {
|
||||
default:
|
||||
println(t)
|
||||
}
|
||||
return false
|
||||
}
|
||||
70
v2/internal/binding/binding.go
Executable file
70
v2/internal/binding/binding.go
Executable file
@@ -0,0 +1,70 @@
|
||||
package binding
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/wailsapp/wails/v2/internal/logger"
|
||||
)
|
||||
|
||||
type Bindings struct {
|
||||
db *DB
|
||||
logger logger.CustomLogger
|
||||
}
|
||||
|
||||
// NewBindings returns a new Bindings object
|
||||
func NewBindings(logger *logger.Logger) *Bindings {
|
||||
return &Bindings{
|
||||
db: newDB(),
|
||||
logger: logger.CustomLogger("Bindings"),
|
||||
}
|
||||
}
|
||||
|
||||
// Add the given struct methods to the Bindings
|
||||
func (b *Bindings) Add(structPtr interface{}) error {
|
||||
|
||||
methods, err := getMethods(structPtr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannout bind value to app: %s", err.Error())
|
||||
}
|
||||
|
||||
for _, method := range methods {
|
||||
splitName := strings.Split(method.Name, ".")
|
||||
packageName := splitName[0]
|
||||
structName := splitName[1]
|
||||
methodName := splitName[2]
|
||||
|
||||
// Is this WailsInit?
|
||||
if method.IsWailsInit() {
|
||||
err := b.db.AddWailsInit(method)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.logger.Trace("Registered WailsInit method: %s", method.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
// Is this WailsShutdown?
|
||||
if method.IsWailsShutdown() {
|
||||
err := b.db.AddWailsShutdown(method)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.logger.Trace("Registered WailsShutdown method: %s", method.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
// Add it as a regular method
|
||||
b.db.AddMethod(packageName, structName, methodName, method)
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Bindings) DB() *DB {
|
||||
return b.db
|
||||
}
|
||||
|
||||
func (b *Bindings) ToJSON() (string, error) {
|
||||
return b.db.ToJSON()
|
||||
}
|
||||
138
v2/internal/binding/boundMethod.go
Normal file
138
v2/internal/binding/boundMethod.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package binding
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// BoundMethod defines all the data related to a Go method that is
|
||||
// bound to the Wails application
|
||||
type BoundMethod struct {
|
||||
Name string `json:"name"`
|
||||
Inputs []*Parameter `json:"inputs,omitempty"`
|
||||
Outputs []*Parameter `json:"outputs,omitempty"`
|
||||
Comments string `json:"comments,omitempty"`
|
||||
Method reflect.Value `json:"-"`
|
||||
}
|
||||
|
||||
// IsWailsInit returns true if the method name is "WailsInit"
|
||||
func (b *BoundMethod) IsWailsInit() bool {
|
||||
return strings.HasSuffix(b.Name, "WailsInit")
|
||||
}
|
||||
|
||||
// IsWailsShutdown returns true if the method name is "WailsShutdown"
|
||||
func (b *BoundMethod) IsWailsShutdown() bool {
|
||||
return strings.HasSuffix(b.Name, "WailsShutdown")
|
||||
}
|
||||
|
||||
// VerifyWailsInit checks if the WailsInit signature is correct
|
||||
func (b *BoundMethod) VerifyWailsInit() error {
|
||||
// Must only have 1 input
|
||||
if b.InputCount() != 1 {
|
||||
return fmt.Errorf("invalid method signature for %s: expected `WailsInit(*wails.Runtime) error`", b.Name)
|
||||
}
|
||||
|
||||
// Check input type
|
||||
if !b.Inputs[0].IsType("*goruntime.Runtime") {
|
||||
return fmt.Errorf("invalid method signature for %s: expected `WailsInit(*wails.Runtime) error`", b.Name)
|
||||
}
|
||||
|
||||
// Must only have 1 output
|
||||
if b.OutputCount() != 1 {
|
||||
return fmt.Errorf("invalid method signature for %s: expected `WailsInit(*wails.Runtime) error`", b.Name)
|
||||
}
|
||||
|
||||
// Check output type
|
||||
if !b.Outputs[0].IsError() {
|
||||
return fmt.Errorf("invalid method signature for %s: expected `WailsInit(*wails.Runtime) error`", b.Name)
|
||||
}
|
||||
|
||||
// Input must be of type Runtime
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifyWailsShutdown checks if the WailsShutdown signature is correct
|
||||
func (b *BoundMethod) VerifyWailsShutdown() error {
|
||||
// Must have no inputs
|
||||
if b.InputCount() != 0 {
|
||||
return fmt.Errorf("invalid method signature for WailsShutdown: expected `WailsShutdown()`")
|
||||
}
|
||||
|
||||
// Must have no outputs
|
||||
if b.OutputCount() != 0 {
|
||||
return fmt.Errorf("invalid method signature for WailsShutdown: expected `WailsShutdown()`")
|
||||
}
|
||||
|
||||
// Input must be of type Runtime
|
||||
return nil
|
||||
}
|
||||
|
||||
// InputCount returns the number of inputs this bound method has
|
||||
func (b *BoundMethod) InputCount() int {
|
||||
return len(b.Inputs)
|
||||
}
|
||||
|
||||
// OutputCount returns the number of outputs this bound method has
|
||||
func (b *BoundMethod) OutputCount() int {
|
||||
return len(b.Outputs)
|
||||
}
|
||||
|
||||
// Call will attempt to call this bound method with the given args
|
||||
func (b *BoundMethod) Call(args []interface{}) (interface{}, error) {
|
||||
// Check inputs
|
||||
expectedInputLength := len(b.Inputs)
|
||||
actualInputLength := len(args)
|
||||
if expectedInputLength != actualInputLength {
|
||||
return nil, fmt.Errorf("%s takes %d inputs. Received %d", b.Name, expectedInputLength, actualInputLength)
|
||||
}
|
||||
|
||||
/** Convert inputs to reflect values **/
|
||||
|
||||
// Create slice for the input arguments to the method call
|
||||
callArgs := make([]reflect.Value, expectedInputLength)
|
||||
|
||||
// Iterate over given arguments
|
||||
for index, arg := range args {
|
||||
|
||||
// Attempt to convert the argument to the type expected by the method
|
||||
value, err := convertArgToValue(arg, b.Inputs[index])
|
||||
|
||||
// If it fails, return a suitable error
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s (parameter %d): %s", b.Name, index+1, err.Error())
|
||||
}
|
||||
|
||||
// Save the converted argument
|
||||
callArgs[index] = value
|
||||
}
|
||||
|
||||
// Do the call
|
||||
callResults := b.Method.Call(callArgs)
|
||||
|
||||
//** Check results **//
|
||||
var returnValue interface{}
|
||||
var err error
|
||||
|
||||
switch b.OutputCount() {
|
||||
case 1:
|
||||
// Loop over results and determine if the result
|
||||
// is an error or not
|
||||
for _, result := range callResults {
|
||||
interfac := result.Interface()
|
||||
temp, ok := interfac.(error)
|
||||
if ok {
|
||||
err = temp
|
||||
} else {
|
||||
returnValue = interfac
|
||||
}
|
||||
}
|
||||
case 2:
|
||||
returnValue = callResults[0].Interface()
|
||||
if temp, ok := callResults[1].Interface().(error); ok {
|
||||
err = temp
|
||||
}
|
||||
}
|
||||
|
||||
return returnValue, err
|
||||
}
|
||||
150
v2/internal/binding/db.go
Normal file
150
v2/internal/binding/db.go
Normal file
@@ -0,0 +1,150 @@
|
||||
package binding
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sync"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// DB is our database of method bindings
|
||||
type DB struct {
|
||||
// map[packagename] -> map[structname] -> map[methodname]*method
|
||||
store map[string]map[string]map[string]*BoundMethod
|
||||
|
||||
// This uses fully qualified method names as a shortcut for store traversal.
|
||||
// It used for performance gains at runtime
|
||||
methodMap map[string]*BoundMethod
|
||||
|
||||
// These are slices of methods registered using WailsInit and WailsShutdown
|
||||
wailsInitMethods []*BoundMethod
|
||||
wailsShutdownMethods []*BoundMethod
|
||||
|
||||
// Lock to ensure sync access to the data
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
func newDB() *DB {
|
||||
return &DB{
|
||||
store: make(map[string]map[string]map[string]*BoundMethod),
|
||||
methodMap: make(map[string]*BoundMethod),
|
||||
}
|
||||
}
|
||||
|
||||
// GetMethodFromStore returns the method for the given package/struct/method names
|
||||
// nil is returned if any one of those does not exist
|
||||
func (d *DB) GetMethodFromStore(packageName string, structName string, methodName string) *BoundMethod {
|
||||
|
||||
// Lock the db whilst processing and unlock on return
|
||||
d.lock.RLock()
|
||||
defer d.lock.RUnlock()
|
||||
|
||||
structMap, exists := d.store[packageName]
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
methodMap, exists := structMap[structName]
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
return methodMap[methodName]
|
||||
}
|
||||
|
||||
// GetMethod returns the method for the given qualified method name
|
||||
// qualifiedMethodName is "packagename.structname.methodname"
|
||||
func (d *DB) GetMethod(qualifiedMethodName string) *BoundMethod {
|
||||
|
||||
// Lock the db whilst processing and unlock on return
|
||||
d.lock.RLock()
|
||||
defer d.lock.RUnlock()
|
||||
|
||||
return d.methodMap[qualifiedMethodName]
|
||||
}
|
||||
|
||||
// AddMethod adds the given method definition to the db using the given qualified path: packageName.structName.methodName
|
||||
func (d *DB) AddMethod(packageName string, structName string, methodName string, methodDefinition *BoundMethod) {
|
||||
|
||||
// TODO: Validate inputs?
|
||||
|
||||
// Lock the db whilst processing and unlock on return
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
|
||||
// Get the map associated with the package name
|
||||
structMap, exists := d.store[packageName]
|
||||
if !exists {
|
||||
// Create a new map for this packagename
|
||||
d.store[packageName] = make(map[string]map[string]*BoundMethod)
|
||||
structMap = d.store[packageName]
|
||||
}
|
||||
|
||||
// Get the map associated with the struct name
|
||||
methodMap, exists := structMap[structName]
|
||||
if !exists {
|
||||
// Create a new map for this packagename
|
||||
structMap[structName] = make(map[string]*BoundMethod)
|
||||
methodMap = structMap[structName]
|
||||
}
|
||||
|
||||
// Store the method definition
|
||||
methodMap[methodName] = methodDefinition
|
||||
|
||||
// Store in the methodMap
|
||||
key := packageName + "." + structName + "." + methodName
|
||||
d.methodMap[key] = methodDefinition
|
||||
|
||||
}
|
||||
|
||||
// AddWailsInit checks the given method is a WailsInit method and if it
|
||||
// is, adds it to the list of WailsInit methods
|
||||
func (d *DB) AddWailsInit(method *BoundMethod) error {
|
||||
err := method.VerifyWailsInit()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Lock the db whilst processing and unlock on return
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
|
||||
d.wailsInitMethods = append(d.wailsInitMethods, method)
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddWailsShutdown checks the given method is a WailsInit method and if it
|
||||
// is, adds it to the list of WailsShutdown methods
|
||||
func (d *DB) AddWailsShutdown(method *BoundMethod) error {
|
||||
err := method.VerifyWailsShutdown()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Lock the db whilst processing and unlock on return
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
|
||||
d.wailsShutdownMethods = append(d.wailsShutdownMethods, method)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToJSON converts the method map to JSON
|
||||
func (d *DB) ToJSON() (string, error) {
|
||||
|
||||
// Lock the db whilst processing and unlock on return
|
||||
d.lock.RLock()
|
||||
defer d.lock.RUnlock()
|
||||
|
||||
bytes, err := json.Marshal(&d.store)
|
||||
|
||||
// Return zero copy string as this string will be read only
|
||||
return *(*string)(unsafe.Pointer(&bytes)), err
|
||||
}
|
||||
|
||||
// WailsInitMethods returns the list of registered WailsInit methods
|
||||
func (d *DB) WailsInitMethods() []*BoundMethod {
|
||||
return d.wailsInitMethods
|
||||
}
|
||||
|
||||
// WailsShutdownMethods returns the list of registered WailsInit methods
|
||||
func (d *DB) WailsShutdownMethods() []*BoundMethod {
|
||||
return d.wailsShutdownMethods
|
||||
}
|
||||
28
v2/internal/binding/parameter.go
Normal file
28
v2/internal/binding/parameter.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package binding
|
||||
|
||||
import "reflect"
|
||||
|
||||
// Parameter defines a Go method parameter
|
||||
type Parameter struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
TypeName string `json:"type"`
|
||||
reflectType reflect.Type
|
||||
}
|
||||
|
||||
func newParameter(Name string, Type reflect.Type) *Parameter {
|
||||
return &Parameter{
|
||||
Name: Name,
|
||||
TypeName: Type.String(),
|
||||
reflectType: Type,
|
||||
}
|
||||
}
|
||||
|
||||
// IsType returns true if the given
|
||||
func (p *Parameter) IsType(typename string) bool {
|
||||
return p.TypeName == typename
|
||||
}
|
||||
|
||||
// IsError returns true if the parameter type is an error
|
||||
func (p *Parameter) IsError() bool {
|
||||
return p.IsType("error")
|
||||
}
|
||||
120
v2/internal/binding/reflect.go
Executable file
120
v2/internal/binding/reflect.go
Executable file
@@ -0,0 +1,120 @@
|
||||
package binding
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// isStructPtr returns true if the value given is a
|
||||
// pointer to a struct
|
||||
func isStructPtr(value interface{}) bool {
|
||||
return reflect.ValueOf(value).Kind() == reflect.Ptr &&
|
||||
reflect.ValueOf(value).Elem().Kind() == reflect.Struct
|
||||
}
|
||||
|
||||
// isStructPtr returns true if the value given is a struct
|
||||
func isStruct(value interface{}) bool {
|
||||
return reflect.ValueOf(value).Kind() == reflect.Struct
|
||||
}
|
||||
|
||||
func getMethods(value interface{}) ([]*BoundMethod, error) {
|
||||
|
||||
// Create result placeholder
|
||||
var result []*BoundMethod
|
||||
|
||||
// Check type
|
||||
if !isStructPtr(value) {
|
||||
|
||||
if isStruct(value) {
|
||||
name := reflect.ValueOf(value).Type().Name()
|
||||
return nil, fmt.Errorf("%s is a struct, not a pointer to a struct", name)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("not a pointer to a struct")
|
||||
}
|
||||
|
||||
// Process Struct
|
||||
structType := reflect.TypeOf(value)
|
||||
structValue := reflect.ValueOf(value)
|
||||
baseName := structType.String()[1:]
|
||||
|
||||
// Process Methods
|
||||
for i := 0; i < structType.NumMethod(); i++ {
|
||||
methodDef := structType.Method(i)
|
||||
methodName := methodDef.Name
|
||||
fullMethodName := baseName + "." + methodName
|
||||
method := structValue.MethodByName(methodName)
|
||||
|
||||
// Create new method
|
||||
boundMethod := &BoundMethod{
|
||||
Name: fullMethodName,
|
||||
Inputs: nil,
|
||||
Outputs: nil,
|
||||
Comments: "",
|
||||
Method: method,
|
||||
}
|
||||
|
||||
// Iterate inputs
|
||||
methodType := method.Type()
|
||||
inputParamCount := methodType.NumIn()
|
||||
var inputs []*Parameter
|
||||
for inputIndex := 0; inputIndex < inputParamCount; inputIndex++ {
|
||||
input := methodType.In(inputIndex)
|
||||
thisParam := newParameter("", input)
|
||||
inputs = append(inputs, thisParam)
|
||||
}
|
||||
|
||||
boundMethod.Inputs = inputs
|
||||
|
||||
// Iterate outputs
|
||||
outputParamCount := methodType.NumOut()
|
||||
var outputs []*Parameter
|
||||
for outputIndex := 0; outputIndex < outputParamCount; outputIndex++ {
|
||||
output := methodType.Out(outputIndex)
|
||||
thisParam := newParameter("", output)
|
||||
outputs = append(outputs, thisParam)
|
||||
}
|
||||
boundMethod.Outputs = outputs
|
||||
|
||||
// Save method in result
|
||||
result = append(result, boundMethod)
|
||||
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// convertArgToValue
|
||||
func convertArgToValue(input interface{}, target *Parameter) (result reflect.Value, err error) {
|
||||
|
||||
// Catch type conversion panics thrown by convert
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
// Modify error
|
||||
err = fmt.Errorf("%s", r.(string)[23:])
|
||||
}
|
||||
}()
|
||||
|
||||
// Do the conversion
|
||||
|
||||
// Handle nil values
|
||||
if input == nil {
|
||||
switch target.reflectType.Kind() {
|
||||
case reflect.Chan,
|
||||
reflect.Func,
|
||||
reflect.Interface,
|
||||
reflect.Map,
|
||||
reflect.Ptr,
|
||||
reflect.Slice:
|
||||
result = reflect.ValueOf(input).Convert(target.reflectType)
|
||||
default:
|
||||
return reflect.Zero(target.reflectType), fmt.Errorf("Unable to use null value")
|
||||
}
|
||||
} else {
|
||||
result = reflect.ValueOf(input).Convert(target.reflectType)
|
||||
}
|
||||
|
||||
// We don't like doing this but it's the only way to
|
||||
// handle recover() correctly
|
||||
return
|
||||
|
||||
}
|
||||
17
v2/internal/crypto/crypto.go
Normal file
17
v2/internal/crypto/crypto.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"log"
|
||||
)
|
||||
|
||||
// RandomID returns a random ID as a string
|
||||
func RandomID() string {
|
||||
b := make([]byte, 16)
|
||||
_, err := rand.Read(b)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return fmt.Sprintf("%x", b)
|
||||
}
|
||||
62
v2/internal/ffenestri/README.md
Normal file
62
v2/internal/ffenestri/README.md
Normal file
File diff suppressed because one or more lines are too long
190
v2/internal/ffenestri/ffenestri.go
Normal file
190
v2/internal/ffenestri/ffenestri.go
Normal file
@@ -0,0 +1,190 @@
|
||||
package ffenestri
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"strings"
|
||||
"unsafe"
|
||||
|
||||
"github.com/wailsapp/wails/v2/internal/logger"
|
||||
"github.com/wailsapp/wails/v2/internal/messagedispatcher"
|
||||
)
|
||||
|
||||
/*
|
||||
|
||||
#cgo linux CFLAGS: -DFFENESTRI_LINUX=1
|
||||
#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.0
|
||||
|
||||
#cgo darwin CFLAGS: -DFFENESTRI_DARWIN=1
|
||||
#cgo darwin LDFLAGS: -framework WebKit -lobjc
|
||||
|
||||
#include <stdlib.h>
|
||||
#include "ffenestri.h"
|
||||
|
||||
|
||||
*/
|
||||
import "C"
|
||||
|
||||
// DEBUG is the global Ffenestri debug flag.
|
||||
// TODO: move to compile time.
|
||||
var DEBUG bool = true
|
||||
|
||||
// Config defines how our application should be configured
|
||||
type Config struct {
|
||||
Title string
|
||||
Width int
|
||||
Height int
|
||||
MinWidth int
|
||||
MinHeight int
|
||||
MaxWidth int
|
||||
MaxHeight int
|
||||
DevTools bool
|
||||
Resizable bool
|
||||
Fullscreen bool
|
||||
Frameless bool
|
||||
StartHidden bool
|
||||
}
|
||||
|
||||
var defaultConfig = &Config{
|
||||
Title: "My Wails App",
|
||||
Width: 800,
|
||||
Height: 600,
|
||||
DevTools: true,
|
||||
Resizable: true,
|
||||
Fullscreen: false,
|
||||
Frameless: false,
|
||||
StartHidden: false,
|
||||
}
|
||||
|
||||
// Application is our main application object
|
||||
type Application struct {
|
||||
config *Config
|
||||
memory []unsafe.Pointer
|
||||
|
||||
// This is the main app pointer
|
||||
app unsafe.Pointer
|
||||
|
||||
// Logger
|
||||
logger logger.CustomLogger
|
||||
}
|
||||
|
||||
func (a *Application) saveMemoryReference(mem unsafe.Pointer) {
|
||||
a.memory = append(a.memory, mem)
|
||||
}
|
||||
|
||||
func (a *Application) string2CString(str string) *C.char {
|
||||
result := C.CString(str)
|
||||
a.saveMemoryReference(unsafe.Pointer(result))
|
||||
return result
|
||||
}
|
||||
|
||||
func init() {
|
||||
runtime.LockOSThread()
|
||||
}
|
||||
|
||||
// NewApplicationWithConfig creates a new application based on the given config
|
||||
func NewApplicationWithConfig(config *Config, logger *logger.Logger) *Application {
|
||||
return &Application{
|
||||
config: config,
|
||||
logger: logger.CustomLogger("Ffenestri"),
|
||||
}
|
||||
}
|
||||
|
||||
// NewApplication creates a new Application with the default config
|
||||
func NewApplication(logger *logger.Logger) *Application {
|
||||
return &Application{
|
||||
config: defaultConfig,
|
||||
logger: logger.CustomLogger("Ffenestri"),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Application) freeMemory() {
|
||||
for _, mem := range a.memory {
|
||||
// fmt.Printf("Freeing memory: %+v\n", mem)
|
||||
C.free(mem)
|
||||
}
|
||||
}
|
||||
|
||||
// bool2Cint converts a Go boolean to a C integer
|
||||
func (a *Application) bool2Cint(value bool) C.int {
|
||||
if value {
|
||||
return C.int(1)
|
||||
}
|
||||
return C.int(0)
|
||||
}
|
||||
|
||||
// dispatcher is the interface to send messages to
|
||||
var dispatcher *messagedispatcher.DispatchClient
|
||||
|
||||
// Dispatcher is what we register out client with
|
||||
type Dispatcher interface {
|
||||
RegisterClient(client messagedispatcher.Client) *messagedispatcher.DispatchClient
|
||||
}
|
||||
|
||||
// DispatchClient is the means for passing messages to the backend
|
||||
type DispatchClient interface {
|
||||
SendMessage(string)
|
||||
}
|
||||
|
||||
// Run the application
|
||||
func (a *Application) Run(incomingDispatcher Dispatcher, bindings string) error {
|
||||
title := a.string2CString(a.config.Title)
|
||||
width := C.int(a.config.Width)
|
||||
height := C.int(a.config.Height)
|
||||
resizable := a.bool2Cint(a.config.Resizable)
|
||||
devtools := a.bool2Cint(a.config.DevTools)
|
||||
fullscreen := a.bool2Cint(a.config.Fullscreen)
|
||||
startHidden := a.bool2Cint(a.config.StartHidden)
|
||||
app := C.NewApplication(title, width, height, resizable, devtools, fullscreen, startHidden)
|
||||
|
||||
// Save app reference
|
||||
a.app = unsafe.Pointer(app)
|
||||
|
||||
// Set Min Window Size
|
||||
minWidth := C.int(a.config.MinWidth)
|
||||
minHeight := C.int(a.config.MinHeight)
|
||||
C.SetMinWindowSize(a.app, minWidth, minHeight)
|
||||
|
||||
// Set Max Window Size
|
||||
maxWidth := C.int(a.config.MaxWidth)
|
||||
maxHeight := C.int(a.config.MaxHeight)
|
||||
C.SetMaxWindowSize(a.app, maxWidth, maxHeight)
|
||||
|
||||
// Set debug if needed
|
||||
C.SetDebug(app, a.bool2Cint(DEBUG))
|
||||
|
||||
// Set Frameless
|
||||
if a.config.Frameless {
|
||||
C.DisableFrame(a.app)
|
||||
}
|
||||
|
||||
// Escape bindings so C doesn't freak out
|
||||
bindings = strings.ReplaceAll(bindings, `"`, `\"`)
|
||||
|
||||
// Set bindings
|
||||
C.SetBindings(app, a.string2CString(bindings))
|
||||
|
||||
// save the dispatcher in a package variable so that the C callbacks
|
||||
// can access it
|
||||
dispatcher = incomingDispatcher.RegisterClient(newClient(a))
|
||||
|
||||
// Check we could initialise the application
|
||||
if app != nil {
|
||||
// Yes - Save memory reference and run app, cleaning up afterwards
|
||||
a.saveMemoryReference(unsafe.Pointer(app))
|
||||
C.Run(app, 0, nil)
|
||||
} else {
|
||||
// Oh no! We couldn't initialise the application
|
||||
a.logger.Fatal("Cannot initialise Application.")
|
||||
}
|
||||
|
||||
a.freeMemory()
|
||||
return nil
|
||||
}
|
||||
|
||||
// messageFromWindowCallback is called by any messages sent in
|
||||
// webkit to window.external.invoke. It relays the message on to
|
||||
// the dispatcher.
|
||||
//export messageFromWindowCallback
|
||||
func messageFromWindowCallback(data *C.char) {
|
||||
dispatcher.DispatchMessage(C.GoString(data))
|
||||
}
|
||||
36
v2/internal/ffenestri/ffenestri.h
Normal file
36
v2/internal/ffenestri/ffenestri.h
Normal file
@@ -0,0 +1,36 @@
|
||||
#ifndef __FFENESTRI_H__
|
||||
#define __FFENESTRI_H__
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
extern void *NewApplication(const char *title, int width, int height, int resizable, int devtools, int fullscreen, int startHidden);
|
||||
extern void SetMinWindowSize(void *app, int minWidth, int minHeight);
|
||||
extern void SetMaxWindowSize(void *app, int maxWidth, int maxHeight);
|
||||
extern void Run(void *app, int argc, char **argv);
|
||||
extern void DestroyApplication(void *app);
|
||||
extern void SetDebug(void *app, int flag);
|
||||
extern void SetBindings(void *app, const char *bindings);
|
||||
extern void ExecJS(void *app, const char *script);
|
||||
extern void Hide(void *app);
|
||||
extern void Show(void *app);
|
||||
extern void Center(void *app);
|
||||
extern void Maximise(void *app);
|
||||
extern void Unmaximise(void *app);
|
||||
extern void ToggleMaximise(void *app);
|
||||
extern void Minimise(void *app);
|
||||
extern void Unminimise(void *app);
|
||||
extern void ToggleMinimise(void *app);
|
||||
extern void SetSize(void *app, int width, int height);
|
||||
extern void SetPosition(void *app, int x, int y);
|
||||
extern void Quit(void *app);
|
||||
extern void SetTitle(void *app, const char *title);
|
||||
extern void Fullscreen(void *app);
|
||||
extern void UnFullscreen(void *app);
|
||||
extern void ToggleFullscreen(void *app);
|
||||
extern int SetColour(void *app, const char *colourString);
|
||||
extern void DisableFrame(void *app);
|
||||
extern char *SaveFileDialog(void *appPointer, char *title, char *filter);
|
||||
extern char *OpenFileDialog(void *appPointer, char *title, char *filter);
|
||||
extern char *OpenDirectoryDialog(void *appPointer, char *title, char *filter);
|
||||
|
||||
#endif
|
||||
159
v2/internal/ffenestri/ffenestri_client.go
Normal file
159
v2/internal/ffenestri/ffenestri_client.go
Normal file
@@ -0,0 +1,159 @@
|
||||
package ffenestri
|
||||
|
||||
/*
|
||||
|
||||
#cgo linux CFLAGS: -DFFENESTRI_LINUX=1
|
||||
#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.0
|
||||
|
||||
#include <stdlib.h>
|
||||
#include "ffenestri.h"
|
||||
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"unsafe"
|
||||
|
||||
"github.com/wailsapp/wails/v2/internal/logger"
|
||||
)
|
||||
|
||||
// Client is our implentation of messageDispatcher.Client
|
||||
type Client struct {
|
||||
app *Application
|
||||
logger logger.CustomLogger
|
||||
}
|
||||
|
||||
func newClient(app *Application) *Client {
|
||||
return &Client{
|
||||
app: app,
|
||||
logger: app.logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Quit the application
|
||||
func (c *Client) Quit() {
|
||||
c.app.logger.Trace("Got shutdown message")
|
||||
C.Quit(c.app.app)
|
||||
}
|
||||
|
||||
// NotifyEvent will pass on the event message to the frontend
|
||||
func (c *Client) NotifyEvent(message string) {
|
||||
eventMessage := `window.wails._.Notify(` + strconv.Quote(message) + `);`
|
||||
c.app.logger.Trace("eventMessage = %+v", eventMessage)
|
||||
C.ExecJS(c.app.app, c.app.string2CString(eventMessage))
|
||||
}
|
||||
|
||||
// CallResult contains the result of the call from JS
|
||||
func (c *Client) CallResult(message string) {
|
||||
callbackMessage := `window.wails._.Callback(` + strconv.Quote(message) + `);`
|
||||
c.app.logger.Trace("callbackMessage = %+v", callbackMessage)
|
||||
C.ExecJS(c.app.app, c.app.string2CString(callbackMessage))
|
||||
}
|
||||
|
||||
// WindowSetTitle sets the window title to the given string
|
||||
func (c *Client) WindowSetTitle(title string) {
|
||||
C.SetTitle(c.app.app, c.app.string2CString(title))
|
||||
}
|
||||
|
||||
// WindowFullscreen will set the window to be fullscreen
|
||||
func (c *Client) WindowFullscreen() {
|
||||
C.Fullscreen(c.app.app)
|
||||
}
|
||||
|
||||
// WindowUnFullscreen will unfullscreen the window
|
||||
func (c *Client) WindowUnFullscreen() {
|
||||
C.UnFullscreen(c.app.app)
|
||||
}
|
||||
|
||||
// WindowShow will show the window
|
||||
func (c *Client) WindowShow() {
|
||||
C.Show(c.app.app)
|
||||
}
|
||||
|
||||
// WindowHide will hide the window
|
||||
func (c *Client) WindowHide() {
|
||||
C.Hide(c.app.app)
|
||||
}
|
||||
|
||||
// WindowCenter will hide the window
|
||||
func (c *Client) WindowCenter() {
|
||||
C.Center(c.app.app)
|
||||
}
|
||||
|
||||
// WindowMaximise will maximise the window
|
||||
func (c *Client) WindowMaximise() {
|
||||
C.Maximise(c.app.app)
|
||||
}
|
||||
|
||||
// WindowMinimise will minimise the window
|
||||
func (c *Client) WindowMinimise() {
|
||||
C.Minimise(c.app.app)
|
||||
}
|
||||
|
||||
// WindowUnmaximise will unmaximise the window
|
||||
func (c *Client) WindowUnmaximise() {
|
||||
C.Unmaximise(c.app.app)
|
||||
}
|
||||
|
||||
// WindowUnminimise will unminimise the window
|
||||
func (c *Client) WindowUnminimise() {
|
||||
C.Unminimise(c.app.app)
|
||||
}
|
||||
|
||||
// WindowPosition will position the window to x,y on the
|
||||
// monitor that the window is mostly on
|
||||
func (c *Client) WindowPosition(x int, y int) {
|
||||
C.SetPosition(c.app.app, C.int(x), C.int(y))
|
||||
}
|
||||
|
||||
// WindowSize will resize the window to the given
|
||||
// width and height
|
||||
func (c *Client) WindowSize(width int, height int) {
|
||||
C.SetSize(c.app.app, C.int(width), C.int(height))
|
||||
}
|
||||
|
||||
// WindowSetColour sets the window colour
|
||||
func (c *Client) WindowSetColour(colour string) bool {
|
||||
result := C.SetColour(c.app.app, c.app.string2CString(colour))
|
||||
return result == 1
|
||||
}
|
||||
|
||||
// OpenFileDialog will open a file dialog with the given title
|
||||
func (c *Client) OpenFileDialog(title string, filter string) string {
|
||||
|
||||
cstring := C.OpenFileDialog(c.app.app, c.app.string2CString(title), c.app.string2CString(filter))
|
||||
var result string
|
||||
if cstring != nil {
|
||||
result = C.GoString(cstring)
|
||||
// Free the C string that was allocated by the dialog
|
||||
C.free(unsafe.Pointer(cstring))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// SaveFileDialog will open a save file dialog with the given title
|
||||
func (c *Client) SaveFileDialog(title string, filter string) string {
|
||||
|
||||
cstring := C.SaveFileDialog(c.app.app, c.app.string2CString(title), c.app.string2CString(filter))
|
||||
var result string
|
||||
if cstring != nil {
|
||||
result = C.GoString(cstring)
|
||||
// Free the C string that was allocated by the dialog
|
||||
C.free(unsafe.Pointer(cstring))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// OpenDirectoryDialog will open a directory dialog with the given title
|
||||
func (c *Client) OpenDirectoryDialog(title string, filter string) string {
|
||||
|
||||
cstring := C.OpenDirectoryDialog(c.app.app, c.app.string2CString(title), c.app.string2CString(filter))
|
||||
var result string
|
||||
if cstring != nil {
|
||||
result = C.GoString(cstring)
|
||||
// Free the C string that was allocated by the dialog
|
||||
C.free(unsafe.Pointer(cstring))
|
||||
}
|
||||
return result
|
||||
}
|
||||
803
v2/internal/ffenestri/ffenestri_darwin.c
Normal file
803
v2/internal/ffenestri/ffenestri_darwin.c
Normal file
@@ -0,0 +1,803 @@
|
||||
|
||||
#ifdef FFENESTRI_DARWIN
|
||||
|
||||
#define OBJC_OLD_DISPATCH_PROTOTYPES 1
|
||||
#include <objc/objc-runtime.h>
|
||||
#include <CoreGraphics/CoreGraphics.h>
|
||||
|
||||
// Macros to make it slightly more sane
|
||||
#define msg objc_msgSend
|
||||
#define msg_stret objc_msgSend_stret
|
||||
|
||||
#define c(str) (id)objc_getClass(str)
|
||||
#define s(str) sel_registerName(str)
|
||||
#define u(str) sel_getUid(str)
|
||||
#define str(input) msg(c("NSString"), s("stringWithUTF8String:"), input)
|
||||
|
||||
#define ON_MAIN_THREAD(str) dispatch( ^{ str; } );
|
||||
#define MAIN_WINDOW_CALL(str) msg(app->mainWindow, s((str)));
|
||||
|
||||
#define NSBackingStoreBuffered 2
|
||||
|
||||
#define NSWindowStyleMaskBorderless 0
|
||||
#define NSWindowStyleMaskTitled 1
|
||||
#define NSWindowStyleMaskClosable 2
|
||||
#define NSWindowStyleMaskMiniaturizable 4
|
||||
#define NSWindowStyleMaskResizable 8
|
||||
#define NSWindowStyleMaskFullscreen 1 << 14
|
||||
|
||||
#define NSWindowTitleHidden 1
|
||||
#define NSWindowStyleMaskFullSizeContentView 1 << 15
|
||||
|
||||
// References to assets
|
||||
extern const unsigned char *assets[];
|
||||
extern const unsigned char runtime;
|
||||
extern const char *icon[];
|
||||
|
||||
// MAIN DEBUG FLAG
|
||||
int debug;
|
||||
|
||||
// Dispatch Method
|
||||
typedef void (^dispatchMethod)(void);
|
||||
|
||||
// dispatch will execute the given `func` pointer
|
||||
void dispatch(dispatchMethod func) {
|
||||
dispatch_async(dispatch_get_main_queue(), func);
|
||||
}
|
||||
|
||||
// App Delegate
|
||||
typedef struct AppDel {
|
||||
Class isa;
|
||||
id window;
|
||||
} AppDelegate;
|
||||
|
||||
// Credit: https://stackoverflow.com/a/8465083
|
||||
char* concat(const char *string1, const char *string2)
|
||||
{
|
||||
const size_t len1 = strlen(string1);
|
||||
const size_t len2 = strlen(string2);
|
||||
char *result = malloc(len1 + len2 + 1);
|
||||
memcpy(result, string1, len1);
|
||||
memcpy(result + len1, string2, len2 + 1);
|
||||
return result;
|
||||
}
|
||||
|
||||
// yes command simply returns YES!
|
||||
BOOL yes(id self, SEL cmd)
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
// Debug works like sprintf but mutes if the global debug flag is true
|
||||
// Credit: https://stackoverflow.com/a/20639708
|
||||
void Debug(char *message, ... ) {
|
||||
if ( debug ) {
|
||||
char *temp = concat("TRACE | Ffenestri (C) | ", message);
|
||||
message = concat(temp, "\n");
|
||||
free(temp);
|
||||
va_list args;
|
||||
va_start(args, message);
|
||||
vprintf(message, args);
|
||||
free(message);
|
||||
va_end(args);
|
||||
}
|
||||
}
|
||||
|
||||
extern void messageFromWindowCallback(const char *);
|
||||
typedef void (*ffenestriCallback)(const char *);
|
||||
|
||||
struct Application {
|
||||
|
||||
// Cocoa data
|
||||
id application;
|
||||
id mainWindow;
|
||||
id wkwebview;
|
||||
id manager;
|
||||
id config;
|
||||
|
||||
// Window Data
|
||||
const char *title;
|
||||
int width;
|
||||
int height;
|
||||
int minWidth;
|
||||
int minHeight;
|
||||
int maxWidth;
|
||||
int maxHeight;
|
||||
int resizable;
|
||||
int devtools;
|
||||
int fullscreen;
|
||||
|
||||
// Features
|
||||
int frame;
|
||||
int maximised;
|
||||
int minimised;
|
||||
|
||||
// User Data
|
||||
char *HTML;
|
||||
|
||||
// Callback
|
||||
ffenestriCallback sendMessageToBackend;
|
||||
|
||||
// Bindings
|
||||
const char *bindings;
|
||||
|
||||
// Lock - used for sync operations (Should we be using g_mutex?)
|
||||
int lock;
|
||||
|
||||
};
|
||||
|
||||
void Hide(void *appPointer) {
|
||||
struct Application *app = (struct Application*) appPointer;
|
||||
ON_MAIN_THREAD(
|
||||
msg(app->application, s("hide:"))
|
||||
)
|
||||
}
|
||||
|
||||
void Show(void *appPointer) {
|
||||
struct Application *app = (struct Application*) appPointer;
|
||||
ON_MAIN_THREAD(
|
||||
msg(app->mainWindow, s("makeKeyAndOrderFront:"), NULL);
|
||||
msg(app->application, s("activateIgnoringOtherApps:"), YES);
|
||||
)
|
||||
}
|
||||
|
||||
// Sends messages to the backend
|
||||
void messageHandler(id self, SEL cmd, id contentController, id message) {
|
||||
struct Application *app = (struct Application *)objc_getAssociatedObject(
|
||||
self, "application");
|
||||
const char *name = (const char *)msg(msg(message, s("name")), s("UTF8String"));
|
||||
if( strcmp(name, "completed") == 0) {
|
||||
// Delete handler
|
||||
msg(app->manager, s("removeScriptMessageHandlerForName:"), str("completed"));
|
||||
// Show window after a short delay so rendering can catch up
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 10000000), dispatch_get_main_queue(), ^{
|
||||
Show(app);
|
||||
msg(app->config, s("setValue:forKey:"), msg(c("NSNumber"), s("numberWithBool:"), 0), str("suppressesIncrementalRendering"));
|
||||
});
|
||||
|
||||
} else {
|
||||
const char *m = (const char *)msg(msg(message, s("body")), s("UTF8String"));
|
||||
app->sendMessageToBackend(m);
|
||||
}
|
||||
}
|
||||
|
||||
// closeWindow is called when the close button is pressed
|
||||
void closeWindow(id self, SEL cmd, id sender) {
|
||||
struct Application *app = (struct Application *) objc_getAssociatedObject(self, "application");
|
||||
app->sendMessageToBackend("WC");
|
||||
}
|
||||
|
||||
void* NewApplication(const char *title, int width, int height, int resizable, int devtools, int fullscreen) {
|
||||
// Setup main application struct
|
||||
struct Application *result = malloc(sizeof(struct Application));
|
||||
result->title = title;
|
||||
result->width = width;
|
||||
result->height = height;
|
||||
result->minWidth = 0;
|
||||
result->minHeight = 0;
|
||||
result->maxWidth = 0;
|
||||
result->maxHeight = 0;
|
||||
result->resizable = resizable;
|
||||
result->devtools = devtools;
|
||||
result->fullscreen = fullscreen;
|
||||
result->lock = 0;
|
||||
result->maximised = 0;
|
||||
result->minimised = 0;
|
||||
|
||||
// Features
|
||||
result->frame = 1;
|
||||
|
||||
result->sendMessageToBackend = (ffenestriCallback) messageFromWindowCallback;
|
||||
|
||||
return (void*) result;
|
||||
}
|
||||
|
||||
void DestroyApplication(void *appPointer) {
|
||||
Debug("Destroying Application");
|
||||
struct Application *app = (struct Application*) appPointer;
|
||||
|
||||
// Free the bindings
|
||||
if (app->bindings != NULL) {
|
||||
free((void*)app->bindings);
|
||||
app->bindings = NULL;
|
||||
} else {
|
||||
Debug("Almost a double free for app->bindings");
|
||||
}
|
||||
|
||||
msg(app->manager, s("removeScriptMessageHandlerForName:"), str("external"));
|
||||
msg(app->mainWindow, s("close"));
|
||||
msg(c("NSApp"), s("terminate:"), NULL);
|
||||
Debug("Finished Destroying Application");
|
||||
}
|
||||
|
||||
// Quit will stop the gtk application and free up all the memory
|
||||
// used by the application
|
||||
void Quit(void *appPointer) {
|
||||
Debug("Quit Called");
|
||||
DestroyApplication(appPointer);
|
||||
}
|
||||
|
||||
// SetTitle sets the main window title to the given string
|
||||
void SetTitle(struct Application *app, const char *title) {
|
||||
Debug("SetTitle Called");
|
||||
ON_MAIN_THREAD(
|
||||
msg(app->mainWindow, s("setTitle:"), str(title));
|
||||
)
|
||||
}
|
||||
|
||||
void ToggleFullscreen(struct Application *app) {
|
||||
ON_MAIN_THREAD(
|
||||
app->fullscreen = !app->fullscreen;
|
||||
MAIN_WINDOW_CALL("toggleFullScreen:")
|
||||
)
|
||||
}
|
||||
|
||||
// Fullscreen sets the main window to be fullscreen
|
||||
void Fullscreen(struct Application *app) {
|
||||
Debug("Fullscreen Called");
|
||||
if( app->fullscreen == 0) {
|
||||
ToggleFullscreen(app);
|
||||
}
|
||||
}
|
||||
|
||||
// UnFullscreen resets the main window after a fullscreen
|
||||
void UnFullscreen(struct Application *app) {
|
||||
Debug("UnFullscreen Called");
|
||||
if( app->fullscreen == 1) {
|
||||
ToggleFullscreen(app);
|
||||
}
|
||||
}
|
||||
|
||||
void Center(struct Application *app) {
|
||||
Debug("Center Called");
|
||||
ON_MAIN_THREAD(
|
||||
MAIN_WINDOW_CALL("center")
|
||||
)
|
||||
}
|
||||
|
||||
void SetMaximumSize(void *appPointer, int width, int height) {
|
||||
Debug("SetMaximumSize Called");
|
||||
// struct Application *app = (struct Application*) appPointer;
|
||||
// GdkGeometry size;
|
||||
// size.max_height = (height == 0 ? INT_MAX: height);
|
||||
// size.max_width = (width == 0 ? INT_MAX: width);
|
||||
// gtk_window_set_geometry_hints(app->mainWindow, NULL, &size, GDK_HINT_MAX_SIZE);
|
||||
}
|
||||
|
||||
void SetMinimumSize(void *appPointer, int width, int height) {
|
||||
Debug("SetMinimumSize Called");
|
||||
// struct Application *app = (struct Application*) appPointer;
|
||||
// GdkGeometry size;
|
||||
// size.max_height = height;
|
||||
// size.max_width = width;
|
||||
// gtk_window_set_geometry_hints(app->mainWindow, NULL, &size, GDK_HINT_MIN_SIZE);
|
||||
}
|
||||
|
||||
void ToggleMaximise(struct Application *app) {
|
||||
ON_MAIN_THREAD(
|
||||
app->maximised = !app->maximised;
|
||||
MAIN_WINDOW_CALL("zoom:")
|
||||
)
|
||||
}
|
||||
|
||||
void Maximise(struct Application *app) {
|
||||
if( app->maximised == 0) {
|
||||
ToggleMaximise(app);
|
||||
}
|
||||
}
|
||||
|
||||
void Unmaximise(struct Application *app) {
|
||||
if( app->maximised == 1) {
|
||||
ToggleMaximise(app);
|
||||
}
|
||||
}
|
||||
|
||||
void ToggleMinimise(struct Application *app) {
|
||||
ON_MAIN_THREAD(
|
||||
MAIN_WINDOW_CALL(app->minimised ? "deminiaturize:" : "miniaturize:" );
|
||||
app->minimised = !app->minimised;
|
||||
)
|
||||
}
|
||||
|
||||
void Minimise(struct Application *app) {
|
||||
if( app->minimised == 0) {
|
||||
ToggleMinimise(app);
|
||||
}
|
||||
}
|
||||
void Unminimise(struct Application *app) {
|
||||
if( app->minimised == 1) {
|
||||
ToggleMinimise(app);
|
||||
}
|
||||
}
|
||||
|
||||
void SetSize(struct Application *app, int width, int height) {
|
||||
ON_MAIN_THREAD(
|
||||
id screen = NULL;
|
||||
screen = msg(app->mainWindow, s("screen"));
|
||||
if( screen == NULL ) {
|
||||
screen = msg(c("NSScreen"), u("mainScreen"));
|
||||
}
|
||||
|
||||
// Get the rect for the mainWindow
|
||||
CGRect* frame = (CGRect*) msg(app->mainWindow, s("valueForKey:"), str("frame"));
|
||||
|
||||
// Get the rect for the current screen
|
||||
CGRect *visibleFrame = (CGRect*) msg(screen, s("valueForKey:"), str("visibleFrame"));
|
||||
|
||||
// Credit: https://github.com/patr0nus/DeskGap/blob/73c0ac9f2c73f55b6e81f64f6673a7962b5719cd/lib/src/platform/mac/util/NSScreen%2BGeometry.m
|
||||
Debug("visibleFrame->origin.x %4.1f", visibleFrame->origin.x);
|
||||
Debug("visibleFrame->origin.y %4.1f", visibleFrame->origin.y);
|
||||
Debug("visibleFrame->size.width %4.1f", visibleFrame->size.width);
|
||||
Debug("visibleFrame->size.height %4.1f", visibleFrame->size.height);
|
||||
Debug("frame->origin.x %4.1f", frame->origin.x);
|
||||
Debug("frame->origin.y %4.1f", frame->origin.y);
|
||||
Debug("frame->size.width %4.1f", frame->size.width);
|
||||
Debug("frame->size.height %4.1f", frame->size.height);
|
||||
|
||||
frame->size.width = width;
|
||||
frame->size.height = height;
|
||||
msg(app->mainWindow, s("setFrame:display:"), *frame, true);
|
||||
|
||||
// Move the window
|
||||
// msg(app->mainWindow, s("setFrame:display:animate:"), *frame, 1, 0);
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
void SetPosition(struct Application *app, int x, int y) {
|
||||
msg(app->mainWindow, s("cascadeTopLeftFromPoint:"), CGPointMake(x, y));
|
||||
}
|
||||
|
||||
// OpenFileDialog opens a dialog to select a file
|
||||
// NOTE: The result is a string that will need to be freed!
|
||||
char* OpenFileDialog(void *appPointer, char *title) {
|
||||
Debug("OpenFileDialog Called");
|
||||
|
||||
// struct Application *app = (struct Application*) appPointer;
|
||||
// GtkFileChooserNative *native;
|
||||
// GtkFileChooserAction action = GTK_FILE_CHOOSER_ACTION_OPEN;
|
||||
// gint res;
|
||||
char *filename = "BogusFilename";
|
||||
|
||||
// native = gtk_file_chooser_native_new (title,
|
||||
// app->mainWindow,
|
||||
// action,
|
||||
// "_Open",
|
||||
// "_Cancel");
|
||||
|
||||
// res = gtk_native_dialog_run (GTK_NATIVE_DIALOG (native));
|
||||
// if (res == GTK_RESPONSE_ACCEPT)
|
||||
// {
|
||||
// GtkFileChooser *chooser = GTK_FILE_CHOOSER (native);
|
||||
// filename = gtk_file_chooser_get_filename (chooser);
|
||||
// }
|
||||
|
||||
// g_object_unref (native);
|
||||
|
||||
return filename;
|
||||
}
|
||||
|
||||
// SaveFileDialog opens a dialog to select a file
|
||||
// NOTE: The result is a string that will need to be freed!
|
||||
char* SaveFileDialog(void *appPointer, char *title) {
|
||||
Debug("SaveFileDialog Called");
|
||||
char *filename = "BogusSaveFilename";
|
||||
/* struct Application *app = (struct Application*) appPointer;
|
||||
GtkFileChooserNative *native;
|
||||
GtkFileChooserAction action = GTK_FILE_CHOOSER_ACTION_SAVE;
|
||||
gint res;
|
||||
|
||||
native = gtk_file_chooser_native_new (title,
|
||||
app->mainWindow,
|
||||
action,
|
||||
"_Save",
|
||||
"_Cancel");
|
||||
|
||||
res = gtk_native_dialog_run (GTK_NATIVE_DIALOG (native));
|
||||
if (res == GTK_RESPONSE_ACCEPT)
|
||||
{
|
||||
GtkFileChooser *chooser = GTK_FILE_CHOOSER (native);
|
||||
filename = gtk_file_chooser_get_filename (chooser);
|
||||
}
|
||||
|
||||
g_object_unref (native);
|
||||
*/
|
||||
return filename;
|
||||
}
|
||||
|
||||
// OpenDirectoryDialog opens a dialog to select a directory
|
||||
// NOTE: The result is a string that will need to be freed!
|
||||
char* OpenDirectoryDialog(void *appPointer, char *title) {
|
||||
Debug("OpenDirectoryDialog Called");
|
||||
char *foldername = "BogusDirectory";
|
||||
/*
|
||||
struct Application *app = (struct Application*) appPointer;
|
||||
GtkFileChooserNative *native;
|
||||
GtkFileChooserAction action = GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER;
|
||||
gint res;
|
||||
|
||||
native = gtk_file_chooser_native_new (title,
|
||||
app->mainWindow,
|
||||
action,
|
||||
"_Open",
|
||||
"_Cancel");
|
||||
|
||||
res = gtk_native_dialog_run (GTK_NATIVE_DIALOG (native));
|
||||
if (res == GTK_RESPONSE_ACCEPT)
|
||||
{
|
||||
GtkFileChooser *chooser = GTK_FILE_CHOOSER (native);
|
||||
foldername = gtk_file_chooser_get_filename (chooser);
|
||||
}
|
||||
|
||||
g_object_unref (native);
|
||||
*/
|
||||
return foldername;
|
||||
}
|
||||
|
||||
// SetColour sets the colour of the webview to the given colour string
|
||||
int SetColour(void *appPointer, const char *colourString) {
|
||||
Debug("SetColour Called with: %s", colourString);
|
||||
|
||||
// struct Application *app = (struct Application*) appPointer;
|
||||
// GdkRGBA rgba;
|
||||
// gboolean result = gdk_rgba_parse(&rgba, colourString);
|
||||
// if (result == FALSE) {
|
||||
// return 0;
|
||||
// }
|
||||
// Debug("Setting webview colour to: %s", colourString);
|
||||
// webkit_web_view_get_background_color((WebKitWebView*)(app->webView), &rgba);
|
||||
// int c = NS_RGBA(1, 0, 0, 0.5);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
const char *invoke = "window.external={invoke:function(x){window.webkit.messageHandlers.external.postMessage(x);}};";
|
||||
|
||||
// DisableFrame disables the window frame
|
||||
void DisableFrame(struct Application *app)
|
||||
{
|
||||
app->frame = 0;
|
||||
}
|
||||
|
||||
void SetMinWindowSize(struct Application *app, int minWidth, int minHeight)
|
||||
{
|
||||
app->minWidth = minWidth;
|
||||
app->minHeight = minHeight;
|
||||
}
|
||||
|
||||
void SetMaxWindowSize(struct Application *app, int maxWidth, int maxHeight)
|
||||
{
|
||||
app->maxWidth = maxWidth;
|
||||
app->maxHeight = maxHeight;
|
||||
}
|
||||
|
||||
void ExecJS(struct Application *app, const char *js) {
|
||||
ON_MAIN_THREAD(
|
||||
msg(app->wkwebview,
|
||||
s("evaluateJavaScript:completionHandler:"),
|
||||
str(js),
|
||||
NULL);
|
||||
)
|
||||
}
|
||||
|
||||
// typedef char* (*dialogMethod)(void *app, void *);
|
||||
|
||||
// struct dialogCall {
|
||||
// struct Application *app;
|
||||
// dialogMethod method;
|
||||
// void *args;
|
||||
// char *result;
|
||||
// int done;
|
||||
// };
|
||||
|
||||
|
||||
// gboolean executeMethodWithReturn(gpointer data) {
|
||||
// struct dialogCall *d = (struct dialogCall *)data;
|
||||
// struct Application *app = (struct Application *)(d->app);
|
||||
// Debug("Webview %p\n", app->webView);
|
||||
// Debug("Args %s\n", d->args);
|
||||
// Debug("Method %p\n", (d->method));
|
||||
// d->result = (d->method)(app, d->args);
|
||||
// d->done = 1;
|
||||
// // Debug("Method Execute Complete. Freeing memory");
|
||||
// return FALSE;
|
||||
// }
|
||||
|
||||
|
||||
char* OpenFileDialogOnMainThread(void *app, char *title) {
|
||||
Debug("OpenFileDialogOnMainThread Called");
|
||||
|
||||
// struct dialogCall *data =
|
||||
// (struct dialogCall *)g_new(struct dialogCall, 1);
|
||||
// data->result = NULL;
|
||||
// data->done = 0;
|
||||
// data->method = (dialogMethod)OpenFileDialog;
|
||||
// data->args = title;
|
||||
// data->app = app;
|
||||
|
||||
// gdk_threads_add_idle(executeMethodWithReturn, data);
|
||||
|
||||
// while( data->done == 0 ) {
|
||||
// // Debug("Waiting for dialog");
|
||||
// usleep(100000);
|
||||
// }
|
||||
// Debug("Dialog done");
|
||||
// Debug("Result = %s\n", data->result);
|
||||
// g_free(data);
|
||||
// // Fingers crossed this wasn't freed by g_free above
|
||||
// return data->result;
|
||||
return "OpenFileDialogOnMainThread result";
|
||||
}
|
||||
|
||||
char* SaveFileDialogOnMainThread(void *app, char *title) {
|
||||
Debug("SaveFileDialogOnMainThread Called");
|
||||
return "SaveFileDialogOnMainThread result";
|
||||
}
|
||||
|
||||
char* OpenDirectoryDialogOnMainThread(void *app, char *title) {
|
||||
Debug("OpenDirectoryDialogOnMainThread Called");
|
||||
return "OpenDirectoryDialogOnMainThread result";
|
||||
}
|
||||
|
||||
// // Sets the icon to the XPM stored in icon
|
||||
// void setIcon( struct Application *app) {
|
||||
// GdkPixbuf *appIcon = gdk_pixbuf_new_from_xpm_data ((const char**)icon);
|
||||
// gtk_window_set_icon (app->mainWindow, appIcon);
|
||||
// }
|
||||
|
||||
|
||||
void SetDebug(void *applicationPointer, int flag) {
|
||||
struct Application *app = (struct Application*) applicationPointer;
|
||||
debug = flag;
|
||||
}
|
||||
|
||||
void SetBindings(void* applicationPointer, const char *bindings) {
|
||||
struct Application *app = (struct Application*) applicationPointer;
|
||||
|
||||
const char* temp = concat("window.wailsbindings = \"", bindings);
|
||||
const char* jscall = concat(temp, "\";");
|
||||
free((void*)temp);
|
||||
app->bindings = jscall;
|
||||
}
|
||||
|
||||
// This is called when the close button on the window is pressed
|
||||
// void close_button_pressed () {
|
||||
// struct Application *app = (struct Application*) user_data;
|
||||
// app->sendMessageToBackend("WC"); // Window Close
|
||||
// return TRUE;
|
||||
// }
|
||||
|
||||
// static void setupWindow(void *applicationPointer) {
|
||||
|
||||
// struct Application *app = (struct Application*) applicationPointer;
|
||||
|
||||
// // Create the window
|
||||
// GtkWidget *mainWindow = gtk_application_window_new (app->application);
|
||||
// // Save reference
|
||||
// app->mainWindow = GTK_WINDOW(mainWindow);
|
||||
|
||||
// // Setup borderless
|
||||
// if (app->borderless) {
|
||||
// gtk_window_set_decorated((GtkWindow*)mainWindow, FALSE);
|
||||
// }
|
||||
|
||||
// // Setup title
|
||||
// printf("Setting title to: %s\n", app->title);
|
||||
// gtk_window_set_title(GTK_WINDOW(mainWindow), app->title);
|
||||
|
||||
// // Setup script handler
|
||||
// WebKitUserContentManager *contentManager = webkit_user_content_manager_new();
|
||||
// webkit_user_content_manager_register_script_message_handler(contentManager, "external");
|
||||
// g_signal_connect(contentManager, "script-message-received::external", G_CALLBACK(sendMessageToBackend), app);
|
||||
// GtkWidget *webView = webkit_web_view_new_with_user_content_manager(contentManager);
|
||||
// // Save reference
|
||||
// app->webView = webView;
|
||||
|
||||
// // Add the webview to the window
|
||||
// gtk_container_add(GTK_CONTAINER(mainWindow), webView);
|
||||
|
||||
// // Load default HTML
|
||||
// g_signal_connect(G_OBJECT(webView), "load-changed", G_CALLBACK(load_finished_cb), app);
|
||||
|
||||
// // Load the user's HTML
|
||||
// webkit_web_view_load_uri(WEBKIT_WEB_VIEW(webView), &userhtml);
|
||||
|
||||
// // Check if we want to enable the dev tools
|
||||
// if( app->devtools ) {
|
||||
// WebKitSettings *settings = webkit_web_view_get_settings(WEBKIT_WEB_VIEW(webView));
|
||||
// // webkit_settings_set_enable_write_console_messages_to_stdout(settings, true);
|
||||
// webkit_settings_set_enable_developer_extras(settings, true);
|
||||
// } else {
|
||||
// g_signal_connect(G_OBJECT(webView), "context-menu", G_CALLBACK(disable_context_menu_cb), app);
|
||||
// }
|
||||
|
||||
// // Listen for close button signal
|
||||
// g_signal_connect (GTK_WIDGET(mainWindow), "delete-event", G_CALLBACK (close_button_pressed), app);
|
||||
|
||||
// }
|
||||
|
||||
void enableBoolConfig(id config, const char *setting) {
|
||||
msg(msg(config, s("preferences")), s("setValue:forKey:"), msg(c("NSNumber"), s("numberWithBool:"), 1), str(setting));
|
||||
}
|
||||
|
||||
void disableBoolConfig(id config, const char *setting) {
|
||||
msg(msg(config, s("preferences")), s("setValue:forKey:"), msg(c("NSNumber"), s("numberWithBool:"), 0), str(setting));
|
||||
}
|
||||
|
||||
void Run(void *applicationPointer, int argc, char **argv) {
|
||||
struct Application *app = (struct Application*) applicationPointer;
|
||||
|
||||
int decorations;
|
||||
|
||||
if (app->frame == 1 ) {
|
||||
decorations = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable;
|
||||
}
|
||||
|
||||
if (app->resizable) {
|
||||
decorations |= NSWindowStyleMaskResizable;
|
||||
}
|
||||
|
||||
if (app->fullscreen) {
|
||||
decorations |= NSWindowStyleMaskFullscreen;
|
||||
}
|
||||
|
||||
if( app->frame == 0) {
|
||||
decorations |= NSWindowStyleMaskFullSizeContentView;
|
||||
}
|
||||
|
||||
id application = msg(c("NSApplication"), s("sharedApplication"));
|
||||
app->application = application;
|
||||
msg(application, s("setActivationPolicy:"), 0);
|
||||
|
||||
// Define delegate
|
||||
Class delegateClass = objc_allocateClassPair((Class) c("NSResponder"), "AppDelegate", 0);
|
||||
class_addProtocol(delegateClass, objc_getProtocol("NSTouchBarProvider"));
|
||||
class_addMethod(delegateClass, s("applicationShouldTerminateAfterLastWindowClosed:"), (IMP) yes, "c@:@");
|
||||
// TODO: add userContentController:didReceiveScriptMessage
|
||||
class_addMethod(delegateClass, s("userContentController:didReceiveScriptMessage:"), (IMP) messageHandler,
|
||||
"v@:@@");
|
||||
objc_registerClassPair(delegateClass);
|
||||
|
||||
// Create delegate
|
||||
id delegate = msg((id)delegateClass, s("new"));
|
||||
objc_setAssociatedObject(delegate, "application", (id)app, OBJC_ASSOCIATION_ASSIGN);
|
||||
msg(application, s("setDelegate:"), delegate);
|
||||
|
||||
// Create main window
|
||||
id mainWindow = msg(c("NSWindow"),s("alloc"));
|
||||
mainWindow = msg(mainWindow, s("initWithContentRect:styleMask:backing:defer:"),
|
||||
CGRectMake(0, 0, app->width, app->height), decorations, NSBackingStoreBuffered, 0);
|
||||
msg(mainWindow, s("autorelease"));
|
||||
|
||||
app->mainWindow = mainWindow;
|
||||
|
||||
// Set the main window title
|
||||
SetTitle(app, app->title);
|
||||
|
||||
// Center Window
|
||||
Center(app);
|
||||
// msg(app->mainWindow, s("cascadeTopLeftFromPoint:"), CGPointMake(100, 100));
|
||||
|
||||
// Set Style Mask
|
||||
msg(mainWindow, s("setStyleMask:"), decorations);
|
||||
|
||||
|
||||
// Setup webview
|
||||
id config = msg(c("WKWebViewConfiguration"), s("new"));
|
||||
app->config = config;
|
||||
id manager = msg(config, s("userContentController"));
|
||||
app->manager = manager;
|
||||
id wkwebview = msg(c("WKWebView"), s("alloc"));
|
||||
app->wkwebview = wkwebview;
|
||||
|
||||
// Only show content when fully rendered
|
||||
msg(config, s("setValue:forKey:"), msg(c("NSNumber"), s("numberWithBool:"), 1), str("suppressesIncrementalRendering"));
|
||||
|
||||
// TODO: Fix "NSWindow warning: adding an unknown subview: <WKInspectorWKWebView: 0x465ed90>. Break on NSLog to debug." error
|
||||
if (app->devtools) {
|
||||
Debug("Enabling devtools");
|
||||
enableBoolConfig(config, "developerExtrasEnabled");
|
||||
}
|
||||
// TODO: Understand why this shouldn't be CGRectMake(0, 0, app->width, app->height)
|
||||
msg(wkwebview, s("initWithFrame:configuration:"), CGRectMake(0, 0, 0, 0), config);
|
||||
|
||||
|
||||
msg(manager, s("addScriptMessageHandler:name:"), delegate, str("external"));
|
||||
msg(manager, s("addScriptMessageHandler:name:"), delegate, str("completed"));
|
||||
msg(mainWindow, s("setContentView:"), wkwebview);
|
||||
|
||||
if( app->frame == 0) {
|
||||
msg(mainWindow, s("setTitlebarAppearsTransparent:"), YES);
|
||||
msg(mainWindow, s("setTitleVisibility:"), NSWindowTitleHidden);
|
||||
// msg( msg( mainWindow, ("standardWindowButton"), str("NSWindowZoomButton")), s("setHidden"), YES);
|
||||
// [[window standardWindowButton:NSWindowZoomButton] setHidden:YES];
|
||||
// [[window standardWindowButton:NSWindowMiniaturizeButton] setHidden:YES];
|
||||
// [[window standardWindowButton:NSWindowCloseButton] setHidden:YES];
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// msg(mainWindow, s("setFrame:display:animate:"), CGRectMake(0, 0, 0, 0), YES, YES);
|
||||
// // Set the icon
|
||||
// setIcon(app);
|
||||
|
||||
// // Setup resize
|
||||
// gtk_window_resize(GTK_WINDOW (app->mainWindow), app->width, app->height);
|
||||
|
||||
// if( app->resizable ) {
|
||||
// gtk_window_set_default_size(GTK_WINDOW (app->mainWindow), app->width, app->height);
|
||||
// } else {
|
||||
// gtk_widget_set_size_request(GTK_WIDGET (app->mainWindow), app->width, app->height);
|
||||
// SetMaximumSize(app, app->width, app->height);
|
||||
// SetMinimumSize(app, app->width, app->height);
|
||||
// gtk_window_resize(GTK_WINDOW (app->mainWindow), app->width, app->height);
|
||||
// }
|
||||
// gtk_window_set_resizable(GTK_WINDOW(app->mainWindow), app->resizable ? TRUE : FALSE );
|
||||
|
||||
|
||||
// Load HTML
|
||||
id html = msg(c("NSURL"), s("URLWithString:"), str(assets[0]));
|
||||
// Debug("HTML: %p", html);
|
||||
msg(wkwebview, s("loadRequest:"), msg(c("NSURLRequest"), s("requestWithURL:"), html));
|
||||
|
||||
// Load assets
|
||||
|
||||
Debug("Loading Internal Code");
|
||||
// We want to evaluate the internal code plus runtime before the assets
|
||||
const char *temp = concat(invoke, app->bindings);
|
||||
const char *internalCode = concat(temp, (const char*)&runtime);
|
||||
// Debug("Internal code: %s", internalCode);
|
||||
free((void*)temp);
|
||||
|
||||
// Loop over assets and build up one giant Mother Of All Evals
|
||||
int index = 1;
|
||||
while(1) {
|
||||
// Get next asset pointer
|
||||
const unsigned char *asset = assets[index];
|
||||
|
||||
// If we have no more assets, break
|
||||
if (asset == 0x00) {
|
||||
break;
|
||||
}
|
||||
|
||||
temp = concat(internalCode, (const char *)asset);
|
||||
free((void*)internalCode);
|
||||
internalCode = temp;
|
||||
index++;
|
||||
};
|
||||
|
||||
class_addMethod(delegateClass, s("closeWindow"), (IMP) closeWindow, "v@:@");
|
||||
// TODO: Check if we can split out the User JS/CSS from the MOAE
|
||||
|
||||
// Debug("MOAE: %s", internalCode);
|
||||
|
||||
// Include callback after evaluation
|
||||
temp = concat(internalCode, "webkit.messageHandlers.completed.postMessage(true);");
|
||||
free((void*)internalCode);
|
||||
internalCode = temp;
|
||||
|
||||
// const char *viewportScriptString = "var meta = document.createElement('meta'); meta.setAttribute('name', 'viewport'); meta.setAttribute('content', 'width=device-width'); meta.setAttribute('initial-scale', '1.0'); meta.setAttribute('maximum-scale', '1.0'); meta.setAttribute('minimum-scale', '1.0'); meta.setAttribute('user-scalable', 'no'); document.getElementsByTagName('head')[0].appendChild(meta);";
|
||||
// ExecJS(app, viewportScriptString);
|
||||
|
||||
|
||||
// This evaluates the MOAE once the Dom has finished loading
|
||||
msg(manager,
|
||||
s("addUserScript:"),
|
||||
msg(msg(c("WKUserScript"), s("alloc")),
|
||||
s("initWithSource:injectionTime:forMainFrameOnly:"),
|
||||
str(internalCode),
|
||||
1,
|
||||
1));
|
||||
|
||||
// Finally call run
|
||||
Debug("Run called");
|
||||
msg(application, s("activateIgnoringOtherApps:"), true);
|
||||
msg(application, s("run"));
|
||||
|
||||
free((void*)internalCode);
|
||||
}
|
||||
|
||||
|
||||
|
||||
#endif
|
||||
984
v2/internal/ffenestri/ffenestri_linux.c
Normal file
984
v2/internal/ffenestri/ffenestri_linux.c
Normal file
@@ -0,0 +1,984 @@
|
||||
|
||||
#ifndef __FFENESTRI_LINUX_H__
|
||||
#define __FFENESTRI_LINUX_H__
|
||||
|
||||
#include "gtk/gtk.h"
|
||||
#include "webkit2/webkit2.h"
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdarg.h>
|
||||
|
||||
// References to assets
|
||||
extern const unsigned char *assets[];
|
||||
extern const unsigned char runtime;
|
||||
extern const char *icon[];
|
||||
|
||||
// Constants
|
||||
#define PRIMARY_MOUSE_BUTTON 1
|
||||
#define MIDDLE_MOUSE_BUTTON 2
|
||||
#define SECONDARY_MOUSE_BUTTON 3
|
||||
|
||||
// MAIN DEBUG FLAG
|
||||
int debug;
|
||||
|
||||
// Credit: https://stackoverflow.com/a/8465083
|
||||
char *concat(const char *s1, const char *s2)
|
||||
{
|
||||
const size_t len1 = strlen(s1);
|
||||
const size_t len2 = strlen(s2);
|
||||
char *result = malloc(len1 + len2 + 1);
|
||||
memcpy(result, s1, len1);
|
||||
memcpy(result + len1, s2, len2 + 1);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Debug works like sprintf but mutes if the global debug flag is true
|
||||
// Credit: https://stackoverflow.com/a/20639708
|
||||
void Debug(char *message, ...)
|
||||
{
|
||||
if (debug)
|
||||
{
|
||||
char *temp = concat("TRACE | Ffenestri (C) | ", message);
|
||||
message = concat(temp, "\n");
|
||||
free(temp);
|
||||
va_list args;
|
||||
va_start(args, message);
|
||||
vprintf(message, args);
|
||||
free(message);
|
||||
va_end(args);
|
||||
}
|
||||
}
|
||||
|
||||
extern void messageFromWindowCallback(const char *);
|
||||
typedef void (*ffenestriCallback)(const char *);
|
||||
|
||||
struct Application
|
||||
{
|
||||
|
||||
// Gtk Data
|
||||
GtkApplication *application;
|
||||
GtkWindow *mainWindow;
|
||||
GtkWidget *webView;
|
||||
int signalInvoke;
|
||||
int signalWindowDrag;
|
||||
int signalButtonPressed;
|
||||
int signalButtonReleased;
|
||||
int signalLoadChanged;
|
||||
|
||||
// Saves the events for the drag mouse button
|
||||
GdkEventButton *dragButtonEvent;
|
||||
|
||||
// The number of the default drag button
|
||||
int dragButton;
|
||||
|
||||
// Window Data
|
||||
const char *title;
|
||||
char *id;
|
||||
int width;
|
||||
int height;
|
||||
int resizable;
|
||||
int devtools;
|
||||
int startHidden;
|
||||
int fullscreen;
|
||||
int minWidth;
|
||||
int minHeight;
|
||||
int maxWidth;
|
||||
int maxHeight;
|
||||
int frame;
|
||||
|
||||
// User Data
|
||||
char *HTML;
|
||||
|
||||
// Callback
|
||||
ffenestriCallback sendMessageToBackend;
|
||||
|
||||
// Bindings
|
||||
const char *bindings;
|
||||
|
||||
// Lock - used for sync operations (Should we be using g_mutex?)
|
||||
int lock;
|
||||
};
|
||||
|
||||
void *NewApplication(const char *title, int width, int height, int resizable, int devtools, int fullscreen, int startHidden)
|
||||
{
|
||||
// Setup main application struct
|
||||
struct Application *result = malloc(sizeof(struct Application));
|
||||
result->title = title;
|
||||
result->width = width;
|
||||
result->height = height;
|
||||
result->resizable = resizable;
|
||||
result->devtools = devtools;
|
||||
result->fullscreen = fullscreen;
|
||||
result->minWidth = 0;
|
||||
result->minHeight = 0;
|
||||
result->maxWidth = 0;
|
||||
result->maxHeight = 0;
|
||||
result->frame = 1;
|
||||
result->startHidden = startHidden;
|
||||
|
||||
// Default drag button is PRIMARY
|
||||
result->dragButton = PRIMARY_MOUSE_BUTTON;
|
||||
|
||||
result->sendMessageToBackend = (ffenestriCallback)messageFromWindowCallback;
|
||||
|
||||
// Create a unique ID based on the current unix timestamp
|
||||
char temp[11];
|
||||
sprintf(&temp[0], "%d", (int)time(NULL));
|
||||
result->id = concat("wails.app", &temp[0]);
|
||||
|
||||
// Create the main GTK application
|
||||
GApplicationFlags flags = G_APPLICATION_FLAGS_NONE;
|
||||
result->application = gtk_application_new(result->id, flags);
|
||||
|
||||
// Return the application struct
|
||||
return (void *)result;
|
||||
}
|
||||
|
||||
void DestroyApplication(struct Application *app)
|
||||
{
|
||||
Debug("Destroying Application");
|
||||
|
||||
g_application_quit(G_APPLICATION(app->application));
|
||||
|
||||
// Release the GTK ID string
|
||||
if (app->id != NULL)
|
||||
{
|
||||
free(app->id);
|
||||
app->id = NULL;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug("Almost a double free for app->id");
|
||||
}
|
||||
|
||||
// Free the bindings
|
||||
if (app->bindings != NULL)
|
||||
{
|
||||
free((void *)app->bindings);
|
||||
app->bindings = NULL;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug("Almost a double free for app->bindings");
|
||||
}
|
||||
|
||||
// Disconnect signal handlers
|
||||
WebKitUserContentManager *manager = webkit_web_view_get_user_content_manager((WebKitWebView *)app->webView);
|
||||
g_signal_handler_disconnect(manager, app->signalInvoke);
|
||||
if( app->frame == 0) {
|
||||
g_signal_handler_disconnect(manager, app->signalWindowDrag);
|
||||
g_signal_handler_disconnect(app->webView, app->signalButtonPressed);
|
||||
g_signal_handler_disconnect(app->webView, app->signalButtonReleased);
|
||||
}
|
||||
g_signal_handler_disconnect(app->webView, app->signalLoadChanged);
|
||||
|
||||
// Release the main GTK Application
|
||||
if (app->application != NULL)
|
||||
{
|
||||
g_object_unref(app->application);
|
||||
app->application = NULL;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug("Almost a double free for app->application");
|
||||
}
|
||||
Debug("Finished Destroying Application");
|
||||
}
|
||||
|
||||
// Quit will stop the gtk application and free up all the memory
|
||||
// used by the application
|
||||
void Quit(struct Application *app)
|
||||
{
|
||||
Debug("Quit Called");
|
||||
gtk_window_close((GtkWindow *)app->mainWindow);
|
||||
g_application_quit((GApplication *)app->application);
|
||||
DestroyApplication(app);
|
||||
}
|
||||
|
||||
// SetTitle sets the main window title to the given string
|
||||
void SetTitle(struct Application *app, const char *title)
|
||||
{
|
||||
gtk_window_set_title(app->mainWindow, title);
|
||||
}
|
||||
|
||||
// Fullscreen sets the main window to be fullscreen
|
||||
void Fullscreen(struct Application *app)
|
||||
{
|
||||
gtk_window_fullscreen(app->mainWindow);
|
||||
}
|
||||
|
||||
// UnFullscreen resets the main window after a fullscreen
|
||||
void UnFullscreen(struct Application *app)
|
||||
{
|
||||
gtk_window_unfullscreen(app->mainWindow);
|
||||
}
|
||||
|
||||
void setMinMaxSize(struct Application *app)
|
||||
{
|
||||
GdkGeometry size;
|
||||
size.min_width = size.min_height = size.max_width = size.max_height = 0;
|
||||
int flags = 0;
|
||||
if (app->maxHeight > 0 && app->maxWidth > 0)
|
||||
{
|
||||
size.max_height = app->maxHeight;
|
||||
size.max_width = app->maxWidth;
|
||||
flags |= GDK_HINT_MAX_SIZE;
|
||||
}
|
||||
if (app->minHeight > 0 && app->minWidth > 0)
|
||||
{
|
||||
size.min_height = app->minHeight;
|
||||
size.min_width = app->minWidth;
|
||||
flags |= GDK_HINT_MIN_SIZE;
|
||||
}
|
||||
gtk_window_set_geometry_hints(app->mainWindow, NULL, &size, flags);
|
||||
}
|
||||
|
||||
char *fileDialogInternal(struct Application *app, GtkFileChooserAction chooserAction, char **args) {
|
||||
GtkFileChooserNative *native;
|
||||
GtkFileChooserAction action = chooserAction;
|
||||
gint res;
|
||||
char *filename;
|
||||
|
||||
char *title = args[0];
|
||||
char *filter = args[1];
|
||||
|
||||
native = gtk_file_chooser_native_new(title,
|
||||
app->mainWindow,
|
||||
action,
|
||||
"_Open",
|
||||
"_Cancel");
|
||||
|
||||
GtkFileChooser *chooser = GTK_FILE_CHOOSER(native);
|
||||
|
||||
// If we have filters, process them
|
||||
if (filter[0] != '\0') {
|
||||
GtkFileFilter *file_filter = gtk_file_filter_new();
|
||||
gchar **filters = g_strsplit(filter, ",", -1);
|
||||
gint i;
|
||||
for(i = 0; filters && filters[i]; i++) {
|
||||
gtk_file_filter_add_pattern(file_filter, filters[i]);
|
||||
// Debug("Adding filter pattern: %s\n", filters[i]);
|
||||
}
|
||||
gtk_file_filter_set_name(file_filter, filter);
|
||||
gtk_file_chooser_add_filter(chooser, file_filter);
|
||||
g_strfreev(filters);
|
||||
}
|
||||
|
||||
res = gtk_native_dialog_run(GTK_NATIVE_DIALOG(native));
|
||||
if (res == GTK_RESPONSE_ACCEPT)
|
||||
{
|
||||
filename = gtk_file_chooser_get_filename(chooser);
|
||||
}
|
||||
|
||||
g_object_unref(native);
|
||||
|
||||
return filename;
|
||||
}
|
||||
|
||||
// openFileDialogInternal opens a dialog to select a file
|
||||
// NOTE: The result is a string that will need to be freed!
|
||||
char *openFileDialogInternal(struct Application *app, char **args)
|
||||
{
|
||||
return fileDialogInternal(app, GTK_FILE_CHOOSER_ACTION_OPEN, args);
|
||||
}
|
||||
|
||||
// saveFileDialogInternal opens a dialog to select a file
|
||||
// NOTE: The result is a string that will need to be freed!
|
||||
char *saveFileDialogInternal(struct Application *app, char **args)
|
||||
{
|
||||
return fileDialogInternal(app, GTK_FILE_CHOOSER_ACTION_SAVE, args);
|
||||
}
|
||||
|
||||
|
||||
// openDirectoryDialogInternal opens a dialog to select a directory
|
||||
// NOTE: The result is a string that will need to be freed!
|
||||
char *openDirectoryDialogInternal(struct Application *app, char **args)
|
||||
{
|
||||
return fileDialogInternal(app, GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER, args);
|
||||
}
|
||||
|
||||
void SetMinWindowSize(struct Application *app, int minWidth, int minHeight)
|
||||
{
|
||||
app->minWidth = minWidth;
|
||||
app->minHeight = minHeight;
|
||||
}
|
||||
|
||||
void SetMaxWindowSize(struct Application *app, int maxWidth, int maxHeight)
|
||||
{
|
||||
app->maxWidth = maxWidth;
|
||||
app->maxHeight = maxHeight;
|
||||
}
|
||||
|
||||
// SetColour sets the colour of the webview to the given colour string
|
||||
int SetColour(struct Application *app, const char *colourString)
|
||||
{
|
||||
GdkRGBA rgba;
|
||||
gboolean result = gdk_rgba_parse(&rgba, colourString);
|
||||
if (result == FALSE)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
// Debug("Setting webview colour to: %s", colourString);
|
||||
webkit_web_view_get_background_color((WebKitWebView *)(app->webView), &rgba);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// DisableFrame disables the window frame
|
||||
void DisableFrame(struct Application *app)
|
||||
{
|
||||
app->frame = 0;
|
||||
}
|
||||
|
||||
void syncCallback(GObject *source_object,
|
||||
GAsyncResult *res,
|
||||
void *data)
|
||||
{
|
||||
struct Application *app = (struct Application *)data;
|
||||
app->lock = 0;
|
||||
}
|
||||
|
||||
void syncEval(struct Application *app, const gchar *script)
|
||||
{
|
||||
|
||||
WebKitWebView *webView = (WebKitWebView *)(app->webView);
|
||||
|
||||
// wait for lock to free
|
||||
while (app->lock == 1)
|
||||
{
|
||||
g_main_context_iteration(0, true);
|
||||
}
|
||||
// Set lock
|
||||
app->lock = 1;
|
||||
|
||||
webkit_web_view_run_javascript(
|
||||
webView,
|
||||
script,
|
||||
NULL, syncCallback, (void*)app);
|
||||
|
||||
while (app->lock == 1)
|
||||
{
|
||||
g_main_context_iteration(0, true);
|
||||
}
|
||||
}
|
||||
|
||||
void asyncEval(WebKitWebView *webView, const gchar *script)
|
||||
{
|
||||
webkit_web_view_run_javascript(
|
||||
webView,
|
||||
script,
|
||||
NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
typedef void (*dispatchMethod)(struct Application *app, void *);
|
||||
|
||||
struct dispatchData
|
||||
{
|
||||
struct Application *app;
|
||||
dispatchMethod method;
|
||||
void *args;
|
||||
};
|
||||
|
||||
gboolean executeMethod(gpointer data)
|
||||
{
|
||||
struct dispatchData *d = (struct dispatchData *)data;
|
||||
(d->method)(d->app, d->args);
|
||||
g_free(d);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
void ExecJS(struct Application *app, char *js)
|
||||
{
|
||||
struct dispatchData *data = (struct dispatchData *)g_new(struct dispatchData, 1);
|
||||
data->method = (dispatchMethod)syncEval;
|
||||
data->args = js;
|
||||
data->app = app;
|
||||
|
||||
gdk_threads_add_idle(executeMethod, data);
|
||||
}
|
||||
|
||||
typedef char *(*dialogMethod)(struct Application *app, void *);
|
||||
|
||||
struct dialogCall
|
||||
{
|
||||
struct Application *app;
|
||||
dialogMethod method;
|
||||
void *args;
|
||||
void *filter;
|
||||
char *result;
|
||||
int done;
|
||||
};
|
||||
|
||||
gboolean executeMethodWithReturn(gpointer data)
|
||||
{
|
||||
struct dialogCall *d = (struct dialogCall *)data;
|
||||
|
||||
d->result = (d->method)(d->app, d->args);
|
||||
d->done = 1;
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
char *OpenFileDialog(struct Application *app, char *title, char *filter)
|
||||
{
|
||||
struct dialogCall *data = (struct dialogCall *)g_new(struct dialogCall, 1);
|
||||
data->result = NULL;
|
||||
data->done = 0;
|
||||
data->method = (dialogMethod)openFileDialogInternal;
|
||||
const char* dialogArgs[]={ title, filter };
|
||||
data->args = dialogArgs;
|
||||
data->app = app;
|
||||
|
||||
gdk_threads_add_idle(executeMethodWithReturn, data);
|
||||
|
||||
while (data->done == 0)
|
||||
{
|
||||
usleep(100000);
|
||||
}
|
||||
g_free(data);
|
||||
return data->result;
|
||||
}
|
||||
|
||||
char *SaveFileDialog(struct Application *app, char *title, char *filter)
|
||||
{
|
||||
struct dialogCall *data = (struct dialogCall *)g_new(struct dialogCall, 1);
|
||||
data->result = NULL;
|
||||
data->done = 0;
|
||||
data->method = (dialogMethod)saveFileDialogInternal;
|
||||
const char* dialogArgs[]={ title, filter };
|
||||
data->args = dialogArgs;
|
||||
data->app = app;
|
||||
|
||||
gdk_threads_add_idle(executeMethodWithReturn, data);
|
||||
|
||||
while (data->done == 0)
|
||||
{
|
||||
usleep(100000);
|
||||
}
|
||||
Debug("Dialog done");
|
||||
Debug("Result = %s\n", data->result);
|
||||
|
||||
g_free(data);
|
||||
// Fingers crossed this wasn't freed by g_free above
|
||||
return data->result;
|
||||
}
|
||||
|
||||
char *OpenDirectoryDialog(struct Application *app, char *title, char *filter)
|
||||
{
|
||||
struct dialogCall *data = (struct dialogCall *)g_new(struct dialogCall, 1);
|
||||
data->result = NULL;
|
||||
data->done = 0;
|
||||
data->method = (dialogMethod)openDirectoryDialogInternal;
|
||||
const char* dialogArgs[]={ title, filter };
|
||||
data->args = dialogArgs;
|
||||
data->app = app;
|
||||
|
||||
gdk_threads_add_idle(executeMethodWithReturn, data);
|
||||
|
||||
while (data->done == 0)
|
||||
{
|
||||
usleep(100000);
|
||||
}
|
||||
Debug("Directory Dialog done");
|
||||
Debug("Result = %s\n", data->result);
|
||||
g_free(data);
|
||||
// Fingers crossed this wasn't freed by g_free above
|
||||
return data->result;
|
||||
}
|
||||
|
||||
// Sets the icon to the XPM stored in icon
|
||||
void setIcon(struct Application *app)
|
||||
{
|
||||
GdkPixbuf *appIcon = gdk_pixbuf_new_from_xpm_data((const char **)icon);
|
||||
gtk_window_set_icon(app->mainWindow, appIcon);
|
||||
}
|
||||
|
||||
static void load_finished_cb(WebKitWebView *webView,
|
||||
WebKitLoadEvent load_event,
|
||||
struct Application *app)
|
||||
{
|
||||
switch (load_event)
|
||||
{
|
||||
// case WEBKIT_LOAD_STARTED:
|
||||
// /* New load, we have now a provisional URI */
|
||||
// // printf("Start downloading %s\n", webkit_web_view_get_uri(web_view));
|
||||
// /* Here we could start a spinner or update the
|
||||
// * location bar with the provisional URI */
|
||||
// break;
|
||||
// case WEBKIT_LOAD_REDIRECTED:
|
||||
// // printf("Redirected to: %s\n", webkit_web_view_get_uri(web_view));
|
||||
// break;
|
||||
// case WEBKIT_LOAD_COMMITTED:
|
||||
// /* The load is being performed. Current URI is
|
||||
// * the final one and it won't change unless a new
|
||||
// * load is requested or a navigation within the
|
||||
// * same page is performed */
|
||||
// // printf("Loading: %s\n", webkit_web_view_get_uri(web_view));
|
||||
// break;
|
||||
case WEBKIT_LOAD_FINISHED:
|
||||
/* Load finished, we can now stop the spinner */
|
||||
// printf("Finished loading: %s\n", webkit_web_view_get_uri(web_view));
|
||||
|
||||
// Bindings
|
||||
Debug("Binding Methods");
|
||||
syncEval(app, app->bindings);
|
||||
|
||||
// Runtime
|
||||
Debug("Setting up Wails runtime");
|
||||
syncEval(app, &runtime);
|
||||
|
||||
// Loop over assets
|
||||
int index = 1;
|
||||
while (1)
|
||||
{
|
||||
// Get next asset pointer
|
||||
const char *asset = assets[index];
|
||||
|
||||
// If we have no more assets, break
|
||||
if (asset == 0x00)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// sync eval the asset
|
||||
syncEval(app, asset);
|
||||
index++;
|
||||
};
|
||||
|
||||
// Set the icon
|
||||
setIcon(app);
|
||||
|
||||
// Setup fullscreen
|
||||
if (app->fullscreen)
|
||||
{
|
||||
Debug("Going fullscreen");
|
||||
Fullscreen(app);
|
||||
}
|
||||
|
||||
// Setup resize
|
||||
gtk_window_resize(GTK_WINDOW(app->mainWindow), app->width, app->height);
|
||||
|
||||
if (app->resizable)
|
||||
{
|
||||
gtk_window_set_default_size(GTK_WINDOW(app->mainWindow), app->width, app->height);
|
||||
}
|
||||
else
|
||||
{
|
||||
gtk_widget_set_size_request(GTK_WIDGET(app->mainWindow), app->width, app->height);
|
||||
gtk_window_resize(GTK_WINDOW(app->mainWindow), app->width, app->height);
|
||||
// Fix the min/max to the window size for good measure
|
||||
app->minHeight = app->maxHeight = app->height;
|
||||
app->minWidth = app->maxWidth = app->width;
|
||||
}
|
||||
gtk_window_set_resizable(GTK_WINDOW(app->mainWindow), app->resizable ? TRUE : FALSE);
|
||||
setMinMaxSize(app);
|
||||
|
||||
// Centre by default
|
||||
gtk_window_set_position(app->mainWindow, GTK_WIN_POS_CENTER);
|
||||
|
||||
// Show window and focus
|
||||
if( app->startHidden == 0) {
|
||||
gtk_widget_show_all(GTK_WIDGET(app->mainWindow));
|
||||
gtk_widget_grab_focus(app->webView);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static gboolean disable_context_menu_cb(
|
||||
WebKitWebView *web_view,
|
||||
WebKitContextMenu *context_menu,
|
||||
GdkEvent *event,
|
||||
WebKitHitTestResult *hit_test_result,
|
||||
gpointer user_data)
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static void printEvent(const char *message, GdkEventButton *event)
|
||||
{
|
||||
Debug("%s: [button:%d] [x:%f] [y:%f] [time:%d]",
|
||||
message,
|
||||
event->button,
|
||||
event->x_root,
|
||||
event->y_root,
|
||||
event->time);
|
||||
}
|
||||
|
||||
|
||||
static void dragWindow(WebKitUserContentManager *contentManager,
|
||||
WebKitJavascriptResult *result,
|
||||
struct Application *app)
|
||||
{
|
||||
// If we get this message erroneously, ignore
|
||||
if (app->dragButtonEvent == NULL)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore non-toplevel widgets
|
||||
GtkWidget *window = gtk_widget_get_toplevel(GTK_WIDGET(app->webView));
|
||||
if (!GTK_IS_WINDOW(window))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Initiate the drag
|
||||
printEvent("Starting drag with event", app->dragButtonEvent);
|
||||
|
||||
gtk_window_begin_move_drag(app->mainWindow,
|
||||
app->dragButton,
|
||||
app->dragButtonEvent->x_root,
|
||||
app->dragButtonEvent->y_root,
|
||||
app->dragButtonEvent->time);
|
||||
// Clear the event
|
||||
app->dragButtonEvent = NULL;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
gboolean buttonPress(GtkWidget *widget, GdkEventButton *event, struct Application *app)
|
||||
{
|
||||
if (event->type == GDK_BUTTON_PRESS && event->button == app->dragButton)
|
||||
{
|
||||
app->dragButtonEvent = event;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
gboolean buttonRelease(GtkWidget *widget, GdkEventButton *event, struct Application *app)
|
||||
{
|
||||
if (event->type == GDK_BUTTON_RELEASE && event->button == app->dragButton)
|
||||
{
|
||||
app->dragButtonEvent = NULL;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
static void sendMessageToBackend(WebKitUserContentManager *contentManager,
|
||||
WebKitJavascriptResult *result,
|
||||
struct Application *app)
|
||||
{
|
||||
#if WEBKIT_MAJOR_VERSION >= 2 && WEBKIT_MINOR_VERSION >= 22
|
||||
JSCValue *value = webkit_javascript_result_get_js_value(result);
|
||||
char *message = jsc_value_to_string(value);
|
||||
#else
|
||||
JSGlobalContextRef context = webkit_javascript_result_get_global_context(result);
|
||||
JSValueRef value = webkit_javascript_result_get_value(result);
|
||||
JSStringRef js = JSValueToStringCopy(context, value, NULL);
|
||||
size_t messageSize = JSStringGetMaximumUTF8CStringSize(js);
|
||||
char *message = g_new(char, messageSize);
|
||||
JSStringGetUTF8CString(js, message, messageSize);
|
||||
JSStringRelease(js);
|
||||
#endif
|
||||
app->sendMessageToBackend(message);
|
||||
g_free(message);
|
||||
}
|
||||
|
||||
void SetDebug(struct Application *app, int flag)
|
||||
{
|
||||
debug = flag;
|
||||
}
|
||||
|
||||
// getCurrentMonitorGeometry gets the geometry of the monitor
|
||||
// that the window is mostly on.
|
||||
GdkRectangle getCurrentMonitorGeometry(GtkWindow *window) {
|
||||
// Get the monitor that the window is currently on
|
||||
GdkDisplay *display = gtk_widget_get_display(GTK_WIDGET(window));
|
||||
GdkWindow *gdk_window = gtk_widget_get_window(GTK_WIDGET(window));
|
||||
GdkMonitor *monitor = gdk_display_get_monitor_at_window (display, gdk_window);
|
||||
|
||||
// Get the geometry of the monitor
|
||||
GdkRectangle result;
|
||||
gdk_monitor_get_geometry (monitor,&result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/*******************
|
||||
* Window Position *
|
||||
*******************/
|
||||
|
||||
// Position holds an x/y corrdinate
|
||||
struct Position {
|
||||
int x;
|
||||
int y;
|
||||
};
|
||||
|
||||
// Internal call for setting the position of the window.
|
||||
void setPositionInternal(struct Application *app, struct Position *pos) {
|
||||
|
||||
// Get the monitor geometry
|
||||
GdkRectangle m = getCurrentMonitorGeometry(app->mainWindow);
|
||||
|
||||
// Move the window relative to the monitor
|
||||
gtk_window_move(app->mainWindow, m.x + pos->x, m.y + pos->y);
|
||||
|
||||
// Free memory
|
||||
free(pos);
|
||||
}
|
||||
|
||||
// SetPosition sets the position of the window to the given x/y
|
||||
// coordinates. The x/y values are relative to the monitor
|
||||
// the window is mostly on.
|
||||
void SetPosition(struct Application *app, int x, int y) {
|
||||
struct dispatchData *data = (struct dispatchData *)g_new(struct dispatchData, 1);
|
||||
data->method = (dispatchMethod)setPositionInternal;
|
||||
struct Position *pos = malloc(sizeof(struct Position));
|
||||
pos->x = x;
|
||||
pos->y = y;
|
||||
data->args = pos;
|
||||
data->app = app;
|
||||
|
||||
gdk_threads_add_idle(executeMethod, data);
|
||||
}
|
||||
|
||||
/***************
|
||||
* Window Size *
|
||||
***************/
|
||||
|
||||
// Size holds a width/height
|
||||
struct Size {
|
||||
int width;
|
||||
int height;
|
||||
};
|
||||
|
||||
// Internal call for setting the size of the window.
|
||||
void setSizeInternal(struct Application *app, struct Size *size) {
|
||||
gtk_window_resize(app->mainWindow, size->width, size->height);
|
||||
free(size);
|
||||
}
|
||||
|
||||
// SetSize sets the size of the window to the given width/height
|
||||
void SetSize(struct Application *app, int width, int height) {
|
||||
struct dispatchData *data = (struct dispatchData *)g_new(struct dispatchData, 1);
|
||||
data->method = (dispatchMethod)setSizeInternal;
|
||||
struct Size *size = malloc(sizeof(struct Size));
|
||||
size->width = width;
|
||||
size->height = height;
|
||||
data->args = size;
|
||||
data->app = app;
|
||||
|
||||
gdk_threads_add_idle(executeMethod, data);
|
||||
}
|
||||
|
||||
|
||||
// centerInternal will center the main window on the monitor it is mostly in
|
||||
void centerInternal(struct Application *app)
|
||||
{
|
||||
// Get the geometry of the monitor
|
||||
GdkRectangle m = getCurrentMonitorGeometry(app->mainWindow);
|
||||
|
||||
// Get the window width/height
|
||||
int windowWidth, windowHeight;
|
||||
gtk_window_get_size(app->mainWindow, &windowWidth, &windowHeight);
|
||||
|
||||
// Place the window at the center of the monitor
|
||||
gtk_window_move(app->mainWindow, ((m.width - windowWidth) / 2) + m.x, ((m.height - windowHeight) / 2) + m.y);
|
||||
}
|
||||
|
||||
// Center the window
|
||||
void Center(struct Application *app) {
|
||||
|
||||
// Setup a call to centerInternal on the main thread
|
||||
struct dispatchData *data = (struct dispatchData *)g_new(struct dispatchData, 1);
|
||||
data->method = (dispatchMethod)centerInternal;
|
||||
data->app = app;
|
||||
|
||||
// Add call to main thread
|
||||
gdk_threads_add_idle(executeMethod, data);
|
||||
}
|
||||
|
||||
// hideInternal hides the main window
|
||||
void hideInternal(struct Application *app) {
|
||||
gtk_widget_hide (GTK_WIDGET(app->mainWindow));
|
||||
}
|
||||
|
||||
// Hide places the hideInternal method onto the main thread for execution
|
||||
void Hide(struct Application *app) {
|
||||
|
||||
// Setup a call to hideInternal on the main thread
|
||||
struct dispatchData *data = (struct dispatchData *)g_new(struct dispatchData, 1);
|
||||
data->method = (dispatchMethod)hideInternal;
|
||||
data->app = app;
|
||||
|
||||
// Add call to main thread
|
||||
gdk_threads_add_idle(executeMethod, data);
|
||||
}
|
||||
|
||||
// showInternal shows the main window
|
||||
void showInternal(struct Application *app) {
|
||||
gtk_widget_show_all(GTK_WIDGET(app->mainWindow));
|
||||
gtk_widget_grab_focus(app->webView);
|
||||
}
|
||||
|
||||
// Show places the showInternal method onto the main thread for execution
|
||||
void Show(struct Application *app) {
|
||||
struct dispatchData *data = (struct dispatchData *)g_new(struct dispatchData, 1);
|
||||
data->method = (dispatchMethod)showInternal;
|
||||
data->app = app;
|
||||
|
||||
gdk_threads_add_idle(executeMethod, data);
|
||||
}
|
||||
|
||||
|
||||
// maximiseInternal maximises the main window
|
||||
void maximiseInternal(struct Application *app) {
|
||||
gtk_window_maximize(GTK_WIDGET(app->mainWindow));
|
||||
}
|
||||
|
||||
// Maximise places the maximiseInternal method onto the main thread for execution
|
||||
void Maximise(struct Application *app) {
|
||||
|
||||
// Setup a call to maximiseInternal on the main thread
|
||||
struct dispatchData *data = (struct dispatchData *)g_new(struct dispatchData, 1);
|
||||
data->method = (dispatchMethod)maximiseInternal;
|
||||
data->app = app;
|
||||
|
||||
// Add call to main thread
|
||||
gdk_threads_add_idle(executeMethod, data);
|
||||
}
|
||||
|
||||
// unmaximiseInternal unmaximises the main window
|
||||
void unmaximiseInternal(struct Application *app) {
|
||||
gtk_window_unmaximize(GTK_WIDGET(app->mainWindow));
|
||||
}
|
||||
|
||||
// Unmaximise places the unmaximiseInternal method onto the main thread for execution
|
||||
void Unmaximise(struct Application *app) {
|
||||
|
||||
// Setup a call to unmaximiseInternal on the main thread
|
||||
struct dispatchData *data = (struct dispatchData *)g_new(struct dispatchData, 1);
|
||||
data->method = (dispatchMethod)unmaximiseInternal;
|
||||
data->app = app;
|
||||
|
||||
// Add call to main thread
|
||||
gdk_threads_add_idle(executeMethod, data);
|
||||
}
|
||||
|
||||
|
||||
// minimiseInternal minimises the main window
|
||||
void minimiseInternal(struct Application *app) {
|
||||
gtk_window_iconify(app->mainWindow);
|
||||
}
|
||||
|
||||
// Minimise places the minimiseInternal method onto the main thread for execution
|
||||
void Minimise(struct Application *app) {
|
||||
|
||||
// Setup a call to minimiseInternal on the main thread
|
||||
struct dispatchData *data = (struct dispatchData *)g_new(struct dispatchData, 1);
|
||||
data->method = (dispatchMethod)minimiseInternal;
|
||||
data->app = app;
|
||||
|
||||
// Add call to main thread
|
||||
gdk_threads_add_idle(executeMethod, data);
|
||||
}
|
||||
|
||||
// unminimiseInternal unminimises the main window
|
||||
void unminimiseInternal(struct Application *app) {
|
||||
gtk_window_present(app->mainWindow);
|
||||
}
|
||||
|
||||
// Unminimise places the unminimiseInternal method onto the main thread for execution
|
||||
void Unminimise(struct Application *app) {
|
||||
|
||||
// Setup a call to unminimiseInternal on the main thread
|
||||
struct dispatchData *data = (struct dispatchData *)g_new(struct dispatchData, 1);
|
||||
data->method = (dispatchMethod)unminimiseInternal;
|
||||
data->app = app;
|
||||
|
||||
// Add call to main thread
|
||||
gdk_threads_add_idle(executeMethod, data);
|
||||
}
|
||||
|
||||
|
||||
void SetBindings(struct Application *app, const char *bindings)
|
||||
{
|
||||
const char *temp = concat("window.wailsbindings = \"", bindings);
|
||||
const char *jscall = concat(temp, "\";");
|
||||
free((void *)temp);
|
||||
app->bindings = jscall;
|
||||
}
|
||||
|
||||
// This is called when the close button on the window is pressed
|
||||
gboolean close_button_pressed(GtkWidget *widget,
|
||||
GdkEvent *event,
|
||||
struct Application *app)
|
||||
{
|
||||
app->sendMessageToBackend("WC"); // Window Close
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static void setupWindow(struct Application *app)
|
||||
{
|
||||
|
||||
// Create the window
|
||||
GtkWidget *mainWindow = gtk_application_window_new(app->application);
|
||||
// Save reference
|
||||
app->mainWindow = GTK_WINDOW(mainWindow);
|
||||
|
||||
// Setup frame
|
||||
gtk_window_set_decorated((GtkWindow *)mainWindow, app->frame);
|
||||
|
||||
// Setup title
|
||||
gtk_window_set_title(GTK_WINDOW(mainWindow), app->title);
|
||||
|
||||
// Setup script handler
|
||||
WebKitUserContentManager *contentManager = webkit_user_content_manager_new();
|
||||
|
||||
// Setup the invoke handler
|
||||
webkit_user_content_manager_register_script_message_handler(contentManager, "external");
|
||||
app->signalInvoke = g_signal_connect(contentManager, "script-message-received::external", G_CALLBACK(sendMessageToBackend), app);
|
||||
|
||||
// Setup the window drag handler if this is a frameless app
|
||||
if ( app->frame == 0 ) {
|
||||
webkit_user_content_manager_register_script_message_handler(contentManager, "windowDrag");
|
||||
app->signalWindowDrag = g_signal_connect(contentManager, "script-message-received::windowDrag", G_CALLBACK(dragWindow), app);
|
||||
// Setup the mouse handlers
|
||||
app->signalButtonPressed = g_signal_connect(app->webView, "button-press-event", G_CALLBACK(buttonPress), app);
|
||||
app->signalButtonReleased = g_signal_connect(app->webView, "button-release-event", G_CALLBACK(buttonRelease), app);
|
||||
}
|
||||
GtkWidget *webView = webkit_web_view_new_with_user_content_manager(contentManager);
|
||||
|
||||
// Save reference
|
||||
app->webView = webView;
|
||||
|
||||
// Add the webview to the window
|
||||
gtk_container_add(GTK_CONTAINER(mainWindow), webView);
|
||||
|
||||
|
||||
// Load default HTML
|
||||
app->signalLoadChanged = g_signal_connect(G_OBJECT(webView), "load-changed", G_CALLBACK(load_finished_cb), app);
|
||||
|
||||
// Load the user's HTML
|
||||
// assets[0] is the HTML because the asset array is bundled like that by convention
|
||||
webkit_web_view_load_uri(WEBKIT_WEB_VIEW(webView), assets[0]);
|
||||
|
||||
// Check if we want to enable the dev tools
|
||||
if (app->devtools)
|
||||
{
|
||||
WebKitSettings *settings = webkit_web_view_get_settings(WEBKIT_WEB_VIEW(webView));
|
||||
// webkit_settings_set_enable_write_console_messages_to_stdout(settings, true);
|
||||
webkit_settings_set_enable_developer_extras(settings, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
g_signal_connect(G_OBJECT(webView), "context-menu", G_CALLBACK(disable_context_menu_cb), app);
|
||||
}
|
||||
|
||||
// Listen for close button signal
|
||||
g_signal_connect(GTK_WIDGET(mainWindow), "delete-event", G_CALLBACK(close_button_pressed), app);
|
||||
}
|
||||
|
||||
static void activate(GtkApplication* _, struct Application *app)
|
||||
{
|
||||
setupWindow(app);
|
||||
}
|
||||
|
||||
void Run(struct Application *app, int argc, char **argv)
|
||||
{
|
||||
g_signal_connect(app->application, "activate", G_CALLBACK(activate), app);
|
||||
g_application_run(G_APPLICATION(app->application), argc, argv);
|
||||
}
|
||||
|
||||
#endif
|
||||
5
v2/internal/ffenestri/runtime.c
Normal file
5
v2/internal/ffenestri/runtime.c
Normal file
File diff suppressed because one or more lines are too long
171
v2/internal/fs/fs.go
Normal file
171
v2/internal/fs/fs.go
Normal file
@@ -0,0 +1,171 @@
|
||||
package fs
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"unsafe"
|
||||
|
||||
"github.com/leaanthony/slicer"
|
||||
)
|
||||
|
||||
// LocalDirectory gets the caller's file directory
|
||||
// Equivalent to node's __DIRNAME
|
||||
func LocalDirectory() string {
|
||||
_, thisFile, _, _ := runtime.Caller(1)
|
||||
return filepath.Dir(thisFile)
|
||||
}
|
||||
|
||||
// Mkdir will create the given directory
|
||||
func Mkdir(dirname string) error {
|
||||
return os.Mkdir(dirname, 0755)
|
||||
}
|
||||
|
||||
// DeleteFile will delete the given file
|
||||
func DeleteFile(filename string) error {
|
||||
return os.Remove(filename)
|
||||
}
|
||||
|
||||
// CopyFile from source to target
|
||||
func CopyFile(source string, target string) error {
|
||||
s, err := os.Open(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer s.Close()
|
||||
d, err := os.Create(target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := io.Copy(d, s); err != nil {
|
||||
d.Close()
|
||||
return err
|
||||
}
|
||||
return d.Close()
|
||||
}
|
||||
|
||||
// DirExists - Returns true if the given path resolves to a directory on the filesystem
|
||||
func DirExists(path string) bool {
|
||||
fi, err := os.Lstat(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return fi.Mode().IsDir()
|
||||
}
|
||||
|
||||
// FileExists returns a boolean value indicating whether
|
||||
// the given file exists
|
||||
func FileExists(path string) bool {
|
||||
fi, err := os.Lstat(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return fi.Mode().IsRegular()
|
||||
}
|
||||
|
||||
// RelativePath returns a qualified path created by joining the
|
||||
// directory of the calling file and the given relative path.
|
||||
//
|
||||
// Example: RelativePath("..") in *this* file would give you '/path/to/wails2/v2/internal`
|
||||
func RelativePath(relativepath string, optionalpaths ...string) string {
|
||||
_, thisFile, _, _ := runtime.Caller(1)
|
||||
localDir := filepath.Dir(thisFile)
|
||||
|
||||
// If we have optional paths, join them to the relativepath
|
||||
if len(optionalpaths) > 0 {
|
||||
paths := []string{relativepath}
|
||||
paths = append(paths, optionalpaths...)
|
||||
relativepath = filepath.Join(paths...)
|
||||
}
|
||||
result, err := filepath.Abs(filepath.Join(localDir, relativepath))
|
||||
if err != nil {
|
||||
// I'm allowing this for 1 reason only: It's fatal if the path
|
||||
// supplied is wrong as it's only used internally in Wails. If we get
|
||||
// that path wrong, we should know about it immediately. The other reason is
|
||||
// that it cuts down a ton of unnecassary error handling.
|
||||
panic(err)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// MustLoadString attempts to load a string and will abort with a fatal message if
|
||||
// something goes wrong
|
||||
func MustLoadString(filename string) string {
|
||||
data, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
fmt.Printf("FATAL: Unable to load file '%s': %s\n", filename, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
return *(*string)(unsafe.Pointer(&data))
|
||||
}
|
||||
|
||||
// MD5File returns the md5sum of the given file
|
||||
func MD5File(filename string) (string, error) {
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
h := md5.New()
|
||||
if _, err := io.Copy(h, f); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%x", h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
// MustMD5File will call MD5File and abort the program on error
|
||||
func MustMD5File(filename string) string {
|
||||
result, err := MD5File(filename)
|
||||
if err != nil {
|
||||
println("FATAL: Unable to MD5Sum file:", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// MustWriteString will attempt to write the given data to the given filename
|
||||
// It will abort the program in the event of a failure
|
||||
func MustWriteString(filename string, data string) {
|
||||
err := ioutil.WriteFile(filename, []byte(data), 0755)
|
||||
if err != nil {
|
||||
fatal("Unable to write file", filename, ":", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// fatal will print the optional messages and die
|
||||
func fatal(message ...string) {
|
||||
if len(message) > 0 {
|
||||
print("FATAL:")
|
||||
for text := range message {
|
||||
print(text)
|
||||
}
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// GetSubdirectories returns a list of subdirectories for the given root directory
|
||||
func GetSubdirectories(rootDir string) (*slicer.StringSlicer, error) {
|
||||
var result slicer.StringSlicer
|
||||
|
||||
// Iterate root dir
|
||||
err := filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// If we have a directory, save it
|
||||
if info.IsDir() {
|
||||
result.Add(path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return &result, err
|
||||
}
|
||||
31
v2/internal/fs/fs_test.go
Normal file
31
v2/internal/fs/fs_test.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package fs
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/matryer/is"
|
||||
)
|
||||
|
||||
func TestRelativePath(t *testing.T) {
|
||||
|
||||
is := is.New(t)
|
||||
|
||||
cwd, err := os.Getwd()
|
||||
is.Equal(err, nil)
|
||||
|
||||
// Check current directory
|
||||
actual := RelativePath(".")
|
||||
is.Equal(actual, cwd)
|
||||
|
||||
// Check 2 parameters
|
||||
actual = RelativePath("..", "fs")
|
||||
is.Equal(actual, cwd)
|
||||
|
||||
// Check 3 parameters including filename
|
||||
actual = RelativePath("..", "fs", "fs.go")
|
||||
expected := filepath.Join(cwd, "fs.go")
|
||||
is.Equal(actual, expected)
|
||||
|
||||
}
|
||||
113
v2/internal/html/asset.go
Normal file
113
v2/internal/html/asset.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package html
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
type assetTypes struct {
|
||||
JS string
|
||||
CSS string
|
||||
FAVICON string
|
||||
HTML string
|
||||
}
|
||||
|
||||
// AssetTypes is an enum for the asset type keys
|
||||
var AssetTypes *assetTypes = &assetTypes{
|
||||
JS: "javascript",
|
||||
CSS: "css",
|
||||
FAVICON: "favicon",
|
||||
HTML: "html",
|
||||
}
|
||||
|
||||
// Asset describes an asset type and its path
|
||||
type Asset struct {
|
||||
Type string
|
||||
Path string
|
||||
Data string
|
||||
}
|
||||
|
||||
// Load the asset from disk
|
||||
func (a *Asset) Load(basedirectory string) error {
|
||||
assetpath := filepath.Join(basedirectory, a.Path)
|
||||
data, err := ioutil.ReadFile(assetpath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.Data = string(data)
|
||||
return nil
|
||||
}
|
||||
|
||||
// AsString returns the data as a READ ONLY string
|
||||
func (a *Asset) AsString() string {
|
||||
return a.Data
|
||||
}
|
||||
|
||||
// AsCHexData processes the asset data so it may be used by C
|
||||
func (a *Asset) AsCHexData() string {
|
||||
|
||||
// This will be our final string to hexify
|
||||
dataString := a.Data
|
||||
|
||||
switch a.Type {
|
||||
case AssetTypes.HTML:
|
||||
|
||||
// Escape HTML
|
||||
var re = regexp.MustCompile(`\s{2,}`)
|
||||
result := re.ReplaceAllString(a.Data, ``)
|
||||
result = strings.ReplaceAll(result, "\n", "")
|
||||
result = strings.ReplaceAll(result, "\r\n", "")
|
||||
result = strings.ReplaceAll(result, "\n", "")
|
||||
|
||||
// Inject wailsloader code
|
||||
result = strings.Replace(result, `</body>`, `<script id='wailsloader'>window.wailsloader = { html: true, runtime: false, userjs: false, usercss: false };var self=document.querySelector('#wailsloader');self.parentNode.removeChild(self);</script></body>`, 1)
|
||||
|
||||
url := url.URL{Path: result}
|
||||
urlString := strings.ReplaceAll(url.String(), "/", "%2f")
|
||||
|
||||
// Save Data uRI string
|
||||
dataString = "data:text/html;charset=utf-8," + urlString
|
||||
|
||||
case AssetTypes.CSS:
|
||||
|
||||
// Escape CSS data
|
||||
var re = regexp.MustCompile(`\s{2,}`)
|
||||
result := re.ReplaceAllString(a.Data, ``)
|
||||
result = strings.ReplaceAll(result, "\n", "")
|
||||
result = strings.ReplaceAll(result, "\r\n", "")
|
||||
result = strings.ReplaceAll(result, "\n", "")
|
||||
result = strings.ReplaceAll(result, "\t", "")
|
||||
result = strings.ReplaceAll(result, `\`, `\\`)
|
||||
result = strings.ReplaceAll(result, `"`, `\"`)
|
||||
result = strings.ReplaceAll(result, `'`, `\'`)
|
||||
result = strings.ReplaceAll(result, ` {`, `{`)
|
||||
result = strings.ReplaceAll(result, `: `, `:`)
|
||||
dataString = fmt.Sprintf("window.wails._.InjectCSS(\"%s\");", result)
|
||||
}
|
||||
|
||||
// Get byte data of the string
|
||||
bytes := *(*[]byte)(unsafe.Pointer(&dataString))
|
||||
|
||||
// Create a strings builder
|
||||
var cdata strings.Builder
|
||||
|
||||
// Set buffer size to 4k
|
||||
cdata.Grow(4096)
|
||||
|
||||
// Convert each byte to hex
|
||||
for _, b := range bytes {
|
||||
cdata.WriteString(fmt.Sprintf("0x%x, ", b))
|
||||
}
|
||||
|
||||
return cdata.String()
|
||||
}
|
||||
|
||||
// Dump will output the asset to the terminal
|
||||
func (a *Asset) Dump() {
|
||||
fmt.Printf("{ Type: %s, Path: %s, Data: %+v }\n", a.Type, a.Path, a.Data[:10])
|
||||
}
|
||||
200
v2/internal/html/assetbundle.go
Normal file
200
v2/internal/html/assetbundle.go
Normal file
@@ -0,0 +1,200 @@
|
||||
package html
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/leaanthony/slicer"
|
||||
"github.com/wailsapp/wails/v2/internal/assetdb"
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
// AssetBundle is a collection of Assets
|
||||
type AssetBundle struct {
|
||||
assets []*Asset
|
||||
basedirectory string
|
||||
}
|
||||
|
||||
// NewAssetBundle creates a new AssetBundle struct containing
|
||||
// the given html and all the assets referenced by it
|
||||
func NewAssetBundle(pathToHTML string) (*AssetBundle, error) {
|
||||
|
||||
// Create result
|
||||
result := &AssetBundle{
|
||||
basedirectory: filepath.Dir(pathToHTML),
|
||||
}
|
||||
|
||||
err := result.loadAssets(pathToHTML)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// loadAssets processes the given html file and loads in
|
||||
// all referenced assets
|
||||
func (a *AssetBundle) loadAssets(pathToHTML string) error {
|
||||
|
||||
// Save HTML
|
||||
htmlAsset := &Asset{
|
||||
Type: AssetTypes.HTML,
|
||||
Path: filepath.Base(pathToHTML),
|
||||
}
|
||||
err := htmlAsset.Load(a.basedirectory)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.assets = append(a.assets, htmlAsset)
|
||||
|
||||
return a.processHTML(htmlAsset.AsString())
|
||||
}
|
||||
|
||||
// Credit to: https://drstearns.github.io/tutorials/tokenizing/
|
||||
func (a *AssetBundle) processHTML(htmldata string) error {
|
||||
|
||||
// Tokenize the html
|
||||
buf := bytes.NewBufferString(htmldata)
|
||||
tokenizer := html.NewTokenizer(buf)
|
||||
|
||||
for {
|
||||
//get the next token type
|
||||
tokenType := tokenizer.Next()
|
||||
|
||||
//if it's an error token, we either reached
|
||||
//the end of the file, or the HTML was malformed
|
||||
if tokenType == html.ErrorToken {
|
||||
err := tokenizer.Err()
|
||||
if err == io.EOF {
|
||||
//end of the file, break out of the loop
|
||||
break
|
||||
}
|
||||
//otherwise, there was an error tokenizing,
|
||||
//which likely means the HTML was malformed.
|
||||
//since this is a simple command-line utility,
|
||||
//we can just use log.Fatalf() to report the error
|
||||
//and exit the process with a non-zero status code
|
||||
return tokenizer.Err()
|
||||
}
|
||||
|
||||
//process the token according to the token type...
|
||||
if tokenType == html.StartTagToken {
|
||||
//get the token
|
||||
token := tokenizer.Token()
|
||||
|
||||
//if the name of the element is "title"
|
||||
if "link" == token.Data {
|
||||
//the next token should be the page title
|
||||
tokenType = tokenizer.Next()
|
||||
//just make sure it's actually a text token
|
||||
asset := &Asset{}
|
||||
for _, attr := range token.Attr {
|
||||
// Favicon
|
||||
if attr.Key == "rel" && attr.Val == "icon" {
|
||||
asset.Type = AssetTypes.FAVICON
|
||||
}
|
||||
if attr.Key == "href" {
|
||||
asset.Path = attr.Val
|
||||
}
|
||||
// stylesheet
|
||||
if attr.Key == "rel" && attr.Val == "stylesheet" {
|
||||
asset.Type = AssetTypes.CSS
|
||||
}
|
||||
}
|
||||
err := asset.Load(a.basedirectory)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.assets = append(a.assets, asset)
|
||||
}
|
||||
if "script" == token.Data {
|
||||
|
||||
tokenType = tokenizer.Next()
|
||||
//just make sure it's actually a text token
|
||||
asset := &Asset{Type: AssetTypes.JS}
|
||||
for _, attr := range token.Attr {
|
||||
if attr.Key == "src" {
|
||||
asset.Path = attr.Val
|
||||
break
|
||||
}
|
||||
}
|
||||
err := asset.Load(a.basedirectory)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.assets = append(a.assets, asset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteToCFile dumps all the assets to C files in the given directory
|
||||
func (a *AssetBundle) WriteToCFile(targetDir string) (string, error) {
|
||||
|
||||
// Write out the assets.c file
|
||||
var cdata strings.Builder
|
||||
|
||||
// Write header
|
||||
header := `// assets.c
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Ă‚ MODIWL.
|
||||
// This file was auto-generated. DO NOT MODIFY.
|
||||
|
||||
`
|
||||
cdata.WriteString(header)
|
||||
|
||||
// Loop over the Assets
|
||||
var err error
|
||||
assetVariables := slicer.String()
|
||||
var variableName string
|
||||
for index, asset := range a.assets {
|
||||
// For desktop we ignore the favicon
|
||||
if asset.Type == AssetTypes.FAVICON {
|
||||
continue
|
||||
}
|
||||
variableName = fmt.Sprintf("%s%d", asset.Type, index)
|
||||
assetCdata := fmt.Sprintf("const unsigned char %s[]={ %s0x00 };\n", variableName, asset.AsCHexData())
|
||||
cdata.WriteString(assetCdata)
|
||||
assetVariables.Add(variableName)
|
||||
}
|
||||
|
||||
if assetVariables.Length() > 0 {
|
||||
cdata.WriteString(fmt.Sprintf("\nconst unsigned char *assets[] = { %s, 0x00 };", assetVariables.Join(", ")))
|
||||
} else {
|
||||
cdata.WriteString("\nconst unsigned char *assets[] = { 0x00 };")
|
||||
}
|
||||
|
||||
// Save file
|
||||
assetsFile := filepath.Join(targetDir, "assets.c")
|
||||
err = ioutil.WriteFile(assetsFile, []byte(cdata.String()), 0600)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return assetsFile, nil
|
||||
}
|
||||
|
||||
// ConvertToAssetDB returns an assetdb.AssetDB initialized with
|
||||
// the items in the AssetBundle
|
||||
func (a *AssetBundle) ConvertToAssetDB() (*assetdb.AssetDB, error) {
|
||||
assetdb := assetdb.NewAssetDB()
|
||||
|
||||
// Loop over the Assets
|
||||
for _, asset := range a.assets {
|
||||
assetdb.AddAsset(asset.Path, []byte(asset.Data))
|
||||
}
|
||||
|
||||
return assetdb, nil
|
||||
}
|
||||
|
||||
// Dump will output the assets to the terminal
|
||||
func (a *AssetBundle) Dump() {
|
||||
println("Assets:")
|
||||
for _, asset := range a.assets {
|
||||
asset.Dump()
|
||||
}
|
||||
}
|
||||
98
v2/internal/logger/custom_logger.go
Normal file
98
v2/internal/logger/custom_logger.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// CustomLogger defines what a user can do with a logger
|
||||
type CustomLogger interface {
|
||||
// Writeln writes directly to the output with no log level plus line ending
|
||||
Writeln(message string) error
|
||||
|
||||
// Write writes directly to the output with no log level
|
||||
Write(message string) error
|
||||
|
||||
// Trace level logging. Works like Sprintf.
|
||||
Trace(format string, args ...interface{}) error
|
||||
|
||||
// Debug level logging. Works like Sprintf.
|
||||
Debug(format string, args ...interface{}) error
|
||||
|
||||
// Info level logging. Works like Sprintf.
|
||||
Info(format string, args ...interface{}) error
|
||||
|
||||
// Warning level logging. Works like Sprintf.
|
||||
Warning(format string, args ...interface{}) error
|
||||
|
||||
// Error level logging. Works like Sprintf.
|
||||
Error(format string, args ...interface{}) error
|
||||
|
||||
// Fatal level logging. Works like Sprintf.
|
||||
Fatal(format string, args ...interface{})
|
||||
}
|
||||
|
||||
// customLogger is a utlility to log messages to a number of destinations
|
||||
type customLogger struct {
|
||||
logger *Logger
|
||||
name string
|
||||
}
|
||||
|
||||
// New creates a new customLogger. You may pass in a number of `io.Writer`s that
|
||||
// are the targets for the logs
|
||||
func newcustomLogger(logger *Logger, name string) *customLogger {
|
||||
result := &customLogger{
|
||||
name: name,
|
||||
logger: logger,
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Writeln writes directly to the output with no log level
|
||||
// Appends a carriage return to the message
|
||||
func (l *customLogger) Writeln(message string) error {
|
||||
return l.logger.Writeln(message)
|
||||
}
|
||||
|
||||
// Write writes directly to the output with no log level
|
||||
func (l *customLogger) Write(message string) error {
|
||||
return l.logger.Write(message)
|
||||
}
|
||||
|
||||
// Trace level logging. Works like Sprintf.
|
||||
func (l *customLogger) Trace(format string, args ...interface{}) error {
|
||||
format = fmt.Sprintf("%s | %s", l.name, format)
|
||||
return l.logger.processLogMessage(TRACE, format, args...)
|
||||
}
|
||||
|
||||
// Debug level logging. Works like Sprintf.
|
||||
func (l *customLogger) Debug(format string, args ...interface{}) error {
|
||||
format = fmt.Sprintf("%s | %s", l.name, format)
|
||||
return l.logger.processLogMessage(DEBUG, format, args...)
|
||||
}
|
||||
|
||||
// Info level logging. Works like Sprintf.
|
||||
func (l *customLogger) Info(format string, args ...interface{}) error {
|
||||
format = fmt.Sprintf("%s | %s", l.name, format)
|
||||
return l.logger.processLogMessage(INFO, format, args...)
|
||||
}
|
||||
|
||||
// Warning level logging. Works like Sprintf.
|
||||
func (l *customLogger) Warning(format string, args ...interface{}) error {
|
||||
format = fmt.Sprintf("%s | %s", l.name, format)
|
||||
return l.logger.processLogMessage(WARNING, format, args...)
|
||||
}
|
||||
|
||||
// Error level logging. Works like Sprintf.
|
||||
func (l *customLogger) Error(format string, args ...interface{}) error {
|
||||
format = fmt.Sprintf("%s | %s", l.name, format)
|
||||
return l.logger.processLogMessage(ERROR, format, args...)
|
||||
|
||||
}
|
||||
|
||||
// Fatal level logging. Works like Sprintf.
|
||||
func (l *customLogger) Fatal(format string, args ...interface{}) {
|
||||
format = fmt.Sprintf("%s | %s", l.name, format)
|
||||
l.logger.processLogMessage(FATAL, format, args...)
|
||||
os.Exit(1)
|
||||
}
|
||||
143
v2/internal/logger/default_logger.go
Normal file
143
v2/internal/logger/default_logger.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Logger is a utlility to log messages to a number of destinations
|
||||
type Logger struct {
|
||||
writers []io.Writer
|
||||
logLevel uint8
|
||||
showLevelInLog bool
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
// New creates a new Logger. You may pass in a number of `io.Writer`s that
|
||||
// are the targets for the logs
|
||||
func New(writers ...io.Writer) *Logger {
|
||||
result := &Logger{
|
||||
logLevel: INFO,
|
||||
showLevelInLog: true,
|
||||
}
|
||||
for _, writer := range writers {
|
||||
result.AddOutput(writer)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Writers gets the log writers
|
||||
func (l *Logger) Writers() []io.Writer {
|
||||
return l.writers
|
||||
}
|
||||
|
||||
// CustomLogger creates a new custom logger that prints out a name/id
|
||||
// before the messages
|
||||
func (l *Logger) CustomLogger(name string) CustomLogger {
|
||||
return newcustomLogger(l, name)
|
||||
}
|
||||
|
||||
// HideLogLevel removes the loglevel text from the start of each logged line
|
||||
func (l *Logger) HideLogLevel() {
|
||||
l.showLevelInLog = true
|
||||
}
|
||||
|
||||
// SetLogLevel sets the minimum level of logs that will be output
|
||||
func (l *Logger) SetLogLevel(level uint8) {
|
||||
l.logLevel = level
|
||||
}
|
||||
|
||||
// AddOutput adds the given `io.Writer` to the list of destinations
|
||||
// that get logged to
|
||||
func (l *Logger) AddOutput(writer io.Writer) {
|
||||
l.writers = append(l.writers, writer)
|
||||
}
|
||||
|
||||
func (l *Logger) write(loglevel uint8, message string) error {
|
||||
|
||||
// Don't print logs lower than the current log level
|
||||
if loglevel < l.logLevel {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Show log level text if enabled
|
||||
if l.showLevelInLog {
|
||||
message = mapLogLevel[loglevel] + message
|
||||
}
|
||||
|
||||
// write out the logs
|
||||
l.lock.Lock()
|
||||
for _, writer := range l.writers {
|
||||
_, err := writer.Write([]byte(message))
|
||||
if err != nil {
|
||||
l.lock.Unlock() // Because defer is slow
|
||||
return err
|
||||
}
|
||||
}
|
||||
l.lock.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeln appends a newline character to the message before writing
|
||||
func (l *Logger) writeln(loglevel uint8, message string) error {
|
||||
return l.write(loglevel, message+"\n")
|
||||
}
|
||||
|
||||
// Writeln writes directly to the output with no log level
|
||||
// Appends a carriage return to the message
|
||||
func (l *Logger) Writeln(message string) error {
|
||||
return l.write(BYPASS, message+"\n")
|
||||
}
|
||||
|
||||
// Write writes directly to the output with no log level
|
||||
func (l *Logger) Write(message string) error {
|
||||
return l.write(BYPASS, message)
|
||||
}
|
||||
|
||||
// processLogMessage formats the given message before writing it out
|
||||
func (l *Logger) processLogMessage(loglevel uint8, format string, args ...interface{}) error {
|
||||
message := fmt.Sprintf(format, args...)
|
||||
return l.writeln(loglevel, message)
|
||||
}
|
||||
|
||||
// Trace level logging. Works like Sprintf.
|
||||
func (l *Logger) Trace(format string, args ...interface{}) error {
|
||||
return l.processLogMessage(TRACE, format, args...)
|
||||
}
|
||||
|
||||
// CustomTrace returns a custom Logging function that will insert the given name before the message
|
||||
func (l *Logger) CustomTrace(name string) func(format string, args ...interface{}) {
|
||||
return func(format string, args ...interface{}) {
|
||||
format = name + " | " + format
|
||||
l.processLogMessage(TRACE, format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// Debug level logging. Works like Sprintf.
|
||||
func (l *Logger) Debug(format string, args ...interface{}) error {
|
||||
return l.processLogMessage(DEBUG, format, args...)
|
||||
}
|
||||
|
||||
// Info level logging. Works like Sprintf.
|
||||
func (l *Logger) Info(format string, args ...interface{}) error {
|
||||
return l.processLogMessage(INFO, format, args...)
|
||||
}
|
||||
|
||||
// Warning level logging. Works like Sprintf.
|
||||
func (l *Logger) Warning(format string, args ...interface{}) error {
|
||||
return l.processLogMessage(WARNING, format, args...)
|
||||
}
|
||||
|
||||
// Error level logging. Works like Sprintf.
|
||||
func (l *Logger) Error(format string, args ...interface{}) error {
|
||||
return l.processLogMessage(ERROR, format, args...)
|
||||
|
||||
}
|
||||
|
||||
// Fatal level logging. Works like Sprintf.
|
||||
func (l *Logger) Fatal(format string, args ...interface{}) {
|
||||
l.processLogMessage(FATAL, format, args...)
|
||||
os.Exit(1)
|
||||
}
|
||||
34
v2/internal/logger/logger.go
Normal file
34
v2/internal/logger/logger.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package logger
|
||||
|
||||
const (
|
||||
// TRACE level
|
||||
TRACE uint8 = 0
|
||||
|
||||
// DEBUG level logging
|
||||
DEBUG uint8 = 1
|
||||
|
||||
// INFO level logging
|
||||
INFO uint8 = 2
|
||||
|
||||
// WARNING level logging
|
||||
WARNING uint8 = 4
|
||||
|
||||
// ERROR level logging
|
||||
ERROR uint8 = 8
|
||||
|
||||
// FATAL level logging
|
||||
FATAL uint8 = 16
|
||||
|
||||
// BYPASS level logging - does not use a log level
|
||||
BYPASS uint8 = 255
|
||||
)
|
||||
|
||||
var mapLogLevel = map[uint8]string{
|
||||
TRACE: "TRACE | ",
|
||||
DEBUG: "DEBUG | ",
|
||||
INFO: "INFO | ",
|
||||
WARNING: "WARN | ",
|
||||
ERROR: "ERROR | ",
|
||||
FATAL: "FATAL | ",
|
||||
BYPASS: "",
|
||||
}
|
||||
202
v2/internal/logger/logger_test.go
Normal file
202
v2/internal/logger/logger_test.go
Normal file
@@ -0,0 +1,202 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/matryer/is"
|
||||
)
|
||||
|
||||
func TestByteBufferLogger(t *testing.T) {
|
||||
|
||||
is := is.New(t)
|
||||
|
||||
// Create new byte buffer logger
|
||||
var buf bytes.Buffer
|
||||
|
||||
myLogger := New(&buf)
|
||||
myLogger.SetLogLevel(TRACE)
|
||||
|
||||
tests := map[uint8]string{
|
||||
TRACE: "TRACE | I am a message!\n",
|
||||
DEBUG: "DEBUG | I am a message!\n",
|
||||
WARNING: "WARN | I am a message!\n",
|
||||
INFO: "INFO | I am a message!\n",
|
||||
ERROR: "ERROR | I am a message!\n",
|
||||
}
|
||||
|
||||
methods := map[uint8]func(string, ...interface{}) error{
|
||||
TRACE: myLogger.Trace,
|
||||
DEBUG: myLogger.Debug,
|
||||
WARNING: myLogger.Warning,
|
||||
INFO: myLogger.Info,
|
||||
ERROR: myLogger.Error,
|
||||
}
|
||||
|
||||
for level, expected := range tests {
|
||||
|
||||
buf.Reset()
|
||||
|
||||
method := methods[level]
|
||||
|
||||
// Write message
|
||||
err := method("I am a message!")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
actual := buf.String()
|
||||
|
||||
is.Equal(actual, expected)
|
||||
}
|
||||
|
||||
}
|
||||
func TestCustomLogger(t *testing.T) {
|
||||
|
||||
is := is.New(t)
|
||||
|
||||
// Create new byte buffer logger
|
||||
var buf bytes.Buffer
|
||||
|
||||
myLogger := New(&buf)
|
||||
myLogger.SetLogLevel(TRACE)
|
||||
customLogger := myLogger.CustomLogger("Test")
|
||||
|
||||
tests := map[uint8]string{
|
||||
TRACE: "TRACE | Test | I am a message!\n",
|
||||
DEBUG: "DEBUG | Test | I am a message!\n",
|
||||
WARNING: "WARN | Test | I am a message!\n",
|
||||
INFO: "INFO | Test | I am a message!\n",
|
||||
ERROR: "ERROR | Test | I am a message!\n",
|
||||
}
|
||||
|
||||
methods := map[uint8]func(string, ...interface{}) error{
|
||||
TRACE: customLogger.Trace,
|
||||
DEBUG: customLogger.Debug,
|
||||
WARNING: customLogger.Warning,
|
||||
INFO: customLogger.Info,
|
||||
ERROR: customLogger.Error,
|
||||
}
|
||||
|
||||
for level, expected := range tests {
|
||||
|
||||
buf.Reset()
|
||||
|
||||
method := methods[level]
|
||||
|
||||
// Write message
|
||||
err := method("I am a message!")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
actual := buf.String()
|
||||
|
||||
is.Equal(actual, expected)
|
||||
}
|
||||
|
||||
}
|
||||
func TestWriteln(t *testing.T) {
|
||||
|
||||
is := is.New(t)
|
||||
|
||||
// Create new byte buffer logger
|
||||
var buf bytes.Buffer
|
||||
|
||||
myLogger := New(&buf)
|
||||
myLogger.SetLogLevel(DEBUG)
|
||||
|
||||
buf.Reset()
|
||||
|
||||
// Write message
|
||||
err := myLogger.Writeln("I am a message!")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
actual := buf.String()
|
||||
|
||||
is.Equal(actual, "I am a message!\n")
|
||||
|
||||
buf.Reset()
|
||||
|
||||
// Write message
|
||||
err = myLogger.Write("I am a message!")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
actual = buf.String()
|
||||
|
||||
is.Equal(actual, "I am a message!")
|
||||
|
||||
}
|
||||
|
||||
func TestLogLevel(t *testing.T) {
|
||||
|
||||
is := is.New(t)
|
||||
|
||||
// Create new byte buffer logger
|
||||
var buf bytes.Buffer
|
||||
|
||||
myLogger := New(&buf)
|
||||
myLogger.SetLogLevel(ERROR)
|
||||
|
||||
tests := map[uint8]string{
|
||||
TRACE: "",
|
||||
DEBUG: "",
|
||||
WARNING: "",
|
||||
INFO: "",
|
||||
ERROR: "ERROR | I am a message!\n",
|
||||
}
|
||||
|
||||
methods := map[uint8]func(string, ...interface{}) error{
|
||||
TRACE: myLogger.Trace,
|
||||
DEBUG: myLogger.Debug,
|
||||
WARNING: myLogger.Warning,
|
||||
INFO: myLogger.Info,
|
||||
ERROR: myLogger.Error,
|
||||
}
|
||||
|
||||
for level := range tests {
|
||||
|
||||
method := methods[level]
|
||||
|
||||
// Write message
|
||||
err := method("I am a message!")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
}
|
||||
actual := buf.String()
|
||||
|
||||
is.Equal(actual, "ERROR | I am a message!\n")
|
||||
}
|
||||
|
||||
func TestFileLogger(t *testing.T) {
|
||||
|
||||
is := is.New(t)
|
||||
|
||||
// Create new byte buffer logger
|
||||
file, err := ioutil.TempFile(".", "wailsv2test")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer os.Remove(file.Name())
|
||||
|
||||
myLogger := New(file)
|
||||
myLogger.SetLogLevel(DEBUG)
|
||||
|
||||
// Write message
|
||||
err = myLogger.Info("I am a message!")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
actual, err := ioutil.ReadFile(file.Name())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
is.Equal(string(actual), "INFO | I am a message!\n")
|
||||
|
||||
}
|
||||
86
v2/internal/messagedispatcher/dispatchclient.go
Normal file
86
v2/internal/messagedispatcher/dispatchclient.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package messagedispatcher
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/wailsapp/wails/v2/internal/logger"
|
||||
"github.com/wailsapp/wails/v2/internal/messagedispatcher/message"
|
||||
"github.com/wailsapp/wails/v2/internal/servicebus"
|
||||
)
|
||||
|
||||
// Client defines what a frontend client can do
|
||||
type Client interface {
|
||||
Quit()
|
||||
NotifyEvent(message string)
|
||||
CallResult(message string)
|
||||
SaveFileDialog(title string, filter string) string
|
||||
OpenFileDialog(title string, filter string) string
|
||||
OpenDirectoryDialog(title string, filter string) string
|
||||
WindowSetTitle(title string)
|
||||
WindowShow()
|
||||
WindowHide()
|
||||
WindowCenter()
|
||||
WindowMaximise()
|
||||
WindowUnmaximise()
|
||||
WindowMinimise()
|
||||
WindowUnminimise()
|
||||
WindowPosition(x int, y int)
|
||||
WindowSize(width int, height int)
|
||||
WindowFullscreen()
|
||||
WindowUnFullscreen()
|
||||
WindowSetColour(colour string) bool
|
||||
}
|
||||
|
||||
// DispatchClient is what the frontends use to interface with the
|
||||
// dispatcher
|
||||
type DispatchClient struct {
|
||||
id string
|
||||
logger logger.CustomLogger
|
||||
|
||||
bus *servicebus.ServiceBus
|
||||
|
||||
// Client
|
||||
frontend Client
|
||||
}
|
||||
|
||||
func newDispatchClient(id string, frontend Client, logger logger.CustomLogger, bus *servicebus.ServiceBus) *DispatchClient {
|
||||
|
||||
return &DispatchClient{
|
||||
id: id,
|
||||
frontend: frontend,
|
||||
logger: logger,
|
||||
bus: bus,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// DispatchMessage is called by the front ends. It is passed
|
||||
// an IPC message, translates it to a more concrete message
|
||||
// type then publishes it on the service bus.
|
||||
func (d *DispatchClient) DispatchMessage(incomingMessage string) {
|
||||
|
||||
// Parse the message
|
||||
d.logger.Trace(fmt.Sprintf("Received message: %+v", incomingMessage))
|
||||
parsedMessage, err := message.Parse(incomingMessage)
|
||||
if err != nil {
|
||||
d.logger.Trace("Error: " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Save this client id
|
||||
parsedMessage.ClientID = d.id
|
||||
|
||||
d.logger.Trace("I got a parsedMessage: %+v", parsedMessage)
|
||||
|
||||
// Check error
|
||||
if err != nil {
|
||||
d.logger.Trace("Error: " + err.Error())
|
||||
// Hrm... what do we do with this?
|
||||
d.bus.PublishForTarget("generic:message", incomingMessage, d.id)
|
||||
return
|
||||
}
|
||||
|
||||
// Publish the parsed message
|
||||
d.bus.PublishForTarget(parsedMessage.Topic, parsedMessage.Data, d.id)
|
||||
|
||||
}
|
||||
37
v2/internal/messagedispatcher/message/call.go
Normal file
37
v2/internal/messagedispatcher/message/call.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package message
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type CallMessage struct {
|
||||
Name string `json:"name"`
|
||||
Args []interface{} `json:"args"`
|
||||
CallbackID string `json:"callbackID,omitempty"`
|
||||
}
|
||||
|
||||
// callMessageParser does what it says on the tin!
|
||||
func callMessageParser(message string) (*parsedMessage, error) {
|
||||
|
||||
// Sanity check: Call messages must be at least 3 bytes `C{}``
|
||||
if len(message) < 3 {
|
||||
return nil, fmt.Errorf("call message was an invalid length")
|
||||
}
|
||||
|
||||
callMessage := new(CallMessage)
|
||||
|
||||
m := message[1:]
|
||||
err := json.Unmarshal([]byte(m), callMessage)
|
||||
if err != nil {
|
||||
println(err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
topic := "call:invoke"
|
||||
|
||||
// Create a new parsed message struct
|
||||
parsedMessage := &parsedMessage{Topic: topic, Data: callMessage}
|
||||
|
||||
return parsedMessage, nil
|
||||
}
|
||||
47
v2/internal/messagedispatcher/message/event.go
Normal file
47
v2/internal/messagedispatcher/message/event.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package message
|
||||
|
||||
import "fmt"
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
type EventMessage struct {
|
||||
Name string `json:"name"`
|
||||
Data []interface{} `json:"data"`
|
||||
}
|
||||
|
||||
type OnEventMessage struct {
|
||||
Name string
|
||||
Callback func(optionalData ...interface{})
|
||||
}
|
||||
|
||||
// eventMessageParser does what it says on the tin!
|
||||
func eventMessageParser(message string) (*parsedMessage, error) {
|
||||
|
||||
// Sanity check: Event messages must be at least 2 bytes
|
||||
if len(message) < 3 {
|
||||
return nil, fmt.Errorf("event message was an invalid length")
|
||||
}
|
||||
|
||||
eventMessage := new(EventMessage)
|
||||
direction := message[1]
|
||||
|
||||
// Switch the event type (with or without data)
|
||||
switch message[0] {
|
||||
case 'e':
|
||||
eventMessage.Name = message[2:]
|
||||
case 'E':
|
||||
m := message[2:]
|
||||
err := json.Unmarshal([]byte(m), eventMessage)
|
||||
if err != nil {
|
||||
println(err.Error())
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
topic := "event:emit:from:" + string(direction)
|
||||
|
||||
// Create a new parsed message struct
|
||||
parsedMessage := &parsedMessage{Topic: topic, Data: eventMessage}
|
||||
|
||||
return parsedMessage, nil
|
||||
}
|
||||
33
v2/internal/messagedispatcher/message/log.go
Normal file
33
v2/internal/messagedispatcher/message/log.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package message
|
||||
|
||||
import "fmt"
|
||||
|
||||
var logMessageMap = map[byte]string{
|
||||
'D': "log:debug",
|
||||
'I': "log:info",
|
||||
'W': "log:warning",
|
||||
'E': "log:error",
|
||||
'F': "log:fatal",
|
||||
}
|
||||
|
||||
// logMessageParser does what it says on the tin!
|
||||
func logMessageParser(message string) (*parsedMessage, error) {
|
||||
|
||||
// Sanity check: Log messages must be at least 2 bytes
|
||||
if len(message) < 2 {
|
||||
return nil, fmt.Errorf("log message was an invalid length")
|
||||
}
|
||||
|
||||
// Switch on the log type
|
||||
messageTopic := logMessageMap[message[1]]
|
||||
|
||||
// If the type is invalid, raise error
|
||||
if messageTopic == "" {
|
||||
return nil, fmt.Errorf("log message type '%b' invalid", message[1])
|
||||
}
|
||||
|
||||
// Create a new parsed message struct
|
||||
parsedMessage := &parsedMessage{Topic: messageTopic, Data: message[2:]}
|
||||
|
||||
return parsedMessage, nil
|
||||
}
|
||||
31
v2/internal/messagedispatcher/message/messageparser.go
Normal file
31
v2/internal/messagedispatcher/message/messageparser.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package message
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Parse
|
||||
type parsedMessage struct {
|
||||
Topic string
|
||||
ClientID string
|
||||
Data interface{}
|
||||
}
|
||||
|
||||
// Map of different message parsers based on the header byte of the message
|
||||
var messageParsers = map[byte]func(string) (*parsedMessage, error){
|
||||
'L': logMessageParser,
|
||||
'R': runtimeMessageParser,
|
||||
'E': eventMessageParser,
|
||||
'e': eventMessageParser,
|
||||
'C': callMessageParser,
|
||||
'W': windowMessageParser,
|
||||
}
|
||||
|
||||
// Parse will attempt to parse the given message
|
||||
func Parse(message string) (*parsedMessage, error) {
|
||||
|
||||
parseMethod := messageParsers[message[0]]
|
||||
if parseMethod == nil {
|
||||
return nil, fmt.Errorf("message type '%b' invalid", message[0])
|
||||
}
|
||||
|
||||
return parseMethod(message)
|
||||
}
|
||||
36
v2/internal/messagedispatcher/message/runtime.go
Normal file
36
v2/internal/messagedispatcher/message/runtime.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package message
|
||||
|
||||
import "fmt"
|
||||
|
||||
// runtimeMessageParser does what it says on the tin!
|
||||
func runtimeMessageParser(message string) (*parsedMessage, error) {
|
||||
|
||||
// Sanity check: Log messages must be at least 2 bytes
|
||||
if len(message) < 3 {
|
||||
return nil, fmt.Errorf("runtime message was an invalid length")
|
||||
}
|
||||
|
||||
// Switch on the runtime module type
|
||||
module := message[1]
|
||||
switch module {
|
||||
case 'B':
|
||||
return processBrowserMessage(message)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unknown message: %s", message)
|
||||
}
|
||||
|
||||
// processBrowserMessage expects messages of the following format:
|
||||
// RB<METHOD><DATA>
|
||||
func processBrowserMessage(message string) (*parsedMessage, error) {
|
||||
method := message[2]
|
||||
switch method {
|
||||
case 'U':
|
||||
// Open URL
|
||||
url := message[3:]
|
||||
return &parsedMessage{Topic: "runtime:browser:openurl", Data: url}, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unknown browser message: %s", message)
|
||||
|
||||
}
|
||||
76
v2/internal/messagedispatcher/message/window.go
Normal file
76
v2/internal/messagedispatcher/message/window.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package message
|
||||
|
||||
import "fmt"
|
||||
|
||||
// windowMessageParser does what it says on the tin!
|
||||
func windowMessageParser(message string) (*parsedMessage, error) {
|
||||
|
||||
// Sanity check: Window messages must be at least 2 bytes
|
||||
if len(message) < 2 {
|
||||
return nil, fmt.Errorf("window message was an invalid length")
|
||||
}
|
||||
|
||||
// Extract event type
|
||||
windowEvent := message[1]
|
||||
parsedMessage := &parsedMessage{}
|
||||
|
||||
// Switch the windowEvent type
|
||||
switch windowEvent {
|
||||
|
||||
// Closed window
|
||||
case 'C':
|
||||
parsedMessage.Topic = "quit"
|
||||
parsedMessage.Data = "Window Closed"
|
||||
|
||||
// Center window
|
||||
case 'c':
|
||||
parsedMessage.Topic = "window:center"
|
||||
parsedMessage.Data = ""
|
||||
|
||||
// Hide window
|
||||
case 'H':
|
||||
parsedMessage.Topic = "window:hide"
|
||||
parsedMessage.Data = ""
|
||||
|
||||
// Show window
|
||||
case 'S':
|
||||
parsedMessage.Topic = "window:show"
|
||||
parsedMessage.Data = ""
|
||||
|
||||
// Position window
|
||||
case 'p':
|
||||
parsedMessage.Topic = "window:position:" + message[3:]
|
||||
parsedMessage.Data = ""
|
||||
|
||||
// Set window size
|
||||
case 's':
|
||||
parsedMessage.Topic = "window:size:" + message[3:]
|
||||
parsedMessage.Data = ""
|
||||
|
||||
// Maximise window
|
||||
case 'M':
|
||||
parsedMessage.Topic = "window:maximise"
|
||||
parsedMessage.Data = ""
|
||||
|
||||
// Unmaximise window
|
||||
case 'U':
|
||||
parsedMessage.Topic = "window:unmaximise"
|
||||
parsedMessage.Data = ""
|
||||
|
||||
// Minimise window
|
||||
case 'm':
|
||||
parsedMessage.Topic = "window:minimise"
|
||||
parsedMessage.Data = ""
|
||||
|
||||
// Unminimise window
|
||||
case 'u':
|
||||
parsedMessage.Topic = "window:unminimise"
|
||||
parsedMessage.Data = ""
|
||||
|
||||
// Unknown event type
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown message: %s", message)
|
||||
}
|
||||
|
||||
return parsedMessage, nil
|
||||
}
|
||||
391
v2/internal/messagedispatcher/messagedispatcher.go
Normal file
391
v2/internal/messagedispatcher/messagedispatcher.go
Normal file
@@ -0,0 +1,391 @@
|
||||
package messagedispatcher
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/wailsapp/wails/v2/internal/crypto"
|
||||
"github.com/wailsapp/wails/v2/internal/logger"
|
||||
"github.com/wailsapp/wails/v2/internal/messagedispatcher/message"
|
||||
"github.com/wailsapp/wails/v2/internal/servicebus"
|
||||
)
|
||||
|
||||
// Dispatcher translates messages received from the frontend
|
||||
// and publishes them onto the service bus
|
||||
type Dispatcher struct {
|
||||
quitChannel <-chan *servicebus.Message
|
||||
resultChannel <-chan *servicebus.Message
|
||||
eventChannel <-chan *servicebus.Message
|
||||
windowChannel <-chan *servicebus.Message
|
||||
dialogChannel <-chan *servicebus.Message
|
||||
running bool
|
||||
|
||||
servicebus *servicebus.ServiceBus
|
||||
logger logger.CustomLogger
|
||||
|
||||
// Clients
|
||||
clients map[string]*DispatchClient
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
// New dispatcher. Needs a service bus to send to.
|
||||
func New(servicebus *servicebus.ServiceBus, logger *logger.Logger) (*Dispatcher, error) {
|
||||
// Subscribe to call result messages
|
||||
resultChannel, err := servicebus.Subscribe("call:result")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Subscribe to event messages
|
||||
eventChannel, err := servicebus.Subscribe("event:emit")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Subscribe to quit messages
|
||||
quitChannel, err := servicebus.Subscribe("quit")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Subscribe to window messages
|
||||
windowChannel, err := servicebus.Subscribe("window")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Subscribe to dialog events
|
||||
dialogChannel, err := servicebus.Subscribe("dialog:select")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &Dispatcher{
|
||||
servicebus: servicebus,
|
||||
eventChannel: eventChannel,
|
||||
logger: logger.CustomLogger("Message Dispatcher"),
|
||||
clients: make(map[string]*DispatchClient),
|
||||
resultChannel: resultChannel,
|
||||
quitChannel: quitChannel,
|
||||
windowChannel: windowChannel,
|
||||
dialogChannel: dialogChannel,
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Start the subsystem
|
||||
func (d *Dispatcher) Start() error {
|
||||
|
||||
d.logger.Trace("Starting")
|
||||
|
||||
d.running = true
|
||||
|
||||
// Spin off a go routine
|
||||
go func() {
|
||||
for d.running {
|
||||
select {
|
||||
case <-d.quitChannel:
|
||||
d.processQuit()
|
||||
d.running = false
|
||||
case resultMessage := <-d.resultChannel:
|
||||
d.processCallResult(resultMessage)
|
||||
case eventMessage := <-d.eventChannel:
|
||||
d.processEvent(eventMessage)
|
||||
case windowMessage := <-d.windowChannel:
|
||||
d.processWindowMessage(windowMessage)
|
||||
case dialogMessage := <-d.dialogChannel:
|
||||
d.processDialogMessage(dialogMessage)
|
||||
}
|
||||
}
|
||||
|
||||
// Call shutdown
|
||||
d.shutdown()
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Dispatcher) processQuit() {
|
||||
d.lock.RLock()
|
||||
defer d.lock.RUnlock()
|
||||
for _, client := range d.clients {
|
||||
client.frontend.Quit()
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Dispatcher) shutdown() {
|
||||
d.logger.Trace("Shutdown")
|
||||
}
|
||||
|
||||
// RegisterClient will register the given callback with the dispatcher
|
||||
// and return a DispatchClient that the caller can use to send messages
|
||||
func (d *Dispatcher) RegisterClient(client Client) *DispatchClient {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
|
||||
// Create ID
|
||||
id := d.getUniqueID()
|
||||
d.clients[id] = newDispatchClient(id, client, d.logger, d.servicebus)
|
||||
|
||||
return d.clients[id]
|
||||
}
|
||||
|
||||
// RemoveClient will remove the registered client
|
||||
func (d *Dispatcher) RemoveClient(dc *DispatchClient) {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
delete(d.clients, dc.id)
|
||||
}
|
||||
|
||||
func (d *Dispatcher) getUniqueID() string {
|
||||
var uid string
|
||||
for {
|
||||
uid = crypto.RandomID()
|
||||
|
||||
if d.clients[uid] == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return uid
|
||||
}
|
||||
|
||||
func (d *Dispatcher) processCallResult(result *servicebus.Message) {
|
||||
target := result.Target()
|
||||
|
||||
if target == "" {
|
||||
// This is an error. Calls are 1:1!
|
||||
d.logger.Fatal("No target for call result: %+v", result)
|
||||
}
|
||||
|
||||
d.lock.RLock()
|
||||
client := d.clients[target]
|
||||
d.lock.RUnlock()
|
||||
if client == nil {
|
||||
// This is fatal - unknown target!
|
||||
d.logger.Fatal("Unknown target for call result: %+v", result)
|
||||
}
|
||||
|
||||
d.logger.Trace("Sending message to client %s: R%s", target, result.Data().(string))
|
||||
client.frontend.CallResult(result.Data().(string))
|
||||
}
|
||||
|
||||
// processEvent will
|
||||
func (d *Dispatcher) processEvent(result *servicebus.Message) {
|
||||
|
||||
d.logger.Trace("Got event in message dispatcher: %+v", result)
|
||||
|
||||
splitTopic := strings.Split(result.Topic(), ":")
|
||||
eventType := splitTopic[1]
|
||||
switch eventType {
|
||||
case "emit":
|
||||
eventFrom := splitTopic[3]
|
||||
if eventFrom == "g" {
|
||||
// This was sent from Go - notify frontend
|
||||
eventData := result.Data().(*message.EventMessage)
|
||||
// Unpack event
|
||||
payload, err := json.Marshal(eventData)
|
||||
if err != nil {
|
||||
d.logger.Error("Unable to marshal eventData: %s", err.Error())
|
||||
return
|
||||
}
|
||||
d.lock.RLock()
|
||||
for _, client := range d.clients {
|
||||
client.frontend.NotifyEvent(string(payload))
|
||||
}
|
||||
d.lock.RUnlock()
|
||||
}
|
||||
default:
|
||||
d.logger.Error("Unknown event type: %s", eventType)
|
||||
}
|
||||
}
|
||||
|
||||
// processWindowMessage processes messages intended for the window
|
||||
func (d *Dispatcher) processWindowMessage(result *servicebus.Message) {
|
||||
d.lock.RLock()
|
||||
defer d.lock.RUnlock()
|
||||
splitTopic := strings.Split(result.Topic(), ":")
|
||||
command := splitTopic[1]
|
||||
switch command {
|
||||
case "settitle":
|
||||
title, ok := result.Data().(string)
|
||||
if !ok {
|
||||
d.logger.Error("Invalid title for 'window:settitle' : %#v", result.Data())
|
||||
return
|
||||
}
|
||||
// Notify clients
|
||||
for _, client := range d.clients {
|
||||
client.frontend.WindowSetTitle(title)
|
||||
}
|
||||
case "fullscreen":
|
||||
// Notify clients
|
||||
for _, client := range d.clients {
|
||||
client.frontend.WindowFullscreen()
|
||||
}
|
||||
case "unfullscreen":
|
||||
// Notify clients
|
||||
for _, client := range d.clients {
|
||||
client.frontend.WindowUnFullscreen()
|
||||
}
|
||||
case "setcolour":
|
||||
colour, ok := result.Data().(string)
|
||||
if !ok {
|
||||
d.logger.Error("Invalid colour for 'window:setcolour' : %#v", result.Data())
|
||||
return
|
||||
}
|
||||
// Notify clients
|
||||
for _, client := range d.clients {
|
||||
client.frontend.WindowSetColour(colour)
|
||||
}
|
||||
case "show":
|
||||
// Notify clients
|
||||
for _, client := range d.clients {
|
||||
client.frontend.WindowShow()
|
||||
}
|
||||
case "hide":
|
||||
// Notify clients
|
||||
for _, client := range d.clients {
|
||||
client.frontend.WindowHide()
|
||||
}
|
||||
case "center":
|
||||
// Notify clients
|
||||
for _, client := range d.clients {
|
||||
client.frontend.WindowCenter()
|
||||
}
|
||||
case "maximise":
|
||||
// Notify clients
|
||||
for _, client := range d.clients {
|
||||
client.frontend.WindowMaximise()
|
||||
}
|
||||
case "unmaximise":
|
||||
// Notify clients
|
||||
for _, client := range d.clients {
|
||||
client.frontend.WindowUnmaximise()
|
||||
}
|
||||
case "minimise":
|
||||
// Notify clients
|
||||
for _, client := range d.clients {
|
||||
client.frontend.WindowMinimise()
|
||||
}
|
||||
case "unminimise":
|
||||
// Notify clients
|
||||
for _, client := range d.clients {
|
||||
client.frontend.WindowUnminimise()
|
||||
}
|
||||
case "position":
|
||||
// We need 2 arguments
|
||||
if len(splitTopic) != 4 {
|
||||
d.logger.Error("Invalid number of parameters for 'window:position' : %#v", result.Data())
|
||||
return
|
||||
}
|
||||
x, err1 := strconv.Atoi(splitTopic[2])
|
||||
y, err2 := strconv.Atoi(splitTopic[3])
|
||||
if err1 != nil || err2 != nil {
|
||||
d.logger.Error("Invalid integer parameters for 'window:position' : %#v", result.Data())
|
||||
return
|
||||
}
|
||||
// Notify clients
|
||||
for _, client := range d.clients {
|
||||
client.frontend.WindowPosition(x, y)
|
||||
}
|
||||
case "size":
|
||||
// We need 2 arguments
|
||||
if len(splitTopic) != 4 {
|
||||
d.logger.Error("Invalid number of parameters for 'window:size' : %#v", result.Data())
|
||||
return
|
||||
}
|
||||
w, err1 := strconv.Atoi(splitTopic[2])
|
||||
h, err2 := strconv.Atoi(splitTopic[3])
|
||||
if err1 != nil || err2 != nil {
|
||||
d.logger.Error("Invalid integer parameters for 'window:size' : %#v", result.Data())
|
||||
return
|
||||
}
|
||||
// Notifh clients
|
||||
for _, client := range d.clients {
|
||||
client.frontend.WindowSize(w, h)
|
||||
}
|
||||
default:
|
||||
d.logger.Error("Unknown window command: %s", command)
|
||||
}
|
||||
d.logger.Trace("Got window in message dispatcher: %+v", result)
|
||||
|
||||
}
|
||||
|
||||
// processDialogMessage processes dialog messages
|
||||
func (d *Dispatcher) processDialogMessage(result *servicebus.Message) {
|
||||
splitTopic := strings.Split(result.Topic(), ":")
|
||||
|
||||
if len(splitTopic) < 4 {
|
||||
d.logger.Error("Invalid dialog message : %#v", result.Data())
|
||||
return
|
||||
}
|
||||
|
||||
command := splitTopic[1]
|
||||
switch command {
|
||||
case "select":
|
||||
dialogType := splitTopic[2]
|
||||
title := splitTopic[3]
|
||||
filter := ""
|
||||
if len(splitTopic) > 4 {
|
||||
filter = splitTopic[4]
|
||||
}
|
||||
switch dialogType {
|
||||
case "file":
|
||||
responseTopic, ok := result.Data().(string)
|
||||
if !ok {
|
||||
d.logger.Error("Invalid responseTopic for 'dialog:select:file' : %#v", result.Data())
|
||||
return
|
||||
}
|
||||
d.logger.Info("Opening File dialog! responseTopic = %s", responseTopic)
|
||||
|
||||
// TODO: Work out what we mean in a multi window environment...
|
||||
// For now we will just pick the first one
|
||||
var result string
|
||||
for _, client := range d.clients {
|
||||
result = client.frontend.OpenFileDialog(title, filter)
|
||||
}
|
||||
|
||||
// Send dummy response
|
||||
d.servicebus.Publish(responseTopic, result)
|
||||
case "filesave":
|
||||
responseTopic, ok := result.Data().(string)
|
||||
if !ok {
|
||||
d.logger.Error("Invalid responseTopic for 'dialog:select:filesave' : %#v", result.Data())
|
||||
return
|
||||
}
|
||||
d.logger.Info("Opening Save File dialog! responseTopic = %s", responseTopic)
|
||||
|
||||
// TODO: Work out what we mean in a multi window environment...
|
||||
// For now we will just pick the first one
|
||||
var result string
|
||||
for _, client := range d.clients {
|
||||
result = client.frontend.SaveFileDialog(title, filter)
|
||||
}
|
||||
|
||||
// Send dummy response
|
||||
d.servicebus.Publish(responseTopic, result)
|
||||
case "directory":
|
||||
responseTopic, ok := result.Data().(string)
|
||||
if !ok {
|
||||
d.logger.Error("Invalid responseTopic for 'dialog:select:directory' : %#v", result.Data())
|
||||
return
|
||||
}
|
||||
d.logger.Info("Opening Directory dialog! responseTopic = %s", responseTopic)
|
||||
|
||||
// TODO: Work out what we mean in a multi window environment...
|
||||
// For now we will just pick the first one
|
||||
var result string
|
||||
for _, client := range d.clients {
|
||||
result = client.frontend.OpenDirectoryDialog(title, filter)
|
||||
}
|
||||
// Send dummy response
|
||||
d.servicebus.Publish(responseTopic, result)
|
||||
|
||||
default:
|
||||
d.logger.Error("Unknown dialog command: %s", command)
|
||||
}
|
||||
}
|
||||
}
|
||||
10
v2/internal/parse/README.md
Normal file
10
v2/internal/parse/README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Parse
|
||||
|
||||
Parse will attempt to parse your Wails project to perform a number of tasks:
|
||||
* Verify that you have bound struct pointers
|
||||
* Generate JS helper files/docs
|
||||
|
||||
It currently checks bindings correctly if your code binds using one of the following methods:
|
||||
* Literal Binding: `app.Bind(&MyStruct{})`
|
||||
* Variable Binding: `app.Bind(m)` - m can be `m := &MyStruct{}` or `m := newMyStruct()`
|
||||
* Function Binding: `app.Bind(newMyStruct())`
|
||||
441
v2/internal/parse/parse.go
Normal file
441
v2/internal/parse/parse.go
Normal file
@@ -0,0 +1,441 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/leaanthony/slicer"
|
||||
"golang.org/x/tools/go/packages"
|
||||
)
|
||||
|
||||
var internalMethods = slicer.String([]string{"WailsInit", "Wails Shutdown"})
|
||||
|
||||
var structCache = make(map[string]*ParsedStruct)
|
||||
var boundStructs = make(map[string]*ParsedStruct)
|
||||
var boundMethods = []string{}
|
||||
var boundStructPointerLiterals = []string{}
|
||||
var boundStructLiterals = slicer.StringSlicer{}
|
||||
var boundVariables = slicer.StringSlicer{}
|
||||
var app = ""
|
||||
var structPointerFunctionDecls = make(map[string]string)
|
||||
var structFunctionDecls = make(map[string]string)
|
||||
var variableStructDecls = make(map[string]string)
|
||||
var variableFunctionDecls = make(map[string]string)
|
||||
|
||||
type Parameter struct {
|
||||
Name string
|
||||
Type string
|
||||
}
|
||||
|
||||
type ParsedMethod struct {
|
||||
Struct string
|
||||
Name string
|
||||
Comments []string
|
||||
Inputs []*Parameter
|
||||
Returns []*Parameter
|
||||
}
|
||||
|
||||
type ParsedStruct struct {
|
||||
Name string
|
||||
Methods []*ParsedMethod
|
||||
}
|
||||
|
||||
type BoundStructs []*ParsedStruct
|
||||
|
||||
func ParseProject(projectPath string) (BoundStructs, error) {
|
||||
|
||||
cfg := &packages.Config{Mode: packages.NeedFiles | packages.NeedSyntax | packages.NeedTypesInfo}
|
||||
pkgs, err := packages.Load(cfg, projectPath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "load: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if packages.PrintErrors(pkgs) > 0 {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Iterate the packages
|
||||
for _, pkg := range pkgs {
|
||||
|
||||
// Iterate the files
|
||||
for _, file := range pkg.Syntax {
|
||||
|
||||
var wailsPkgVar = ""
|
||||
|
||||
ast.Inspect(file, func(n ast.Node) bool {
|
||||
var s string
|
||||
switch x := n.(type) {
|
||||
// Parse import declarations
|
||||
case *ast.ImportSpec:
|
||||
// Determine what wails has been imported as
|
||||
if x.Path.Value == `"github.com/wailsapp/wails/v2"` {
|
||||
wailsPkgVar = x.Name.Name
|
||||
}
|
||||
// Parse calls. We are looking for app.Bind() calls
|
||||
case *ast.CallExpr:
|
||||
f, ok := x.Fun.(*ast.SelectorExpr)
|
||||
if ok {
|
||||
n, ok := f.X.(*ast.Ident)
|
||||
if ok {
|
||||
//Check this is the Bind() call associated with the app variable
|
||||
if n.Name == app && f.Sel.Name == "Bind" {
|
||||
if len(x.Args) == 1 {
|
||||
ce, ok := x.Args[0].(*ast.CallExpr)
|
||||
if ok {
|
||||
n, ok := ce.Fun.(*ast.Ident)
|
||||
if ok {
|
||||
// We found a bind method using a function call
|
||||
// EG: app.Bind( newMyStruct() )
|
||||
boundMethods = append(boundMethods, n.Name)
|
||||
}
|
||||
} else {
|
||||
// We also want to check for Bind( &MyStruct{} )
|
||||
ue, ok := x.Args[0].(*ast.UnaryExpr)
|
||||
if ok {
|
||||
if ue.Op.String() == "&" {
|
||||
cl, ok := ue.X.(*ast.CompositeLit)
|
||||
if ok {
|
||||
t, ok := cl.Type.(*ast.Ident)
|
||||
if ok {
|
||||
// We have found Bind( &MyStruct{} )
|
||||
boundStructPointerLiterals = append(boundStructPointerLiterals, t.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Let's check when the user binds a struct,
|
||||
// rather than a struct pointer: Bind( MyStruct{} )
|
||||
// We do this to provide better hints to the user
|
||||
cl, ok := x.Args[0].(*ast.CompositeLit)
|
||||
if ok {
|
||||
t, ok := cl.Type.(*ast.Ident)
|
||||
if ok {
|
||||
boundStructLiterals.Add(t.Name)
|
||||
}
|
||||
} else {
|
||||
// Also check for when we bind a variable
|
||||
// myVariable := &MyStruct{}
|
||||
// app.Bind( myVariable )
|
||||
i, ok := x.Args[0].(*ast.Ident)
|
||||
if ok {
|
||||
boundVariables.Add(i.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We scan assignments for a number of reasons:
|
||||
// * Determine the variable containing the main application
|
||||
// * Determine the type of variables that get used in Bind()
|
||||
// * Determine the type of variables that get created with var := &MyStruct{}
|
||||
case *ast.AssignStmt:
|
||||
for _, rhs := range x.Rhs {
|
||||
ce, ok := rhs.(*ast.CallExpr)
|
||||
if ok {
|
||||
se, ok := ce.Fun.(*ast.SelectorExpr)
|
||||
if ok {
|
||||
i, ok := se.X.(*ast.Ident)
|
||||
if ok {
|
||||
// Have we found the wails package name?
|
||||
if i.Name == wailsPkgVar {
|
||||
// Check we are calling a function to create the app
|
||||
if se.Sel.Name == "CreateApp" || se.Sel.Name == "CreateAppWithOptions" {
|
||||
if len(x.Lhs) == 1 {
|
||||
i, ok := x.Lhs[0].(*ast.Ident)
|
||||
if ok {
|
||||
// Found the app variable name
|
||||
app = i.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Check for function assignment
|
||||
// a := newMyStruct()
|
||||
fe, ok := ce.Fun.(*ast.Ident)
|
||||
if ok {
|
||||
if len(x.Lhs) == 1 {
|
||||
i, ok := x.Lhs[0].(*ast.Ident)
|
||||
if ok {
|
||||
// Store the variable -> Function mapping
|
||||
// so we can later resolve the type
|
||||
variableFunctionDecls[i.Name] = fe.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Check for literal assignment of struct
|
||||
// EG: myvar := MyStruct{}
|
||||
ue, ok := rhs.(*ast.UnaryExpr)
|
||||
if ok {
|
||||
cl, ok := ue.X.(*ast.CompositeLit)
|
||||
if ok {
|
||||
t, ok := cl.Type.(*ast.Ident)
|
||||
if ok {
|
||||
if len(x.Lhs) == 1 {
|
||||
i, ok := x.Lhs[0].(*ast.Ident)
|
||||
if ok {
|
||||
variableStructDecls[i.Name] = t.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// We scan for functions to build up a list of function names
|
||||
// for a number of reasons:
|
||||
// * Determine which functions are struct methods that are bound
|
||||
// * Determine
|
||||
case *ast.FuncDecl:
|
||||
if x.Recv != nil {
|
||||
// This is a struct method
|
||||
for _, field := range x.Recv.List {
|
||||
se, ok := field.Type.(*ast.StarExpr)
|
||||
if ok {
|
||||
// This is a struct pointer method
|
||||
i, ok := se.X.(*ast.Ident)
|
||||
if ok {
|
||||
// We want to ignore Internal functions
|
||||
if internalMethods.Contains(x.Name.Name) {
|
||||
continue
|
||||
}
|
||||
// If we haven't already found this struct,
|
||||
// Create a placeholder in the cache
|
||||
parsedStruct := structCache[i.Name]
|
||||
if parsedStruct == nil {
|
||||
structCache[i.Name] = &ParsedStruct{
|
||||
Name: i.Name,
|
||||
}
|
||||
parsedStruct = structCache[i.Name]
|
||||
}
|
||||
|
||||
// If this method is Public
|
||||
if string(x.Name.Name[0]) == strings.ToUpper((string(x.Name.Name[0]))) {
|
||||
structMethod := &ParsedMethod{
|
||||
Struct: i.Name,
|
||||
Name: x.Name.Name,
|
||||
}
|
||||
// Check if the method has comments.
|
||||
// If so, save it with the parsed method
|
||||
if x.Doc != nil {
|
||||
for _, comment := range x.Doc.List {
|
||||
stringComment := comment.Text
|
||||
if strings.HasPrefix(stringComment, "//") {
|
||||
stringComment = stringComment[2:]
|
||||
}
|
||||
structMethod.Comments = append(structMethod.Comments, strings.TrimSpace(stringComment))
|
||||
}
|
||||
}
|
||||
|
||||
// Save the input parameters
|
||||
for _, inputField := range x.Type.Params.List {
|
||||
t, ok := inputField.Type.(*ast.Ident)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, name := range inputField.Names {
|
||||
structMethod.Inputs = append(structMethod.Inputs, &Parameter{Name: name.Name, Type: t.Name})
|
||||
}
|
||||
}
|
||||
|
||||
// Save the output parameters
|
||||
for _, outputField := range x.Type.Results.List {
|
||||
t, ok := outputField.Type.(*ast.Ident)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if len(outputField.Names) == 0 {
|
||||
structMethod.Returns = append(structMethod.Returns, &Parameter{Type: t.Name})
|
||||
} else {
|
||||
for _, name := range outputField.Names {
|
||||
structMethod.Returns = append(structMethod.Returns, &Parameter{Name: name.Name, Type: t.Name})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Append this method to the parsed struct
|
||||
parsedStruct.Methods = append(parsedStruct.Methods, structMethod)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// This is a function declaration
|
||||
// We care about its name and return type
|
||||
// This will allow us to resolve types later
|
||||
functionName := x.Name.Name
|
||||
|
||||
// Look for one that returns a single value
|
||||
if x.Type != nil && x.Type.Results != nil && x.Type.Results.List != nil {
|
||||
if len(x.Type.Results.List) == 1 {
|
||||
// Check for *struct
|
||||
t, ok := x.Type.Results.List[0].Type.(*ast.StarExpr)
|
||||
if ok {
|
||||
s, ok := t.X.(*ast.Ident)
|
||||
if ok {
|
||||
// println("*** Function", functionName, "found which returns: *"+s.Name)
|
||||
structPointerFunctionDecls[functionName] = s.Name
|
||||
}
|
||||
} else {
|
||||
// Check for functions that return a struct
|
||||
// This is to help us provide hints if the user binds a struct
|
||||
t, ok := x.Type.Results.List[0].Type.(*ast.Ident)
|
||||
if ok {
|
||||
// println("*** Function", functionName, "found which returns: "+t.Name)
|
||||
structFunctionDecls[functionName] = t.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
// spew.Dump(file)
|
||||
}
|
||||
}
|
||||
|
||||
/***** Update bound structs ******/
|
||||
|
||||
// Resolve bound Methods
|
||||
for _, method := range boundMethods {
|
||||
s, ok := structPointerFunctionDecls[method]
|
||||
if !ok {
|
||||
s, ok = structFunctionDecls[method]
|
||||
if !ok {
|
||||
println("Fatal: Bind statement using", method, "but cannot find", method, "declaration")
|
||||
} else {
|
||||
println("Fatal: Cannot bind struct using method `" + method + "` because it returns a struct (" + s + "). Return a pointer to " + s + " instead.")
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
structDefinition := structCache[s]
|
||||
if structDefinition == nil {
|
||||
println("Fatal: Bind statement using `"+method+"` but cannot find struct", s, "definition")
|
||||
os.Exit(1)
|
||||
}
|
||||
boundStructs[s] = structDefinition
|
||||
}
|
||||
|
||||
// Resolve bound vars
|
||||
for _, structLiteral := range boundStructPointerLiterals {
|
||||
s, ok := structCache[structLiteral]
|
||||
if !ok {
|
||||
println("Fatal: Bind statement using", structLiteral, "but cannot find", structLiteral, "declaration")
|
||||
os.Exit(1)
|
||||
}
|
||||
boundStructs[structLiteral] = s
|
||||
}
|
||||
|
||||
// Resolve bound variables
|
||||
boundVariables.Each(func(variable string) {
|
||||
v, ok := variableStructDecls[variable]
|
||||
if !ok {
|
||||
method, ok := variableFunctionDecls[variable]
|
||||
if !ok {
|
||||
println("Fatal: Bind statement using variable `" + variable + "` which does not resolve to a struct pointer")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Resolve function name
|
||||
v, ok = structPointerFunctionDecls[method]
|
||||
if !ok {
|
||||
v, ok = structFunctionDecls[method]
|
||||
if !ok {
|
||||
println("Fatal: Bind statement using", method, "but cannot find", method, "declaration")
|
||||
} else {
|
||||
println("Fatal: Cannot bind variable `" + variable + "` because it resolves to a struct (" + v + "). Return a pointer to " + v + " instead.")
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
s, ok := structCache[v]
|
||||
if !ok {
|
||||
println("Fatal: Bind statement using variable `" + variable + "` which resolves to a `" + v + "` but cannot find its declaration")
|
||||
os.Exit(1)
|
||||
}
|
||||
boundStructs[v] = s
|
||||
|
||||
})
|
||||
|
||||
// Check for struct literals
|
||||
boundStructLiterals.Each(func(structName string) {
|
||||
println("Fatal: Cannot bind struct using struct literal `" + structName + "{}`. Create a pointer to " + structName + " instead.")
|
||||
os.Exit(1)
|
||||
})
|
||||
|
||||
// Check for bound variables
|
||||
// boundVariables.Each(func(varName string) {
|
||||
// println("Fatal: Cannot bind struct using struct literal `" + structName + "{}`. Create a pointer to " + structName + " instead.")
|
||||
// })
|
||||
|
||||
// spew.Dump(boundStructs)
|
||||
// os.Exit(0)
|
||||
|
||||
// }
|
||||
// Inspect the AST and print all identifiers and literals.
|
||||
|
||||
println("export {")
|
||||
|
||||
noOfStructs := len(boundStructs)
|
||||
structCount := 0
|
||||
for _, s := range boundStructs {
|
||||
structCount++
|
||||
println()
|
||||
println(" " + s.Name + ": {")
|
||||
println()
|
||||
noOfMethods := len(s.Methods)
|
||||
for methodCount, m := range s.Methods {
|
||||
println(" /****************")
|
||||
for _, comment := range m.Comments {
|
||||
println(" *", comment)
|
||||
}
|
||||
if len(m.Comments) > 0 {
|
||||
println(" *")
|
||||
}
|
||||
inputNames := ""
|
||||
for _, input := range m.Inputs {
|
||||
println(" * @param {"+input.Type+"}", input.Name)
|
||||
inputNames += input.Name + ", "
|
||||
}
|
||||
print(" * @return Promise<")
|
||||
for _, output := range m.Returns {
|
||||
print(output.Type + "|")
|
||||
}
|
||||
println("Error>")
|
||||
println(" *")
|
||||
println(" ***/")
|
||||
if len(inputNames) > 2 {
|
||||
inputNames = inputNames[:len(inputNames)-2]
|
||||
}
|
||||
println(" ", m.Name+": function("+inputNames+") {")
|
||||
println(" return window.backend." + s.Name + "." + m.Name + "(" + inputNames + ");")
|
||||
print(" }")
|
||||
if methodCount < noOfMethods-1 {
|
||||
print(",")
|
||||
}
|
||||
println()
|
||||
println()
|
||||
}
|
||||
print(" }")
|
||||
if structCount < noOfStructs-1 {
|
||||
print(",")
|
||||
}
|
||||
println()
|
||||
}
|
||||
println()
|
||||
println("}")
|
||||
println()
|
||||
}
|
||||
63
v2/internal/process/process.go
Normal file
63
v2/internal/process/process.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package process
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
|
||||
"github.com/wailsapp/wails/v2/internal/logger"
|
||||
)
|
||||
|
||||
// Process defines a process that can be executed
|
||||
type Process struct {
|
||||
logger *logger.Logger
|
||||
cmd *exec.Cmd
|
||||
exitChannel chan bool
|
||||
Running bool
|
||||
}
|
||||
|
||||
// NewProcess creates a new process struct
|
||||
func NewProcess(logger *logger.Logger, cmd string, args ...string) *Process {
|
||||
return &Process{
|
||||
logger: logger,
|
||||
cmd: exec.Command(cmd, args...),
|
||||
exitChannel: make(chan bool, 1),
|
||||
}
|
||||
}
|
||||
|
||||
// Start the process
|
||||
func (p *Process) Start() error {
|
||||
|
||||
err := p.cmd.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.Running = true
|
||||
|
||||
go func(cmd *exec.Cmd, running *bool, logger *logger.Logger, exitChannel chan bool) {
|
||||
logger.Info("Starting process (PID: %d)", cmd.Process.Pid)
|
||||
cmd.Wait()
|
||||
logger.Info("Exiting process (PID: %d)", cmd.Process.Pid)
|
||||
*running = false
|
||||
exitChannel <- true
|
||||
}(p.cmd, &p.Running, p.logger, p.exitChannel)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Kill the process
|
||||
func (p *Process) Kill() error {
|
||||
if !p.Running {
|
||||
return nil
|
||||
}
|
||||
err := p.cmd.Process.Kill()
|
||||
|
||||
// Wait for command to exit properly
|
||||
<-p.exitChannel
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// PID returns the process PID
|
||||
func (p *Process) PID() int {
|
||||
return p.cmd.Process.Pid
|
||||
}
|
||||
80
v2/internal/project/project.go
Normal file
80
v2/internal/project/project.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package project
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Project holds the data related to a Wails project
|
||||
type Project struct {
|
||||
|
||||
/*** Application Data ***/
|
||||
Name string `json:"name"`
|
||||
|
||||
// Application HTML, JS and CSS filenames
|
||||
HTML string `json:"html"`
|
||||
JS string `json:"js"`
|
||||
CSS string `json:"css"`
|
||||
BuildCommand string `json:"frontend:build"`
|
||||
InstallCommand string `json:"frontend:install"`
|
||||
/*** Internal Data ***/
|
||||
|
||||
// The path to the project directory
|
||||
Path string
|
||||
|
||||
// The output filename
|
||||
OutputFilename string `json:"outputfilename"`
|
||||
|
||||
// The type of application. EG: Desktop, Server, etc
|
||||
OutputType string
|
||||
|
||||
// The platform to target
|
||||
Platform string
|
||||
}
|
||||
|
||||
// Load the project from the current working directory
|
||||
func Load(projectPath string) (*Project, error) {
|
||||
|
||||
// Attempt to load project.json
|
||||
projectFile := filepath.Join(projectPath, "wails.json")
|
||||
rawBytes, err := ioutil.ReadFile(projectFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Unmarshal JSON
|
||||
var result Project
|
||||
err = json.Unmarshal(rawBytes, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Fix up our project paths
|
||||
result.Path = filepath.ToSlash(projectPath) + "/"
|
||||
result.HTML = filepath.Join(projectPath, result.HTML)
|
||||
result.JS = filepath.Join(projectPath, result.JS)
|
||||
result.CSS = filepath.Join(projectPath, result.CSS)
|
||||
|
||||
// Create default name if not given
|
||||
if result.Name == "" {
|
||||
result.Name = "wailsapp"
|
||||
}
|
||||
|
||||
// Fix up OutputFilename
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
if !strings.HasSuffix(result.OutputFilename, ".exe") {
|
||||
result.OutputFilename += ".exe"
|
||||
}
|
||||
case "darwin", "linux":
|
||||
if strings.HasSuffix(result.OutputFilename, ".exe") {
|
||||
result.OutputFilename = strings.TrimSuffix(result.OutputFilename, ".exe")
|
||||
}
|
||||
}
|
||||
|
||||
// Return our project data
|
||||
return &result, nil
|
||||
}
|
||||
1
v2/internal/runtime/assets/desktop.js
Normal file
1
v2/internal/runtime/assets/desktop.js
Normal file
File diff suppressed because one or more lines are too long
1
v2/internal/runtime/assets/wails.js
Normal file
1
v2/internal/runtime/assets/wails.js
Normal file
File diff suppressed because one or more lines are too long
36
v2/internal/runtime/goruntime/browser.go
Normal file
36
v2/internal/runtime/goruntime/browser.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package goruntime
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// Browser defines all browser related operations
|
||||
type Browser interface {
|
||||
Open(url string) error
|
||||
}
|
||||
|
||||
type browser struct{}
|
||||
|
||||
// Open a url / file using the system default application
|
||||
// Credit: https://gist.github.com/hyg/9c4afcd91fe24316cbf0
|
||||
func (b *browser) Open(url string) error {
|
||||
var err error
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
err = exec.Command("xdg-open", url).Start()
|
||||
case "windows":
|
||||
err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
|
||||
case "darwin":
|
||||
err = exec.Command("open", url).Start()
|
||||
default:
|
||||
err = fmt.Errorf("unsupported platform")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func newBrowser() *browser {
|
||||
return &browser{}
|
||||
}
|
||||
16
v2/internal/runtime/goruntime/browser_test.go
Normal file
16
v2/internal/runtime/goruntime/browser_test.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package goruntime
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/wailsapp/wails/v2/internal/logger"
|
||||
"github.com/wailsapp/wails/v2/internal/servicebus"
|
||||
)
|
||||
|
||||
func TestBrowserOpen(t *testing.T) {
|
||||
mylogger := logger.New(os.Stdout)
|
||||
myServiceBus := servicebus.New(mylogger)
|
||||
myRuntime := New(myServiceBus)
|
||||
myRuntime.Browser.Open("http://www.google.com")
|
||||
}
|
||||
140
v2/internal/runtime/goruntime/dialog.go
Normal file
140
v2/internal/runtime/goruntime/dialog.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package goruntime
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/wailsapp/wails/v2/internal/crypto"
|
||||
"github.com/wailsapp/wails/v2/internal/servicebus"
|
||||
)
|
||||
|
||||
// Dialog defines all Dialog related operations
|
||||
type Dialog interface {
|
||||
SaveFile(params ...string) string
|
||||
SelectFile(params ...string) string
|
||||
SelectDirectory(params ...string) string
|
||||
}
|
||||
|
||||
// dialog exposes the Dialog interface
|
||||
type dialog struct {
|
||||
bus *servicebus.ServiceBus
|
||||
}
|
||||
|
||||
// newDialogs creates a new Dialogs struct
|
||||
func newDialog(bus *servicebus.ServiceBus) Dialog {
|
||||
return &dialog{
|
||||
bus: bus,
|
||||
}
|
||||
}
|
||||
|
||||
// processTitleAndFilter return the title and filter from the given params.
|
||||
// title is the first string, filter is the second
|
||||
func (r *dialog) processTitleAndFilter(params ...string) (string, string) {
|
||||
|
||||
var title, filter string
|
||||
|
||||
if len(params) > 0 {
|
||||
title = params[0]
|
||||
}
|
||||
|
||||
if len(params) > 1 {
|
||||
filter = params[1]
|
||||
}
|
||||
|
||||
return title, filter
|
||||
}
|
||||
|
||||
// SelectFile prompts the user to select a file
|
||||
func (r *dialog) SelectFile(params ...string) string {
|
||||
|
||||
// Extract title + filter
|
||||
title, filter := r.processTitleAndFilter(params...)
|
||||
|
||||
// Create unique dialog callback
|
||||
uniqueCallback := crypto.RandomID()
|
||||
|
||||
// Subscribe to the respose channel
|
||||
responseTopic := "dialog:fileselected:" + uniqueCallback
|
||||
dialogResponseChannel, err := r.bus.Subscribe(responseTopic)
|
||||
if err != nil {
|
||||
fmt.Printf("ERROR: Cannot subscribe to bus topic: %+v\n", err.Error())
|
||||
}
|
||||
|
||||
// Publish dialog request
|
||||
message := "dialog:select:file:" + title
|
||||
if filter != "" {
|
||||
message += ":" + filter
|
||||
}
|
||||
r.bus.Publish(message, responseTopic)
|
||||
|
||||
// Wait for result
|
||||
result := <-dialogResponseChannel
|
||||
|
||||
// Delete subscription to response topic
|
||||
r.bus.UnSubscribe(responseTopic)
|
||||
|
||||
return result.Data().(string)
|
||||
}
|
||||
|
||||
// SaveFile prompts the user to select a file to save to
|
||||
func (r *dialog) SaveFile(params ...string) string {
|
||||
|
||||
// Extract title + filter
|
||||
title, filter := r.processTitleAndFilter(params...)
|
||||
|
||||
// Create unique dialog callback
|
||||
uniqueCallback := crypto.RandomID()
|
||||
|
||||
// Subscribe to the respose channel
|
||||
responseTopic := "dialog:filesaveselected:" + uniqueCallback
|
||||
dialogResponseChannel, err := r.bus.Subscribe(responseTopic)
|
||||
if err != nil {
|
||||
fmt.Printf("ERROR: Cannot subscribe to bus topic: %+v\n", err.Error())
|
||||
}
|
||||
|
||||
// Publish dialog request
|
||||
message := "dialog:select:filesave:" + title
|
||||
if filter != "" {
|
||||
message += ":" + filter
|
||||
}
|
||||
r.bus.Publish(message, responseTopic)
|
||||
|
||||
// Wait for result
|
||||
result := <-dialogResponseChannel
|
||||
|
||||
// Delete subscription to response topic
|
||||
r.bus.UnSubscribe(responseTopic)
|
||||
|
||||
return result.Data().(string)
|
||||
}
|
||||
|
||||
// SelectDirectory prompts the user to select a file
|
||||
func (r *dialog) SelectDirectory(params ...string) string {
|
||||
|
||||
// Extract title + filter
|
||||
title, filter := r.processTitleAndFilter(params...)
|
||||
|
||||
// Create unique dialog callback
|
||||
uniqueCallback := crypto.RandomID()
|
||||
|
||||
// Subscribe to the respose channel
|
||||
responseTopic := "dialog:directoryselected:" + uniqueCallback
|
||||
dialogResponseChannel, err := r.bus.Subscribe(responseTopic)
|
||||
if err != nil {
|
||||
fmt.Printf("ERROR: Cannot subscribe to bus topic: %+v\n", err.Error())
|
||||
}
|
||||
|
||||
// Publish dialog request
|
||||
message := "dialog:select:directory:" + title
|
||||
if filter != "" {
|
||||
message += ":" + filter
|
||||
}
|
||||
r.bus.Publish(message, responseTopic)
|
||||
|
||||
// Wait for result
|
||||
var result *servicebus.Message = <-dialogResponseChannel
|
||||
|
||||
// Delete subscription to response topic
|
||||
r.bus.UnSubscribe(responseTopic)
|
||||
|
||||
return result.Data().(string)
|
||||
}
|
||||
43
v2/internal/runtime/goruntime/events.go
Normal file
43
v2/internal/runtime/goruntime/events.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package goruntime
|
||||
|
||||
import (
|
||||
"github.com/wailsapp/wails/v2/internal/messagedispatcher/message"
|
||||
"github.com/wailsapp/wails/v2/internal/servicebus"
|
||||
)
|
||||
|
||||
// Events defines all events related operations
|
||||
type Events interface {
|
||||
On(eventName string, callback func(optionalData ...interface{}))
|
||||
Emit(eventName string, optionalData ...interface{})
|
||||
}
|
||||
|
||||
// event exposes the events interface
|
||||
type event struct {
|
||||
bus *servicebus.ServiceBus
|
||||
}
|
||||
|
||||
// newEvents creates a new Events struct
|
||||
func newEvents(bus *servicebus.ServiceBus) Events {
|
||||
return &event{
|
||||
bus: bus,
|
||||
}
|
||||
}
|
||||
|
||||
// On pass through
|
||||
func (r *event) On(eventName string, callback func(optionalData ...interface{})) {
|
||||
eventMessage := &message.OnEventMessage{
|
||||
Name: eventName,
|
||||
Callback: callback,
|
||||
}
|
||||
r.bus.Publish("event:on", eventMessage)
|
||||
}
|
||||
|
||||
// Emit pass through
|
||||
func (r *event) Emit(eventName string, optionalData ...interface{}) {
|
||||
eventMessage := &message.EventMessage{
|
||||
Name: eventName,
|
||||
Data: optionalData,
|
||||
}
|
||||
|
||||
r.bus.Publish("event:emit:from:g", eventMessage)
|
||||
}
|
||||
28
v2/internal/runtime/goruntime/runtime.go
Normal file
28
v2/internal/runtime/goruntime/runtime.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package goruntime
|
||||
|
||||
import "github.com/wailsapp/wails/v2/internal/servicebus"
|
||||
|
||||
// Runtime is a means for the user to interact with the application at runtime
|
||||
type Runtime struct {
|
||||
Browser Browser
|
||||
Events Events
|
||||
Window Window
|
||||
Dialog Dialog
|
||||
bus *servicebus.ServiceBus
|
||||
}
|
||||
|
||||
// New creates a new runtime
|
||||
func New(serviceBus *servicebus.ServiceBus) *Runtime {
|
||||
return &Runtime{
|
||||
Browser: newBrowser(),
|
||||
Events: newEvents(serviceBus),
|
||||
Window: newWindow(serviceBus),
|
||||
Dialog: newDialog(serviceBus),
|
||||
bus: serviceBus,
|
||||
}
|
||||
}
|
||||
|
||||
// Quit the application
|
||||
func (r *Runtime) Quit() {
|
||||
r.bus.Publish("quit", "runtime.Quit()")
|
||||
}
|
||||
102
v2/internal/runtime/goruntime/window.go
Normal file
102
v2/internal/runtime/goruntime/window.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package goruntime
|
||||
|
||||
import (
|
||||
"github.com/wailsapp/wails/v2/internal/servicebus"
|
||||
)
|
||||
|
||||
// Window defines all Window related operations
|
||||
type Window interface {
|
||||
Close()
|
||||
Show()
|
||||
Hide()
|
||||
Maximise()
|
||||
Unmaximise()
|
||||
Minimise()
|
||||
Unminimise()
|
||||
SetTitle(title string)
|
||||
Fullscreen()
|
||||
UnFullscreen()
|
||||
SetColour(colour string)
|
||||
}
|
||||
|
||||
// Window exposes the Windows interface
|
||||
type window struct {
|
||||
bus *servicebus.ServiceBus
|
||||
}
|
||||
|
||||
// newWindow creates a new window struct
|
||||
func newWindow(bus *servicebus.ServiceBus) Window {
|
||||
return &window{
|
||||
bus: bus,
|
||||
}
|
||||
}
|
||||
|
||||
// Close the Window
|
||||
// DISCUSSION:
|
||||
// Should we even be doing this now we have a server build?
|
||||
// Runtime.Quit() makes more sense than closing a window...
|
||||
func (w *window) Close() {
|
||||
w.bus.Publish("quit", "runtime.Close()")
|
||||
}
|
||||
|
||||
// SetTitle sets the title of the window
|
||||
func (w *window) SetTitle(title string) {
|
||||
w.bus.Publish("window:settitle", title)
|
||||
}
|
||||
|
||||
// Fullscreen makes the window fullscreen
|
||||
func (w *window) Fullscreen() {
|
||||
w.bus.Publish("window:fullscreen", "")
|
||||
}
|
||||
|
||||
// UnFullscreen makes the window UnFullscreen
|
||||
func (w *window) UnFullscreen() {
|
||||
w.bus.Publish("window:unfullscreen", "")
|
||||
}
|
||||
|
||||
// SetColour sets the window colour to the given string
|
||||
func (w *window) SetColour(colour string) {
|
||||
w.bus.Publish("window:setcolour", colour)
|
||||
}
|
||||
|
||||
// Show shows the window if hidden
|
||||
func (w *window) Show() {
|
||||
w.bus.Publish("window:show", "")
|
||||
}
|
||||
|
||||
// Hide the window
|
||||
func (w *window) Hide() {
|
||||
w.bus.Publish("window:hide", "")
|
||||
}
|
||||
|
||||
// SetSize sets the size of the window
|
||||
func (w *window) SetSize(width int, height int) {
|
||||
size := []int{width, height}
|
||||
w.bus.Publish("window:setsize", size)
|
||||
}
|
||||
|
||||
// SetPosition sets the position of the window
|
||||
func (w *window) SetPosition(x int, y int) {
|
||||
position := []int{x, y}
|
||||
w.bus.Publish("window:position", position)
|
||||
}
|
||||
|
||||
// Maximise the window
|
||||
func (w *window) Maximise() {
|
||||
w.bus.Publish("window:maximise", "")
|
||||
}
|
||||
|
||||
// Unmaximise the window
|
||||
func (w *window) Unmaximise() {
|
||||
w.bus.Publish("window:unmaximise", "")
|
||||
}
|
||||
|
||||
// Minimise the window
|
||||
func (w *window) Minimise() {
|
||||
w.bus.Publish("window:minimise", "")
|
||||
}
|
||||
|
||||
// Unminimise the window
|
||||
func (w *window) Unminimise() {
|
||||
w.bus.Publish("window:unminimise", "")
|
||||
}
|
||||
24
v2/internal/runtime/js/.eslintrc
Normal file
24
v2/internal/runtime/js/.eslintrc
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"amd": true,
|
||||
"node": true,
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2016,
|
||||
"sourceType": "module",
|
||||
},
|
||||
"rules": {
|
||||
"linebreak-style": 0,
|
||||
"quotes": [
|
||||
"error",
|
||||
"single"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
]
|
||||
}
|
||||
}
|
||||
22
v2/internal/runtime/js/babel.config.js
Normal file
22
v2/internal/runtime/js/babel.config.js
Normal file
@@ -0,0 +1,22 @@
|
||||
/* eslint-disable */
|
||||
|
||||
module.exports = function (api) {
|
||||
api.cache(true);
|
||||
|
||||
const presets = [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"useBuiltIns": "entry",
|
||||
"corejs": {
|
||||
"version": 3,
|
||||
"proposals": true
|
||||
}
|
||||
}
|
||||
]
|
||||
];
|
||||
|
||||
return {
|
||||
presets,
|
||||
};
|
||||
}
|
||||
107
v2/internal/runtime/js/core/bindings.js
Normal file
107
v2/internal/runtime/js/core/bindings.js
Normal file
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
_ __ _ __
|
||||
| | / /___ _(_) /____
|
||||
| | /| / / __ `/ / / ___/
|
||||
| |/ |/ / /_/ / / (__ )
|
||||
|__/|__/\__,_/_/_/____/
|
||||
The lightweight framework for web-like apps
|
||||
(c) Lea Anthony 2019-present
|
||||
*/
|
||||
/* jshint esversion: 6 */
|
||||
|
||||
import { Call } from './calls';
|
||||
|
||||
window.backend = {};
|
||||
|
||||
/**
|
||||
|
||||
Map of this format:
|
||||
|
||||
{
|
||||
packageName: {
|
||||
structName: {
|
||||
methodName: {
|
||||
name: "",
|
||||
inputs: [
|
||||
{
|
||||
type: <type>
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
type: <type>
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
export function SetBindings(bindingsMap) {
|
||||
try {
|
||||
bindingsMap = JSON.parse(bindingsMap);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
// Initialise the backend map
|
||||
window.backend = window.backend || {};
|
||||
|
||||
// Iterate package names
|
||||
Object.keys(bindingsMap).forEach((packageName) => {
|
||||
|
||||
// Create inner map if it doesn't exist
|
||||
window.backend[packageName] = window.backend[packageName] || {};
|
||||
|
||||
// Iterate struct names
|
||||
Object.keys(bindingsMap[packageName]).forEach((structName) => {
|
||||
|
||||
// Create inner map if it doesn't exist
|
||||
window.backend[packageName][structName] = window.backend[packageName][structName] || {};
|
||||
|
||||
Object.keys(bindingsMap[packageName][structName]).forEach((methodName) => {
|
||||
|
||||
window.backend[packageName][structName][methodName] = function () {
|
||||
|
||||
// No timeout by default
|
||||
var timeout = 0;
|
||||
|
||||
// Actual function
|
||||
function dynamic() {
|
||||
var args = [].slice.call(arguments);
|
||||
return Call([packageName, structName, methodName].join('.'), args, timeout);
|
||||
}
|
||||
|
||||
// Allow setting timeout to function
|
||||
dynamic.setTimeout = function (newTimeout) {
|
||||
timeout = newTimeout;
|
||||
};
|
||||
|
||||
// Allow getting timeout to function
|
||||
dynamic.getTimeout = function () {
|
||||
return timeout;
|
||||
};
|
||||
|
||||
return dynamic;
|
||||
}();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
// /**
|
||||
// * Determines if the given identifier is valid Javascript
|
||||
// *
|
||||
// * @param {boolean} name
|
||||
// * @returns
|
||||
// */
|
||||
// function isValidIdentifier(name) {
|
||||
// // Don't xss yourself :-)
|
||||
// try {
|
||||
// new Function('var ' + name);
|
||||
// return true;
|
||||
// } catch (e) {
|
||||
// return false;
|
||||
// }
|
||||
// }
|
||||
|
||||
34
v2/internal/runtime/js/core/browser.js
Normal file
34
v2/internal/runtime/js/core/browser.js
Normal file
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
_ __ _ __
|
||||
| | / /___ _(_) /____
|
||||
| | /| / / __ `/ / / ___/
|
||||
| |/ |/ / /_/ / / (__ )
|
||||
|__/|__/\__,_/_/_/____/
|
||||
The lightweight framework for web-like apps
|
||||
(c) Lea Anthony 2019-present
|
||||
*/
|
||||
/* jshint esversion: 6 */
|
||||
|
||||
import { SendMessage } from 'ipc';
|
||||
|
||||
/**
|
||||
* Opens the given URL in the system browser
|
||||
*
|
||||
* @export
|
||||
* @param {string} url
|
||||
* @returns
|
||||
*/
|
||||
export function OpenURL(url) {
|
||||
return SendMessage('RBU' + url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the given filename using the system's default file handler
|
||||
*
|
||||
* @export
|
||||
* @param {sting} filename
|
||||
* @returns
|
||||
*/
|
||||
export function OpenFile(filename) {
|
||||
return SendMessage('runtime:browser:openfile', filename);
|
||||
}
|
||||
156
v2/internal/runtime/js/core/calls.js
Normal file
156
v2/internal/runtime/js/core/calls.js
Normal file
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
_ __ _ __
|
||||
| | / /___ _(_) /____
|
||||
| | /| / / __ `/ / / ___/
|
||||
| |/ |/ / /_/ / / (__ )
|
||||
|__/|__/\__,_/_/_/____/
|
||||
The lightweight framework for web-like apps
|
||||
(c) Lea Anthony 2019-present
|
||||
*/
|
||||
/* jshint esversion: 6 */
|
||||
|
||||
import { Debug } from './log';
|
||||
import { SendMessage } from 'ipc';
|
||||
|
||||
var callbacks = {};
|
||||
|
||||
/**
|
||||
* Returns a number from the native browser random function
|
||||
*
|
||||
* @returns number
|
||||
*/
|
||||
function cryptoRandom() {
|
||||
var array = new Uint32Array(1);
|
||||
return window.crypto.getRandomValues(array)[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a number using da old-skool Math.Random
|
||||
* I likes to call it LOLRandom
|
||||
*
|
||||
* @returns number
|
||||
*/
|
||||
function basicRandom() {
|
||||
return Math.random() * 9007199254740991;
|
||||
}
|
||||
|
||||
// Pick a random number function based on browser capability
|
||||
var randomFunc;
|
||||
if (window.crypto) {
|
||||
randomFunc = cryptoRandom;
|
||||
} else {
|
||||
randomFunc = basicRandom;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Call sends a message to the backend to call the binding with the
|
||||
* given data. A promise is returned and will be completed when the
|
||||
* backend responds. This will be resolved when the call was successful
|
||||
* or rejected if an error is passed back.
|
||||
* There is a timeout mechanism. If the call doesn't respond in the given
|
||||
* time (in milliseconds) then the promise is rejected.
|
||||
*
|
||||
* @export
|
||||
* @param {string} name
|
||||
* @param {string} args
|
||||
* @param {number=} timeout
|
||||
* @returns
|
||||
*/
|
||||
export function Call(name, args, timeout) {
|
||||
|
||||
// Timeout infinite by default
|
||||
if (timeout == null || timeout == undefined) {
|
||||
timeout = 0;
|
||||
}
|
||||
|
||||
// Create a promise
|
||||
return new Promise(function (resolve, reject) {
|
||||
|
||||
// Create a unique callbackID
|
||||
var callbackID;
|
||||
do {
|
||||
callbackID = name + '-' + randomFunc();
|
||||
} while (callbacks[callbackID]);
|
||||
|
||||
// Set timeout
|
||||
if (timeout > 0) {
|
||||
var timeoutHandle = setTimeout(function () {
|
||||
reject(Error('Call to ' + name + ' timed out. Request ID: ' + callbackID));
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
// Store callback
|
||||
callbacks[callbackID] = {
|
||||
timeoutHandle: timeoutHandle,
|
||||
reject: reject,
|
||||
resolve: resolve
|
||||
};
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
name,
|
||||
args,
|
||||
callbackID,
|
||||
};
|
||||
|
||||
// Make the call
|
||||
SendMessage('C' + JSON.stringify(payload));
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Called by the backend to return data to a previously called
|
||||
* binding invocation
|
||||
*
|
||||
* @export
|
||||
* @param {string} incomingMessage
|
||||
*/
|
||||
export function Callback(incomingMessage) {
|
||||
// Decode the message - Credit: https://stackoverflow.com/a/13865680
|
||||
//incomingMessage = decodeURIComponent(incomingMessage.replace(/\s+/g, '').replace(/[0-9a-f]{2}/g, '%$&'));
|
||||
|
||||
// Parse the message
|
||||
var message;
|
||||
try {
|
||||
message = JSON.parse(incomingMessage);
|
||||
} catch (e) {
|
||||
const error = `Invalid JSON passed to callback: ${e.message}. Message: ${incomingMessage}`;
|
||||
Debug(error);
|
||||
throw new Error(error);
|
||||
}
|
||||
var callbackID = message.callbackid;
|
||||
var callbackData = callbacks[callbackID];
|
||||
if (!callbackData) {
|
||||
const error = `Callback '${callbackID}' not registered!!!`;
|
||||
console.error(error); // eslint-disable-line
|
||||
throw new Error(error);
|
||||
}
|
||||
clearTimeout(callbackData.timeoutHandle);
|
||||
|
||||
delete callbacks[callbackID];
|
||||
|
||||
if (message.error) {
|
||||
callbackData.reject(message.error);
|
||||
} else {
|
||||
callbackData.resolve(message.result);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SystemCall is used to call wails methods from the frontend
|
||||
*
|
||||
* @export
|
||||
* @param {string} method
|
||||
* @param {any[]=} data
|
||||
* @returns
|
||||
*/
|
||||
export function SystemCall(method, data) {
|
||||
return Call('.wails.' + method, data);
|
||||
}
|
||||
35
v2/internal/runtime/js/core/desktop.js
Normal file
35
v2/internal/runtime/js/core/desktop.js
Normal file
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
_ __ _ __
|
||||
| | / /___ _(_) /____
|
||||
| | /| / / __ `/ / / ___/
|
||||
| |/ |/ / /_/ / / (__ )
|
||||
|__/|__/\__,_/_/_/____/
|
||||
The lightweight framework for web-like apps
|
||||
(c) Lea Anthony 2019-present
|
||||
*/
|
||||
/* jshint esversion: 6 */
|
||||
import { SetBindings } from './bindings';
|
||||
import { Init } from './main';
|
||||
|
||||
// Setup global error handler
|
||||
window.onerror = function (/*msg, url, lineNo, columnNo, error*/) {
|
||||
// window.wails.Log.Error('**** Caught Unhandled Error ****');
|
||||
// window.wails.Log.Error('Message: ' + msg);
|
||||
// window.wails.Log.Error('URL: ' + url);
|
||||
// window.wails.Log.Error('Line No: ' + lineNo);
|
||||
// window.wails.Log.Error('Column No: ' + columnNo);
|
||||
// window.wails.Log.Error('error: ' + error);
|
||||
(function () { window.wails.Log.Error(new Error().stack); })();
|
||||
};
|
||||
|
||||
// Initialise the Runtime
|
||||
Init();
|
||||
|
||||
// Load Bindings if they exist
|
||||
if (window.wailsbindings) {
|
||||
SetBindings(window.wailsbindings);
|
||||
}
|
||||
|
||||
// Emit loaded event. Leaving this for now. It will show any errors if runtime fails to load.
|
||||
window.wails.Events.Emit('wails:loaded');
|
||||
|
||||
202
v2/internal/runtime/js/core/events.js
Normal file
202
v2/internal/runtime/js/core/events.js
Normal file
@@ -0,0 +1,202 @@
|
||||
/*
|
||||
_ __ _ __
|
||||
| | / /___ _(_) /____
|
||||
| | /| / / __ `/ / / ___/
|
||||
| |/ |/ / /_/ / / (__ )
|
||||
|__/|__/\__,_/_/_/____/
|
||||
The lightweight framework for web-like apps
|
||||
(c) Lea Anthony 2019-present
|
||||
*/
|
||||
/* jshint esversion: 6 */
|
||||
|
||||
import { Error } from './log';
|
||||
import { SendMessage } from 'ipc';
|
||||
|
||||
// Defines a single listener with a maximum number of times to callback
|
||||
/**
|
||||
* The Listener class defines a listener! :-)
|
||||
*
|
||||
* @class Listener
|
||||
*/
|
||||
class Listener {
|
||||
/**
|
||||
* Creates an instance of Listener.
|
||||
* @param {function} callback
|
||||
* @param {number} maxCallbacks
|
||||
* @memberof Listener
|
||||
*/
|
||||
constructor(callback, maxCallbacks) {
|
||||
// Default of -1 means infinite
|
||||
maxCallbacks = maxCallbacks || -1;
|
||||
// Callback invokes the callback with the given data
|
||||
// Returns true if this listener should be destroyed
|
||||
this.Callback = (data) => {
|
||||
callback.apply(null, data);
|
||||
// If maxCallbacks is infinite, return false (do not destroy)
|
||||
if (maxCallbacks === -1) {
|
||||
return false;
|
||||
}
|
||||
// Decrement maxCallbacks. Return true if now 0, otherwise false
|
||||
maxCallbacks -= 1;
|
||||
return maxCallbacks === 0;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
var eventListeners = {};
|
||||
|
||||
/**
|
||||
* Registers an event listener that will be invoked `maxCallbacks` times before being destroyed
|
||||
*
|
||||
* @export
|
||||
* @param {string} eventName
|
||||
* @param {function} callback
|
||||
* @param {number} maxCallbacks
|
||||
*/
|
||||
export function OnMultiple(eventName, callback, maxCallbacks) {
|
||||
eventListeners[eventName] = eventListeners[eventName] || [];
|
||||
const thisListener = new Listener(callback, maxCallbacks);
|
||||
console.log('Pushing event listener: ' + eventName);
|
||||
eventListeners[eventName].push(thisListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an event listener that will be invoked every time the event is emitted
|
||||
*
|
||||
* @export
|
||||
* @param {string} eventName
|
||||
* @param {function} callback
|
||||
*/
|
||||
export function On(eventName, callback) {
|
||||
OnMultiple(eventName, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an event listener that will be invoked once then destroyed
|
||||
*
|
||||
* @export
|
||||
* @param {string} eventName
|
||||
* @param {function} callback
|
||||
*/
|
||||
export function Once(eventName, callback) {
|
||||
OnMultiple(eventName, callback, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify informs frontend listeners that an event was emitted with the given data
|
||||
*
|
||||
* @export
|
||||
* @param {string} encoded notification message
|
||||
|
||||
*/
|
||||
export function Notify(notifyMessage) {
|
||||
|
||||
// Parse the message
|
||||
var message;
|
||||
try {
|
||||
message = JSON.parse(notifyMessage);
|
||||
} catch (e) {
|
||||
const error = 'Invalid JSON passed to Notify: ' + notifyMessage;
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
var eventName = message.name;
|
||||
|
||||
// Check if we have any listeners for this event
|
||||
if (eventListeners[eventName]) {
|
||||
|
||||
// Keep a list of listener indexes to destroy
|
||||
const newEventListenerList = eventListeners[eventName].slice();
|
||||
|
||||
// Iterate listeners
|
||||
for (let count = 0; count < eventListeners[eventName].length; count += 1) {
|
||||
|
||||
// Get next listener
|
||||
const listener = eventListeners[eventName][count];
|
||||
|
||||
var data = message.data;
|
||||
|
||||
// Do the callback
|
||||
const destroy = listener.Callback(data);
|
||||
if (destroy) {
|
||||
// if the listener indicated to destroy itself, add it to the destroy list
|
||||
newEventListenerList.splice(count, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Update callbacks with new list of listners
|
||||
eventListeners[eventName] = newEventListenerList;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit an event with the given name and data
|
||||
*
|
||||
* @export
|
||||
* @param {string} eventName
|
||||
*/
|
||||
export function Emit(eventName) {
|
||||
|
||||
// Calculate the data
|
||||
if (arguments.length > 1) {
|
||||
// Notify backend
|
||||
const payload = {
|
||||
name: eventName,
|
||||
data: [].slice.apply(arguments).slice(1),
|
||||
};
|
||||
SendMessage('Ej' + JSON.stringify(payload));
|
||||
} else {
|
||||
SendMessage('ej' + eventName);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Callbacks for the heartbeat calls
|
||||
const heartbeatCallbacks = {};
|
||||
|
||||
/**
|
||||
* Heartbeat emits the event `eventName`, every `timeInMilliseconds` milliseconds until
|
||||
* the event is acknowledged via `Event.Acknowledge`. Once this happens, `callback` is invoked ONCE
|
||||
*
|
||||
* @export
|
||||
* @param {string} eventName
|
||||
* @param {number} timeInMilliseconds
|
||||
* @param {function} callback
|
||||
*/
|
||||
export function Heartbeat(eventName, timeInMilliseconds, callback) {
|
||||
|
||||
// Declare interval variable
|
||||
let interval = null;
|
||||
|
||||
// Setup callback
|
||||
function dynamicCallback() {
|
||||
// Kill interval
|
||||
clearInterval(interval);
|
||||
// Callback
|
||||
callback();
|
||||
}
|
||||
|
||||
// Register callback
|
||||
heartbeatCallbacks[eventName] = dynamicCallback;
|
||||
|
||||
// Start emitting the event
|
||||
interval = setInterval(function () {
|
||||
Emit(eventName);
|
||||
}, timeInMilliseconds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Acknowledges a heartbeat event by name
|
||||
*
|
||||
* @export
|
||||
* @param {string} eventName
|
||||
*/
|
||||
export function Acknowledge(eventName) {
|
||||
// If we are waiting for acknowledgement for this event type
|
||||
if (heartbeatCallbacks[eventName]) {
|
||||
// Acknowledge!
|
||||
heartbeatCallbacks[eventName]();
|
||||
} else {
|
||||
throw new Error(`Cannot acknowledge unknown heartbeat '${eventName}'`);
|
||||
}
|
||||
}
|
||||
76
v2/internal/runtime/js/core/log.js
Normal file
76
v2/internal/runtime/js/core/log.js
Normal file
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
_ __ _ __
|
||||
| | / /___ _(_) /____
|
||||
| | /| / / __ `/ / / ___/
|
||||
| |/ |/ / /_/ / / (__ )
|
||||
|__/|__/\__,_/_/_/____/
|
||||
The lightweight framework for web-like apps
|
||||
(c) Lea Anthony 2019-present
|
||||
*/
|
||||
|
||||
/* jshint esversion: 6 */
|
||||
|
||||
import { SendMessage } from 'ipc';
|
||||
|
||||
/**
|
||||
* Sends a log message to the backend with the given level + message
|
||||
*
|
||||
* @param {string} level
|
||||
* @param {string} message
|
||||
*/
|
||||
function sendLogMessage(level, message) {
|
||||
|
||||
// Log Message format:
|
||||
// l[type][message]
|
||||
SendMessage('L' + level + message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log the given debug message with the backend
|
||||
*
|
||||
* @export
|
||||
* @param {string} message
|
||||
*/
|
||||
export function Debug(message) {
|
||||
sendLogMessage('D', message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log the given info message with the backend
|
||||
*
|
||||
* @export
|
||||
* @param {string} message
|
||||
*/
|
||||
export function Info(message) {
|
||||
sendLogMessage('I', message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log the given warning message with the backend
|
||||
*
|
||||
* @export
|
||||
* @param {string} message
|
||||
*/
|
||||
export function Warning(message) {
|
||||
sendLogMessage('W', message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log the given error message with the backend
|
||||
*
|
||||
* @export
|
||||
* @param {string} message
|
||||
*/
|
||||
export function Error(message) {
|
||||
sendLogMessage('E', message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log the given fatal message with the backend
|
||||
*
|
||||
* @export
|
||||
* @param {string} message
|
||||
*/
|
||||
export function Fatal(message) {
|
||||
sendLogMessage('F', message);
|
||||
}
|
||||
51
v2/internal/runtime/js/core/main.js
Normal file
51
v2/internal/runtime/js/core/main.js
Normal file
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
_ __ _ __
|
||||
| | / /___ _(_) /____
|
||||
| | /| / / __ `/ / / ___/
|
||||
| |/ |/ / /_/ / / (__ )
|
||||
|__/|__/\__,_/_/_/____/
|
||||
The lightweight framework for web-like apps
|
||||
(c) Lea Anthony 2019-present
|
||||
*/
|
||||
/* jshint esversion: 6 */
|
||||
import * as Log from './log';
|
||||
import * as Browser from './browser';
|
||||
import * as Window from './window';
|
||||
import { On, OnMultiple, Emit, Notify, Heartbeat, Acknowledge } from './events';
|
||||
import { Callback } from './calls';
|
||||
import { AddScript, InjectCSS } from './utils';
|
||||
import { AddIPCListener } from 'ipc';
|
||||
import * as Platform from 'platform';
|
||||
|
||||
export function Init() {
|
||||
// Backend is where the Go struct wrappers get bound to
|
||||
window.backend = {};
|
||||
|
||||
// Initialise global if not already
|
||||
window.wails = {
|
||||
System: Platform.System,
|
||||
Log,
|
||||
Browser,
|
||||
Window,
|
||||
Events: {
|
||||
On,
|
||||
OnMultiple,
|
||||
Emit,
|
||||
Heartbeat,
|
||||
Acknowledge,
|
||||
},
|
||||
_: {
|
||||
Callback,
|
||||
Notify,
|
||||
AddScript,
|
||||
InjectCSS,
|
||||
Init,
|
||||
AddIPCListener
|
||||
}
|
||||
};
|
||||
|
||||
// Do platform specific Init
|
||||
Platform.Init();
|
||||
|
||||
window.wailsloader.runtime = true;
|
||||
}
|
||||
182
v2/internal/runtime/js/core/server.js
Normal file
182
v2/internal/runtime/js/core/server.js
Normal file
@@ -0,0 +1,182 @@
|
||||
/*
|
||||
_ __ _ __
|
||||
| | / /___ _(_) /____
|
||||
| | /| / / __ `/ / / ___/
|
||||
| |/ |/ / /_/ / / (__ )
|
||||
|__/|__/\__,_/_/_/____/
|
||||
The lightweight framework for web-like apps
|
||||
(c) Lea Anthony 2019-present
|
||||
*/
|
||||
/* jshint esversion: 6 */
|
||||
|
||||
import { Init } from './main';
|
||||
import { SetBindings } from './bindings';
|
||||
|
||||
function init() {
|
||||
// Bridge object
|
||||
window.wailsbridge = {
|
||||
reconnectOverlay: null,
|
||||
reconnectTimer: 300,
|
||||
wsURL: 'ws://' + window.location.hostname + ':8080/ws',
|
||||
connectionState: null,
|
||||
config: {},
|
||||
websocket: null,
|
||||
callback: null,
|
||||
overlayHTML:
|
||||
'<div class="wails-reconnect-overlay"><div class="wails-reconnect-overlay-content"><div class="wails-reconnect-overlay-title">Wails Bridge</div><br><div class="wails-reconnect-overlay-loadingspinner"></div><br><div id="wails-reconnect-overlay-message">Waiting for backend</div></div></div>',
|
||||
overlayCSS:
|
||||
'.wails-reconnect-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.6);font-family:sans-serif;display:none;z-index:999999}.wails-reconnect-overlay-content{padding:20px 30px;text-align:center;width:20em;position:relative;height:14em;border-radius:1em;margin:5% auto 0;background-color:#fff;box-shadow:1px 1px 20px 3px;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAC8AAAAuCAMAAACPpbA7AAAAqFBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQAAAAAAAAEBAQAAAAAAAAAAAAEBAQEBAQDAwMBAQEAAAABAQEAAAAAAAAAAAABAQEAAAAAAAACAgICAgIBAQEAAAAAAAAAAAAAAAAAAAAAAAABAQEAAAACAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFBQWKCj6oAAAAN3RSTlMALiIqDhkGBAswJjP0GxP6NR4W9/ztjRDMhWU50G9g5eHXvbZ9XEI9xZTcqZl2aldKo55QwoCvZUgzhAAAAs9JREFUSMeNleeWqjAUhU0BCaH3Itiw9zKT93+zG02QK1hm/5HF+jzZJ6fQe6cyXE+jg9X7o9wxuylIIf4Tv2V3+bOrEXnf8dwQ/KQIGDN2/S+4OmVCVXL/ScBnfibxURqIByP/hONE8r8T+bDMlQ98KSl7Y8hzjpS8v1qtDh8u5f8KQpGpfnPPhqG8JeogN37Hq9eaN2xRhIwAaGnvws8F1ShxqK5ob2twYi1FAMD4rXsYtnC/JEiRbl4cUrCWhnMCLRFemXezXbb59QK4WASOsm6n2W1+4CBT2JmtzQ6fsrbGubR/NFbd2g5Y179+5w/GEHaKsHjYCet7CgrXU3txarNC7YxOVJtIj4/ERzMdZfzc31hp+8cD6eGILgarZY9uZ12hAs03vfBD9C171gS5Omz7OcvxALQIn4u8RRBBBcsi9WW2woO9ipLgfzpYlggg3ZRdROUC8KT7QLqq3W9KB5BbdFVg4929kdwp6+qaZnMCCNBdj+NyN1W885Ry/AL3D4AQbsVV4noCiM/C83kyYq80XlDAYQtralOiDzoRAHlotWl8q2tjvYlOgcg1A8jEApZa+C06TBdAz2Qv0wu11I/zZOyJQ6EwGez2P2b8PIQr1hwwnAZsAxwA4UAYOyXUxM/xp6tHAn4GUmPGM9R28oVxgC0e/zQJJI6DyhyZ1r7uzRQhpcW7x7vTaWSzKSG6aep77kroTEl3U81uSVaUTtgEINfC8epx+Q4F9SpplHG84Ek6m4RAq9/TLkOBrxyeuddZhHvGIp1XXfFy3Z3vtwNblKGiDn+J+92vwwABHghj7HnzlS1H5kB49AZvdGCFgiBPq69qfXPr3y++yilF0ON4R8eR7spAsLpZ95NqAW5tab1c4vkZm6aleajchMwYTdILQQTwE2OV411ZM9WztDjPql12caBi6gDpUKmDd4U1XNdQxZ4LIXQ5/Tr4P7I9tYcFrDK3AAAAAElFTkSuQmCC);background-repeat:no-repeat;background-position:center}.wails-reconnect-overlay-title{font-size:2em}.wails-reconnect-overlay-message{font-size:1.3em}.wails-reconnect-overlay-loadingspinner{pointer-events:none;width:2.5em;height:2.5em;border:.4em solid transparent;border-color:#3E67EC #eee #eee;border-radius:50%;animation:loadingspin 1s linear infinite;margin:auto;padding:2.5em}@keyframes loadingspin{100%{transform:rotate(360deg)}}',
|
||||
log: function (message) {
|
||||
// eslint-disable-next-line
|
||||
console.log(
|
||||
'%c wails bridge %c ' + message + ' ',
|
||||
'background: #aa0000; color: #fff; border-radius: 3px 0px 0px 3px; padding: 1px; font-size: 0.7rem',
|
||||
'background: #009900; color: #fff; border-radius: 0px 3px 3px 0px; padding: 1px; font-size: 0.7rem'
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Adapted from webview - thanks zserge!
|
||||
function injectCSS(css) {
|
||||
var elem = document.createElement('style');
|
||||
elem.setAttribute('type', 'text/css');
|
||||
if (elem.styleSheet) {
|
||||
elem.styleSheet.cssText = css;
|
||||
} else {
|
||||
elem.appendChild(document.createTextNode(css));
|
||||
}
|
||||
var head = document.head || document.getElementsByTagName('head')[0];
|
||||
head.appendChild(elem);
|
||||
}
|
||||
|
||||
// Creates a node in the Dom
|
||||
function createNode(parent, elementType, id, className, content) {
|
||||
var d = document.createElement(elementType);
|
||||
if (id) {
|
||||
d.id = id;
|
||||
}
|
||||
if (className) {
|
||||
d.className = className;
|
||||
}
|
||||
if (content) {
|
||||
d.innerHTML = content;
|
||||
}
|
||||
parent.appendChild(d);
|
||||
return d;
|
||||
}
|
||||
|
||||
// Sets up the overlay
|
||||
function setupOverlay() {
|
||||
var body = document.body;
|
||||
|
||||
var wailsBridgeNode = createNode(body, 'div', 'wails-bridge');
|
||||
wailsBridgeNode.innerHTML = window.wailsbridge.overlayHTML;
|
||||
|
||||
// Inject the overlay CSS
|
||||
injectCSS(window.wailsbridge.overlayCSS);
|
||||
}
|
||||
|
||||
// Start the Wails Bridge
|
||||
function startBridge() {
|
||||
// Setup the overlay
|
||||
setupOverlay();
|
||||
|
||||
window.wailsbridge.websocket = null;
|
||||
window.wailsbridge.connectTimer = null;
|
||||
window.wailsbridge.reconnectOverlay = document.querySelector(
|
||||
'.wails-reconnect-overlay'
|
||||
);
|
||||
window.wailsbridge.connectionState = 'disconnected';
|
||||
|
||||
// Shows the overlay
|
||||
function showReconnectOverlay() {
|
||||
window.wailsbridge.reconnectOverlay.style.display = 'block';
|
||||
}
|
||||
|
||||
// Hides the overlay
|
||||
function hideReconnectOverlay() {
|
||||
window.wailsbridge.reconnectOverlay.style.display = 'none';
|
||||
}
|
||||
|
||||
// Handles incoming websocket connections
|
||||
function handleConnect() {
|
||||
window.wailsbridge.log('Connected to backend');
|
||||
hideReconnectOverlay();
|
||||
clearInterval(window.wailsbridge.connectTimer);
|
||||
window.wailsbridge.websocket.onclose = handleDisconnect;
|
||||
window.wailsbridge.websocket.onmessage = handleMessage;
|
||||
window.wailsbridge.connectionState = 'connected';
|
||||
}
|
||||
|
||||
// Handles websocket disconnects
|
||||
function handleDisconnect() {
|
||||
window.wailsbridge.log('Disconnected from backend');
|
||||
window.wailsbridge.websocket = null;
|
||||
window.wailsbridge.connectionState = 'disconnected';
|
||||
showReconnectOverlay();
|
||||
connect();
|
||||
}
|
||||
|
||||
// Try to connect to the backend every 300ms (default value).
|
||||
// Change this value in the main wailsbridge object.
|
||||
function connect() {
|
||||
window.wailsbridge.connectTimer = setInterval(function () {
|
||||
if (window.wailsbridge.websocket == null) {
|
||||
window.wailsbridge.websocket = new WebSocket(window.wailsbridge.wsURL);
|
||||
window.wailsbridge.websocket.onopen = handleConnect;
|
||||
window.wailsbridge.websocket.onerror = function (e) {
|
||||
e.stopImmediatePropagation();
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
window.wailsbridge.websocket = null;
|
||||
return false;
|
||||
};
|
||||
}
|
||||
}, window.wailsbridge.reconnectTimer);
|
||||
}
|
||||
|
||||
function handleMessage(message) {
|
||||
switch (message.data[0]) {
|
||||
case 'e':
|
||||
case 'E':
|
||||
window.wails._.Notify(message.data.slice(1));
|
||||
break;
|
||||
case 'R':
|
||||
window.wails._.Callback(message.data.slice(1));
|
||||
break;
|
||||
default:
|
||||
window.wails.Log.Error('Unknown message type received: ' + message.data[0]);
|
||||
}
|
||||
}
|
||||
|
||||
// Start by showing the overlay...
|
||||
showReconnectOverlay();
|
||||
|
||||
// ...and attempt to connect
|
||||
connect();
|
||||
}
|
||||
|
||||
function start() {
|
||||
|
||||
// Set up the bridge
|
||||
init();
|
||||
|
||||
// Start Bridge
|
||||
startBridge();
|
||||
|
||||
// Load bindings
|
||||
window.wailspreinit = function () {
|
||||
if (window.wailsbindings) {
|
||||
SetBindings(window.wailsbindings);
|
||||
}
|
||||
};
|
||||
|
||||
Init();
|
||||
|
||||
// Save the binding script
|
||||
window.SetBindings = SetBindings;
|
||||
}
|
||||
|
||||
// Start your engines!
|
||||
start();
|
||||
38
v2/internal/runtime/js/core/utils.js
Normal file
38
v2/internal/runtime/js/core/utils.js
Normal file
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
_ __ _ __
|
||||
| | / /___ _(_) /____
|
||||
| | /| / / __ `/ / / ___/
|
||||
| |/ |/ / /_/ / / (__ )
|
||||
|__/|__/\__,_/_/_/____/
|
||||
The lightweight framework for web-like apps
|
||||
(c) Lea Anthony 2019-present
|
||||
*/
|
||||
/* jshint esversion: 6 */
|
||||
|
||||
import { Emit } from './events';
|
||||
|
||||
export function AddScript(js, callbackID) {
|
||||
var script = document.createElement('script');
|
||||
script.text = js;
|
||||
document.body.appendChild(script);
|
||||
if (callbackID) {
|
||||
Emit(callbackID);
|
||||
}
|
||||
}
|
||||
|
||||
// Adapted from webview - thanks zserge!
|
||||
export function InjectCSS(css) {
|
||||
try {
|
||||
var elem = document.createElement('style');
|
||||
elem.setAttribute('type', 'text/css');
|
||||
if (elem.styleSheet) {
|
||||
elem.styleSheet.cssText = css;
|
||||
} else {
|
||||
elem.appendChild(document.createTextNode(css));
|
||||
}
|
||||
var head = document.head || document.getElementsByTagName('head')[0];
|
||||
head.appendChild(elem);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
107
v2/internal/runtime/js/core/window.js
Normal file
107
v2/internal/runtime/js/core/window.js
Normal file
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
_ __ _ __
|
||||
| | / /___ _(_) /____
|
||||
| | /| / / __ `/ / / ___/
|
||||
| |/ |/ / /_/ / / (__ )
|
||||
|__/|__/\__,_/_/_/____/
|
||||
The lightweight framework for web-like apps
|
||||
(c) Lea Anthony 2019-present
|
||||
*/
|
||||
|
||||
/* jshint esversion: 6 */
|
||||
|
||||
import { SendMessage } from 'ipc';
|
||||
|
||||
/**
|
||||
* Place the window in the center of the screen
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export function Center() {
|
||||
SendMessage('Wc');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Size of the window
|
||||
*
|
||||
* @export
|
||||
* @param {number} width
|
||||
* @param {number} height
|
||||
*/
|
||||
export function SetSize(width, height) {
|
||||
SendMessage('Ws:' + width + ':' + height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Position of the window
|
||||
*
|
||||
* @export
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
*/
|
||||
export function SetPosition(x, y) {
|
||||
SendMessage('Wp:' + x + ':' + y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the Window
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export function Hide() {
|
||||
SendMessage('WH');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the Window
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export function Show() {
|
||||
SendMessage('WS');
|
||||
}
|
||||
|
||||
/**
|
||||
* Maximise the Window
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export function Maximise() {
|
||||
SendMessage('WM');
|
||||
}
|
||||
|
||||
/**
|
||||
* Unmaximise the Window
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export function Unmaximise() {
|
||||
SendMessage('WU');
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimise the Window
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export function Minimise() {
|
||||
SendMessage('Wm');
|
||||
}
|
||||
|
||||
/**
|
||||
* Unminimise the Window
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export function Unminimise() {
|
||||
SendMessage('Wu');
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the Window
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export function Close() {
|
||||
SendMessage('WC');
|
||||
}
|
||||
41
v2/internal/runtime/js/desktop/darwin.js
Normal file
41
v2/internal/runtime/js/desktop/darwin.js
Normal file
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
_ __ _ __
|
||||
| | / /___ _(_) /____
|
||||
| | /| / / __ `/ / / ___/
|
||||
| |/ |/ / /_/ / / (__ )
|
||||
|__/|__/\__,_/_/_/____/
|
||||
The lightweight framework for web-like apps
|
||||
(c) Lea Anthony 2019-present
|
||||
*/
|
||||
/* jshint esversion: 6 */
|
||||
|
||||
/**
|
||||
* Initialises platform specific code
|
||||
*/
|
||||
|
||||
export const System = {
|
||||
Platform: "darwin",
|
||||
AppType: "desktop"
|
||||
}
|
||||
|
||||
export function SendMessage(message) {
|
||||
window.webkit.messageHandlers.external.postMessage(message);
|
||||
}
|
||||
|
||||
export function Init() {
|
||||
|
||||
// Setup drag handler
|
||||
// Based on code from: https://github.com/patr0nus/DeskGap
|
||||
window.addEventListener('mousedown', function (e) {
|
||||
var currentElement = e.target;
|
||||
while (currentElement != null) {
|
||||
if (currentElement.hasAttribute('data-wails-no-drag')) {
|
||||
break;
|
||||
} else if (currentElement.hasAttribute('data-wails-drag')) {
|
||||
window.webkit.messageHandlers.windowDrag.postMessage(null);
|
||||
break;
|
||||
}
|
||||
currentElement = currentElement.parentElement;
|
||||
}
|
||||
});
|
||||
}
|
||||
41
v2/internal/runtime/js/desktop/ipc.js
Normal file
41
v2/internal/runtime/js/desktop/ipc.js
Normal file
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
_ __ _ __
|
||||
| | / /___ _(_) /____
|
||||
| | /| / / __ `/ / / ___/
|
||||
| |/ |/ / /_/ / / (__ )
|
||||
|__/|__/\__,_/_/_/____/
|
||||
The lightweight framework for web-like apps
|
||||
(c) Lea Anthony 2019-present
|
||||
*/
|
||||
/* jshint esversion: 6 */
|
||||
|
||||
import * as Platform from 'platform';
|
||||
|
||||
// IPC Listeners
|
||||
var listeners = [];
|
||||
|
||||
/**
|
||||
* Adds a listener to IPC messages
|
||||
* @param {function} callback
|
||||
*/
|
||||
export function AddIPCListener(callback) {
|
||||
listeners.push(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* SendMessage sends the given message to the backend
|
||||
*
|
||||
* @param {string} message
|
||||
*/
|
||||
export function SendMessage(message) {
|
||||
|
||||
// Call Platform specific invoke method
|
||||
Platform.SendMessage(message);
|
||||
|
||||
// Also send to listeners
|
||||
if (listeners.length > 0) {
|
||||
for (var i = 0; i < listeners.length; i++) {
|
||||
listeners[i](message);
|
||||
}
|
||||
}
|
||||
}
|
||||
41
v2/internal/runtime/js/desktop/linux.js
Normal file
41
v2/internal/runtime/js/desktop/linux.js
Normal file
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
_ __ _ __
|
||||
| | / /___ _(_) /____
|
||||
| | /| / / __ `/ / / ___/
|
||||
| |/ |/ / /_/ / / (__ )
|
||||
|__/|__/\__,_/_/_/____/
|
||||
The lightweight framework for web-like apps
|
||||
(c) Lea Anthony 2019-present
|
||||
*/
|
||||
/* jshint esversion: 6 */
|
||||
|
||||
/**
|
||||
* Initialises platform specific code
|
||||
*/
|
||||
|
||||
export const System = {
|
||||
Platform: "linux",
|
||||
AppType: "desktop"
|
||||
}
|
||||
|
||||
export function SendMessage(message) {
|
||||
window.webkit.messageHandlers.external.postMessage(message);
|
||||
}
|
||||
|
||||
export function Init() {
|
||||
|
||||
// Setup drag handler
|
||||
// Based on code from: https://github.com/patr0nus/DeskGap
|
||||
window.addEventListener('mousedown', function (e) {
|
||||
var currentElement = e.target;
|
||||
while (currentElement != null) {
|
||||
if (currentElement.hasAttribute('data-wails-no-drag')) {
|
||||
break;
|
||||
} else if (currentElement.hasAttribute('data-wails-drag')) {
|
||||
window.webkit.messageHandlers.windowDrag.postMessage(null);
|
||||
break;
|
||||
}
|
||||
currentElement = currentElement.parentElement;
|
||||
}
|
||||
});
|
||||
}
|
||||
5988
v2/internal/runtime/js/package-lock.json
generated
Normal file
5988
v2/internal/runtime/js/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user