mirror of
https://github.com/tauri-apps/tauri.git
synced 2024-12-24 19:25:12 +03:00
feat(scaffolding) folders, templates, rust, c, node WOW
* feat(proton) initial packages for webview and binding-rust * feat(proton) new folder structure * chore(compliance) readmes and licenses * chore(npm) create package.json * chore(proton) rename packages and create lib/rust * chore(proton) create templates directory * feat(rust) rustfmt tab_spaces = 2 * feat(rust) run fmt and fix template structure * chore(npm) update package - package name (@quasar/proton) - node 10, npm 6.6, yarn 1.17.3 (security) - .gitignore - .npmignore - add docs and spec dirs Signed-off-by: Daniel Thompson-Yvetot <denjell@quasar.dev>
This commit is contained in:
parent
d7308a1a27
commit
85854c4391
61
.gitignore
vendored
Normal file
61
.gitignore
vendored
Normal file
@ -0,0 +1,61 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Typescript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
|
||||
/.vs
|
||||
.DS_Store
|
||||
.Thumbs.db
|
||||
*.sublime*
|
||||
.idea/
|
||||
debug.log
|
||||
package-lock.json
|
||||
.vscode/settings.json
|
11
.npmignore
Normal file
11
.npmignore
Normal file
@ -0,0 +1,11 @@
|
||||
test
|
||||
bindings
|
||||
docs
|
||||
lib
|
||||
node_modules
|
||||
spec
|
||||
ui
|
||||
.git
|
||||
.github
|
||||
.idea
|
||||
SECURITY.md
|
2
LICENSE
2
LICENSE
@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Quasar Framework
|
||||
Copyright (c) 2017 - Present Quasar Framework Contributors, Boscop, Serge Zaitsev
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
11
README.md
11
README.md
@ -117,7 +117,8 @@ This has been done with our best attempt at due diligence and in
|
||||
respect of the original authors. Thankyou - this project would never have
|
||||
been possible without your amazing contribution to open-source and we are
|
||||
honoured to carry the torch further. Of special note:
|
||||
- [zserge](https://github.com/zserge) for the original webview approach
|
||||
- [zserge](https://github.com/zserge) for the original webview approach and
|
||||
go bindings
|
||||
- [Boscop](https://github.com/Boscop) for the Rust Bindings
|
||||
|
||||
## Documentation
|
||||
@ -149,13 +150,9 @@ Thank you to all the people who already <a href="https://github.com/quasarframew
|
||||
quasarframework/proton is following [Semantic Versioning 2.0](https://semver.org/).
|
||||
|
||||
## Licenses
|
||||
Code: (c) 2019 - Daniel Thompson-Yvetot, Razvan Stoenescu, Lucas Nogueira.
|
||||
MIT and where applicable Apache
|
||||
Code: (c) 2019 - Daniel Thompson-Yvetot, Razvan Stoenescu, Lucas Nogueira, Boscop, Serge Zaitsev.
|
||||
MIT
|
||||
|
||||
Logo: CC-BY-NC-ND
|
||||
- Original Proton Logo Design by [Daniel Thompson-Yvetot](https://github.com/nothingismagick)
|
||||
- Based on the prior work by [Emanuele Bertoldi](https://github.com/zuck)
|
||||
|
||||
Note: This license notice will not be complete until we have performed an
|
||||
upstream audit. If you feel that your name should be listed here, please
|
||||
create a PR to this file and provide references so we can fact-check. Thanks!
|
||||
|
21
bindings/go/LICENSE
Normal file
21
bindings/go/LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 Serge Zaitsev
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
1
bindings/go/README.md
Normal file
1
bindings/go/README.md
Normal file
@ -0,0 +1 @@
|
||||
# go-bindings
|
572
bindings/go/proton.go
Executable file
572
bindings/go/proton.go
Executable file
@ -0,0 +1,572 @@
|
||||
//
|
||||
// Package proton implements Go bindings to Proton UI.
|
||||
//
|
||||
// Bindings closely repeat the C APIs and include both, a simplified
|
||||
// single-function API to just open a full-screen webview window, and a more
|
||||
// advanced and featureful set of APIs, including Go-to-JavaScript bindings.
|
||||
//
|
||||
// The library uses gtk-webkit, Cocoa/Webkit and MSHTML (IE8..11) as a browser
|
||||
// engine and supports Linux, MacOS and Windows 7..10 respectively.
|
||||
//
|
||||
package proton
|
||||
|
||||
/*
|
||||
#cgo linux openbsd freebsd CFLAGS: -DWEBVIEW_GTK=1
|
||||
#cgo linux openbsd freebsd pkg-config: gtk+-3.0 webkit2gtk-4.0
|
||||
|
||||
#cgo windows CFLAGS: -DWEBVIEW_WINAPI=1
|
||||
#cgo windows LDFLAGS: -lole32 -lcomctl32 -loleaut32 -luuid -lgdi32
|
||||
|
||||
#cgo darwin CFLAGS: -DWEBVIEW_COCOA=1
|
||||
#cgo darwin LDFLAGS: -framework WebKit
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
#define WEBVIEW_STATIC
|
||||
#define WEBVIEW_IMPLEMENTATION
|
||||
#include "webview.h"
|
||||
|
||||
extern void _webviewExternalInvokeCallback(void *, void *);
|
||||
|
||||
static inline void CgoWebViewFree(void *w) {
|
||||
free((void *)((struct webview *)w)->title);
|
||||
free((void *)((struct webview *)w)->url);
|
||||
free(w);
|
||||
}
|
||||
|
||||
static inline void *CgoWebViewCreate(int width, int height, char *title, char *url, int resizable, int debug) {
|
||||
struct webview *w = (struct webview *) calloc(1, sizeof(*w));
|
||||
w->width = width;
|
||||
w->height = height;
|
||||
w->title = title;
|
||||
w->url = url;
|
||||
w->resizable = resizable;
|
||||
w->debug = debug;
|
||||
w->external_invoke_cb = (webview_external_invoke_cb_t) _webviewExternalInvokeCallback;
|
||||
if (webview_init(w) != 0) {
|
||||
CgoWebViewFree(w);
|
||||
return NULL;
|
||||
}
|
||||
return (void *)w;
|
||||
}
|
||||
|
||||
static inline int CgoWebViewLoop(void *w, int blocking) {
|
||||
return webview_loop((struct webview *)w, blocking);
|
||||
}
|
||||
|
||||
static inline void CgoWebViewTerminate(void *w) {
|
||||
webview_terminate((struct webview *)w);
|
||||
}
|
||||
|
||||
static inline void CgoWebViewExit(void *w) {
|
||||
webview_exit((struct webview *)w);
|
||||
}
|
||||
|
||||
static inline void CgoWebViewSetTitle(void *w, char *title) {
|
||||
webview_set_title((struct webview *)w, title);
|
||||
}
|
||||
|
||||
static inline void CgoWebViewSetFullscreen(void *w, int fullscreen) {
|
||||
webview_set_fullscreen((struct webview *)w, fullscreen);
|
||||
}
|
||||
|
||||
static inline void CgoWebViewSetColor(void *w, uint8_t r, uint8_t g, uint8_t b, uint8_t a) {
|
||||
webview_set_color((struct webview *)w, r, g, b, a);
|
||||
}
|
||||
|
||||
static inline void CgoDialog(void *w, int dlgtype, int flags,
|
||||
char *title, char *arg, char *res, size_t ressz) {
|
||||
webview_dialog(w, dlgtype, flags,
|
||||
(const char*)title, (const char*) arg, res, ressz);
|
||||
}
|
||||
|
||||
static inline int CgoWebViewEval(void *w, char *js) {
|
||||
return webview_eval((struct webview *)w, js);
|
||||
}
|
||||
|
||||
static inline void CgoWebViewInjectCSS(void *w, char *css) {
|
||||
webview_inject_css((struct webview *)w, css);
|
||||
}
|
||||
|
||||
extern void _webviewDispatchGoCallback(void *);
|
||||
static inline void _webview_dispatch_cb(struct webview *w, void *arg) {
|
||||
_webviewDispatchGoCallback(arg);
|
||||
}
|
||||
static inline void CgoWebViewDispatch(void *w, uintptr_t arg) {
|
||||
webview_dispatch((struct webview *)w, _webview_dispatch_cb, (void *)arg);
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"sync"
|
||||
"unicode"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Ensure that main.main is called from the main thread
|
||||
runtime.LockOSThread()
|
||||
}
|
||||
|
||||
// Open is a simplified API to open a single native window with a full-size webview in
|
||||
// it. It can be helpful if you want to communicate with the core app using XHR
|
||||
// or WebSockets (as opposed to using JavaScript bindings).
|
||||
//
|
||||
// Window appearance can be customized using title, width, height and resizable parameters.
|
||||
// URL must be provided and can user either a http or https protocol, or be a
|
||||
// local file:// URL. On some platforms "data:" URLs are also supported
|
||||
// (Linux/MacOS).
|
||||
func Open(title, url string, w, h int, resizable bool) error {
|
||||
titleStr := C.CString(title)
|
||||
defer C.free(unsafe.Pointer(titleStr))
|
||||
urlStr := C.CString(url)
|
||||
defer C.free(unsafe.Pointer(urlStr))
|
||||
resize := C.int(0)
|
||||
if resizable {
|
||||
resize = C.int(1)
|
||||
}
|
||||
|
||||
r := C.webview(titleStr, urlStr, C.int(w), C.int(h), resize)
|
||||
if r != 0 {
|
||||
return errors.New("failed to create webview")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Debug prints a debug string using stderr on Linux/BSD, NSLog on MacOS and
|
||||
// OutputDebugString on Windows.
|
||||
func Debug(a ...interface{}) {
|
||||
s := C.CString(fmt.Sprint(a...))
|
||||
defer C.free(unsafe.Pointer(s))
|
||||
C.webview_print_log(s)
|
||||
}
|
||||
|
||||
// Debugf prints a formatted debug string using stderr on Linux/BSD, NSLog on
|
||||
// MacOS and OutputDebugString on Windows.
|
||||
func Debugf(format string, a ...interface{}) {
|
||||
s := C.CString(fmt.Sprintf(format, a...))
|
||||
defer C.free(unsafe.Pointer(s))
|
||||
C.webview_print_log(s)
|
||||
}
|
||||
|
||||
// ExternalInvokeCallbackFunc is a function type that is called every time
|
||||
// "window.external.invoke()" is called from JavaScript. Data is the only
|
||||
// obligatory string parameter passed into the "invoke(data)" function from
|
||||
// JavaScript. To pass more complex data serialized JSON or base64 encoded
|
||||
// string can be used.
|
||||
type ExternalInvokeCallbackFunc func(w WebView, data string)
|
||||
|
||||
// Settings is a set of parameters to customize the initial WebView appearance
|
||||
// and behavior. It is passed into the webview.New() constructor.
|
||||
type Settings struct {
|
||||
// WebView main window title
|
||||
Title string
|
||||
// URL to open in a webview
|
||||
URL string
|
||||
// Window width in pixels
|
||||
Width int
|
||||
// Window height in pixels
|
||||
Height int
|
||||
// Allows/disallows window resizing
|
||||
Resizable bool
|
||||
// Enable debugging tools (Linux/BSD/MacOS, on Windows use Firebug)
|
||||
Debug bool
|
||||
// A callback that is executed when JavaScript calls "window.external.invoke()"
|
||||
ExternalInvokeCallback ExternalInvokeCallbackFunc
|
||||
}
|
||||
|
||||
// WebView is an interface that wraps the basic methods for controlling the UI
|
||||
// loop, handling multithreading and providing JavaScript bindings.
|
||||
type WebView interface {
|
||||
// Run() starts the main UI loop until the user closes the webview window or
|
||||
// Terminate() is called.
|
||||
Run()
|
||||
// Loop() runs a single iteration of the main UI.
|
||||
Loop(blocking bool) bool
|
||||
// SetTitle() changes window title. This method must be called from the main
|
||||
// thread only. See Dispatch() for more details.
|
||||
SetTitle(title string)
|
||||
// SetFullscreen() controls window full-screen mode. This method must be
|
||||
// called from the main thread only. See Dispatch() for more details.
|
||||
SetFullscreen(fullscreen bool)
|
||||
// SetColor() changes window background color. This method must be called from
|
||||
// the main thread only. See Dispatch() for more details.
|
||||
SetColor(r, g, b, a uint8)
|
||||
// Eval() evaluates an arbitrary JS code inside the webview. This method must
|
||||
// be called from the main thread only. See Dispatch() for more details.
|
||||
Eval(js string) error
|
||||
// InjectJS() injects an arbitrary block of CSS code using the JS API. This
|
||||
// method must be called from the main thread only. See Dispatch() for more
|
||||
// details.
|
||||
InjectCSS(css string)
|
||||
// Dialog() opens a system dialog of the given type and title. String
|
||||
// argument can be provided for certain dialogs, such as alert boxes. For
|
||||
// alert boxes argument is a message inside the dialog box.
|
||||
Dialog(dlgType DialogType, flags int, title string, arg string) string
|
||||
// Terminate() breaks the main UI loop. This method must be called from the main thread
|
||||
// only. See Dispatch() for more details.
|
||||
Terminate()
|
||||
// Dispatch() schedules some arbitrary function to be executed on the main UI
|
||||
// thread. This may be helpful if you want to run some JavaScript from
|
||||
// background threads/goroutines, or to terminate the app.
|
||||
Dispatch(func())
|
||||
// Exit() closes the window and cleans up the resources. Use Terminate() to
|
||||
// forcefully break out of the main UI loop.
|
||||
Exit()
|
||||
// Bind() registers a binding between a given value and a JavaScript object with the
|
||||
// given name. A value must be a struct or a struct pointer. All methods are
|
||||
// available under their camel-case names, starting with a lower-case letter,
|
||||
// e.g. "FooBar" becomes "fooBar" in JavaScript.
|
||||
// Bind() returns a function that updates JavaScript object with the current
|
||||
// Go value. You only need to call it if you change Go value asynchronously.
|
||||
Bind(name string, v interface{}) (sync func(), err error)
|
||||
}
|
||||
|
||||
// DialogType is an enumeration of all supported system dialog types
|
||||
type DialogType int
|
||||
|
||||
const (
|
||||
// DialogTypeOpen is a system file open dialog
|
||||
DialogTypeOpen DialogType = iota
|
||||
// DialogTypeSave is a system file save dialog
|
||||
DialogTypeSave
|
||||
// DialogTypeAlert is a system alert dialog (message box)
|
||||
DialogTypeAlert
|
||||
)
|
||||
|
||||
const (
|
||||
// DialogFlagFile is a normal file picker dialog
|
||||
DialogFlagFile = C.WEBVIEW_DIALOG_FLAG_FILE
|
||||
// DialogFlagDirectory is an open directory dialog
|
||||
DialogFlagDirectory = C.WEBVIEW_DIALOG_FLAG_DIRECTORY
|
||||
// DialogFlagInfo is an info alert dialog
|
||||
DialogFlagInfo = C.WEBVIEW_DIALOG_FLAG_INFO
|
||||
// DialogFlagWarning is a warning alert dialog
|
||||
DialogFlagWarning = C.WEBVIEW_DIALOG_FLAG_WARNING
|
||||
// DialogFlagError is an error dialog
|
||||
DialogFlagError = C.WEBVIEW_DIALOG_FLAG_ERROR
|
||||
)
|
||||
|
||||
var (
|
||||
m sync.Mutex
|
||||
index uintptr
|
||||
fns = map[uintptr]func(){}
|
||||
cbs = map[WebView]ExternalInvokeCallbackFunc{}
|
||||
)
|
||||
|
||||
type webview struct {
|
||||
w unsafe.Pointer
|
||||
}
|
||||
|
||||
var _ WebView = &webview{}
|
||||
|
||||
func boolToInt(b bool) int {
|
||||
if b {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// New creates and opens a new webview window using the given settings. The
|
||||
// returned object implements the WebView interface. This function returns nil
|
||||
// if a window can not be created.
|
||||
func New(settings Settings) WebView {
|
||||
if settings.Width == 0 {
|
||||
settings.Width = 640
|
||||
}
|
||||
if settings.Height == 0 {
|
||||
settings.Height = 480
|
||||
}
|
||||
if settings.Title == "" {
|
||||
settings.Title = "WebView"
|
||||
}
|
||||
w := &webview{}
|
||||
w.w = C.CgoWebViewCreate(C.int(settings.Width), C.int(settings.Height),
|
||||
C.CString(settings.Title), C.CString(settings.URL),
|
||||
C.int(boolToInt(settings.Resizable)), C.int(boolToInt(settings.Debug)))
|
||||
m.Lock()
|
||||
if settings.ExternalInvokeCallback != nil {
|
||||
cbs[w] = settings.ExternalInvokeCallback
|
||||
} else {
|
||||
cbs[w] = func(w WebView, data string) {}
|
||||
}
|
||||
m.Unlock()
|
||||
return w
|
||||
}
|
||||
|
||||
func (w *webview) Loop(blocking bool) bool {
|
||||
block := C.int(0)
|
||||
if blocking {
|
||||
block = 1
|
||||
}
|
||||
return C.CgoWebViewLoop(w.w, block) == 0
|
||||
}
|
||||
|
||||
func (w *webview) Run() {
|
||||
for w.Loop(true) {
|
||||
}
|
||||
}
|
||||
|
||||
func (w *webview) Exit() {
|
||||
C.CgoWebViewExit(w.w)
|
||||
}
|
||||
|
||||
func (w *webview) Dispatch(f func()) {
|
||||
m.Lock()
|
||||
for ; fns[index] != nil; index++ {
|
||||
}
|
||||
fns[index] = f
|
||||
m.Unlock()
|
||||
C.CgoWebViewDispatch(w.w, C.uintptr_t(index))
|
||||
}
|
||||
|
||||
func (w *webview) SetTitle(title string) {
|
||||
p := C.CString(title)
|
||||
defer C.free(unsafe.Pointer(p))
|
||||
C.CgoWebViewSetTitle(w.w, p)
|
||||
}
|
||||
|
||||
func (w *webview) SetColor(r, g, b, a uint8) {
|
||||
C.CgoWebViewSetColor(w.w, C.uint8_t(r), C.uint8_t(g), C.uint8_t(b), C.uint8_t(a))
|
||||
}
|
||||
|
||||
func (w *webview) SetFullscreen(fullscreen bool) {
|
||||
C.CgoWebViewSetFullscreen(w.w, C.int(boolToInt(fullscreen)))
|
||||
}
|
||||
|
||||
func (w *webview) Dialog(dlgType DialogType, flags int, title string, arg string) string {
|
||||
const maxPath = 4096
|
||||
titlePtr := C.CString(title)
|
||||
defer C.free(unsafe.Pointer(titlePtr))
|
||||
argPtr := C.CString(arg)
|
||||
defer C.free(unsafe.Pointer(argPtr))
|
||||
resultPtr := (*C.char)(C.calloc((C.size_t)(unsafe.Sizeof((*C.char)(nil))), (C.size_t)(maxPath)))
|
||||
defer C.free(unsafe.Pointer(resultPtr))
|
||||
C.CgoDialog(w.w, C.int(dlgType), C.int(flags), titlePtr,
|
||||
argPtr, resultPtr, C.size_t(maxPath))
|
||||
return C.GoString(resultPtr)
|
||||
}
|
||||
|
||||
func (w *webview) Eval(js string) error {
|
||||
p := C.CString(js)
|
||||
defer C.free(unsafe.Pointer(p))
|
||||
switch C.CgoWebViewEval(w.w, p) {
|
||||
case -1:
|
||||
return errors.New("evaluation failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *webview) InjectCSS(css string) {
|
||||
p := C.CString(css)
|
||||
defer C.free(unsafe.Pointer(p))
|
||||
C.CgoWebViewInjectCSS(w.w, p)
|
||||
}
|
||||
|
||||
func (w *webview) Terminate() {
|
||||
C.CgoWebViewTerminate(w.w)
|
||||
}
|
||||
|
||||
//export _webviewDispatchGoCallback
|
||||
func _webviewDispatchGoCallback(index unsafe.Pointer) {
|
||||
var f func()
|
||||
m.Lock()
|
||||
f = fns[uintptr(index)]
|
||||
delete(fns, uintptr(index))
|
||||
m.Unlock()
|
||||
f()
|
||||
}
|
||||
|
||||
//export _webviewExternalInvokeCallback
|
||||
func _webviewExternalInvokeCallback(w unsafe.Pointer, data unsafe.Pointer) {
|
||||
m.Lock()
|
||||
var (
|
||||
cb ExternalInvokeCallbackFunc
|
||||
wv WebView
|
||||
)
|
||||
for wv, cb = range cbs {
|
||||
if wv.(*webview).w == w {
|
||||
break
|
||||
}
|
||||
}
|
||||
m.Unlock()
|
||||
cb(wv, C.GoString((*C.char)(data)))
|
||||
}
|
||||
|
||||
var bindTmpl = template.Must(template.New("").Parse(`
|
||||
if (typeof {{.Name}} === 'undefined') {
|
||||
{{.Name}} = {};
|
||||
}
|
||||
{{ range .Methods }}
|
||||
{{$.Name}}.{{.JSName}} = function({{.JSArgs}}) {
|
||||
window.external.invoke(JSON.stringify({scope: "{{$.Name}}", method: "{{.Name}}", params: [{{.JSArgs}}]}));
|
||||
};
|
||||
{{ end }}
|
||||
`))
|
||||
|
||||
type binding struct {
|
||||
Value interface{}
|
||||
Name string
|
||||
Methods []methodInfo
|
||||
}
|
||||
|
||||
func newBinding(name string, v interface{}) (*binding, error) {
|
||||
methods, err := getMethods(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &binding{Name: name, Value: v, Methods: methods}, nil
|
||||
}
|
||||
|
||||
func (b *binding) JS() (string, error) {
|
||||
js := &bytes.Buffer{}
|
||||
err := bindTmpl.Execute(js, b)
|
||||
return js.String(), err
|
||||
}
|
||||
|
||||
func (b *binding) Sync() (string, error) {
|
||||
js, err := json.Marshal(b.Value)
|
||||
if err == nil {
|
||||
return fmt.Sprintf("%[1]s.data=%[2]s;if(%[1]s.render){%[1]s.render(%[2]s);}", b.Name, string(js)), nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
func (b *binding) Call(js string) bool {
|
||||
type rpcCall struct {
|
||||
Scope string `json:"scope"`
|
||||
Method string `json:"method"`
|
||||
Params []interface{} `json:"params"`
|
||||
}
|
||||
|
||||
rpc := rpcCall{}
|
||||
if err := json.Unmarshal([]byte(js), &rpc); err != nil {
|
||||
return false
|
||||
}
|
||||
if rpc.Scope != b.Name {
|
||||
return false
|
||||
}
|
||||
var mi *methodInfo
|
||||
for i := 0; i < len(b.Methods); i++ {
|
||||
if b.Methods[i].Name == rpc.Method {
|
||||
mi = &b.Methods[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if mi == nil {
|
||||
return false
|
||||
}
|
||||
args := make([]reflect.Value, mi.Arity(), mi.Arity())
|
||||
for i := 0; i < mi.Arity(); i++ {
|
||||
val := reflect.ValueOf(rpc.Params[i])
|
||||
arg := mi.Value.Type().In(i)
|
||||
u := reflect.New(arg)
|
||||
if b, err := json.Marshal(val.Interface()); err == nil {
|
||||
if err = json.Unmarshal(b, u.Interface()); err == nil {
|
||||
args[i] = reflect.Indirect(u)
|
||||
}
|
||||
}
|
||||
if !args[i].IsValid() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
mi.Value.Call(args)
|
||||
return true
|
||||
}
|
||||
|
||||
type methodInfo struct {
|
||||
Name string
|
||||
Value reflect.Value
|
||||
}
|
||||
|
||||
func (mi methodInfo) Arity() int { return mi.Value.Type().NumIn() }
|
||||
|
||||
func (mi methodInfo) JSName() string {
|
||||
r := []rune(mi.Name)
|
||||
if len(r) > 0 {
|
||||
r[0] = unicode.ToLower(r[0])
|
||||
}
|
||||
return string(r)
|
||||
}
|
||||
|
||||
func (mi methodInfo) JSArgs() (js string) {
|
||||
for i := 0; i < mi.Arity(); i++ {
|
||||
if i > 0 {
|
||||
js = js + ","
|
||||
}
|
||||
js = js + fmt.Sprintf("a%d", i)
|
||||
}
|
||||
return js
|
||||
}
|
||||
|
||||
func getMethods(obj interface{}) ([]methodInfo, error) {
|
||||
p := reflect.ValueOf(obj)
|
||||
v := reflect.Indirect(p)
|
||||
t := reflect.TypeOf(obj)
|
||||
if t == nil {
|
||||
return nil, errors.New("object can not be nil")
|
||||
}
|
||||
k := t.Kind()
|
||||
if k == reflect.Ptr {
|
||||
k = v.Type().Kind()
|
||||
}
|
||||
if k != reflect.Struct {
|
||||
return nil, errors.New("must be a struct or a pointer to a struct")
|
||||
}
|
||||
|
||||
methods := []methodInfo{}
|
||||
for i := 0; i < t.NumMethod(); i++ {
|
||||
method := t.Method(i)
|
||||
if !unicode.IsUpper([]rune(method.Name)[0]) {
|
||||
continue
|
||||
}
|
||||
mi := methodInfo{
|
||||
Name: method.Name,
|
||||
Value: p.MethodByName(method.Name),
|
||||
}
|
||||
methods = append(methods, mi)
|
||||
}
|
||||
|
||||
return methods, nil
|
||||
}
|
||||
|
||||
func (w *webview) Bind(name string, v interface{}) (sync func(), err error) {
|
||||
b, err := newBinding(name, v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
js, err := b.JS()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sync = func() {
|
||||
if js, err := b.Sync(); err != nil {
|
||||
log.Println(err)
|
||||
} else {
|
||||
w.Eval(js)
|
||||
}
|
||||
}
|
||||
|
||||
m.Lock()
|
||||
cb := cbs[w]
|
||||
cbs[w] = func(w WebView, data string) {
|
||||
if ok := b.Call(data); ok {
|
||||
sync()
|
||||
} else {
|
||||
cb(w, data)
|
||||
}
|
||||
}
|
||||
m.Unlock()
|
||||
|
||||
w.Eval(js)
|
||||
sync()
|
||||
return sync, nil
|
||||
}
|
97
bindings/go/proton_test.go
Normal file
97
bindings/go/proton_test.go
Normal file
@ -0,0 +1,97 @@
|
||||
package proton
|
||||
|
||||
import (
|
||||
"image"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type foo struct {
|
||||
Result interface{}
|
||||
}
|
||||
|
||||
func (f *foo) Foo1(a int, b float32) {
|
||||
f.Result = float64(a) + float64(b)
|
||||
}
|
||||
func (f *foo) Foo2(a []int, b [3]float32, c map[int]int) {
|
||||
f.Result = map[string]interface{}{"a": a, "b": b, "c": c}
|
||||
}
|
||||
func (f *foo) Foo3(a []image.Point, b struct{ Z int }) {
|
||||
f.Result = map[string]interface{}{"a": a, "b": b}
|
||||
}
|
||||
|
||||
func TestBadBinding(t *testing.T) {
|
||||
x := 123
|
||||
for _, v := range []interface{}{
|
||||
nil,
|
||||
true,
|
||||
123,
|
||||
123.4,
|
||||
"hello",
|
||||
'a',
|
||||
make(chan struct{}, 0),
|
||||
func() {},
|
||||
map[string]string{},
|
||||
[]int{},
|
||||
[3]int{0, 0, 0},
|
||||
&x,
|
||||
} {
|
||||
if _, err := newBinding("test", v); err == nil {
|
||||
t.Errorf("should return an error: %#v", v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBindingCall(t *testing.T) {
|
||||
foo := &foo{}
|
||||
b, err := newBinding("test", foo)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Run("Primitives", func(t *testing.T) {
|
||||
if !b.Call(`{"scope":"test","method":"Foo1","params":[3,4.5]}`) {
|
||||
t.Fatal()
|
||||
}
|
||||
if foo.Result.(float64) != 7.5 {
|
||||
t.Fatal(foo)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Collections", func(t *testing.T) {
|
||||
// Call with slices, arrays and maps
|
||||
if !b.Call(`{"scope":"test","method":"Foo2","params":[[1,2,3],[4.5,4.6,4.7],{"1":2,"3":4}]}`) {
|
||||
t.Fatal()
|
||||
}
|
||||
m := foo.Result.(map[string]interface{})
|
||||
if ints := m["a"].([]int); ints[0] != 1 || ints[1] != 2 || ints[2] != 3 {
|
||||
t.Fatal(foo)
|
||||
}
|
||||
if floats := m["b"].([3]float32); floats[0] != 4.5 || floats[1] != 4.6 || floats[2] != 4.7 {
|
||||
t.Fatal(foo)
|
||||
}
|
||||
if dict := m["c"].(map[int]int); len(dict) != 2 || dict[1] != 2 || dict[3] != 4 {
|
||||
t.Fatal(foo)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Structs", func(t *testing.T) {
|
||||
if !b.Call(`{"scope":"test","method":"Foo3","params":[[{"X":1,"Y":2},{"X":3,"Y":4}],{"Z":42}]}`) {
|
||||
t.Fatal()
|
||||
}
|
||||
m := foo.Result.(map[string]interface{})
|
||||
if p := m["a"].([]image.Point); p[0].X != 1 || p[0].Y != 2 || p[1].X != 3 || p[1].Y != 4 {
|
||||
t.Fatal(foo)
|
||||
}
|
||||
if z := m["b"].(struct{ Z int }); z.Z != 42 {
|
||||
t.Fatal(foo)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Errors", func(t *testing.T) {
|
||||
if b.Call(`{"scope":"foo"}`) || b.Call(`{"scope":"test", "method":"Bar"}`) {
|
||||
t.Fatal()
|
||||
}
|
||||
if b.Call(`{"scope":"test","method":"Foo1","params":["3",4.5]}`) {
|
||||
t.Fatal()
|
||||
}
|
||||
})
|
||||
}
|
4
bindings/rust/.gitignore
vendored
Normal file
4
bindings/rust/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
target/
|
||||
**/*.rs.bk
|
||||
Cargo.lock
|
||||
.idea
|
24
bindings/rust/Cargo.toml
Normal file
24
bindings/rust/Cargo.toml
Normal file
@ -0,0 +1,24 @@
|
||||
[package]
|
||||
name = "proton-ui"
|
||||
version = "0.1.0"
|
||||
authors = ["Boscop", "rstoenescu", "nothingismagick", "lucasfernog"]
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/quasarframework/proton"
|
||||
description = "Rust bindings for proton, a toolchain for building more secure native apps that have tiny binaries and are very fast."
|
||||
keywords = ["quasar", "web", "gui", "desktop", "webkit"]
|
||||
categories = ["quasar", "gui", "web-programming", "api-bindings", "rendering", "visualization"]
|
||||
|
||||
[dependencies]
|
||||
urlencoding = "1.0"
|
||||
proton-sys = { path = "proton-sys", version = "0.1.0" }
|
||||
boxfnonce = "0.1"
|
||||
|
||||
[features]
|
||||
default = ["V1_30"]
|
||||
V1_30 = []
|
||||
|
||||
[dev-dependencies]
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
serde_json = "1.0"
|
45
bindings/rust/LICENSE
Normal file
45
bindings/rust/LICENSE
Normal file
@ -0,0 +1,45 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Boscop
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
----
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Quasar Framework
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
1
bindings/rust/README.md
Normal file
1
bindings/rust/README.md
Normal file
@ -0,0 +1 @@
|
||||
# rust bindings
|
22
bindings/rust/proton-sys/Cargo.toml
Normal file
22
bindings/rust/proton-sys/Cargo.toml
Normal file
@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "proton-sys"
|
||||
version = "0.1.0"
|
||||
authors = ["Boscop", "rstoenescu", "nothingismagick", "lucasfernog"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/quasarframework/proton"
|
||||
description = "Rust native ffi bindings for proton UI"
|
||||
keywords = ["quasar", "web", "gui", "desktop", "webkit"]
|
||||
categories = ["quasar", "gui", "web-programming", "api-bindings", "rendering", "visualization"]
|
||||
build = "build.rs"
|
||||
links = "proton"
|
||||
|
||||
[lib]
|
||||
name = "proton_sys"
|
||||
path = "lib.rs"
|
||||
|
||||
[dependencies]
|
||||
bitflags = "1.0"
|
||||
|
||||
[build-dependencies]
|
||||
cc = "1"
|
||||
pkg-config = "0.3"
|
56
bindings/rust/proton-sys/build.rs
Normal file
56
bindings/rust/proton-sys/build.rs
Normal file
@ -0,0 +1,56 @@
|
||||
extern crate cc;
|
||||
extern crate pkg_config;
|
||||
|
||||
use std::{
|
||||
env,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
let proton_path = PathBuf::from("../../../ui");
|
||||
|
||||
let mut build = cc::Build::new();
|
||||
|
||||
build
|
||||
.include(&proton_path)
|
||||
.file("proton.c")
|
||||
.flag_if_supported("-std=c11")
|
||||
.flag_if_supported("-w");
|
||||
|
||||
if env::var("DEBUG").is_err() {
|
||||
build.define("NDEBUG", None);
|
||||
} else {
|
||||
build.define("DEBUG", None);
|
||||
}
|
||||
|
||||
let target = env::var("TARGET").unwrap();
|
||||
|
||||
if target.contains("windows") {
|
||||
build.define("WEBVIEW_WINAPI", None);
|
||||
for &lib in &["ole32", "comctl32", "oleaut32", "uuid", "gdi32"] {
|
||||
println!("cargo:rustc-link-lib={}", lib);
|
||||
}
|
||||
} else if target.contains("linux") || target.contains("bsd") {
|
||||
let webkit = pkg_config::Config::new()
|
||||
.atleast_version("2.8")
|
||||
.probe("webkit2gtk-4.0")
|
||||
.unwrap();
|
||||
|
||||
for path in webkit.include_paths {
|
||||
build.include(path);
|
||||
}
|
||||
build.define("WEBVIEW_GTK", None);
|
||||
} else if target.contains("apple") {
|
||||
build
|
||||
.define("WEBVIEW_COCOA", None)
|
||||
.flag("-x")
|
||||
.flag("objective-c");
|
||||
println!("cargo:rustc-link-lib=framework=Cocoa");
|
||||
println!("cargo:rustc-link-lib=framework=WebKit");
|
||||
} else {
|
||||
panic!("unsupported target");
|
||||
}
|
||||
|
||||
build.compile("proton");
|
||||
}
|
45
bindings/rust/proton-sys/lib.rs
Normal file
45
bindings/rust/proton-sys/lib.rs
Normal file
@ -0,0 +1,45 @@
|
||||
//! Raw FFI bindings to proton UI.
|
||||
|
||||
#[macro_use]
|
||||
extern crate bitflags;
|
||||
|
||||
use std::os::raw::*;
|
||||
|
||||
pub enum CWebView {} // opaque type, only used in ffi pointers
|
||||
|
||||
type ErasedExternalInvokeFn = extern "C" fn(webview: *mut CWebView, arg: *const c_char);
|
||||
type ErasedDispatchFn = extern "C" fn(webview: *mut CWebView, arg: *mut c_void);
|
||||
|
||||
#[repr(C)]
|
||||
pub enum DialogType {
|
||||
Open = 0,
|
||||
Save = 1,
|
||||
Alert = 2,
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
#[repr(C)]
|
||||
pub struct DialogFlags: u32 {
|
||||
const FILE = 0b0000;
|
||||
const DIRECTORY = 0b0001;
|
||||
const INFO = 0b0010;
|
||||
const WARNING = 0b0100;
|
||||
const ERROR = 0b0110;
|
||||
}
|
||||
}
|
||||
|
||||
extern {
|
||||
pub fn wrapper_webview_free(this: *mut CWebView);
|
||||
pub fn wrapper_webview_new(title: *const c_char, url: *const c_char, width: c_int, height: c_int, resizable: c_int, debug: c_int, external_invoke_cb: Option<ErasedExternalInvokeFn>, userdata: *mut c_void) -> *mut CWebView;
|
||||
pub fn webview_loop(this: *mut CWebView, blocking: c_int) -> c_int;
|
||||
pub fn webview_terminate(this: *mut CWebView);
|
||||
pub fn webview_exit(this: *mut CWebView);
|
||||
pub fn wrapper_webview_get_userdata(this: *mut CWebView) -> *mut c_void;
|
||||
pub fn webview_dispatch(this: *mut CWebView, f: Option<ErasedDispatchFn>, arg: *mut c_void);
|
||||
pub fn webview_eval(this: *mut CWebView, js: *const c_char) -> c_int;
|
||||
pub fn webview_inject_css(this: *mut CWebView, css: *const c_char) -> c_int;
|
||||
pub fn webview_set_title(this: *mut CWebView, title: *const c_char);
|
||||
pub fn webview_set_fullscreen(this: *mut CWebView, fullscreen: c_int);
|
||||
pub fn webview_set_color(this: *mut CWebView, red: u8, green: u8, blue: u8, alpha: u8);
|
||||
pub fn webview_dialog(this: *mut CWebView, dialog_type: DialogType, flags: DialogFlags, title: *const c_char, arg: *const c_char, result: *mut c_char, result_size: usize);
|
||||
}
|
28
bindings/rust/proton-sys/proton.c
Normal file
28
bindings/rust/proton-sys/proton.c
Normal file
@ -0,0 +1,28 @@
|
||||
#define WEBVIEW_IMPLEMENTATION
|
||||
#include "proton.h"
|
||||
|
||||
void wrapper_webview_free(struct webview* w) {
|
||||
free(w);
|
||||
}
|
||||
|
||||
struct webview* wrapper_webview_new(const char* title, const char* url, int width, int height, int resizable, int debug, webview_external_invoke_cb_t external_invoke_cb, void* userdata) {
|
||||
struct webview* w = (struct webview*)calloc(1, sizeof(*w));
|
||||
w->width = width;
|
||||
w->height = height;
|
||||
w->title = title;
|
||||
w->url = url;
|
||||
w->resizable = resizable;
|
||||
w->debug = debug;
|
||||
w->external_invoke_cb = external_invoke_cb;
|
||||
w->userdata = userdata;
|
||||
if (webview_init(w) != 0) {
|
||||
wrapper_webview_free(w);
|
||||
return NULL;
|
||||
}
|
||||
return w;
|
||||
}
|
||||
|
||||
void* wrapper_webview_get_userdata(struct webview* w) {
|
||||
return w->userdata;
|
||||
}
|
||||
|
13
bindings/rust/rustfmt.toml
Normal file
13
bindings/rust/rustfmt.toml
Normal file
@ -0,0 +1,13 @@
|
||||
max_width = 100
|
||||
hard_tabs = false
|
||||
tab_spaces = 2
|
||||
newline_style = "Auto"
|
||||
use_small_heuristics = "Default"
|
||||
reorder_imports = true
|
||||
reorder_modules = true
|
||||
remove_nested_parens = true
|
||||
edition = "2015"
|
||||
merge_derives = true
|
||||
use_try_shorthand = false
|
||||
use_field_init_shorthand = false
|
||||
force_explicit_abi = true
|
52
bindings/rust/src/color.rs
Normal file
52
bindings/rust/src/color.rs
Normal file
@ -0,0 +1,52 @@
|
||||
/// An RGBA color.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Color {
|
||||
pub r: u8,
|
||||
pub g: u8,
|
||||
pub b: u8,
|
||||
pub a: u8,
|
||||
}
|
||||
|
||||
impl From<(u8, u8, u8, u8)> for Color {
|
||||
fn from(tuple: (u8, u8, u8, u8)) -> Color {
|
||||
Color {
|
||||
r: tuple.0,
|
||||
g: tuple.1,
|
||||
b: tuple.2,
|
||||
a: tuple.3,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(u8, u8, u8)> for Color {
|
||||
fn from(tuple: (u8, u8, u8)) -> Color {
|
||||
Color {
|
||||
r: tuple.0,
|
||||
g: tuple.1,
|
||||
b: tuple.2,
|
||||
a: 255,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<[u8; 4]> for Color {
|
||||
fn from(array: [u8; 4]) -> Color {
|
||||
Color {
|
||||
r: array[0],
|
||||
g: array[1],
|
||||
b: array[2],
|
||||
a: array[3],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<[u8; 3]> for Color {
|
||||
fn from(array: [u8; 3]) -> Color {
|
||||
Color {
|
||||
r: array[0],
|
||||
g: array[1],
|
||||
b: array[2],
|
||||
a: 255,
|
||||
}
|
||||
}
|
||||
}
|
141
bindings/rust/src/dialog.rs
Normal file
141
bindings/rust/src/dialog.rs
Normal file
@ -0,0 +1,141 @@
|
||||
use ffi::{self, DialogFlags, DialogType};
|
||||
use std::{ffi::CString, path::PathBuf};
|
||||
use {read_str, WVResult, WebView};
|
||||
|
||||
const STR_BUF_SIZE: usize = 4096;
|
||||
|
||||
/// A builder for opening a new dialog window.
|
||||
#[derive(Debug)]
|
||||
pub struct DialogBuilder<'a: 'b, 'b, T: 'a> {
|
||||
webview: &'b mut WebView<'a, T>,
|
||||
}
|
||||
|
||||
impl<'a: 'b, 'b, T: 'a> DialogBuilder<'a, 'b, T> {
|
||||
/// Creates a new dialog builder for a WebView.
|
||||
pub fn new(webview: &'b mut WebView<'a, T>) -> DialogBuilder<'a, 'b, T> {
|
||||
DialogBuilder { webview }
|
||||
}
|
||||
|
||||
fn dialog(
|
||||
&mut self,
|
||||
title: String,
|
||||
arg: String,
|
||||
dialog_type: DialogType,
|
||||
dialog_flags: DialogFlags,
|
||||
) -> WVResult<String> {
|
||||
let mut s = [0u8; STR_BUF_SIZE];
|
||||
|
||||
let title_cstr = CString::new(title)?;
|
||||
let arg_cstr = CString::new(arg)?;
|
||||
|
||||
unsafe {
|
||||
ffi::webview_dialog(
|
||||
self.webview.inner,
|
||||
dialog_type,
|
||||
dialog_flags,
|
||||
title_cstr.as_ptr(),
|
||||
arg_cstr.as_ptr(),
|
||||
s.as_mut_ptr() as _,
|
||||
s.len(),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(read_str(&s))
|
||||
}
|
||||
|
||||
/// Opens a new open file dialog and returns the chosen file path.
|
||||
pub fn open_file<S, P>(&mut self, title: S, default_file: P) -> WVResult<Option<PathBuf>>
|
||||
where
|
||||
S: Into<String>,
|
||||
P: Into<PathBuf>,
|
||||
{
|
||||
self
|
||||
.dialog(
|
||||
title.into(),
|
||||
default_file.into().to_string_lossy().into_owned(),
|
||||
DialogType::Open,
|
||||
DialogFlags::FILE,
|
||||
)
|
||||
.map(|path| {
|
||||
if path.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(PathBuf::from(path))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Opens a new choose directory dialog as returns the chosen directory path.
|
||||
pub fn choose_directory<S, P>(
|
||||
&mut self,
|
||||
title: S,
|
||||
default_directory: P,
|
||||
) -> WVResult<Option<PathBuf>>
|
||||
where
|
||||
S: Into<String>,
|
||||
P: Into<PathBuf>,
|
||||
{
|
||||
self
|
||||
.dialog(
|
||||
title.into(),
|
||||
default_directory.into().to_string_lossy().into_owned(),
|
||||
DialogType::Open,
|
||||
DialogFlags::DIRECTORY,
|
||||
)
|
||||
.map(|path| {
|
||||
if path.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(PathBuf::from(path))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Opens an info alert dialog.
|
||||
pub fn info<TS, MS>(&mut self, title: TS, message: MS) -> WVResult
|
||||
where
|
||||
TS: Into<String>,
|
||||
MS: Into<String>,
|
||||
{
|
||||
self
|
||||
.dialog(
|
||||
title.into(),
|
||||
message.into(),
|
||||
DialogType::Alert,
|
||||
DialogFlags::INFO,
|
||||
)
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
/// Opens a warning alert dialog.
|
||||
pub fn warning<TS, MS>(&mut self, title: TS, message: MS) -> WVResult
|
||||
where
|
||||
TS: Into<String>,
|
||||
MS: Into<String>,
|
||||
{
|
||||
self
|
||||
.dialog(
|
||||
title.into(),
|
||||
message.into(),
|
||||
DialogType::Alert,
|
||||
DialogFlags::WARNING,
|
||||
)
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
/// Opens an error alert dialog.
|
||||
pub fn error<TS, MS>(&mut self, title: TS, message: MS) -> WVResult
|
||||
where
|
||||
TS: Into<String>,
|
||||
MS: Into<String>,
|
||||
{
|
||||
self
|
||||
.dialog(
|
||||
title.into(),
|
||||
message.into(),
|
||||
DialogType::Alert,
|
||||
DialogFlags::ERROR,
|
||||
)
|
||||
.map(|_| ())
|
||||
}
|
||||
}
|
79
bindings/rust/src/error.rs
Normal file
79
bindings/rust/src/error.rs
Normal file
@ -0,0 +1,79 @@
|
||||
use std::{
|
||||
error,
|
||||
ffi::NulError,
|
||||
fmt::{self, Debug, Display},
|
||||
};
|
||||
|
||||
pub trait CustomError: Display + Debug + Send + Sync + 'static {}
|
||||
|
||||
impl<T: Display + Debug + Send + Sync + 'static> CustomError for T {}
|
||||
|
||||
/// A WebView error.
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// While attempting to build a WebView instance, a required field was not initialized.
|
||||
UninitializedField(&'static str),
|
||||
/// An error occurred while initializing a WebView instance.
|
||||
Initialization,
|
||||
/// A nul-byte was found in a provided string.
|
||||
NulByte(NulError),
|
||||
/// An error occurred while evaluating JavaScript in a WebView instance.
|
||||
JsEvaluation,
|
||||
/// An error occurred while injecting CSS into a WebView instance.
|
||||
CssInjection,
|
||||
/// Failure to dispatch a closure to a WebView instance via a handle, likely because the
|
||||
/// WebView was dropped.
|
||||
Dispatch,
|
||||
/// An user-specified error occurred. For use inside invoke and dispatch closures.
|
||||
Custom(Box<CustomError>),
|
||||
}
|
||||
|
||||
impl Error {
|
||||
/// Creates a custom error from a `T: Display + Debug + Send + Sync + 'static`.
|
||||
pub fn custom<E: CustomError>(error: E) -> Error {
|
||||
Error::Custom(Box::new(error))
|
||||
}
|
||||
}
|
||||
|
||||
impl error::Error for Error {
|
||||
fn cause(&self) -> Option<&error::Error> {
|
||||
match self {
|
||||
Error::NulByte(cause) => Some(cause),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "V1_30")]
|
||||
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
|
||||
match self {
|
||||
Error::NulByte(ref cause) => Some(cause),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Error::UninitializedField(field) => write!(f, "Required field uninitialized: {}.", field),
|
||||
Error::Initialization => write!(f, "Webview failed to initialize."),
|
||||
Error::NulByte(cause) => write!(f, "{}", cause),
|
||||
Error::JsEvaluation => write!(f, "Failed to evaluate JavaScript."),
|
||||
Error::CssInjection => write!(f, "Failed to inject CSS."),
|
||||
Error::Dispatch => write!(
|
||||
f,
|
||||
"Closure could not be dispatched. WebView was likely dropped."
|
||||
),
|
||||
Error::Custom(e) => write!(f, "Error: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A WebView result.
|
||||
pub type WVResult<T = ()> = Result<T, Error>;
|
||||
|
||||
impl From<NulError> for Error {
|
||||
fn from(e: NulError) -> Error {
|
||||
Error::NulByte(e)
|
||||
}
|
||||
}
|
81
bindings/rust/src/escape.rs
Normal file
81
bindings/rust/src/escape.rs
Normal file
@ -0,0 +1,81 @@
|
||||
use std::fmt::{self, Write};
|
||||
|
||||
/// Escape a string to pass it into JavaScript.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use web_view::WebView;
|
||||
/// # use std::mem;
|
||||
/// #
|
||||
/// # let mut view: WebView<()> = unsafe { mem::uninitialized() };
|
||||
/// #
|
||||
/// let string = "Hello, world!";
|
||||
///
|
||||
/// // Calls the function callback with "Hello, world!" as its parameter.
|
||||
///
|
||||
/// view.eval(&format!("callback({});", web_view::escape(string)));
|
||||
/// ```
|
||||
pub fn escape(string: &str) -> Escaper {
|
||||
Escaper(string)
|
||||
}
|
||||
|
||||
// "All code points may appear literally in a string literal except for the
|
||||
// closing quote code points, U+005C (REVERSE SOLIDUS), U+000D (CARRIAGE
|
||||
// RETURN), U+2028 (LINE SEPARATOR), U+2029 (PARAGRAPH SEPARATOR), and U+000A
|
||||
// (LINE FEED)." - ES6 Specification
|
||||
|
||||
pub struct Escaper<'a>(&'a str);
|
||||
|
||||
const SPECIAL: &[char] = &[
|
||||
'\n', // U+000A (LINE FEED)
|
||||
'\r', // U+000D (CARRIAGE RETURN)
|
||||
'\'', // U+0027 (APOSTROPHE)
|
||||
'\\', // U+005C (REVERSE SOLIDUS)
|
||||
'\u{2028}', // U+2028 (LINE SEPARATOR)
|
||||
'\u{2029}', // U+2029 (PARAGRAPH SEPARATOR)
|
||||
];
|
||||
|
||||
impl<'a> fmt::Display for Escaper<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let &Escaper(mut string) = self;
|
||||
|
||||
f.write_char('\'')?;
|
||||
|
||||
while !string.is_empty() {
|
||||
if let Some(i) = string.find(SPECIAL) {
|
||||
if i > 0 {
|
||||
f.write_str(&string[..i])?;
|
||||
}
|
||||
|
||||
let mut chars = string[i..].chars();
|
||||
|
||||
f.write_str(match chars.next().unwrap() {
|
||||
'\n' => "\\n",
|
||||
'\r' => "\\r",
|
||||
'\'' => "\\'",
|
||||
'\\' => "\\\\",
|
||||
'\u{2028}' => "\\u2028",
|
||||
'\u{2029}' => "\\u2029",
|
||||
_ => unreachable!(),
|
||||
})?;
|
||||
|
||||
string = chars.as_str();
|
||||
} else {
|
||||
f.write_str(string)?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
f.write_char('\'')?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
let plain = "ABC \n\r' abc \\ \u{2028} \u{2029}123";
|
||||
let escaped = escape(plain).to_string();
|
||||
assert!(escaped == "'ABC \\n\\r\\' abc \\\\ \\u2028 \\u2029123'");
|
||||
}
|
526
bindings/rust/src/lib.rs
Normal file
526
bindings/rust/src/lib.rs
Normal file
@ -0,0 +1,526 @@
|
||||
extern crate boxfnonce;
|
||||
extern crate proton_sys as ffi;
|
||||
extern crate urlencoding;
|
||||
|
||||
mod color;
|
||||
mod dialog;
|
||||
mod error;
|
||||
mod escape;
|
||||
pub use color::Color;
|
||||
pub use dialog::DialogBuilder;
|
||||
pub use error::{CustomError, Error, WVResult};
|
||||
pub use escape::escape;
|
||||
|
||||
use boxfnonce::SendBoxFnOnce;
|
||||
use ffi::*;
|
||||
use std::{
|
||||
ffi::{CStr, CString},
|
||||
marker::PhantomData,
|
||||
mem,
|
||||
os::raw::*,
|
||||
sync::{Arc, RwLock, Weak},
|
||||
};
|
||||
use urlencoding::encode;
|
||||
|
||||
/// Content displayable inside a [`WebView`].
|
||||
///
|
||||
/// # Variants
|
||||
///
|
||||
/// - `Url` - Content to be fetched from a URL.
|
||||
/// - `Html` - A string containing literal HTML.
|
||||
///
|
||||
/// [`WebView`]: struct.WebView.html
|
||||
#[derive(Debug)]
|
||||
pub enum Content<T> {
|
||||
Url(T),
|
||||
Html(T),
|
||||
}
|
||||
|
||||
/// Builder for constructing a [`WebView`] instance.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// extern crate web_view;
|
||||
///
|
||||
/// use web_view::*;
|
||||
///
|
||||
/// fn main() {
|
||||
/// WebViewBuilder::new()
|
||||
/// .title("Minimal webview example")
|
||||
/// .content(Content::Url("https://en.m.wikipedia.org/wiki/Main_Page"))
|
||||
/// .size(800, 600)
|
||||
/// .resizable(true)
|
||||
/// .debug(true)
|
||||
/// .user_data(())
|
||||
/// .invoke_handler(|_webview, _arg| Ok(()))
|
||||
/// .build()
|
||||
/// .unwrap()
|
||||
/// .run()
|
||||
/// .unwrap();
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// [`WebView`]: struct.WebView.html
|
||||
pub struct WebViewBuilder<'a, T: 'a, I, C> {
|
||||
pub title: &'a str,
|
||||
pub content: Option<Content<C>>,
|
||||
pub width: i32,
|
||||
pub height: i32,
|
||||
pub resizable: bool,
|
||||
pub debug: bool,
|
||||
pub invoke_handler: Option<I>,
|
||||
pub user_data: Option<T>,
|
||||
}
|
||||
|
||||
impl<'a, T: 'a, I, C> Default for WebViewBuilder<'a, T, I, C>
|
||||
where
|
||||
I: FnMut(&mut WebView<T>, &str) -> WVResult + 'a,
|
||||
C: AsRef<str>,
|
||||
{
|
||||
fn default() -> Self {
|
||||
#[cfg(debug_assertions)]
|
||||
let debug = true;
|
||||
#[cfg(not(debug_assertions))]
|
||||
let debug = false;
|
||||
|
||||
WebViewBuilder {
|
||||
title: "Application",
|
||||
content: None,
|
||||
width: 800,
|
||||
height: 600,
|
||||
resizable: true,
|
||||
debug,
|
||||
invoke_handler: None,
|
||||
user_data: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: 'a, I, C> WebViewBuilder<'a, T, I, C>
|
||||
where
|
||||
I: FnMut(&mut WebView<T>, &str) -> WVResult + 'a,
|
||||
C: AsRef<str>,
|
||||
{
|
||||
/// Alias for [`WebViewBuilder::default()`].
|
||||
///
|
||||
/// [`WebViewBuilder::default()`]: struct.WebviewBuilder.html#impl-Default
|
||||
pub fn new() -> Self {
|
||||
WebViewBuilder::default()
|
||||
}
|
||||
|
||||
/// Sets the title of the WebView window.
|
||||
///
|
||||
/// Defaults to `"Application"`.
|
||||
pub fn title(mut self, title: &'a str) -> Self {
|
||||
self.title = title;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the content of the WebView. Either a URL or a HTML string.
|
||||
pub fn content(mut self, content: Content<C>) -> Self {
|
||||
self.content = Some(content);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the size of the WebView window.
|
||||
///
|
||||
/// Defaults to 800 x 600.
|
||||
pub fn size(mut self, width: i32, height: i32) -> Self {
|
||||
self.width = width;
|
||||
self.height = height;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the resizability of the WebView window. If set to false, the window cannot be resized.
|
||||
///
|
||||
/// Defaults to `true`.
|
||||
pub fn resizable(mut self, resizable: bool) -> Self {
|
||||
self.resizable = resizable;
|
||||
self
|
||||
}
|
||||
|
||||
/// Enables or disables debug mode.
|
||||
///
|
||||
/// Defaults to `true` for debug builds, `false` for release builds.
|
||||
pub fn debug(mut self, debug: bool) -> Self {
|
||||
self.debug = debug;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the invoke handler callback. This will be called when a message is received from
|
||||
/// JavaScript.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// If the closure returns an `Err`, it will be returned on the next call to [`step()`].
|
||||
///
|
||||
/// [`step()`]: struct.WebView.html#method.step
|
||||
pub fn invoke_handler(mut self, invoke_handler: I) -> Self {
|
||||
self.invoke_handler = Some(invoke_handler);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the initial state of the user data. This is an arbitrary value stored on the WebView
|
||||
/// thread, accessible from dispatched closures without synchronization overhead.
|
||||
pub fn user_data(mut self, user_data: T) -> Self {
|
||||
self.user_data = Some(user_data);
|
||||
self
|
||||
}
|
||||
|
||||
/// Validates provided arguments and returns a new WebView if successful.
|
||||
pub fn build(self) -> WVResult<WebView<'a, T>> {
|
||||
macro_rules! require_field {
|
||||
($name:ident) => {
|
||||
self
|
||||
.$name
|
||||
.ok_or_else(|| Error::UninitializedField(stringify!($name)))?
|
||||
};
|
||||
}
|
||||
|
||||
let title = CString::new(self.title)?;
|
||||
let content = require_field!(content);
|
||||
let url = match content {
|
||||
Content::Url(url) => CString::new(url.as_ref())?,
|
||||
Content::Html(html) => CString::new(format!("data:text/html,{}", encode(html.as_ref())))?,
|
||||
};
|
||||
let user_data = require_field!(user_data);
|
||||
let invoke_handler = require_field!(invoke_handler);
|
||||
|
||||
WebView::new(
|
||||
&title,
|
||||
&url,
|
||||
self.width,
|
||||
self.height,
|
||||
self.resizable,
|
||||
self.debug,
|
||||
user_data,
|
||||
invoke_handler,
|
||||
)
|
||||
}
|
||||
|
||||
/// Validates provided arguments and runs a new WebView to completion, returning the user data.
|
||||
///
|
||||
/// Equivalent to `build()?.run()`.
|
||||
pub fn run(self) -> WVResult<T> {
|
||||
self.build()?.run()
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructs a new builder for a [`WebView`].
|
||||
///
|
||||
/// Alias for [`WebViewBuilder::default()`].
|
||||
///
|
||||
/// [`WebView`]: struct.Webview.html
|
||||
/// [`WebViewBuilder::default()`]: struct.WebviewBuilder.html#impl-Default
|
||||
pub fn builder<'a, T, I, C>() -> WebViewBuilder<'a, T, I, C>
|
||||
where
|
||||
I: FnMut(&mut WebView<T>, &str) -> WVResult + 'a,
|
||||
C: AsRef<str>,
|
||||
{
|
||||
WebViewBuilder::new()
|
||||
}
|
||||
|
||||
struct UserData<'a, T> {
|
||||
inner: T,
|
||||
live: Arc<RwLock<()>>,
|
||||
invoke_handler: Box<FnMut(&mut WebView<T>, &str) -> WVResult + 'a>,
|
||||
result: WVResult,
|
||||
}
|
||||
|
||||
/// An owned webview instance.
|
||||
///
|
||||
/// Construct via a [`WebViewBuilder`].
|
||||
///
|
||||
/// [`WebViewBuilder`]: struct.WebViewBuilder.html
|
||||
#[derive(Debug)]
|
||||
pub struct WebView<'a, T: 'a> {
|
||||
inner: *mut CWebView,
|
||||
_phantom: PhantomData<&'a mut T>,
|
||||
}
|
||||
|
||||
impl<'a, T> WebView<'a, T> {
|
||||
#![cfg_attr(feature = "cargo-clippy", allow(clippy::too_many_arguments))]
|
||||
fn new<I>(
|
||||
title: &CStr,
|
||||
url: &CStr,
|
||||
width: i32,
|
||||
height: i32,
|
||||
resizable: bool,
|
||||
debug: bool,
|
||||
user_data: T,
|
||||
invoke_handler: I,
|
||||
) -> WVResult<WebView<'a, T>>
|
||||
where
|
||||
I: FnMut(&mut WebView<T>, &str) -> WVResult + 'a,
|
||||
{
|
||||
let user_data = Box::new(UserData {
|
||||
inner: user_data,
|
||||
live: Arc::new(RwLock::new(())),
|
||||
invoke_handler: Box::new(invoke_handler),
|
||||
result: Ok(()),
|
||||
});
|
||||
let user_data_ptr = Box::into_raw(user_data);
|
||||
|
||||
unsafe {
|
||||
let inner = wrapper_webview_new(
|
||||
title.as_ptr(),
|
||||
url.as_ptr(),
|
||||
width,
|
||||
height,
|
||||
resizable as _,
|
||||
debug as _,
|
||||
Some(ffi_invoke_handler::<T>),
|
||||
user_data_ptr as _,
|
||||
);
|
||||
|
||||
if inner.is_null() {
|
||||
Box::<UserData<T>>::from_raw(user_data_ptr);
|
||||
Err(Error::Initialization)
|
||||
} else {
|
||||
Ok(WebView::from_ptr(inner))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn from_ptr(inner: *mut CWebView) -> WebView<'a, T> {
|
||||
WebView {
|
||||
inner,
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a thread-safe [`Handle`] to the `WebView`, from which closures can be dispatched.
|
||||
///
|
||||
/// [`Handle`]: struct.Handle.html
|
||||
pub fn handle(&self) -> Handle<T> {
|
||||
Handle {
|
||||
inner: self.inner,
|
||||
live: Arc::downgrade(&self.user_data_wrapper().live),
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
fn user_data_wrapper_ptr(&self) -> *mut UserData<'a, T> {
|
||||
unsafe { wrapper_webview_get_userdata(self.inner) as _ }
|
||||
}
|
||||
|
||||
fn user_data_wrapper(&self) -> &UserData<'a, T> {
|
||||
unsafe { &(*self.user_data_wrapper_ptr()) }
|
||||
}
|
||||
|
||||
fn user_data_wrapper_mut(&mut self) -> &mut UserData<'a, T> {
|
||||
unsafe { &mut (*self.user_data_wrapper_ptr()) }
|
||||
}
|
||||
|
||||
/// Borrows the user data immutably.
|
||||
pub fn user_data(&self) -> &T {
|
||||
&self.user_data_wrapper().inner
|
||||
}
|
||||
|
||||
/// Borrows the user data mutably.
|
||||
pub fn user_data_mut(&mut self) -> &mut T {
|
||||
&mut self.user_data_wrapper_mut().inner
|
||||
}
|
||||
|
||||
/// Forces the `WebView` instance to end, without dropping.
|
||||
pub fn terminate(&mut self) {
|
||||
unsafe { webview_terminate(self.inner) }
|
||||
}
|
||||
|
||||
/// Executes the provided string as JavaScript code within the `WebView` instance.
|
||||
pub fn eval(&mut self, js: &str) -> WVResult {
|
||||
let js = CString::new(js)?;
|
||||
let ret = unsafe { webview_eval(self.inner, js.as_ptr()) };
|
||||
if ret != 0 {
|
||||
Err(Error::JsEvaluation)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Injects the provided string as CSS within the `WebView` instance.
|
||||
pub fn inject_css(&mut self, css: &str) -> WVResult {
|
||||
let css = CString::new(css)?;
|
||||
let ret = unsafe { webview_inject_css(self.inner, css.as_ptr()) };
|
||||
if ret != 0 {
|
||||
Err(Error::CssInjection)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the color of the title bar.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Without specifying alpha (defaults to 255):
|
||||
/// ```ignore
|
||||
/// webview.set_color((123, 321, 213));
|
||||
/// ```
|
||||
///
|
||||
/// Specifying alpha:
|
||||
/// ```ignore
|
||||
/// webview.set_color((123, 321, 213, 127));
|
||||
/// ```
|
||||
pub fn set_color<C: Into<Color>>(&mut self, color: C) {
|
||||
let color = color.into();
|
||||
unsafe { webview_set_color(self.inner, color.r, color.g, color.b, color.a) }
|
||||
}
|
||||
|
||||
/// Sets the title displayed at the top of the window.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// If `title` contain a nul byte, returns [`Error::NulByte`].
|
||||
///
|
||||
/// [`Error::NulByte`]: enum.Error.html#variant.NulByte
|
||||
pub fn set_title(&mut self, title: &str) -> WVResult {
|
||||
let title = CString::new(title)?;
|
||||
unsafe { webview_set_title(self.inner, title.as_ptr()) }
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Enables or disables fullscreen.
|
||||
pub fn set_fullscreen(&mut self, fullscreen: bool) {
|
||||
unsafe { webview_set_fullscreen(self.inner, fullscreen as _) };
|
||||
}
|
||||
|
||||
/// Returns a builder for opening a new dialog window.
|
||||
pub fn dialog<'b>(&'b mut self) -> DialogBuilder<'a, 'b, T> {
|
||||
DialogBuilder::new(self)
|
||||
}
|
||||
|
||||
/// Iterates the event loop. Returns `None` if the view has been closed or terminated.
|
||||
pub fn step(&mut self) -> Option<WVResult> {
|
||||
unsafe {
|
||||
match webview_loop(self.inner, 1) {
|
||||
0 => {
|
||||
let closure_result = &mut self.user_data_wrapper_mut().result;
|
||||
match closure_result {
|
||||
Ok(_) => Some(Ok(())),
|
||||
e => Some(mem::replace(e, Ok(()))),
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs the event loop to completion and returns the user data.
|
||||
pub fn run(mut self) -> WVResult<T> {
|
||||
loop {
|
||||
match self.step() {
|
||||
Some(Ok(_)) => (),
|
||||
Some(e) => e?,
|
||||
None => return Ok(self.into_inner()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Consumes the `WebView` and returns ownership of the user data.
|
||||
pub fn into_inner(mut self) -> T {
|
||||
unsafe {
|
||||
let user_data = self._into_inner();
|
||||
mem::forget(self);
|
||||
user_data
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn _into_inner(&mut self) -> T {
|
||||
let _lock = self
|
||||
.user_data_wrapper()
|
||||
.live
|
||||
.write()
|
||||
.expect("A dispatch channel thread panicked while holding mutex to WebView.");
|
||||
|
||||
let user_data_ptr = self.user_data_wrapper_ptr();
|
||||
webview_exit(self.inner);
|
||||
wrapper_webview_free(self.inner);
|
||||
let user_data = *Box::from_raw(user_data_ptr);
|
||||
user_data.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> Drop for WebView<'a, T> {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
self._into_inner();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A thread-safe handle to a [`WebView`] instance. Used to dispatch closures onto its task queue.
|
||||
///
|
||||
/// [`WebView`]: struct.WebView.html
|
||||
pub struct Handle<T> {
|
||||
inner: *mut CWebView,
|
||||
live: Weak<RwLock<()>>,
|
||||
_phantom: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T> Handle<T> {
|
||||
/// Schedules a closure to be run on the [`WebView`] thread.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::Dispatch`] if the [`WebView`] has been dropped.
|
||||
///
|
||||
/// If the closure returns an `Err`, it will be returned on the next call to [`step()`].
|
||||
///
|
||||
/// [`WebView`]: struct.WebView.html
|
||||
/// [`Error::Dispatch`]: enum.Error.html#variant.Dispatch
|
||||
/// [`step()`]: struct.WebView.html#method.step
|
||||
pub fn dispatch<F>(&self, f: F) -> WVResult
|
||||
where
|
||||
F: FnOnce(&mut WebView<T>) -> WVResult + Send + 'static,
|
||||
{
|
||||
// Abort if WebView has been dropped. Otherwise, keep it alive until closure has been
|
||||
// dispatched.
|
||||
let mutex = self.live.upgrade().ok_or(Error::Dispatch)?;
|
||||
let closure = Box::new(SendBoxFnOnce::new(f));
|
||||
let _lock = mutex.read().map_err(|_| Error::Dispatch)?;
|
||||
|
||||
// Send closure to webview.
|
||||
unsafe {
|
||||
webview_dispatch(
|
||||
self.inner,
|
||||
Some(ffi_dispatch_handler::<T> as _),
|
||||
Box::into_raw(closure) as _,
|
||||
)
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl<T> Send for Handle<T> {}
|
||||
unsafe impl<T> Sync for Handle<T> {}
|
||||
|
||||
fn read_str(s: &[u8]) -> String {
|
||||
let end = s.iter().position(|&b| b == 0).map_or(0, |i| i + 1);
|
||||
match CStr::from_bytes_with_nul(&s[..end]) {
|
||||
Ok(s) => s.to_string_lossy().into_owned(),
|
||||
Err(_) => "".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn ffi_dispatch_handler<T>(webview: *mut CWebView, arg: *mut c_void) {
|
||||
unsafe {
|
||||
let mut handle = mem::ManuallyDrop::new(WebView::<T>::from_ptr(webview));
|
||||
let result = {
|
||||
let callback =
|
||||
Box::<SendBoxFnOnce<'static, (&mut WebView<T>,), WVResult>>::from_raw(arg as _);
|
||||
callback.call(&mut handle)
|
||||
};
|
||||
handle.user_data_wrapper_mut().result = result;
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn ffi_invoke_handler<T>(webview: *mut CWebView, arg: *const c_char) {
|
||||
unsafe {
|
||||
let arg = CStr::from_ptr(arg).to_string_lossy().to_string();
|
||||
let mut handle = mem::ManuallyDrop::new(WebView::<T>::from_ptr(webview));
|
||||
let result = ((*handle.user_data_wrapper_ptr()).invoke_handler)(&mut *handle, &arg);
|
||||
handle.user_data_wrapper_mut().result = result;
|
||||
}
|
||||
}
|
1
docs/README.md
Normal file
1
docs/README.md
Normal file
@ -0,0 +1 @@
|
||||
[WIP]
|
27
lib/rust/Cargo.toml
Normal file
27
lib/rust/Cargo.toml
Normal file
@ -0,0 +1,27 @@
|
||||
[package]
|
||||
name = "proton"
|
||||
version = "0.1.0"
|
||||
authors = ["Lucas Fernandes Gonçalves Nogueira <lucas@quasar.dev>"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
proton-ui = { path = "../../bindings/rust" }
|
||||
serde_json = "1.0.39"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
dirs = "1.0"
|
||||
ignore = "0.4.7"
|
||||
phf = "0.7.21"
|
||||
threadpool = "1.7"
|
||||
rand = "0.7"
|
||||
reqwest = "0.9"
|
||||
pbr = "1"
|
||||
zip = "0.5.0"
|
||||
tempdir = "0.3"
|
||||
semver = "0.9"
|
||||
tempfile = "3"
|
||||
either = "1.5.0"
|
||||
tar = "0.4"
|
||||
flate2 = "1"
|
||||
hyper-old-types = "0.11.0"
|
||||
sysinfo = "0.9"
|
13
lib/rust/rustfmt.toml
Normal file
13
lib/rust/rustfmt.toml
Normal file
@ -0,0 +1,13 @@
|
||||
max_width = 100
|
||||
hard_tabs = false
|
||||
tab_spaces = 2
|
||||
newline_style = "Auto"
|
||||
use_small_heuristics = "Default"
|
||||
reorder_imports = true
|
||||
reorder_modules = true
|
||||
remove_nested_parens = true
|
||||
edition = "2015"
|
||||
merge_derives = true
|
||||
use_try_shorthand = false
|
||||
use_field_init_shorthand = false
|
||||
force_explicit_abi = true
|
40
lib/rust/src/api/cmd.rs
Normal file
40
lib/rust/src/api/cmd.rs
Normal file
@ -0,0 +1,40 @@
|
||||
#[derive(Deserialize)]
|
||||
#[serde(tag = "cmd", rename_all = "camelCase")]
|
||||
pub enum Cmd {
|
||||
Init,
|
||||
ReadAsString {
|
||||
path: String,
|
||||
callback: String,
|
||||
error: String,
|
||||
},
|
||||
ReadAsBinary {
|
||||
path: String,
|
||||
callback: String,
|
||||
error: String,
|
||||
},
|
||||
Write {
|
||||
file: String,
|
||||
contents: String,
|
||||
callback: String,
|
||||
error: String,
|
||||
},
|
||||
List {
|
||||
path: String,
|
||||
callback: String,
|
||||
error: String,
|
||||
},
|
||||
ListDirs {
|
||||
path: String,
|
||||
callback: String,
|
||||
error: String,
|
||||
},
|
||||
SetTitle {
|
||||
title: String,
|
||||
},
|
||||
Call {
|
||||
command: String,
|
||||
args: Vec<String>,
|
||||
callback: String,
|
||||
error: String,
|
||||
},
|
||||
}
|
63
lib/rust/src/api/mod.rs
Normal file
63
lib/rust/src/api/mod.rs
Normal file
@ -0,0 +1,63 @@
|
||||
mod cmd;
|
||||
|
||||
use proton_ui::WebView;
|
||||
|
||||
pub fn handler<T: 'static>(webview: &mut WebView<T>, arg: &str) -> bool {
|
||||
use cmd::Cmd::*;
|
||||
match serde_json::from_str(arg) {
|
||||
Err(_) => false,
|
||||
Ok(command) => {
|
||||
match command {
|
||||
Init => (),
|
||||
ReadAsString {
|
||||
path,
|
||||
callback,
|
||||
error,
|
||||
} => {
|
||||
super::file_system::read_text_file(webview, path, callback, error);
|
||||
}
|
||||
ReadAsBinary {
|
||||
path,
|
||||
callback,
|
||||
error,
|
||||
} => {
|
||||
super::file_system::read_binary_file(webview, path, callback, error);
|
||||
}
|
||||
Write {
|
||||
file,
|
||||
contents,
|
||||
callback,
|
||||
error,
|
||||
} => {
|
||||
super::file_system::write_file(webview, file, contents, callback, error);
|
||||
}
|
||||
ListDirs {
|
||||
path,
|
||||
callback,
|
||||
error,
|
||||
} => {
|
||||
super::file_system::list_dirs(webview, path, callback, error);
|
||||
}
|
||||
List {
|
||||
path,
|
||||
callback,
|
||||
error,
|
||||
} => {
|
||||
super::file_system::list(webview, path, callback, error);
|
||||
}
|
||||
SetTitle { title } => {
|
||||
webview.set_title(&title).unwrap();
|
||||
}
|
||||
Call {
|
||||
command,
|
||||
args,
|
||||
callback,
|
||||
error,
|
||||
} => {
|
||||
super::command::call(webview, command, args, callback, error);
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
78
lib/rust/src/command.rs
Executable file
78
lib/rust/src/command.rs
Executable file
@ -0,0 +1,78 @@
|
||||
use proton_ui::WebView;
|
||||
|
||||
use std::process::{Child, Command, Stdio};
|
||||
|
||||
use super::run_async;
|
||||
|
||||
pub fn get_output(cmd: String, args: Vec<String>, stdout: Stdio) -> Result<String, String> {
|
||||
Command::new(cmd)
|
||||
.args(args)
|
||||
.stdout(stdout)
|
||||
.output()
|
||||
.map_err(|err| err.to_string())
|
||||
.and_then(|output| {
|
||||
if output.status.success() {
|
||||
return Result::Ok(String::from_utf8_lossy(&output.stdout).to_string());
|
||||
} else {
|
||||
return Result::Err(String::from_utf8_lossy(&output.stderr).to_string());
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TODO use .exe for windows builds
|
||||
pub fn format_command(path: String, command: String) -> String {
|
||||
return format!("{}/./{}", path, command);
|
||||
}
|
||||
|
||||
pub fn relative_command(command: String) -> Result<String, std::io::Error> {
|
||||
match std::env::current_exe()?.parent() {
|
||||
Some(exe_dir) => return Ok(format_command(exe_dir.display().to_string(), command)),
|
||||
None => {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
"Could not evaluate executable dir".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO append .exe for windows builds
|
||||
pub fn command_path(command: String) -> Result<String, std::io::Error> {
|
||||
match std::env::current_exe()?.parent() {
|
||||
Some(exe_dir) => return Ok(format!("{}/{}", exe_dir.display().to_string(), command)),
|
||||
None => {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
"Could not evaluate executable dir".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spawn_relative_command(
|
||||
command: String,
|
||||
args: Vec<String>,
|
||||
stdout: Stdio,
|
||||
) -> Result<Child, std::io::Error> {
|
||||
let cmd = relative_command(command)?;
|
||||
Ok(Command::new(cmd).args(args).stdout(stdout).spawn()?)
|
||||
}
|
||||
|
||||
pub fn call<T: 'static>(
|
||||
webview: &mut WebView<T>,
|
||||
command: String,
|
||||
args: Vec<String>,
|
||||
callback: String,
|
||||
error: String,
|
||||
) {
|
||||
run_async(
|
||||
webview,
|
||||
|| {
|
||||
get_output(command, args, Stdio::piped())
|
||||
.map_err(|err| format!("`{}`", err))
|
||||
.map(|output| format!("`{}`", output))
|
||||
},
|
||||
callback,
|
||||
error,
|
||||
);
|
||||
}
|
77
lib/rust/src/dir/mod.rs
Executable file
77
lib/rust/src/dir/mod.rs
Executable file
@ -0,0 +1,77 @@
|
||||
extern crate dirs;
|
||||
extern crate tempfile;
|
||||
|
||||
mod utils;
|
||||
use ignore::Walk;
|
||||
use std::fs;
|
||||
use std::fs::metadata;
|
||||
use utils::get_dir_name_from_path;
|
||||
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct DiskEntry {
|
||||
pub path: String,
|
||||
pub is_dir: bool,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
fn is_dir(file_name: String) -> Result<bool, String> {
|
||||
match metadata(file_name.to_string()) {
|
||||
Ok(md) => return Result::Ok(md.is_dir()),
|
||||
Err(err) => return Result::Err(err.to_string()),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn walk_dir(path_copy: String) -> Result<Vec<DiskEntry>, String> {
|
||||
println!("Trying to walk: {}", path_copy.as_str());
|
||||
let mut files_and_dirs: Vec<DiskEntry> = vec![];
|
||||
for result in Walk::new(path_copy) {
|
||||
match result {
|
||||
Ok(entry) => {
|
||||
let display_value = entry.path().display();
|
||||
let _dir_name = display_value.to_string();
|
||||
|
||||
match is_dir(display_value.to_string()) {
|
||||
Ok(flag) => {
|
||||
files_and_dirs.push(DiskEntry {
|
||||
path: display_value.to_string(),
|
||||
is_dir: flag,
|
||||
name: display_value.to_string(),
|
||||
});
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
return Result::Ok(files_and_dirs);
|
||||
}
|
||||
|
||||
pub fn list_dir_contents(dir_path: &String) -> Result<Vec<DiskEntry>, String> {
|
||||
fs::read_dir(dir_path)
|
||||
.map_err(|err| err.to_string())
|
||||
.and_then(|paths| {
|
||||
let mut dirs: Vec<DiskEntry> = vec![];
|
||||
for path in paths {
|
||||
let dir_path = path.expect("dirpath error").path();
|
||||
let _dir_name = dir_path.display();
|
||||
dirs.push(DiskEntry {
|
||||
path: format!("{}", _dir_name),
|
||||
is_dir: true,
|
||||
name: get_dir_name_from_path(_dir_name.to_string()),
|
||||
});
|
||||
}
|
||||
Ok(dirs)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn with_temp_dir<F: FnOnce(&tempfile::TempDir) -> ()>(
|
||||
callback: F,
|
||||
) -> Result<(), std::io::Error> {
|
||||
let dir = tempdir()?;
|
||||
callback(&dir);
|
||||
dir.close()?;
|
||||
Ok(())
|
||||
}
|
4
lib/rust/src/dir/utils.rs
Executable file
4
lib/rust/src/dir/utils.rs
Executable file
@ -0,0 +1,4 @@
|
||||
pub fn get_dir_name_from_path(path: String) -> String {
|
||||
let path_collect: Vec<&str> = path.split("/").collect();
|
||||
return path_collect[path_collect.len() - 1].to_string();
|
||||
}
|
46
lib/rust/src/file/error.rs
Normal file
46
lib/rust/src/file/error.rs
Normal file
@ -0,0 +1,46 @@
|
||||
use std;
|
||||
use zip::result::ZipError;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Extract(String),
|
||||
Io(std::io::Error),
|
||||
Zip(ZipError),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
use Error::*;
|
||||
match *self {
|
||||
Extract(ref s) => write!(f, "ExtractError: {}", s),
|
||||
Io(ref e) => write!(f, "IoError: {}", e),
|
||||
Zip(ref e) => write!(f, "ZipError: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {
|
||||
fn description(&self) -> &str {
|
||||
"File Error"
|
||||
}
|
||||
|
||||
fn cause(&self) -> Option<&std::error::Error> {
|
||||
use Error::*;
|
||||
Some(match *self {
|
||||
Io(ref e) => e,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for Error {
|
||||
fn from(e: std::io::Error) -> Self {
|
||||
Error::Io(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ZipError> for Error {
|
||||
fn from(e: ZipError) -> Self {
|
||||
Error::Zip(e)
|
||||
}
|
||||
}
|
189
lib/rust/src/file/extract.rs
Normal file
189
lib/rust/src/file/extract.rs
Normal file
@ -0,0 +1,189 @@
|
||||
extern crate either;
|
||||
extern crate flate2;
|
||||
extern crate tar;
|
||||
extern crate zip;
|
||||
|
||||
use super::error::*;
|
||||
use either::Either;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::path;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum ArchiveFormat {
|
||||
Tar(Option<Compression>),
|
||||
Plain(Option<Compression>),
|
||||
Zip,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum Compression {
|
||||
Gz,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Extract<'a> {
|
||||
source: &'a path::Path,
|
||||
archive_format: Option<ArchiveFormat>,
|
||||
}
|
||||
|
||||
fn detect_archive_type(path: &path::Path) -> ArchiveFormat {
|
||||
match path.extension() {
|
||||
Some(extension) if extension == std::ffi::OsStr::new("zip") => ArchiveFormat::Zip,
|
||||
Some(extension) if extension == std::ffi::OsStr::new("tar") => ArchiveFormat::Tar(None),
|
||||
Some(extension) if extension == std::ffi::OsStr::new("gz") => match path
|
||||
.file_stem()
|
||||
.map(|e| path::Path::new(e))
|
||||
.and_then(|f| f.extension())
|
||||
{
|
||||
Some(extension) if extension == std::ffi::OsStr::new("tar") => {
|
||||
ArchiveFormat::Tar(Some(Compression::Gz))
|
||||
}
|
||||
_ => ArchiveFormat::Plain(Some(Compression::Gz)),
|
||||
},
|
||||
_ => ArchiveFormat::Plain(None),
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Extract<'a> {
|
||||
/// Create an `Extractor from a source path
|
||||
pub fn from_source(source: &'a path::Path) -> Extract<'a> {
|
||||
Self {
|
||||
source,
|
||||
archive_format: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Specify an archive format of the source being extracted. If not specified, the
|
||||
/// archive format will determined from the file extension.
|
||||
pub fn archive_format(&mut self, format: ArchiveFormat) -> &mut Self {
|
||||
self.archive_format = Some(format);
|
||||
self
|
||||
}
|
||||
|
||||
fn get_archive_reader(
|
||||
source: fs::File,
|
||||
compression: Option<Compression>,
|
||||
) -> Either<fs::File, flate2::read::GzDecoder<fs::File>> {
|
||||
match compression {
|
||||
Some(Compression::Gz) => Either::Right(flate2::read::GzDecoder::new(source)),
|
||||
None => Either::Left(source),
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract an entire source archive into a specified path. If the source is a single compressed
|
||||
/// file and not an archive, it will be extracted into a file with the same name inside of
|
||||
/// `into_dir`.
|
||||
pub fn extract_into(&self, into_dir: &path::Path) -> Result<(), Error> {
|
||||
let source = fs::File::open(self.source)?;
|
||||
let archive = self
|
||||
.archive_format
|
||||
.unwrap_or_else(|| detect_archive_type(&self.source));
|
||||
|
||||
match archive {
|
||||
ArchiveFormat::Plain(compression) | ArchiveFormat::Tar(compression) => {
|
||||
let mut reader = Self::get_archive_reader(source, compression);
|
||||
|
||||
match archive {
|
||||
ArchiveFormat::Plain(_) => {
|
||||
match fs::create_dir_all(into_dir) {
|
||||
Ok(_) => (),
|
||||
Err(e) => {
|
||||
if e.kind() != io::ErrorKind::AlreadyExists {
|
||||
return Err(Error::Io(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
let file_name = self
|
||||
.source
|
||||
.file_name()
|
||||
.ok_or_else(|| Error::Extract("Extractor source has no file-name".into()))?;
|
||||
let mut out_path = into_dir.join(file_name);
|
||||
out_path.set_extension("");
|
||||
let mut out_file = fs::File::create(&out_path)?;
|
||||
io::copy(&mut reader, &mut out_file)?;
|
||||
}
|
||||
ArchiveFormat::Tar(_) => {
|
||||
let mut archive = tar::Archive::new(reader);
|
||||
archive.unpack(into_dir)?;
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
}
|
||||
ArchiveFormat::Zip => {
|
||||
let mut archive = zip::ZipArchive::new(source)?;
|
||||
for i in 0..archive.len() {
|
||||
let mut file = archive.by_index(i)?;
|
||||
let path = into_dir.join(file.name());
|
||||
let mut output = fs::File::create(path)?;
|
||||
io::copy(&mut file, &mut output)?;
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Extract a single file from a source and save to a file of the same name in `into_dir`.
|
||||
/// If the source is a single compressed file, it will be saved with the name `file_to_extract`
|
||||
/// in the specified `into_dir`.
|
||||
pub fn extract_file<T: AsRef<path::Path>>(
|
||||
&self,
|
||||
into_dir: &path::Path,
|
||||
file_to_extract: T,
|
||||
) -> Result<(), Error> {
|
||||
let file_to_extract = file_to_extract.as_ref();
|
||||
let source = fs::File::open(self.source)?;
|
||||
let archive = self
|
||||
.archive_format
|
||||
.unwrap_or_else(|| detect_archive_type(&self.source));
|
||||
|
||||
match archive {
|
||||
ArchiveFormat::Plain(compression) | ArchiveFormat::Tar(compression) => {
|
||||
let mut reader = Self::get_archive_reader(source, compression);
|
||||
|
||||
match archive {
|
||||
ArchiveFormat::Plain(_) => {
|
||||
match fs::create_dir_all(into_dir) {
|
||||
Ok(_) => (),
|
||||
Err(e) => {
|
||||
if e.kind() != io::ErrorKind::AlreadyExists {
|
||||
return Err(Error::Io(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
let file_name = file_to_extract
|
||||
.file_name()
|
||||
.ok_or_else(|| Error::Extract("Extractor source has no file-name".into()))?;
|
||||
let out_path = into_dir.join(file_name);
|
||||
let mut out_file = fs::File::create(&out_path)?;
|
||||
io::copy(&mut reader, &mut out_file)?;
|
||||
}
|
||||
ArchiveFormat::Tar(_) => {
|
||||
let mut archive = tar::Archive::new(reader);
|
||||
let mut entry = archive
|
||||
.entries()?
|
||||
.filter_map(|e| e.ok())
|
||||
.find(|e| e.path().ok().filter(|p| p == file_to_extract).is_some())
|
||||
.ok_or_else(|| {
|
||||
Error::Extract(format!(
|
||||
"Could not find the required path in the archive: {:?}",
|
||||
file_to_extract
|
||||
))
|
||||
})?;
|
||||
entry.unpack_in(into_dir)?;
|
||||
}
|
||||
_ => {
|
||||
panic!("Unreasonable code");
|
||||
}
|
||||
};
|
||||
}
|
||||
ArchiveFormat::Zip => {
|
||||
let mut archive = zip::ZipArchive::new(source)?;
|
||||
let mut file = archive.by_name(file_to_extract.to_str().unwrap())?;
|
||||
let mut output = fs::File::create(into_dir.join(file.name()))?;
|
||||
io::copy(&mut file, &mut output)?;
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
}
|
62
lib/rust/src/file/file_move.rs
Normal file
62
lib/rust/src/file/file_move.rs
Normal file
@ -0,0 +1,62 @@
|
||||
use std::fs;
|
||||
use std::path;
|
||||
|
||||
use super::error::*;
|
||||
|
||||
/// Moves a file from the given path to the specified destination.
|
||||
///
|
||||
/// `source` and `dest` must be on the same filesystem.
|
||||
/// If `replace_using_temp` is specified, the destination file will be
|
||||
/// replaced using the given temporary path.
|
||||
///
|
||||
/// * Errors:
|
||||
/// * Io - copying / renaming
|
||||
#[derive(Debug)]
|
||||
pub struct Move<'a> {
|
||||
source: &'a path::Path,
|
||||
temp: Option<&'a path::Path>,
|
||||
}
|
||||
impl<'a> Move<'a> {
|
||||
/// Specify source file
|
||||
pub fn from_source(source: &'a path::Path) -> Move<'a> {
|
||||
Self { source, temp: None }
|
||||
}
|
||||
|
||||
/// If specified and the destination file already exists, the "destination"
|
||||
/// file will be moved to the given temporary location before the "source"
|
||||
/// file is moved to the "destination" file.
|
||||
///
|
||||
/// In the event of an `io` error while renaming "source" to "destination",
|
||||
/// the temporary file will be moved back to "destination".
|
||||
///
|
||||
/// The `temp` dir must be explicitly provided since `rename` operations require
|
||||
/// files to live on the same filesystem.
|
||||
pub fn replace_using_temp(&mut self, temp: &'a path::Path) -> &mut Self {
|
||||
self.temp = Some(temp);
|
||||
self
|
||||
}
|
||||
|
||||
/// Move source file to specified destination
|
||||
pub fn to_dest(&self, dest: &path::Path) -> Result<(), Error> {
|
||||
match self.temp {
|
||||
None => {
|
||||
fs::rename(self.source, dest)?;
|
||||
}
|
||||
Some(temp) => {
|
||||
println!("dest {}", dest.to_str().unwrap());
|
||||
println!("temp {}", temp.to_str().unwrap());
|
||||
println!("source {}", self.source.to_str().unwrap());
|
||||
if dest.exists() {
|
||||
fs::rename(dest, temp)?;
|
||||
if let Err(e) = fs::rename(self.source, dest) {
|
||||
fs::rename(temp, dest)?;
|
||||
return Err(Error::from(e));
|
||||
}
|
||||
} else {
|
||||
fs::rename(self.source, dest)?;
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
}
|
21
lib/rust/src/file/mod.rs
Normal file
21
lib/rust/src/file/mod.rs
Normal file
@ -0,0 +1,21 @@
|
||||
use std::fs;
|
||||
|
||||
extern crate serde_json;
|
||||
|
||||
mod error;
|
||||
mod extract;
|
||||
mod file_move;
|
||||
|
||||
pub use error::Error;
|
||||
pub use extract::*;
|
||||
pub use file_move::*;
|
||||
|
||||
pub fn read_string(file: String) -> Result<String, String> {
|
||||
fs::read_to_string(file)
|
||||
.map_err(|err| err.to_string())
|
||||
.map(|c| c)
|
||||
}
|
||||
|
||||
pub fn read_binary(file: String) -> Result<Vec<u8>, String> {
|
||||
fs::read(file).map_err(|err| err.to_string()).map(|b| b)
|
||||
}
|
100
lib/rust/src/file_system.rs
Executable file
100
lib/rust/src/file_system.rs
Executable file
@ -0,0 +1,100 @@
|
||||
use proton_ui::WebView;
|
||||
|
||||
use super::dir;
|
||||
use super::file;
|
||||
use super::run_async;
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
|
||||
pub fn list<T: 'static>(webview: &mut WebView<T>, path: String, callback: String, error: String) {
|
||||
run_async(
|
||||
webview,
|
||||
move || {
|
||||
dir::walk_dir(path.to_string())
|
||||
.and_then(|f| serde_json::to_string(&f).map_err(|err| err.to_string()))
|
||||
},
|
||||
callback,
|
||||
error,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn list_dirs<T: 'static>(
|
||||
webview: &mut WebView<T>,
|
||||
path: String,
|
||||
callback: String,
|
||||
error: String,
|
||||
) {
|
||||
run_async(
|
||||
webview,
|
||||
move || {
|
||||
dir::list_dir_contents(&path)
|
||||
.and_then(|f| serde_json::to_string(&f).map_err(|err| err.to_string()))
|
||||
},
|
||||
callback,
|
||||
error,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn write_file<T: 'static>(
|
||||
webview: &mut WebView<T>,
|
||||
file: String,
|
||||
contents: String,
|
||||
callback: String,
|
||||
error: String,
|
||||
) {
|
||||
run_async(
|
||||
webview,
|
||||
move || {
|
||||
File::create(file)
|
||||
.map_err(|err| err.to_string())
|
||||
.and_then(|mut f| {
|
||||
f.write_all(contents.as_bytes())
|
||||
.map_err(|err| err.to_string())
|
||||
.map(|_| "".to_string())
|
||||
})
|
||||
},
|
||||
callback,
|
||||
error,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn read_text_file<T: 'static>(
|
||||
webview: &mut WebView<T>,
|
||||
path: String,
|
||||
callback: String,
|
||||
error: String,
|
||||
) {
|
||||
run_async(
|
||||
webview,
|
||||
move || {
|
||||
file::read_string(path).and_then(|f| {
|
||||
serde_json::to_string(&f)
|
||||
.map_err(|err| err.to_string())
|
||||
.map(|s| s.to_string())
|
||||
})
|
||||
},
|
||||
callback,
|
||||
error,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn read_binary_file<T: 'static>(
|
||||
webview: &mut WebView<T>,
|
||||
path: String,
|
||||
callback: String,
|
||||
error: String,
|
||||
) {
|
||||
run_async(
|
||||
webview,
|
||||
move || {
|
||||
file::read_binary(path).and_then(|f| {
|
||||
serde_json::to_string(&f)
|
||||
.map_err(|err| err.to_string())
|
||||
.map(|s| s.to_string())
|
||||
})
|
||||
},
|
||||
callback,
|
||||
error,
|
||||
);
|
||||
}
|
57
lib/rust/src/http/error.rs
Normal file
57
lib/rust/src/http/error.rs
Normal file
@ -0,0 +1,57 @@
|
||||
use reqwest;
|
||||
use serde_json;
|
||||
use std;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Download(String),
|
||||
Json(serde_json::Error),
|
||||
Reqwest(reqwest::Error),
|
||||
Io(std::io::Error),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
use Error::*;
|
||||
match *self {
|
||||
Download(ref s) => write!(f, "DownloadError: {}", s),
|
||||
Json(ref e) => write!(f, "JsonError: {}", e),
|
||||
Reqwest(ref e) => write!(f, "ReqwestError: {}", e),
|
||||
Io(ref e) => write!(f, "IoError: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {
|
||||
fn description(&self) -> &str {
|
||||
"Http Error"
|
||||
}
|
||||
|
||||
fn cause(&self) -> Option<&std::error::Error> {
|
||||
use Error::*;
|
||||
Some(match *self {
|
||||
Json(ref e) => e,
|
||||
Reqwest(ref e) => e,
|
||||
Io(ref e) => e,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for Error {
|
||||
fn from(e: serde_json::Error) -> Self {
|
||||
Error::Json(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<reqwest::Error> for Error {
|
||||
fn from(e: reqwest::Error) -> Self {
|
||||
Error::Reqwest(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for Error {
|
||||
fn from(e: std::io::Error) -> Self {
|
||||
Error::Io(e)
|
||||
}
|
||||
}
|
80
lib/rust/src/http/mod.rs
Normal file
80
lib/rust/src/http/mod.rs
Normal file
@ -0,0 +1,80 @@
|
||||
extern crate pbr;
|
||||
extern crate reqwest;
|
||||
|
||||
use serde::Serialize;
|
||||
use std::io;
|
||||
mod error;
|
||||
pub use self::error::Error;
|
||||
|
||||
pub fn get(url: &String) -> Result<reqwest::Response, Error> {
|
||||
let response = reqwest::Client::new().get(url).send()?;
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub fn post_as_json<T: Serialize + ?Sized>(
|
||||
url: &String,
|
||||
payload: &T,
|
||||
) -> Result<reqwest::Response, Error> {
|
||||
let response = reqwest::Client::new().post(url).json(payload).send()?;
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub fn download<T: io::Write>(
|
||||
url: &String,
|
||||
mut dest: T,
|
||||
display_progress: bool,
|
||||
) -> Result<(), Error> {
|
||||
use io::BufRead;
|
||||
|
||||
set_ssl_vars!();
|
||||
|
||||
let resp = get(url)?;
|
||||
let size = resp
|
||||
.headers()
|
||||
.get(reqwest::header::CONTENT_LENGTH)
|
||||
.map(|val| {
|
||||
val
|
||||
.to_str()
|
||||
.map(|s| s.parse::<u64>().unwrap_or(0))
|
||||
.unwrap_or(0)
|
||||
})
|
||||
.unwrap_or(0);
|
||||
|
||||
if !resp.status().is_success() {
|
||||
bail!(
|
||||
Error::Download,
|
||||
"Download request failed with status: {:?}",
|
||||
resp.status()
|
||||
)
|
||||
}
|
||||
|
||||
let show_progress = if size == 0 { false } else { display_progress };
|
||||
|
||||
let mut src = io::BufReader::new(resp);
|
||||
let mut bar = if show_progress {
|
||||
let mut bar = pbr::ProgressBar::new(size);
|
||||
bar.set_units(pbr::Units::Bytes);
|
||||
bar.format("[=> ]");
|
||||
Some(bar)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
loop {
|
||||
let n = {
|
||||
let buf = src.fill_buf()?;
|
||||
dest.write_all(&buf)?;
|
||||
buf.len()
|
||||
};
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
src.consume(n);
|
||||
if let Some(ref mut bar) = bar {
|
||||
bar.add(n as u64);
|
||||
}
|
||||
}
|
||||
if show_progress {
|
||||
println!(" ... Done");
|
||||
}
|
||||
Ok(())
|
||||
}
|
43
lib/rust/src/lib.rs
Normal file
43
lib/rust/src/lib.rs
Normal file
@ -0,0 +1,43 @@
|
||||
extern crate threadpool;
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
|
||||
#[macro_use]
|
||||
mod macros;
|
||||
|
||||
pub mod api;
|
||||
pub mod command;
|
||||
pub mod dir;
|
||||
pub mod file;
|
||||
pub mod file_system;
|
||||
pub mod http;
|
||||
pub mod platform;
|
||||
pub mod process;
|
||||
pub mod rpc;
|
||||
pub mod tcp;
|
||||
pub mod updater;
|
||||
pub mod version;
|
||||
|
||||
extern crate proton_ui;
|
||||
use proton_ui::WebView;
|
||||
|
||||
use threadpool::ThreadPool;
|
||||
|
||||
thread_local!(static POOL: ThreadPool = ThreadPool::new(4));
|
||||
|
||||
pub fn run_async<T: 'static, F: FnOnce() -> Result<String, String> + Send + 'static>(
|
||||
webview: &mut WebView<T>,
|
||||
what: F,
|
||||
callback: String,
|
||||
error: String,
|
||||
) {
|
||||
let handle = webview.handle();
|
||||
POOL.with(|thread| {
|
||||
thread.execute(move || {
|
||||
let callback_string = rpc::format_callback_result(what(), callback, error);
|
||||
handle
|
||||
.dispatch(move |_webview| _webview.eval(callback_string.as_str()))
|
||||
.unwrap()
|
||||
});
|
||||
});
|
||||
}
|
46
lib/rust/src/macros.rs
Normal file
46
lib/rust/src/macros.rs
Normal file
@ -0,0 +1,46 @@
|
||||
/// Helper for formatting `errors::Error`s
|
||||
macro_rules! format_err {
|
||||
($e_type:expr, $literal:expr) => {
|
||||
$e_type(format!($literal))
|
||||
};
|
||||
($e_type:expr, $literal:expr, $($arg:expr),*) => {
|
||||
$e_type(format!($literal, $($arg),*))
|
||||
};
|
||||
}
|
||||
|
||||
/// Helper for formatting `errors::Error`s and returning early
|
||||
macro_rules! bail {
|
||||
($e_type:expr, $literal:expr) => {
|
||||
return Err(format_err!($e_type, $literal))
|
||||
};
|
||||
($e_type:expr, $literal:expr, $($arg:expr),*) => {
|
||||
return Err(format_err!($e_type, $literal, $($arg),*))
|
||||
};
|
||||
}
|
||||
|
||||
/// Helper to `print!` and immediately `flush` `stdout`
|
||||
macro_rules! print_flush {
|
||||
($literal:expr) => {
|
||||
print!($literal);
|
||||
::std::io::Write::flush(&mut ::std::io::stdout())?;
|
||||
};
|
||||
($literal:expr, $($arg:expr),*) => {
|
||||
print!($literal, $($arg),*);
|
||||
::std::io::Write::flush(&mut ::std::io::stdout())?;
|
||||
}
|
||||
}
|
||||
|
||||
/// Set ssl cert env. vars to make sure openssl can find required files
|
||||
macro_rules! set_ssl_vars {
|
||||
() => {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
if ::std::env::var_os("SSL_CERT_FILE").is_none() {
|
||||
::std::env::set_var("SSL_CERT_FILE", "/etc/ssl/certs/ca-certificates.crt");
|
||||
}
|
||||
if ::std::env::var_os("SSL_CERT_DIR").is_none() {
|
||||
::std::env::set_var("SSL_CERT_DIR", "/etc/ssl/certs");
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
29
lib/rust/src/platform/error.rs
Normal file
29
lib/rust/src/platform/error.rs
Normal file
@ -0,0 +1,29 @@
|
||||
use std;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Arch(String),
|
||||
Target(String),
|
||||
Abi(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
use Error::*;
|
||||
match *self {
|
||||
Arch(ref s) => write!(f, "ArchError: {}", s),
|
||||
Target(ref e) => write!(f, "TargetError: {}", e),
|
||||
Abi(ref e) => write!(f, "AbiError: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {
|
||||
fn description(&self) -> &str {
|
||||
"Platform Error"
|
||||
}
|
||||
|
||||
fn cause(&self) -> Option<&std::error::Error> {
|
||||
return None;
|
||||
}
|
||||
}
|
54
lib/rust/src/platform/mod.rs
Normal file
54
lib/rust/src/platform/mod.rs
Normal file
@ -0,0 +1,54 @@
|
||||
pub mod error;
|
||||
use error::*;
|
||||
|
||||
/// Try to determine the current target triple.
|
||||
///
|
||||
/// Returns a target triple (e.g. `x86_64-unknown-linux-gnu` or `i686-pc-windows-msvc`) or an
|
||||
/// `Error::Config` if the current config cannot be determined or is not some combination of the
|
||||
/// following values:
|
||||
/// `linux, mac, windows` -- `i686, x86, armv7` -- `gnu, musl, msvc`
|
||||
///
|
||||
/// * Errors:
|
||||
/// * Unexpected system config
|
||||
pub fn target_triple() -> Result<String, Error> {
|
||||
let arch = if cfg!(target_arch = "x86") {
|
||||
"i686"
|
||||
} else if cfg!(target_arch = "x86_64") {
|
||||
"x86_64"
|
||||
} else if cfg!(target_arch = "arm") {
|
||||
"armv7"
|
||||
} else {
|
||||
bail!(Error::Arch, "Unable to determine target-architecture")
|
||||
};
|
||||
|
||||
let os = if cfg!(target_os = "linux") {
|
||||
"unknown-linux"
|
||||
} else if cfg!(target_os = "macos") {
|
||||
"apple-darwin"
|
||||
} else if cfg!(target_os = "windows") {
|
||||
"pc-windows"
|
||||
} else if cfg!(target_os = "freebsd") {
|
||||
"unknown-freebsd"
|
||||
} else {
|
||||
bail!(Error::Target, "Unable to determine target-os");
|
||||
};
|
||||
|
||||
let s;
|
||||
let os = if cfg!(target_os = "macos") || cfg!(target_os = "freebsd") {
|
||||
os
|
||||
} else {
|
||||
let env = if cfg!(target_env = "gnu") {
|
||||
"gnu"
|
||||
} else if cfg!(target_env = "gnu") {
|
||||
"musl"
|
||||
} else if cfg!(target_env = "msvc") {
|
||||
"msvc"
|
||||
} else {
|
||||
bail!(Error::Abi, "Unable to determine target-environment")
|
||||
};
|
||||
s = format!("{}-{}", os, env);
|
||||
&s
|
||||
};
|
||||
|
||||
Ok(format!("{}-{}", arch, os))
|
||||
}
|
18
lib/rust/src/process.rs
Normal file
18
lib/rust/src/process.rs
Normal file
@ -0,0 +1,18 @@
|
||||
extern crate sysinfo;
|
||||
|
||||
pub use sysinfo::{Process, ProcessExt, Signal, System, SystemExt};
|
||||
|
||||
pub fn get_parent_process(system: &mut sysinfo::System) -> Result<&Process, String> {
|
||||
let pid = sysinfo::get_current_pid().unwrap();
|
||||
system.refresh_process(pid);
|
||||
let current_process = system
|
||||
.get_process(pid)
|
||||
.ok_or("Could not get current process")?;
|
||||
let parent_pid = current_process.parent().ok_or("Could not get parent PID")?;
|
||||
let parent_process = system
|
||||
.get_process(parent_pid)
|
||||
.ok_or("Could not get parent process")?;
|
||||
|
||||
println!("{}", pid);
|
||||
Ok(parent_process)
|
||||
}
|
15
lib/rust/src/rpc.rs
Executable file
15
lib/rust/src/rpc.rs
Executable file
@ -0,0 +1,15 @@
|
||||
pub fn format_callback(function_name: String, arg: String) -> String {
|
||||
let formatted_string = &format!("window[\"{}\"]({})", function_name, arg);
|
||||
return formatted_string.to_string();
|
||||
}
|
||||
|
||||
pub fn format_callback_result(
|
||||
result: Result<String, String>,
|
||||
callback: String,
|
||||
error_callback: String,
|
||||
) -> String {
|
||||
match result {
|
||||
Ok(res) => return format_callback(callback, res),
|
||||
Err(err) => return format_callback(error_callback, format!("\"{}\"", err)),
|
||||
}
|
||||
}
|
25
lib/rust/src/tcp.rs
Normal file
25
lib/rust/src/tcp.rs
Normal file
@ -0,0 +1,25 @@
|
||||
use std::net::TcpListener;
|
||||
|
||||
extern crate rand;
|
||||
|
||||
use rand::distributions::{Distribution, Uniform};
|
||||
|
||||
pub fn get_available_port() -> Option<u16> {
|
||||
let mut rng = rand::thread_rng();
|
||||
let die = Uniform::from(8000..9000);
|
||||
|
||||
for _i in 0..100 {
|
||||
let port = die.sample(&mut rng);
|
||||
if port_is_available(port) {
|
||||
return Some(port);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn port_is_available(port: u16) -> bool {
|
||||
match TcpListener::bind(("127.0.0.1", port)) {
|
||||
Ok(_) => true,
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
78
lib/rust/src/updater/error.rs
Normal file
78
lib/rust/src/updater/error.rs
Normal file
@ -0,0 +1,78 @@
|
||||
use super::super::file;
|
||||
use super::super::http;
|
||||
use super::super::version;
|
||||
use reqwest;
|
||||
use std;
|
||||
use zip::result::ZipError;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Updater(String),
|
||||
Release(String),
|
||||
Network(String),
|
||||
Config(String),
|
||||
Io(std::io::Error),
|
||||
Zip(ZipError),
|
||||
File(file::Error),
|
||||
Version(version::Error),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
use Error::*;
|
||||
match *self {
|
||||
Updater(ref s) => write!(f, "UpdaterError: {}", s),
|
||||
Release(ref s) => write!(f, "ReleaseError: {}", s),
|
||||
Network(ref s) => write!(f, "NetworkError: {}", s),
|
||||
Config(ref s) => write!(f, "ConfigError: {}", s),
|
||||
Io(ref e) => write!(f, "IoError: {}", e),
|
||||
Zip(ref e) => write!(f, "ZipError: {}", e),
|
||||
File(ref e) => write!(f, "FileError: {}", e),
|
||||
Version(ref e) => write!(f, "VersionError: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {
|
||||
fn description(&self) -> &str {
|
||||
"Updater Error"
|
||||
}
|
||||
|
||||
fn cause(&self) -> Option<&std::error::Error> {
|
||||
use Error::*;
|
||||
Some(match *self {
|
||||
Io(ref e) => e,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for Error {
|
||||
fn from(e: std::io::Error) -> Self {
|
||||
Error::Io(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<file::Error> for Error {
|
||||
fn from(e: file::Error) -> Self {
|
||||
Error::File(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<http::Error> for Error {
|
||||
fn from(e: http::Error) -> Self {
|
||||
Error::Network(e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<reqwest::Error> for Error {
|
||||
fn from(e: reqwest::Error) -> Self {
|
||||
Error::Network(e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<version::Error> for Error {
|
||||
fn from(e: version::Error) -> Self {
|
||||
Error::Version(e)
|
||||
}
|
||||
}
|
43
lib/rust/src/updater/github/mod.rs
Normal file
43
lib/rust/src/updater/github/mod.rs
Normal file
@ -0,0 +1,43 @@
|
||||
mod release;
|
||||
pub use super::error::Error;
|
||||
pub use release::*;
|
||||
|
||||
use super::super::http;
|
||||
|
||||
pub fn get_latest_release(repo_owner: &str, repo_name: &str) -> Result<Release, Error> {
|
||||
set_ssl_vars!();
|
||||
let api_url = format!(
|
||||
"https://api.github.com/repos/{}/{}/releases/latest",
|
||||
repo_owner, repo_name
|
||||
);
|
||||
let mut resp = http::get(&api_url)?;
|
||||
if !resp.status().is_success() {
|
||||
bail!(
|
||||
Error::Network,
|
||||
"api request failed with status: {:?} - for: {:?}",
|
||||
resp.status(),
|
||||
api_url
|
||||
)
|
||||
}
|
||||
let json = resp.json::<serde_json::Value>()?;
|
||||
Ok(Release::parse(&json)?)
|
||||
}
|
||||
|
||||
pub fn get_release_version(repo_owner: &str, repo_name: &str, ver: &str) -> Result<Release, Error> {
|
||||
set_ssl_vars!();
|
||||
let api_url = format!(
|
||||
"https://api.github.com/repos/{}/{}/releases/tags/{}",
|
||||
repo_owner, repo_name, ver
|
||||
);
|
||||
let mut resp = http::get(&api_url)?;
|
||||
if !resp.status().is_success() {
|
||||
bail!(
|
||||
Error::Network,
|
||||
"api request failed with status: {:?} - for: {:?}",
|
||||
resp.status(),
|
||||
api_url
|
||||
)
|
||||
}
|
||||
let json = resp.json::<serde_json::Value>()?;
|
||||
Ok(Release::parse(&json)?)
|
||||
}
|
215
lib/rust/src/updater/github/release.rs
Normal file
215
lib/rust/src/updater/github/release.rs
Normal file
@ -0,0 +1,215 @@
|
||||
use super::super::error::*;
|
||||
use hyper_old_types::header::{LinkValue, RelationType};
|
||||
use serde_json;
|
||||
|
||||
/// GitHub release-asset information
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ReleaseAsset {
|
||||
pub download_url: String,
|
||||
pub name: String,
|
||||
}
|
||||
impl ReleaseAsset {
|
||||
/// Parse a release-asset json object
|
||||
///
|
||||
/// Errors:
|
||||
/// * Missing required name & browser_download_url keys
|
||||
fn from_asset(asset: &serde_json::Value) -> Result<ReleaseAsset, Error> {
|
||||
let download_url = asset["browser_download_url"]
|
||||
.as_str()
|
||||
.ok_or_else(|| format_err!(Error::Release, "Asset missing `browser_download_url`"))?;
|
||||
let name = asset["name"]
|
||||
.as_str()
|
||||
.ok_or_else(|| format_err!(Error::Release, "Asset missing `name`"))?;
|
||||
Ok(ReleaseAsset {
|
||||
download_url: download_url.to_owned(),
|
||||
name: name.to_owned(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Release {
|
||||
pub name: String,
|
||||
pub body: String,
|
||||
pub tag: String,
|
||||
pub date_created: String,
|
||||
pub assets: Vec<ReleaseAsset>,
|
||||
}
|
||||
impl Release {
|
||||
pub fn parse(release: &serde_json::Value) -> Result<Release, Error> {
|
||||
let tag = release["tag_name"]
|
||||
.as_str()
|
||||
.ok_or_else(|| format_err!(Error::Release, "Release missing `tag_name`"))?;
|
||||
let date_created = release["created_at"]
|
||||
.as_str()
|
||||
.ok_or_else(|| format_err!(Error::Release, "Release missing `created_at`"))?;
|
||||
let name = release["name"].as_str().unwrap_or(tag);
|
||||
let body = release["body"].as_str().unwrap_or("");
|
||||
let assets = release["assets"]
|
||||
.as_array()
|
||||
.ok_or_else(|| format_err!(Error::Release, "No assets found"))?;
|
||||
let assets = assets
|
||||
.iter()
|
||||
.map(ReleaseAsset::from_asset)
|
||||
.collect::<Result<Vec<ReleaseAsset>, Error>>()?;
|
||||
Ok(Release {
|
||||
name: name.to_owned(),
|
||||
body: body.to_owned(),
|
||||
tag: tag.to_owned(),
|
||||
date_created: date_created.to_owned(),
|
||||
assets,
|
||||
})
|
||||
}
|
||||
|
||||
/// Check if release has an asset who's name contains the specified `target`
|
||||
pub fn has_target_asset(&self, target: &str) -> bool {
|
||||
self.assets.iter().any(|asset| asset.name.contains(target))
|
||||
}
|
||||
|
||||
/// Return the first `ReleaseAsset` for the current release who's name
|
||||
/// contains the specified `target`
|
||||
pub fn asset_for(&self, target: &str) -> Option<ReleaseAsset> {
|
||||
self
|
||||
.assets
|
||||
.iter()
|
||||
.filter(|asset| asset.name.contains(target))
|
||||
.cloned()
|
||||
.nth(0)
|
||||
}
|
||||
|
||||
pub fn version(&self) -> &str {
|
||||
self.tag.trim_start_matches('v')
|
||||
}
|
||||
}
|
||||
|
||||
/// `ReleaseList` Builder
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ReleaseListBuilder {
|
||||
repo_owner: Option<String>,
|
||||
repo_name: Option<String>,
|
||||
target: Option<String>,
|
||||
}
|
||||
impl ReleaseListBuilder {
|
||||
/// Set the repo owner, used to build a github api url
|
||||
pub fn repo_owner(&mut self, owner: &str) -> &mut Self {
|
||||
self.repo_owner = Some(owner.to_owned());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the repo name, used to build a github api url
|
||||
pub fn repo_name(&mut self, name: &str) -> &mut Self {
|
||||
self.repo_name = Some(name.to_owned());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the optional arch `target` name, used to filter available releases
|
||||
pub fn target(&mut self, target: &str) -> &mut Self {
|
||||
self.target = Some(target.to_owned());
|
||||
self
|
||||
}
|
||||
|
||||
/// Verify builder args, returning a `ReleaseList`
|
||||
pub fn build(&self) -> Result<ReleaseList, Error> {
|
||||
Ok(ReleaseList {
|
||||
repo_owner: if let Some(ref owner) = self.repo_owner {
|
||||
owner.to_owned()
|
||||
} else {
|
||||
bail!(Error::Config, "`repo_owner` required")
|
||||
},
|
||||
repo_name: if let Some(ref name) = self.repo_name {
|
||||
name.to_owned()
|
||||
} else {
|
||||
bail!(Error::Config, "`repo_name` required")
|
||||
},
|
||||
target: self.target.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// `ReleaseList` provides a builder api for querying a GitHub repo,
|
||||
/// returning a `Vec` of available `Release`s
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ReleaseList {
|
||||
repo_owner: String,
|
||||
repo_name: String,
|
||||
target: Option<String>,
|
||||
}
|
||||
impl ReleaseList {
|
||||
/// Initialize a ReleaseListBuilder
|
||||
pub fn configure() -> ReleaseListBuilder {
|
||||
ReleaseListBuilder {
|
||||
repo_owner: None,
|
||||
repo_name: None,
|
||||
target: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieve a list of `Release`s.
|
||||
/// If specified, filter for those containing a specified `target`
|
||||
pub fn fetch(self) -> Result<Vec<Release>, Error> {
|
||||
set_ssl_vars!();
|
||||
let api_url = format!(
|
||||
"https://api.github.com/repos/{}/{}/releases",
|
||||
self.repo_owner, self.repo_name
|
||||
);
|
||||
let releases = Self::fetch_releases(&api_url)?;
|
||||
let releases = match self.target {
|
||||
None => releases,
|
||||
Some(ref target) => releases
|
||||
.into_iter()
|
||||
.filter(|r| r.has_target_asset(target))
|
||||
.collect::<Vec<_>>(),
|
||||
};
|
||||
Ok(releases)
|
||||
}
|
||||
|
||||
fn fetch_releases(url: &str) -> Result<Vec<Release>, Error> {
|
||||
let mut resp = reqwest::get(url)?;
|
||||
if !resp.status().is_success() {
|
||||
bail!(
|
||||
Error::Network,
|
||||
"api request failed with status: {:?} - for: {:?}",
|
||||
resp.status(),
|
||||
url
|
||||
)
|
||||
}
|
||||
let releases = resp.json::<serde_json::Value>()?;
|
||||
let releases = releases
|
||||
.as_array()
|
||||
.ok_or_else(|| format_err!(Error::Release, "No releases found"))?;
|
||||
let mut releases = releases
|
||||
.iter()
|
||||
.map(Release::parse)
|
||||
.collect::<Result<Vec<Release>, Error>>()?;
|
||||
|
||||
// handle paged responses containing `Link` header:
|
||||
// `Link: <https://api.github.com/resource?page=2>; rel="next"`
|
||||
let headers = resp.headers();
|
||||
let links = headers.get_all(reqwest::header::LINK);
|
||||
|
||||
let next_link = links
|
||||
.iter()
|
||||
.filter_map(|link| {
|
||||
if let Ok(link) = link.to_str() {
|
||||
let lv = LinkValue::new(link.to_owned());
|
||||
if let Some(rels) = lv.rel() {
|
||||
if rels.contains(&RelationType::Next) {
|
||||
return Some(link);
|
||||
}
|
||||
}
|
||||
None
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.nth(0);
|
||||
|
||||
Ok(match next_link {
|
||||
None => releases,
|
||||
Some(link) => {
|
||||
releases.extend(Self::fetch_releases(link)?);
|
||||
releases
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
270
lib/rust/src/updater/mod.rs
Normal file
270
lib/rust/src/updater/mod.rs
Normal file
@ -0,0 +1,270 @@
|
||||
extern crate hyper_old_types;
|
||||
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use super::file::{Extract, Move};
|
||||
use super::http;
|
||||
|
||||
pub mod github;
|
||||
|
||||
mod error;
|
||||
pub use error::Error;
|
||||
|
||||
/// Status returned after updating
|
||||
///
|
||||
/// Wrapped `String`s are version tags
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Status {
|
||||
UpToDate(String),
|
||||
Updated(String),
|
||||
}
|
||||
impl Status {
|
||||
/// Return the version tag
|
||||
pub fn version(&self) -> &str {
|
||||
use Status::*;
|
||||
match *self {
|
||||
UpToDate(ref s) => s,
|
||||
Updated(ref s) => s,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if `Status::UpToDate`
|
||||
pub fn uptodate(&self) -> bool {
|
||||
match *self {
|
||||
Status::UpToDate(_) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if `Status::Updated`
|
||||
pub fn updated(&self) -> bool {
|
||||
match *self {
|
||||
Status::Updated(_) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Release {
|
||||
pub version: String,
|
||||
pub asset_name: String,
|
||||
pub download_url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct UpdateBuilder {
|
||||
release: Option<Release>,
|
||||
bin_name: Option<String>,
|
||||
bin_install_path: Option<PathBuf>,
|
||||
bin_path_in_archive: Option<PathBuf>,
|
||||
show_download_progress: bool,
|
||||
show_output: bool,
|
||||
current_version: Option<String>,
|
||||
}
|
||||
impl UpdateBuilder {
|
||||
/// Initialize a new builder, defaulting the `bin_install_path` to the current
|
||||
/// executable's path
|
||||
///
|
||||
/// * Errors:
|
||||
/// * Io - Determining current exe path
|
||||
pub fn new() -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
release: None,
|
||||
bin_name: None,
|
||||
bin_install_path: Some(env::current_exe()?),
|
||||
bin_path_in_archive: None,
|
||||
show_download_progress: false,
|
||||
show_output: true,
|
||||
current_version: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn release(&mut self, release: Release) -> &mut Self {
|
||||
self.release = Some(release);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the current app version, used to compare against the latest available version.
|
||||
/// The `cargo_crate_version!` macro can be used to pull the version from your `Cargo.toml`
|
||||
pub fn current_version(&mut self, ver: &str) -> &mut Self {
|
||||
self.current_version = Some(ver.to_owned());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the exe's name. Also sets `bin_path_in_archive` if it hasn't already been set.
|
||||
pub fn bin_name(&mut self, name: &str) -> &mut Self {
|
||||
self.bin_name = Some(name.to_owned());
|
||||
if self.bin_path_in_archive.is_none() {
|
||||
self.bin_path_in_archive = Some(PathBuf::from(name));
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the installation path for the new exe, defaults to the current
|
||||
/// executable's path
|
||||
pub fn bin_install_path(&mut self, bin_install_path: &str) -> &mut Self {
|
||||
self.bin_install_path = Some(PathBuf::from(bin_install_path));
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the path of the exe inside the release tarball. This is the location
|
||||
/// of the executable relative to the base of the tar'd directory and is the
|
||||
/// path that will be copied to the `bin_install_path`. If not specified, this
|
||||
/// will default to the value of `bin_name`. This only needs to be specified if
|
||||
/// the path to the binary (from the root of the tarball) is not equal to just
|
||||
/// the `bin_name`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// For a tarball `myapp.tar.gz` with the contents:
|
||||
///
|
||||
/// ```shell
|
||||
/// myapp.tar/
|
||||
/// |------- bin/
|
||||
/// | |--- myapp # <-- executable
|
||||
/// ```
|
||||
///
|
||||
/// The path provided should be:
|
||||
///
|
||||
/// ```
|
||||
/// # use proton::updater::Update;
|
||||
/// # fn run() -> Result<(), Box<::std::error::Error>> {
|
||||
/// Update::configure()?
|
||||
/// .bin_path_in_archive("bin/myapp")
|
||||
/// # .build()?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn bin_path_in_archive(&mut self, bin_path: &str) -> &mut Self {
|
||||
self.bin_path_in_archive = Some(PathBuf::from(bin_path));
|
||||
self
|
||||
}
|
||||
|
||||
/// Toggle download progress bar, defaults to `off`.
|
||||
pub fn show_download_progress(&mut self, show: bool) -> &mut Self {
|
||||
self.show_download_progress = show;
|
||||
self
|
||||
}
|
||||
|
||||
/// Toggle update output information, defaults to `true`.
|
||||
pub fn show_output(&mut self, show: bool) -> &mut Self {
|
||||
self.show_output = show;
|
||||
self
|
||||
}
|
||||
|
||||
/// Confirm config and create a ready-to-use `Update`
|
||||
///
|
||||
/// * Errors:
|
||||
/// * Config - Invalid `Update` configuration
|
||||
pub fn build(&self) -> Result<Update, Error> {
|
||||
Ok(Update {
|
||||
release: if let Some(ref release) = self.release {
|
||||
release.to_owned()
|
||||
} else {
|
||||
bail!(Error::Config, "`release` required")
|
||||
},
|
||||
bin_name: if let Some(ref name) = self.bin_name {
|
||||
name.to_owned()
|
||||
} else {
|
||||
bail!(Error::Config, "`bin_name` required")
|
||||
},
|
||||
bin_install_path: if let Some(ref path) = self.bin_install_path {
|
||||
path.to_owned()
|
||||
} else {
|
||||
bail!(Error::Config, "`bin_install_path` required")
|
||||
},
|
||||
bin_path_in_archive: if let Some(ref path) = self.bin_path_in_archive {
|
||||
path.to_owned()
|
||||
} else {
|
||||
bail!(Error::Config, "`bin_path_in_archive` required")
|
||||
},
|
||||
current_version: if let Some(ref ver) = self.current_version {
|
||||
ver.to_owned()
|
||||
} else {
|
||||
bail!(Error::Config, "`current_version` required")
|
||||
},
|
||||
show_download_progress: self.show_download_progress,
|
||||
show_output: self.show_output,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates to a specified or latest release distributed
|
||||
#[derive(Debug)]
|
||||
pub struct Update {
|
||||
release: Release,
|
||||
current_version: String,
|
||||
bin_name: String,
|
||||
bin_install_path: PathBuf,
|
||||
bin_path_in_archive: PathBuf,
|
||||
show_download_progress: bool,
|
||||
show_output: bool,
|
||||
}
|
||||
impl Update {
|
||||
/// Initialize a new `Update` builder
|
||||
pub fn configure() -> Result<UpdateBuilder, Error> {
|
||||
UpdateBuilder::new()
|
||||
}
|
||||
|
||||
fn print_flush(&self, msg: &str) -> Result<(), Error> {
|
||||
if self.show_output {
|
||||
print_flush!("{}", msg);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn println(&self, msg: &str) {
|
||||
if self.show_output {
|
||||
println!("{}", msg);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(self) -> Result<Status, Error> {
|
||||
self.println(&format!(
|
||||
"Checking current version... v{}",
|
||||
self.current_version
|
||||
));
|
||||
|
||||
if self.show_output {
|
||||
println!("\n{} release status:", self.bin_name);
|
||||
println!(" * Current exe: {:?}", self.bin_install_path);
|
||||
println!(" * New exe download url: {:?}", self.release.download_url);
|
||||
println!(
|
||||
"\nThe new release will be downloaded/extracted and the existing binary will be replaced."
|
||||
);
|
||||
}
|
||||
|
||||
let tmp_dir_parent = self
|
||||
.bin_install_path
|
||||
.parent()
|
||||
.ok_or_else(|| Error::Updater("Failed to determine parent dir".into()))?;
|
||||
let tmp_dir =
|
||||
tempdir::TempDir::new_in(&tmp_dir_parent, &format!("{}_download", self.bin_name))?;
|
||||
let tmp_archive_path = tmp_dir.path().join(&self.release.asset_name);
|
||||
let mut tmp_archive = fs::File::create(&tmp_archive_path)?;
|
||||
|
||||
self.println("Downloading...");
|
||||
http::download(
|
||||
&self.release.download_url,
|
||||
&mut tmp_archive,
|
||||
self.show_download_progress,
|
||||
)?;
|
||||
|
||||
self.print_flush("Extracting archive... ")?;
|
||||
Extract::from_source(&tmp_archive_path)
|
||||
.extract_file(&tmp_dir.path(), &self.bin_path_in_archive)?;
|
||||
let new_exe = tmp_dir.path().join(&self.bin_path_in_archive);
|
||||
self.println("Done");
|
||||
|
||||
self.print_flush("Replacing binary file... ")?;
|
||||
let tmp_file = tmp_dir.path().join(&format!("__{}_backup", self.bin_name));
|
||||
Move::from_source(&new_exe)
|
||||
.replace_using_temp(&tmp_file)
|
||||
.to_dest(&self.bin_install_path)?;
|
||||
self.println("Done");
|
||||
Ok(Status::Updated(self.release.version))
|
||||
}
|
||||
}
|
35
lib/rust/src/version/error.rs
Normal file
35
lib/rust/src/version/error.rs
Normal file
@ -0,0 +1,35 @@
|
||||
use semver;
|
||||
use std;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
SemVer(semver::SemVerError),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
use Error::*;
|
||||
match *self {
|
||||
SemVer(ref e) => write!(f, "SemVerError: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {
|
||||
fn description(&self) -> &str {
|
||||
"Version Error"
|
||||
}
|
||||
|
||||
fn cause(&self) -> Option<&std::error::Error> {
|
||||
use Error::*;
|
||||
Some(match *self {
|
||||
SemVer(ref e) => e,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<semver::SemVerError> for Error {
|
||||
fn from(e: semver::SemVerError) -> Self {
|
||||
Error::SemVer(e)
|
||||
}
|
||||
}
|
53
lib/rust/src/version/mod.rs
Normal file
53
lib/rust/src/version/mod.rs
Normal file
@ -0,0 +1,53 @@
|
||||
use semver::Version;
|
||||
|
||||
mod error;
|
||||
pub use self::error::Error;
|
||||
|
||||
/// Compare two semver versions
|
||||
pub fn compare(first: &str, second: &str) -> Result<i32, Error> {
|
||||
let v1 = Version::parse(first)?;
|
||||
let v2 = Version::parse(second)?;
|
||||
if v1 > v2 {
|
||||
Ok(-1)
|
||||
} else if v1 == v2 {
|
||||
Ok(0)
|
||||
} else {
|
||||
Ok(1)
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the "second" semver is compatible with the "first"
|
||||
pub fn is_compatible(first: &str, second: &str) -> Result<bool, Error> {
|
||||
let first = Version::parse(first)?;
|
||||
let second = Version::parse(second)?;
|
||||
Ok(if second.major == 0 && first.major == 0 {
|
||||
first.minor == second.minor && second.patch > first.patch
|
||||
} else if second.major > 0 {
|
||||
first.major == second.major
|
||||
&& ((second.minor > first.minor)
|
||||
|| (first.minor == second.minor && second.patch > first.patch))
|
||||
} else {
|
||||
false
|
||||
})
|
||||
}
|
||||
|
||||
/// Check if a the "other" version is a major bump from the "current"
|
||||
pub fn is_major(current: &str, other: &str) -> Result<bool, Error> {
|
||||
let current = Version::parse(current)?;
|
||||
let other = Version::parse(other)?;
|
||||
Ok(other.major > current.major)
|
||||
}
|
||||
|
||||
/// Check if a the "other" version is a minor bump from the "current"
|
||||
pub fn is_minor(current: &str, other: &str) -> Result<bool, Error> {
|
||||
let current = Version::parse(current)?;
|
||||
let other = Version::parse(other)?;
|
||||
Ok(current.major == other.major && other.minor > current.minor)
|
||||
}
|
||||
|
||||
/// Check if a the "other" version is a patch bump from the "current"
|
||||
pub fn is_patch(current: &str, other: &str) -> Result<bool, Error> {
|
||||
let current = Version::parse(current)?;
|
||||
let other = Version::parse(other)?;
|
||||
Ok(current.major == other.major && current.minor == other.minor && other.patch > current.patch)
|
||||
}
|
27
package.json
Normal file
27
package.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "@quasar/proton",
|
||||
"version": "1.0.0-alpha.1",
|
||||
"description": "Multi-binding collection of libraries and templates for building Proton",
|
||||
"main": "templates/index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/quasarframework/proton.git"
|
||||
},
|
||||
"author": "Quasar Framework",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/quasarframework/proton/issues"
|
||||
},
|
||||
"homepage": "https://github.com/quasarframework/proton#readme",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.16.0",
|
||||
"npm": ">= 6.6.0",
|
||||
"yarn": ">= 1.17.3"
|
||||
}
|
||||
}
|
1
spec/README.md
Normal file
1
spec/README.md
Normal file
@ -0,0 +1 @@
|
||||
[WIP]
|
39
templates/rust/Cargo.toml
Executable file
39
templates/rust/Cargo.toml
Executable file
@ -0,0 +1,39 @@
|
||||
[package]
|
||||
name = "app"
|
||||
version = "0.1.0"
|
||||
description = "A Quasar app"
|
||||
authors = ["Lucas Fernandes Nogueira <lucasfernandesnog@gmail.com>"]
|
||||
edition = "2018"
|
||||
build = "build.rs"
|
||||
include = ["data"]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
proton-ui = { path = "../../proton/bindings/rust" }
|
||||
serde_json = "1.0.39"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
tiny_http = "0.6"
|
||||
clap = {version = "2.33", features = ["yaml"]}
|
||||
phf = "0.7.21"
|
||||
includedir = "0.5.0"
|
||||
proton = { path = "../../proton/lib/rust" }
|
||||
|
||||
[build-dependencies]
|
||||
includedir_codegen = "0.5.0"
|
||||
|
||||
[features]
|
||||
dev = [] # has no explicit dependencies
|
||||
|
||||
[package.metadata.bundle]
|
||||
identifier = "com.quasar.dev"
|
||||
icon = ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"]
|
||||
|
||||
[[bin]]
|
||||
name = "updater"
|
||||
path = "src/updater.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "app"
|
||||
path = "src/main.rs"
|
10
templates/rust/_gitignore
Executable file
10
templates/rust/_gitignore
Executable file
@ -0,0 +1,10 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
|
||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||
Cargo.lock
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
10
templates/rust/build.rs
Executable file
10
templates/rust/build.rs
Executable file
@ -0,0 +1,10 @@
|
||||
extern crate includedir_codegen;
|
||||
|
||||
use includedir_codegen::Compression;
|
||||
|
||||
fn main() {
|
||||
includedir_codegen::start("ASSETS")
|
||||
.dir("./target/compiled-web", Compression::Gzip)
|
||||
.build("data.rs")
|
||||
.unwrap();
|
||||
}
|
BIN
templates/rust/icons/128x128.png
Normal file
BIN
templates/rust/icons/128x128.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
BIN
templates/rust/icons/128x128@2x.png
Normal file
BIN
templates/rust/icons/128x128@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 28 KiB |
BIN
templates/rust/icons/32x32.png
Normal file
BIN
templates/rust/icons/32x32.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.2 KiB |
BIN
templates/rust/icons/icon.icns
Normal file
BIN
templates/rust/icons/icon.icns
Normal file
Binary file not shown.
BIN
templates/rust/icons/icon.ico
Normal file
BIN
templates/rust/icons/icon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 43 KiB |
13
templates/rust/rustfmt.toml
Normal file
13
templates/rust/rustfmt.toml
Normal file
@ -0,0 +1,13 @@
|
||||
max_width = 100
|
||||
hard_tabs = false
|
||||
tab_spaces = 2
|
||||
newline_style = "Auto"
|
||||
use_small_heuristics = "Default"
|
||||
reorder_imports = true
|
||||
reorder_modules = true
|
||||
remove_nested_parens = true
|
||||
edition = "2015"
|
||||
merge_derives = true
|
||||
use_try_shorthand = false
|
||||
use_field_init_shorthand = false
|
||||
force_explicit_abi = true
|
8
templates/rust/src/cmd.rs
Executable file
8
templates/rust/src/cmd.rs
Executable file
@ -0,0 +1,8 @@
|
||||
#[derive(Deserialize)]
|
||||
#[serde(tag = "cmd", rename_all = "camelCase")]
|
||||
pub enum Cmd {
|
||||
// your custom commands
|
||||
// multiple arguments are allowed
|
||||
// note that rename_all = "camelCase": you need to use "myCustomCommand" on JS
|
||||
MyCustomCommand { argument: String },
|
||||
}
|
112
templates/rust/src/main.rs
Executable file
112
templates/rust/src/main.rs
Executable file
@ -0,0 +1,112 @@
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
extern crate clap;
|
||||
extern crate proton;
|
||||
extern crate proton_ui;
|
||||
extern crate serde_json;
|
||||
|
||||
#[cfg(not(feature = "dev"))]
|
||||
extern crate tiny_http;
|
||||
|
||||
#[cfg(feature = "dev")]
|
||||
use clap::{App, Arg};
|
||||
|
||||
#[cfg(not(feature = "dev"))]
|
||||
use std::thread;
|
||||
|
||||
mod cmd;
|
||||
|
||||
#[cfg(not(feature = "dev"))]
|
||||
mod server;
|
||||
|
||||
fn main() {
|
||||
let debug;
|
||||
let content;
|
||||
let _matches: clap::ArgMatches;
|
||||
|
||||
#[cfg(not(feature = "dev"))]
|
||||
{
|
||||
thread::spawn(|| {
|
||||
proton::command::spawn_relative_command(
|
||||
"updater".to_string(),
|
||||
Vec::new(),
|
||||
std::process::Stdio::inherit(),
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(feature = "dev")]
|
||||
{
|
||||
let app = App::new("app")
|
||||
.version("1.0.0")
|
||||
.author("Author")
|
||||
.about("About")
|
||||
.arg(
|
||||
Arg::with_name("url")
|
||||
.short("u")
|
||||
.long("url")
|
||||
.value_name("URL")
|
||||
.help("Loads the specified URL into webview")
|
||||
.required(true)
|
||||
.takes_value(true),
|
||||
);
|
||||
|
||||
_matches = app.get_matches();
|
||||
content = proton_ui::Content::Url(_matches.value_of("url").unwrap());
|
||||
debug = true;
|
||||
}
|
||||
#[cfg(not(feature = "dev"))]
|
||||
{
|
||||
if let Some(available_port) = proton::tcp::get_available_port() {
|
||||
let server_url = format!("{}:{}", "127.0.0.1", available_port);
|
||||
content = proton_ui::Content::Url(format!("http://{}", server_url));
|
||||
debug = cfg!(debug_assertions);
|
||||
|
||||
thread::spawn(move || {
|
||||
let server = tiny_http::Server::http(server_url).unwrap();
|
||||
for request in server.incoming_requests() {
|
||||
let mut url = request.url().to_string();
|
||||
if url == "/" {
|
||||
url = "/index.html".to_string();
|
||||
}
|
||||
request.respond(server::asset_response(&url)).unwrap();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
panic!("Could not find an open port");
|
||||
}
|
||||
}
|
||||
|
||||
let webview = proton_ui::builder()
|
||||
.title("MyAppTitle")
|
||||
.content(content)
|
||||
.size(800, 600) // TODO:Resolution is fixed right now, change this later to be dynamic
|
||||
.resizable(true)
|
||||
.debug(debug)
|
||||
.user_data(())
|
||||
.invoke_handler(|_webview, arg| {
|
||||
// leave this as is to use the proton API from your JS code
|
||||
if !proton::api::handler(_webview, arg) {
|
||||
use cmd::Cmd::*;
|
||||
match serde_json::from_str(arg) {
|
||||
Err(_) => {}
|
||||
Ok(command) => {
|
||||
match command {
|
||||
// definitions for your custom commands from Cmd here
|
||||
MyCustomCommand { argument } => {
|
||||
// your command code
|
||||
println!("{}", argument);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
webview.run().unwrap();
|
||||
}
|
26
templates/rust/src/server.rs
Normal file
26
templates/rust/src/server.rs
Normal file
@ -0,0 +1,26 @@
|
||||
use tiny_http::{Header, Response};
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/data.rs"));
|
||||
|
||||
pub fn asset_response(path: &str) -> Response<std::io::Cursor<Vec<u8>>> {
|
||||
let asset = ASSETS
|
||||
.get(&format!("./target/compiled-web{}", path))
|
||||
.unwrap()
|
||||
.into_owned();
|
||||
let mut response = Response::from_data(asset);
|
||||
let header;
|
||||
|
||||
if path.ends_with(".svg") {
|
||||
header = Header::from_bytes(&b"Content-Type"[..], &b"image/svg+xml"[..]).unwrap();
|
||||
} else if path.ends_with(".css") {
|
||||
header = Header::from_bytes(&b"Content-Type"[..], &b"text/css"[..]).unwrap();
|
||||
} else if path.ends_with(".html") {
|
||||
header = Header::from_bytes(&b"Content-Type"[..], &b"text/html"[..]).unwrap();
|
||||
} else {
|
||||
header = Header::from_bytes(&b"Content-Type"[..], &b"appication/octet-stream"[..]).unwrap();
|
||||
}
|
||||
|
||||
response.add_header(header);
|
||||
|
||||
response
|
||||
}
|
73
templates/rust/src/updater.rs
Normal file
73
templates/rust/src/updater.rs
Normal file
@ -0,0 +1,73 @@
|
||||
extern crate proton;
|
||||
extern crate serde_derive;
|
||||
extern crate serde_json;
|
||||
|
||||
use crate::proton::process::{ProcessExt, Signal, SystemExt};
|
||||
|
||||
fn update() -> Result<(), String> {
|
||||
let target = proton::platform::target_triple().map_err(|_| "Could not determine target")?;
|
||||
let github_release = proton::updater::github::get_latest_release("jaemk", "self_update")
|
||||
.map_err(|_| "Could not fetch latest release")?;
|
||||
match github_release.asset_for(&target) {
|
||||
Some(github_release_asset) => {
|
||||
let release = proton::updater::Release {
|
||||
version: github_release.tag.trim_start_matches('v').to_string(),
|
||||
download_url: github_release_asset.download_url,
|
||||
asset_name: github_release_asset.name,
|
||||
};
|
||||
|
||||
let status = proton::updater::Update::configure()
|
||||
.unwrap()
|
||||
.release(release)
|
||||
.bin_path_in_archive("github")
|
||||
.bin_name("app")
|
||||
.bin_install_path(&proton::command::command_path("app".to_string()).unwrap())
|
||||
.show_download_progress(true)
|
||||
.current_version(env!("CARGO_PKG_VERSION"))
|
||||
.build()
|
||||
.unwrap()
|
||||
.update()
|
||||
.unwrap();
|
||||
|
||||
println!("found release: {}", status.version());
|
||||
|
||||
/*let tmp_dir = proton::dir::with_temp_dir(|dir| {
|
||||
let file_path = dir.path().join("my-temporary-note.pdf");
|
||||
let mut tmp_archive = std::fs::File::create(file_path).unwrap();
|
||||
proton::http::download(&"https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf".to_string(), &mut tmp_archive, true).unwrap();
|
||||
});*/
|
||||
|
||||
Ok(())
|
||||
}
|
||||
None => Err(format!("Could not find release for target {}", target)),
|
||||
}
|
||||
}
|
||||
|
||||
fn restart_app(app_command: String) -> Result<(), String> {
|
||||
let mut system = proton::process::System::new();
|
||||
let parent_process = proton::process::get_parent_process(&mut system)
|
||||
.map_err(|_| "Could not determine parent process")?;
|
||||
if parent_process.name() == "app" {
|
||||
parent_process.kill(Signal::Kill);
|
||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||
std::process::Command::new(app_command)
|
||||
.spawn()
|
||||
.map_err(|_| "Could not start app")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_updater() -> Result<(), String> {
|
||||
let app_command = proton::command::relative_command("app".to_string())
|
||||
.map_err(|_| "Could not determine app path")?;
|
||||
update()?;
|
||||
restart_app(app_command)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
match run_updater() {
|
||||
Ok(_) => {}
|
||||
Err(err) => panic!(err),
|
||||
};
|
||||
}
|
1
ui/.gitattributes
vendored
Normal file
1
ui/.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
||||
*.h linguist-language=c
|
2
ui/.gitignore
vendored
Normal file
2
ui/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
# Build atrifacts
|
||||
/build
|
32
ui/CMakeLists.txt
Executable file
32
ui/CMakeLists.txt
Executable file
@ -0,0 +1,32 @@
|
||||
cmake_minimum_required(VERSION 2.8)
|
||||
|
||||
project(webview)
|
||||
|
||||
if(APPLE)
|
||||
set(WEBVIEW_COMPILE_DEFS "-DWEBVIEW_COCOA=1")
|
||||
set(WEBVIEW_LIBS "-framework WebKit")
|
||||
elseif(WIN32)
|
||||
set(WEBVIEW_COMPILE_DEFS "-DWEBVIEW_WINAPI=1")
|
||||
set(WEBVIEW_LIBS "ole32 comctl32 oleaut32 uuid")
|
||||
else()
|
||||
set(WEBVIEW_COMPILE_DEFS "-DWEBVIEW_GTK=1")
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(GTK3 REQUIRED gtk+-3.0)
|
||||
pkg_check_modules(WEBKIT2 REQUIRED webkit2gtk-4.0)
|
||||
set(WEBVIEW_COMPILE_INCS ${GTK3_INCLUDE_DIRS} ${WEBKIT2_INCLUDE_DIRS} ${PROJECT_SOURCE_DIR})
|
||||
set(WEBVIEW_LIBS ${GTK3_LIBRARIES} ${WEBKIT2_LIBRARIES})
|
||||
endif()
|
||||
|
||||
add_library(proton ${CMAKE_CURRENT_BINARY_DIR}/proton.c)
|
||||
file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/proton.c "#define WEBVIEW_IMPLEMENTATION\n#include <proton.h>")
|
||||
target_include_directories(proton PUBLIC ${PROJECT_SOURCE_DIR} ${WEBVIEW_COMPILE_INCS})
|
||||
target_compile_definitions(proton PUBLIC ${WEBVIEW_COMPILE_DEFS})
|
||||
target_compile_options(proton PRIVATE ${WEBVIEW_COMPILE_OPTS})
|
||||
target_link_libraries(proton ${WEBVIEW_LIBS})
|
||||
|
||||
add_executable(proton_test WIN32 MACOSX_BUNDLE proton_test.cc)
|
||||
set_target_properties(proton_test PROPERTIES CXX_STANDARD 11 CXX_STANDARD_REQUIRED YES CXX_EXTENSIONS NO)
|
||||
find_package(Threads)
|
||||
target_link_libraries(proton_test PRIVATE proton ${CMAKE_THREAD_LIBS_INIT})
|
||||
enable_testing ()
|
||||
add_test(NAME proton_test COMMAND proton_test)
|
21
ui/LICENSE
Normal file
21
ui/LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 - present Serge Zaitsev & Quasar Framework Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
3
ui/README.md
Executable file
3
ui/README.md
Executable file
@ -0,0 +1,3 @@
|
||||
# PROTON WEBVIEW
|
||||
|
||||
Documentation forthcoming.
|
2265
ui/proton.h
Normal file
2265
ui/proton.h
Normal file
File diff suppressed because it is too large
Load Diff
176
ui/proton_test.cc
Normal file
176
ui/proton_test.cc
Normal file
@ -0,0 +1,176 @@
|
||||
// +build ignore
|
||||
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
#include <functional>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#define WEBVIEW_IMPLEMENTATION
|
||||
#include "proton.h"
|
||||
|
||||
extern "C" void webview_dispatch_proxy(struct webview *w, void *arg) {
|
||||
(*static_cast<std::function<void(struct webview *)> *>(arg))(w);
|
||||
}
|
||||
|
||||
class runner {
|
||||
public:
|
||||
runner(struct webview *w) : w(w) { webview_init(this->w); }
|
||||
~runner() { webview_exit(this->w); }
|
||||
runner &then(std::function<void(struct webview *w)> fn) {
|
||||
auto arg = new std::pair<std::function<void(struct webview *)>, void *>(
|
||||
fn, nullptr);
|
||||
this->queue.push_back([=](struct webview *w) {
|
||||
webview_dispatch(
|
||||
w,
|
||||
[](struct webview *w, void *arg) {
|
||||
auto dispatch_arg = reinterpret_cast<
|
||||
std::pair<std::function<void(struct webview *)>, void *> *>(
|
||||
arg);
|
||||
dispatch_arg->first(w);
|
||||
delete dispatch_arg;
|
||||
},
|
||||
reinterpret_cast<void *>(arg));
|
||||
});
|
||||
return *this;
|
||||
}
|
||||
runner &sleep(const int millis) {
|
||||
this->queue.push_back([=](struct webview *w) {
|
||||
(void)w;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(millis));
|
||||
});
|
||||
return *this;
|
||||
}
|
||||
void wait() {
|
||||
this->then([](struct webview *w) { webview_terminate(w); });
|
||||
auto q = this->queue;
|
||||
auto w = this->w;
|
||||
std::thread bg_thread([w, q]() {
|
||||
for (auto f : q) {
|
||||
f(w);
|
||||
}
|
||||
});
|
||||
while (webview_loop(w, 1) == 0) {
|
||||
}
|
||||
bg_thread.join();
|
||||
}
|
||||
|
||||
private:
|
||||
struct webview *w;
|
||||
std::vector<std::function<void(struct webview *)>> queue;
|
||||
};
|
||||
|
||||
static void test_minimal() {
|
||||
struct webview w = {};
|
||||
std::cout << "TEST: minimal" << std::endl;
|
||||
w.title = "Minimal test";
|
||||
w.width = 480;
|
||||
w.height = 320;
|
||||
webview_init(&w);
|
||||
webview_dispatch(&w,
|
||||
[](struct webview *w, void *arg) {
|
||||
(void)arg;
|
||||
webview_terminate(w);
|
||||
},
|
||||
nullptr);
|
||||
while (webview_loop(&w, 1) == 0) {
|
||||
}
|
||||
webview_exit(&w);
|
||||
}
|
||||
|
||||
static void test_window_size() {
|
||||
struct webview w = {};
|
||||
std::vector<std::string> results;
|
||||
std::cout << "TEST: window size" << std::endl;
|
||||
w.width = 480;
|
||||
w.height = 320;
|
||||
w.resizable = 1;
|
||||
w.userdata = static_cast<void *>(&results);
|
||||
w.external_invoke_cb = [](struct webview *w, const char *arg) {
|
||||
auto *v = static_cast<std::vector<std::string> *>(w->userdata);
|
||||
v->push_back(std::string(arg));
|
||||
};
|
||||
runner(&w)
|
||||
.then([](struct webview *w) {
|
||||
webview_eval(w, "window.external.invoke(''+window.screen.width+' ' + "
|
||||
"window.screen.height)");
|
||||
webview_eval(w, "window.external.invoke(''+window.innerWidth+' ' + "
|
||||
"window.innerHeight)");
|
||||
})
|
||||
.sleep(200)
|
||||
.then([](struct webview *w) { webview_set_fullscreen(w, 1); })
|
||||
.sleep(500)
|
||||
.then([](struct webview *w) {
|
||||
webview_eval(w, "window.external.invoke(''+window.innerWidth+' ' + "
|
||||
"window.innerHeight)");
|
||||
})
|
||||
.sleep(200)
|
||||
.then([](struct webview *w) { webview_set_fullscreen(w, 0); })
|
||||
.sleep(500)
|
||||
.then([](struct webview *w) {
|
||||
webview_eval(w, "window.external.invoke(''+window.innerWidth+' ' + "
|
||||
"window.innerHeight)");
|
||||
})
|
||||
.wait();
|
||||
assert(results.size() == 4);
|
||||
assert(results[1] == "480 320");
|
||||
assert(results[0] == results[2]);
|
||||
assert(results[1] == results[3]);
|
||||
}
|
||||
|
||||
static void test_inject_js() {
|
||||
struct webview w = {};
|
||||
std::vector<std::string> results;
|
||||
std::cout << "TEST: inject JS" << std::endl;
|
||||
w.width = 480;
|
||||
w.height = 320;
|
||||
w.userdata = static_cast<void *>(&results);
|
||||
w.external_invoke_cb = [](struct webview *w, const char *arg) {
|
||||
auto *v = static_cast<std::vector<std::string> *>(w->userdata);
|
||||
v->push_back(std::string(arg));
|
||||
};
|
||||
runner(&w)
|
||||
.then([](struct webview *w) {
|
||||
webview_eval(w,
|
||||
R"(document.body.innerHTML = '<div id="foo">Foo</div>';)");
|
||||
webview_eval(
|
||||
w,
|
||||
"window.external.invoke(document.getElementById('foo').innerText)");
|
||||
})
|
||||
.wait();
|
||||
assert(results.size() == 1);
|
||||
assert(results[0] == "Foo");
|
||||
}
|
||||
|
||||
static void test_inject_css() {
|
||||
struct webview w = {};
|
||||
std::vector<std::string> results;
|
||||
std::cout << "TEST: inject CSS" << std::endl;
|
||||
w.width = 480;
|
||||
w.height = 320;
|
||||
w.userdata = static_cast<void *>(&results);
|
||||
w.external_invoke_cb = [](struct webview *w, const char *arg) {
|
||||
auto *v = static_cast<std::vector<std::string> *>(w->userdata);
|
||||
v->push_back(std::string(arg));
|
||||
};
|
||||
runner(&w)
|
||||
.then([](struct webview *w) {
|
||||
webview_inject_css(w, "#app { margin-left: 4px; }");
|
||||
webview_eval(w, "window.external.invoke(getComputedStyle(document."
|
||||
"getElementById('app')).marginLeft)");
|
||||
})
|
||||
.wait();
|
||||
assert(results.size() == 1);
|
||||
assert(results[0] == "4px");
|
||||
}
|
||||
|
||||
int main() {
|
||||
test_minimal();
|
||||
test_window_size();
|
||||
test_inject_js();
|
||||
test_inject_css();
|
||||
return 0;
|
||||
}
|
Loading…
Reference in New Issue
Block a user