chore(merge) dev to master (#4)

* feat(readme) clarifications and styling
* fix(readme) logo position
* 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:
nothingismagick 2019-07-14 14:50:49 +02:00 committed by GitHub
parent 7bd6e0ddb1
commit 13734c338b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
72 changed files with 6567 additions and 29 deletions

61
.gitignore vendored Normal file
View 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
View File

@ -0,0 +1,11 @@
test
bindings
docs
lib
node_modules
spec
ui
.git
.github
.idea
SECURITY.md

View File

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

113
README.md
View File

@ -1,14 +1,14 @@
# quasar-proton [WIP]
## A fresh take on creating cross-platform apps.
[![official icon](https://img.shields.io/badge/Quasar%201.0-Official-blue.svg)](https://quasar.dev)
[![official icon](https://img.shields.io/badge/Quasar%201.0-Official-blue.svg)](https://quasar.dev)
[![status](https://img.shields.io/badge/Status-Internal%20Review-yellow.svg)](https://github.com/quasarframework/quasar/tree/proton)
[![version](https://img.shields.io/badge/Version-unreleased-yellow.svg)](https://github.com/quasarframework/quasar/tree/proton)
[![version](https://img.shields.io/badge/Version-unreleased-yellow.svg)](https://github.com/quasarframework/quasar/tree/proton) <img align="right" src="https://cdn.quasar.dev/logo/proton/proton-logo-240x240.png">
[![Join the chat at https://chat.quasar.dev](https://img.shields.io/badge/chat-on%20discord-7289da.svg)](https://chat.quasar.dev)
<a href="https://forum.quasar.dev" target="_blank"><img src="https://img.shields.io/badge/community-forum-brightgreen.svg"></a>
[![https://good-labs.github.io/greater-good-affirmation/assets/images/badge.svg](https://good-labs.github.io/greater-good-affirmation/assets/images/badge.svg)](https://good-labs.github.io/greater-good-affirmation)
**Proton** brings a mode to build Quasar Apps that creates tiny, blazing <img align="right" src="https://cdn.quasar.dev/logo/proton/proton-logo-240x240.png">
**Proton** brings a mode to build Quasar Apps that creates tiny, blazing
fast binaries for all major desktop platforms. In Quasar's
[neverending quest](https://quasar.dev/introduction-to-quasar#Why-Quasar%3F)
for performance and security, the core team is proud to offer an
@ -18,25 +18,77 @@ Whether you are just starting out making apps for your meetup or
regularly crunch terabyte datasets, we are absolutely confident that
you will love using Proton as much as we love making and maintaining it.
It leverages Cocoa/WebKit on macOS, gtk-webkit2 on Linux and MSHTML
(IE10/11) or Webkit via Edge on Windows. **Proton** is based on the
MIT licensed prior work known as [webview](https://github.com/zserge/webview).
The default binding to the underlying webview library uses Rust, but
other languages are possible (and only a PR away).
## Who Proton is For
Anyone who can create a Quasar app can use Proton, as it is *merely* a new
build target. All components and plugins (suitable for Native Desktop) can
be used. For the User Interface, nothing has changed, except you will
probably notice that everything seems much faster.
Because of the way Proton has been built and can be extended, developers
are able to interface not only with the entire Rust ecosystem, but also
with many other programming languages. Being freed of the heaviest thing
in the universe and the many shortcomings of server-side Javascript
suddenly opens up whole new avenues for high-performance, security-focused
applications that need the purebred power, agility and community
acceptance of a low-level language.
We expect to witness an entire new class of applications being built with
Quasar Proton. From a simple calender to locally crunching massive realtime
feeds at particle colliders or even mesh-network based distributed message-
passing ecosystems - the bar has been raised and gauntlet thrown.
What will you make?
## Technical Details
The user interface in Proton apps currently leverages Cocoa/WebKit on macOS,
gtk-webkit2 on Linux and MSHTML (IE10/11) or Webkit via Edge on Windows.
**Proton** is based on the MIT licensed prior work known as
[webview](https://github.com/zserge/webview).
The default binding to the underlying webview library currently uses Rust,
but other languages like Golang or Python (and many others) are possible
(and only a PR away).
> Rust is blazingly fast and memory-efficient: with no runtime or garbage
collector, it can power performance-critical services, run on embedded
devices, and easily integrate with other languages. Rusts rich type system
and ownership model guarantee memory-safety and thread-safety — and enable
you to eliminate many classes of bugs at compile-time. Rust has great
documentation, a friendly compiler with useful error messages, and top-notch
tooling — an integrated package manager and build tool, smart multi-editor
support with auto-completion and type inspections, an auto-formatter, and
more. - [https://www.rust-lang.org/](https://www.rust-lang.org/)
This combination of power, safety and usability are why we chose Rust to be
the default binding for Proton. It is our intention to provide the most safe
and performant native app experience (for devs and app consumers), out of
the box.
### Current Status
We are in the process of vetting this new mode. It is not yet available to
use without jumping through some development hurdles. If you don't care,
please reach out to the team at https://chat.quasar.dev and we'll guide
you through the process.
you through the process. Here is a bit of a status report.
### Comparison between Proton and Electron
- [x] Promise based File System Access
- [x] App Icons and integration with Icon-Genie
- [ ] Frameless Mode
- [x] Build on MacOS
- [x] Build on Linux
- [ ] Build on Windows
- [x] STDOUT Passthrough with Command Invocation
- [x] Self Updater
- [x] Inter Process Communication (IPC)
- [x] Documentation
### Comparison between Proton 1 and Electron 5
| | Proton | Electron |
|--|--------|----------|
| Binary Size MacOS | 3.6 MB | 148.7 MB |
| Memory Consumption MacOS | 13 MB | 34.1 MB |
| Benchmark FPS † | TODO | TODO |
| Benchmark FPS | TODO | TODO |
| Interface Service Provider | Varies | Chromium |
| Quasar UI | VueJS | VueJS |
| Backend Binding | Rust | Node.js (ECMAScript) |
@ -44,8 +96,9 @@ you through the process.
| FLOSS | Yes | No |
| Multithreading | Yes | No |
| Bytecode Delivery | Yes | No |
| Multiple Windows | Yes | Yes |
| Can Render PDF | Yes | No |
| Multiple Windows | Yes | Yes |
| GPU Access | Yes | Yes |
| Updater | Yes | Yes |
| Inter Process Communication (IPC) | Yes | Yes |
| Cross Platform | Yes | Yes |
@ -64,38 +117,42 @@ 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
Head over to the Quasar Framework official website: [https://quasar.dev](https://quasar.dev)
Head over to the Quasar Framework official website:
[https://quasar.dev](https://quasar.dev)
## Stay in Touch
For latest releases and announcements, follow on Twitter: [@quasarframework](https://twitter.com/quasarframework)
For latest releases and announcements, follow on Twitter:
[@quasarframework](https://twitter.com/quasarframework)
## Chat Support
Get realtime help at the official community Discord server: [https://chat.quasar.dev](https://chat.quasar.dev)
Get realtime help at the official community Discord server:
[https://chat.quasar.dev](https://chat.quasar.dev)
## Community Forum
Ask complicated questions at the official community forum: [https://forum.quasar.dev](https://forum.quasar.dev)
Ask complicated questions at the official community forum:
[https://forum.quasar.dev](https://forum.quasar.dev)
## Contributing
Please make sure to read the [Contributing Guide](./.github/CONTRIBUTING.md) before making a pull request. If you have a Quasar-related project/component/tool, add it with a pull request to [this curated list](https://github.com/quasarframework/quasar-awesome)!
Please make sure to read the [Contributing Guide](./.github/CONTRIBUTING.md)
before making a pull request. If you have a Quasar-related
project/component/tool, add it with a pull request to
[this curated list](https://github.com/quasarframework/quasar-awesome)!
Thank you to all the people who already <a href="https://github.com/quasarframework/quasar/graphs/contributors">contributed to Proton</a>!
Thank you to all the people who already <a href="https://github.com/quasarframework/proton/graphs/contributors">contributed to Proton</a>!
## Semver
Quasar is following [Semantic Versioning 2.0](https://semver.org/).
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!
- Original Proton Logo Design by [Daniel Thompson-Yvetot](https://github.com/nothingismagick)
- Based on the prior work by [Emanuele Bertoldi](https://github.com/zuck)

21
bindings/go/LICENSE Normal file
View 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
View File

@ -0,0 +1 @@
# go-bindings

572
bindings/go/proton.go Executable file
View 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
}

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

@ -0,0 +1,4 @@
target/
**/*.rs.bk
Cargo.lock
.idea

24
bindings/rust/Cargo.toml Normal file
View 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
View 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
View File

@ -0,0 +1 @@
# rust bindings

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

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

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

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

View 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

View 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
View 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(|_| ())
}
}

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

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

@ -0,0 +1 @@
[WIP]

27
lib/rust/Cargo.toml Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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();
}

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

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

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

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

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

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

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

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

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

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

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

@ -0,0 +1 @@
[WIP]

39
templates/rust/Cargo.toml Executable file
View 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
View 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
View 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();
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

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

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

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

@ -0,0 +1 @@
*.h linguist-language=c

2
ui/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
# Build atrifacts
/build

32
ui/CMakeLists.txt Executable file
View 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
View 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
View File

@ -0,0 +1,3 @@
# PROTON WEBVIEW
Documentation forthcoming.

2265
ui/proton.h Normal file

File diff suppressed because it is too large Load Diff

176
ui/proton_test.cc Normal file
View 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;
}