diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..aee53c804 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/.npmignore b/.npmignore new file mode 100644 index 000000000..36ad2b169 --- /dev/null +++ b/.npmignore @@ -0,0 +1,11 @@ +test +bindings +docs +lib +node_modules +spec +ui +.git +.github +.idea +SECURITY.md diff --git a/LICENSE b/LICENSE index c123e6407..76b8255d3 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019 Quasar Framework +Copyright (c) 2017 - Present Quasar Framework Contributors, Boscop, Serge Zaitsev Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index c451024ee..f9ed9448f 100644 --- a/README.md +++ b/README.md @@ -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) [![Join the chat at https://chat.quasar.dev](https://img.shields.io/badge/chat-on%20discord-7289da.svg)](https://chat.quasar.dev) [![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 +**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. Rust’s 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 contributed to Proton! +Thank you to all the people who already contributed to Proton! ## 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) diff --git a/bindings/go/LICENSE b/bindings/go/LICENSE new file mode 100644 index 000000000..b18604bf4 --- /dev/null +++ b/bindings/go/LICENSE @@ -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. diff --git a/bindings/go/README.md b/bindings/go/README.md new file mode 100644 index 000000000..e0ee100b5 --- /dev/null +++ b/bindings/go/README.md @@ -0,0 +1 @@ +# go-bindings diff --git a/bindings/go/proton.go b/bindings/go/proton.go new file mode 100755 index 000000000..0b065f70e --- /dev/null +++ b/bindings/go/proton.go @@ -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 +#include +#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 +} diff --git a/bindings/go/proton_test.go b/bindings/go/proton_test.go new file mode 100644 index 000000000..49fc951e8 --- /dev/null +++ b/bindings/go/proton_test.go @@ -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() + } + }) +} diff --git a/bindings/rust/.gitignore b/bindings/rust/.gitignore new file mode 100644 index 000000000..ace562d6e --- /dev/null +++ b/bindings/rust/.gitignore @@ -0,0 +1,4 @@ +target/ +**/*.rs.bk +Cargo.lock +.idea diff --git a/bindings/rust/Cargo.toml b/bindings/rust/Cargo.toml new file mode 100644 index 000000000..93d6250f0 --- /dev/null +++ b/bindings/rust/Cargo.toml @@ -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" \ No newline at end of file diff --git a/bindings/rust/LICENSE b/bindings/rust/LICENSE new file mode 100644 index 000000000..c592e15be --- /dev/null +++ b/bindings/rust/LICENSE @@ -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. diff --git a/bindings/rust/README.md b/bindings/rust/README.md new file mode 100644 index 000000000..795f7eb8e --- /dev/null +++ b/bindings/rust/README.md @@ -0,0 +1 @@ +# rust bindings diff --git a/bindings/rust/proton-sys/Cargo.toml b/bindings/rust/proton-sys/Cargo.toml new file mode 100644 index 000000000..f63a7fb7e --- /dev/null +++ b/bindings/rust/proton-sys/Cargo.toml @@ -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" \ No newline at end of file diff --git a/bindings/rust/proton-sys/build.rs b/bindings/rust/proton-sys/build.rs new file mode 100644 index 000000000..f76754355 --- /dev/null +++ b/bindings/rust/proton-sys/build.rs @@ -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"); +} diff --git a/bindings/rust/proton-sys/lib.rs b/bindings/rust/proton-sys/lib.rs new file mode 100644 index 000000000..559040b84 --- /dev/null +++ b/bindings/rust/proton-sys/lib.rs @@ -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, 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, 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); +} diff --git a/bindings/rust/proton-sys/proton.c b/bindings/rust/proton-sys/proton.c new file mode 100644 index 000000000..a4bb108e5 --- /dev/null +++ b/bindings/rust/proton-sys/proton.c @@ -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; +} + diff --git a/bindings/rust/rustfmt.toml b/bindings/rust/rustfmt.toml new file mode 100644 index 000000000..9da25275e --- /dev/null +++ b/bindings/rust/rustfmt.toml @@ -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 diff --git a/bindings/rust/src/color.rs b/bindings/rust/src/color.rs new file mode 100644 index 000000000..9bddcd257 --- /dev/null +++ b/bindings/rust/src/color.rs @@ -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, + } + } +} diff --git a/bindings/rust/src/dialog.rs b/bindings/rust/src/dialog.rs new file mode 100644 index 000000000..2a6b8e9ed --- /dev/null +++ b/bindings/rust/src/dialog.rs @@ -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 { + 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(&mut self, title: S, default_file: P) -> WVResult> + where + S: Into, + P: Into, + { + 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( + &mut self, + title: S, + default_directory: P, + ) -> WVResult> + where + S: Into, + P: Into, + { + 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(&mut self, title: TS, message: MS) -> WVResult + where + TS: Into, + MS: Into, + { + self + .dialog( + title.into(), + message.into(), + DialogType::Alert, + DialogFlags::INFO, + ) + .map(|_| ()) + } + + /// Opens a warning alert dialog. + pub fn warning(&mut self, title: TS, message: MS) -> WVResult + where + TS: Into, + MS: Into, + { + self + .dialog( + title.into(), + message.into(), + DialogType::Alert, + DialogFlags::WARNING, + ) + .map(|_| ()) + } + + /// Opens an error alert dialog. + pub fn error(&mut self, title: TS, message: MS) -> WVResult + where + TS: Into, + MS: Into, + { + self + .dialog( + title.into(), + message.into(), + DialogType::Alert, + DialogFlags::ERROR, + ) + .map(|_| ()) + } +} diff --git a/bindings/rust/src/error.rs b/bindings/rust/src/error.rs new file mode 100644 index 000000000..07ed11a67 --- /dev/null +++ b/bindings/rust/src/error.rs @@ -0,0 +1,79 @@ +use std::{ + error, + ffi::NulError, + fmt::{self, Debug, Display}, +}; + +pub trait CustomError: Display + Debug + Send + Sync + 'static {} + +impl 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), +} + +impl Error { + /// Creates a custom error from a `T: Display + Debug + Send + Sync + 'static`. + pub fn custom(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 = Result; + +impl From for Error { + fn from(e: NulError) -> Error { + Error::NulByte(e) + } +} diff --git a/bindings/rust/src/escape.rs b/bindings/rust/src/escape.rs new file mode 100644 index 000000000..e949cd9f4 --- /dev/null +++ b/bindings/rust/src/escape.rs @@ -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'"); +} diff --git a/bindings/rust/src/lib.rs b/bindings/rust/src/lib.rs new file mode 100644 index 000000000..d0b146948 --- /dev/null +++ b/bindings/rust/src/lib.rs @@ -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 { + 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>, + pub width: i32, + pub height: i32, + pub resizable: bool, + pub debug: bool, + pub invoke_handler: Option, + pub user_data: Option, +} + +impl<'a, T: 'a, I, C> Default for WebViewBuilder<'a, T, I, C> +where + I: FnMut(&mut WebView, &str) -> WVResult + 'a, + C: AsRef, +{ + 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, &str) -> WVResult + 'a, + C: AsRef, +{ + /// 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) -> 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> { + 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 { + 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, &str) -> WVResult + 'a, + C: AsRef, +{ + WebViewBuilder::new() +} + +struct UserData<'a, T> { + inner: T, + live: Arc>, + invoke_handler: Box, &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( + title: &CStr, + url: &CStr, + width: i32, + height: i32, + resizable: bool, + debug: bool, + user_data: T, + invoke_handler: I, + ) -> WVResult> + where + I: FnMut(&mut WebView, &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::), + user_data_ptr as _, + ); + + if inner.is_null() { + Box::>::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 { + 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>(&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 { + 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 { + 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 { + inner: *mut CWebView, + live: Weak>, + _phantom: PhantomData, +} + +impl Handle { + /// 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(&self, f: F) -> WVResult + where + F: FnOnce(&mut WebView) -> 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:: as _), + Box::into_raw(closure) as _, + ) + } + Ok(()) + } +} + +unsafe impl Send for Handle {} +unsafe impl Sync for Handle {} + +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(webview: *mut CWebView, arg: *mut c_void) { + unsafe { + let mut handle = mem::ManuallyDrop::new(WebView::::from_ptr(webview)); + let result = { + let callback = + Box::,), WVResult>>::from_raw(arg as _); + callback.call(&mut handle) + }; + handle.user_data_wrapper_mut().result = result; + } +} + +extern "C" fn ffi_invoke_handler(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::::from_ptr(webview)); + let result = ((*handle.user_data_wrapper_ptr()).invoke_handler)(&mut *handle, &arg); + handle.user_data_wrapper_mut().result = result; + } +} diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..6ddf4a4e4 --- /dev/null +++ b/docs/README.md @@ -0,0 +1 @@ +[WIP] diff --git a/lib/rust/Cargo.toml b/lib/rust/Cargo.toml new file mode 100644 index 000000000..b819a01f7 --- /dev/null +++ b/lib/rust/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "proton" +version = "0.1.0" +authors = ["Lucas Fernandes Gonçalves Nogueira "] +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" \ No newline at end of file diff --git a/lib/rust/rustfmt.toml b/lib/rust/rustfmt.toml new file mode 100644 index 000000000..9da25275e --- /dev/null +++ b/lib/rust/rustfmt.toml @@ -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 diff --git a/lib/rust/src/api/cmd.rs b/lib/rust/src/api/cmd.rs new file mode 100644 index 000000000..1891a1e77 --- /dev/null +++ b/lib/rust/src/api/cmd.rs @@ -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, + callback: String, + error: String, + }, +} diff --git a/lib/rust/src/api/mod.rs b/lib/rust/src/api/mod.rs new file mode 100644 index 000000000..a7dbf6d11 --- /dev/null +++ b/lib/rust/src/api/mod.rs @@ -0,0 +1,63 @@ +mod cmd; + +use proton_ui::WebView; + +pub fn handler(webview: &mut WebView, 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 + } + } +} diff --git a/lib/rust/src/command.rs b/lib/rust/src/command.rs new file mode 100755 index 000000000..d4ad38a7a --- /dev/null +++ b/lib/rust/src/command.rs @@ -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, stdout: Stdio) -> Result { + 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 { + 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 { + 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, + stdout: Stdio, +) -> Result { + let cmd = relative_command(command)?; + Ok(Command::new(cmd).args(args).stdout(stdout).spawn()?) +} + +pub fn call( + webview: &mut WebView, + command: String, + args: Vec, + callback: String, + error: String, +) { + run_async( + webview, + || { + get_output(command, args, Stdio::piped()) + .map_err(|err| format!("`{}`", err)) + .map(|output| format!("`{}`", output)) + }, + callback, + error, + ); +} diff --git a/lib/rust/src/dir/mod.rs b/lib/rust/src/dir/mod.rs new file mode 100755 index 000000000..079e2675a --- /dev/null +++ b/lib/rust/src/dir/mod.rs @@ -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 { + 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, String> { + println!("Trying to walk: {}", path_copy.as_str()); + let mut files_and_dirs: Vec = 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, String> { + fs::read_dir(dir_path) + .map_err(|err| err.to_string()) + .and_then(|paths| { + let mut dirs: Vec = 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 ()>( + callback: F, +) -> Result<(), std::io::Error> { + let dir = tempdir()?; + callback(&dir); + dir.close()?; + Ok(()) +} diff --git a/lib/rust/src/dir/utils.rs b/lib/rust/src/dir/utils.rs new file mode 100755 index 000000000..bd0f6c993 --- /dev/null +++ b/lib/rust/src/dir/utils.rs @@ -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(); +} diff --git a/lib/rust/src/file/error.rs b/lib/rust/src/file/error.rs new file mode 100644 index 000000000..d9a198516 --- /dev/null +++ b/lib/rust/src/file/error.rs @@ -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 for Error { + fn from(e: std::io::Error) -> Self { + Error::Io(e) + } +} + +impl From for Error { + fn from(e: ZipError) -> Self { + Error::Zip(e) + } +} diff --git a/lib/rust/src/file/extract.rs b/lib/rust/src/file/extract.rs new file mode 100644 index 000000000..1e80bf759 --- /dev/null +++ b/lib/rust/src/file/extract.rs @@ -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), + Plain(Option), + Zip, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Compression { + Gz, +} + +#[derive(Debug)] +pub struct Extract<'a> { + source: &'a path::Path, + archive_format: Option, +} + +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, + ) -> Either> { + 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>( + &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(()) + } +} diff --git a/lib/rust/src/file/file_move.rs b/lib/rust/src/file/file_move.rs new file mode 100644 index 000000000..c11c622d1 --- /dev/null +++ b/lib/rust/src/file/file_move.rs @@ -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(()) + } +} diff --git a/lib/rust/src/file/mod.rs b/lib/rust/src/file/mod.rs new file mode 100644 index 000000000..c81d4040d --- /dev/null +++ b/lib/rust/src/file/mod.rs @@ -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 { + fs::read_to_string(file) + .map_err(|err| err.to_string()) + .map(|c| c) +} + +pub fn read_binary(file: String) -> Result, String> { + fs::read(file).map_err(|err| err.to_string()).map(|b| b) +} diff --git a/lib/rust/src/file_system.rs b/lib/rust/src/file_system.rs new file mode 100755 index 000000000..11450d58c --- /dev/null +++ b/lib/rust/src/file_system.rs @@ -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(webview: &mut WebView, 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( + webview: &mut WebView, + 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( + webview: &mut WebView, + 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( + webview: &mut WebView, + 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( + webview: &mut WebView, + 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, + ); +} diff --git a/lib/rust/src/http/error.rs b/lib/rust/src/http/error.rs new file mode 100644 index 000000000..4ac9ffe91 --- /dev/null +++ b/lib/rust/src/http/error.rs @@ -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 for Error { + fn from(e: serde_json::Error) -> Self { + Error::Json(e) + } +} + +impl From for Error { + fn from(e: reqwest::Error) -> Self { + Error::Reqwest(e) + } +} + +impl From for Error { + fn from(e: std::io::Error) -> Self { + Error::Io(e) + } +} diff --git a/lib/rust/src/http/mod.rs b/lib/rust/src/http/mod.rs new file mode 100644 index 000000000..0097e64ee --- /dev/null +++ b/lib/rust/src/http/mod.rs @@ -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 { + let response = reqwest::Client::new().get(url).send()?; + Ok(response) +} + +pub fn post_as_json( + url: &String, + payload: &T, +) -> Result { + let response = reqwest::Client::new().post(url).json(payload).send()?; + Ok(response) +} + +pub fn download( + 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::().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(()) +} diff --git a/lib/rust/src/lib.rs b/lib/rust/src/lib.rs new file mode 100644 index 000000000..2ad400f4b --- /dev/null +++ b/lib/rust/src/lib.rs @@ -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 Result + Send + 'static>( + webview: &mut WebView, + 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() + }); + }); +} diff --git a/lib/rust/src/macros.rs b/lib/rust/src/macros.rs new file mode 100644 index 000000000..4012fd566 --- /dev/null +++ b/lib/rust/src/macros.rs @@ -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"); + } + } + }; +} diff --git a/lib/rust/src/platform/error.rs b/lib/rust/src/platform/error.rs new file mode 100644 index 000000000..e8413d5e0 --- /dev/null +++ b/lib/rust/src/platform/error.rs @@ -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; + } +} diff --git a/lib/rust/src/platform/mod.rs b/lib/rust/src/platform/mod.rs new file mode 100644 index 000000000..bbc17d7f9 --- /dev/null +++ b/lib/rust/src/platform/mod.rs @@ -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 { + 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)) +} diff --git a/lib/rust/src/process.rs b/lib/rust/src/process.rs new file mode 100644 index 000000000..dcaa7dca1 --- /dev/null +++ b/lib/rust/src/process.rs @@ -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) +} diff --git a/lib/rust/src/rpc.rs b/lib/rust/src/rpc.rs new file mode 100755 index 000000000..f44220338 --- /dev/null +++ b/lib/rust/src/rpc.rs @@ -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, + callback: String, + error_callback: String, +) -> String { + match result { + Ok(res) => return format_callback(callback, res), + Err(err) => return format_callback(error_callback, format!("\"{}\"", err)), + } +} diff --git a/lib/rust/src/tcp.rs b/lib/rust/src/tcp.rs new file mode 100644 index 000000000..433413004 --- /dev/null +++ b/lib/rust/src/tcp.rs @@ -0,0 +1,25 @@ +use std::net::TcpListener; + +extern crate rand; + +use rand::distributions::{Distribution, Uniform}; + +pub fn get_available_port() -> Option { + 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, + } +} diff --git a/lib/rust/src/updater/error.rs b/lib/rust/src/updater/error.rs new file mode 100644 index 000000000..5476ecccd --- /dev/null +++ b/lib/rust/src/updater/error.rs @@ -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 for Error { + fn from(e: std::io::Error) -> Self { + Error::Io(e) + } +} + +impl From for Error { + fn from(e: file::Error) -> Self { + Error::File(e) + } +} + +impl From for Error { + fn from(e: http::Error) -> Self { + Error::Network(e.to_string()) + } +} + +impl From for Error { + fn from(e: reqwest::Error) -> Self { + Error::Network(e.to_string()) + } +} + +impl From for Error { + fn from(e: version::Error) -> Self { + Error::Version(e) + } +} diff --git a/lib/rust/src/updater/github/mod.rs b/lib/rust/src/updater/github/mod.rs new file mode 100644 index 000000000..ba41cf1c0 --- /dev/null +++ b/lib/rust/src/updater/github/mod.rs @@ -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 { + 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::()?; + Ok(Release::parse(&json)?) +} + +pub fn get_release_version(repo_owner: &str, repo_name: &str, ver: &str) -> Result { + 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::()?; + Ok(Release::parse(&json)?) +} diff --git a/lib/rust/src/updater/github/release.rs b/lib/rust/src/updater/github/release.rs new file mode 100644 index 000000000..79c98469e --- /dev/null +++ b/lib/rust/src/updater/github/release.rs @@ -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 { + 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, +} +impl Release { + pub fn parse(release: &serde_json::Value) -> Result { + 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::, 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 { + 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, + repo_name: Option, + target: Option, +} +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 { + 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, +} +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, 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::>(), + }; + Ok(releases) + } + + fn fetch_releases(url: &str) -> Result, 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::()?; + let releases = releases + .as_array() + .ok_or_else(|| format_err!(Error::Release, "No releases found"))?; + let mut releases = releases + .iter() + .map(Release::parse) + .collect::, Error>>()?; + + // handle paged responses containing `Link` header: + // `Link: ; 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 + } + }) + } +} diff --git a/lib/rust/src/updater/mod.rs b/lib/rust/src/updater/mod.rs new file mode 100644 index 000000000..bbac888a4 --- /dev/null +++ b/lib/rust/src/updater/mod.rs @@ -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, + bin_name: Option, + bin_install_path: Option, + bin_path_in_archive: Option, + show_download_progress: bool, + show_output: bool, + current_version: Option, +} +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 { + 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 { + 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::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 { + 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)) + } +} diff --git a/lib/rust/src/version/error.rs b/lib/rust/src/version/error.rs new file mode 100644 index 000000000..1b084e60c --- /dev/null +++ b/lib/rust/src/version/error.rs @@ -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 for Error { + fn from(e: semver::SemVerError) -> Self { + Error::SemVer(e) + } +} diff --git a/lib/rust/src/version/mod.rs b/lib/rust/src/version/mod.rs new file mode 100644 index 000000000..2082c23e8 --- /dev/null +++ b/lib/rust/src/version/mod.rs @@ -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 { + 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 { + 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 { + 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 { + 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 { + let current = Version::parse(current)?; + let other = Version::parse(other)?; + Ok(current.major == other.major && current.minor == other.minor && other.patch > current.patch) +} diff --git a/package.json b/package.json new file mode 100644 index 000000000..ead99ac9f --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/spec/README.md b/spec/README.md new file mode 100644 index 000000000..6ddf4a4e4 --- /dev/null +++ b/spec/README.md @@ -0,0 +1 @@ +[WIP] diff --git a/templates/rust/Cargo.toml b/templates/rust/Cargo.toml new file mode 100755 index 000000000..78f8b2f21 --- /dev/null +++ b/templates/rust/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "app" +version = "0.1.0" +description = "A Quasar app" +authors = ["Lucas Fernandes Nogueira "] +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" diff --git a/templates/rust/_gitignore b/templates/rust/_gitignore new file mode 100755 index 000000000..50c83018e --- /dev/null +++ b/templates/rust/_gitignore @@ -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 \ No newline at end of file diff --git a/templates/rust/build.rs b/templates/rust/build.rs new file mode 100755 index 000000000..07829cd86 --- /dev/null +++ b/templates/rust/build.rs @@ -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(); +} diff --git a/templates/rust/icons/128x128.png b/templates/rust/icons/128x128.png new file mode 100644 index 000000000..4ab8a828e Binary files /dev/null and b/templates/rust/icons/128x128.png differ diff --git a/templates/rust/icons/128x128@2x.png b/templates/rust/icons/128x128@2x.png new file mode 100644 index 000000000..018c12cd8 Binary files /dev/null and b/templates/rust/icons/128x128@2x.png differ diff --git a/templates/rust/icons/32x32.png b/templates/rust/icons/32x32.png new file mode 100644 index 000000000..a21c3721d Binary files /dev/null and b/templates/rust/icons/32x32.png differ diff --git a/templates/rust/icons/icon.icns b/templates/rust/icons/icon.icns new file mode 100644 index 000000000..cf57cfb49 Binary files /dev/null and b/templates/rust/icons/icon.icns differ diff --git a/templates/rust/icons/icon.ico b/templates/rust/icons/icon.ico new file mode 100644 index 000000000..2ae4b7a1f Binary files /dev/null and b/templates/rust/icons/icon.ico differ diff --git a/templates/rust/rustfmt.toml b/templates/rust/rustfmt.toml new file mode 100644 index 000000000..9da25275e --- /dev/null +++ b/templates/rust/rustfmt.toml @@ -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 diff --git a/templates/rust/src/cmd.rs b/templates/rust/src/cmd.rs new file mode 100755 index 000000000..53b696461 --- /dev/null +++ b/templates/rust/src/cmd.rs @@ -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 }, +} diff --git a/templates/rust/src/main.rs b/templates/rust/src/main.rs new file mode 100755 index 000000000..41c5f4fa5 --- /dev/null +++ b/templates/rust/src/main.rs @@ -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(); +} diff --git a/templates/rust/src/server.rs b/templates/rust/src/server.rs new file mode 100644 index 000000000..5a7534d15 --- /dev/null +++ b/templates/rust/src/server.rs @@ -0,0 +1,26 @@ +use tiny_http::{Header, Response}; + +include!(concat!(env!("OUT_DIR"), "/data.rs")); + +pub fn asset_response(path: &str) -> Response>> { + 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 +} diff --git a/templates/rust/src/updater.rs b/templates/rust/src/updater.rs new file mode 100644 index 000000000..d8e66ae80 --- /dev/null +++ b/templates/rust/src/updater.rs @@ -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), + }; +} diff --git a/ui/.gitattributes b/ui/.gitattributes new file mode 100644 index 000000000..5170675f3 --- /dev/null +++ b/ui/.gitattributes @@ -0,0 +1 @@ +*.h linguist-language=c diff --git a/ui/.gitignore b/ui/.gitignore new file mode 100644 index 000000000..27a766120 --- /dev/null +++ b/ui/.gitignore @@ -0,0 +1,2 @@ +# Build atrifacts +/build diff --git a/ui/CMakeLists.txt b/ui/CMakeLists.txt new file mode 100755 index 000000000..a616430bb --- /dev/null +++ b/ui/CMakeLists.txt @@ -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 ") +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) diff --git a/ui/LICENSE b/ui/LICENSE new file mode 100644 index 000000000..a12fe0483 --- /dev/null +++ b/ui/LICENSE @@ -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. diff --git a/ui/README.md b/ui/README.md new file mode 100755 index 000000000..95d9ca04e --- /dev/null +++ b/ui/README.md @@ -0,0 +1,3 @@ +# PROTON WEBVIEW + +Documentation forthcoming. diff --git a/ui/proton.h b/ui/proton.h new file mode 100644 index 000000000..b2a6f83d5 --- /dev/null +++ b/ui/proton.h @@ -0,0 +1,2265 @@ +/* + * 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. + */ +#ifndef WEBVIEW_H +#define WEBVIEW_H + +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef WEBVIEW_STATIC +#define WEBVIEW_API static +#else +#define WEBVIEW_API extern +#endif + +#include +#include +#include + +#if defined(WEBVIEW_GTK) +#include +#include +#include + +struct webview_priv { + GtkWidget *window; + GtkWidget *scroller; + GtkWidget *webview; + GtkWidget *inspector_window; + GAsyncQueue *queue; + int ready; + int js_busy; + int should_exit; +}; +#elif defined(WEBVIEW_WINAPI) +#define CINTERFACE +#include + +#include +#include +#include +#include +#include + +#include + +struct webview_priv { + HWND hwnd; + IOleObject **browser; + BOOL is_fullscreen; + DWORD saved_style; + DWORD saved_ex_style; + RECT saved_rect; +}; +#elif defined(WEBVIEW_COCOA) +#include +#include +#include + +struct webview_priv { + id pool; + id window; + id webview; + id windowDelegate; + int should_exit; +}; +#else +#error "Define one of: WEBVIEW_GTK, WEBVIEW_COCOA or WEBVIEW_WINAPI" +#endif + +struct webview; + +typedef void (*webview_external_invoke_cb_t)(struct webview *w, + const char *arg); + +struct webview { + const char *url; + const char *title; + int width; + int height; + int resizable; + int debug; + webview_external_invoke_cb_t external_invoke_cb; + struct webview_priv priv; + void *userdata; +}; + +enum webview_dialog_type { + WEBVIEW_DIALOG_TYPE_OPEN = 0, + WEBVIEW_DIALOG_TYPE_SAVE = 1, + WEBVIEW_DIALOG_TYPE_ALERT = 2 +}; + +#define WEBVIEW_DIALOG_FLAG_FILE (0 << 0) +#define WEBVIEW_DIALOG_FLAG_DIRECTORY (1 << 0) + +#define WEBVIEW_DIALOG_FLAG_INFO (1 << 1) +#define WEBVIEW_DIALOG_FLAG_WARNING (2 << 1) +#define WEBVIEW_DIALOG_FLAG_ERROR (3 << 1) +#define WEBVIEW_DIALOG_FLAG_ALERT_MASK (3 << 1) + +typedef void (*webview_dispatch_fn)(struct webview *w, void *arg); + +struct webview_dispatch_arg { + webview_dispatch_fn fn; + struct webview *w; + void *arg; +}; + +#define DEFAULT_URL \ + "data:text/" \ + "html,%3C%21DOCTYPE%20html%3E%0A%3Chtml%20lang=%22en%22%3E%0A%3Chead%3E%" \ + "3Cmeta%20charset=%22utf-8%22%3E%3Cmeta%20http-equiv=%22X-UA-Compatible%22%" \ + "20content=%22IE=edge%22%3E%3C%2Fhead%3E%0A%3Cbody%3E%3Cdiv%20id=%22app%22%" \ + "3E%3C%2Fdiv%3E%3Cscript%20type=%22text%2Fjavascript%22%3E%3C%2Fscript%3E%" \ + "3C%2Fbody%3E%0A%3C%2Fhtml%3E" + +#define CSS_INJECT_FUNCTION \ + "(function(e){var " \ + "t=document.createElement('style'),d=document.head||document." \ + "getElementsByTagName('head')[0];t.setAttribute('type','text/" \ + "css'),t.styleSheet?t.styleSheet.cssText=e:t.appendChild(document." \ + "createTextNode(e)),d.appendChild(t)})" + +static const char *webview_check_url(const char *url) { + if (url == NULL || strlen(url) == 0) { + return DEFAULT_URL; + } + return url; +} + +WEBVIEW_API int webview(const char *title, const char *url, int width, + int height, int resizable); + +WEBVIEW_API int webview_init(struct webview *w); +WEBVIEW_API int webview_loop(struct webview *w, int blocking); +WEBVIEW_API int webview_eval(struct webview *w, const char *js); +WEBVIEW_API int webview_inject_css(struct webview *w, const char *css); +WEBVIEW_API void webview_set_title(struct webview *w, const char *title); +WEBVIEW_API void webview_set_fullscreen(struct webview *w, int fullscreen); +WEBVIEW_API void webview_set_color(struct webview *w, uint8_t r, uint8_t g, + uint8_t b, uint8_t a); +WEBVIEW_API void webview_dialog(struct webview *w, + enum webview_dialog_type dlgtype, int flags, + const char *title, const char *arg, + char *result, size_t resultsz); +WEBVIEW_API void webview_dispatch(struct webview *w, webview_dispatch_fn fn, + void *arg); +WEBVIEW_API void webview_terminate(struct webview *w); +WEBVIEW_API void webview_exit(struct webview *w); +WEBVIEW_API void webview_debug(const char *format, ...); +WEBVIEW_API void webview_print_log(const char *s); + +#ifdef WEBVIEW_IMPLEMENTATION +#undef WEBVIEW_IMPLEMENTATION + +WEBVIEW_API int webview(const char *title, const char *url, int width, + int height, int resizable) { + struct webview webview; + memset(&webview, 0, sizeof(webview)); + webview.title = title; + webview.url = url; + webview.width = width; + webview.height = height; + webview.resizable = resizable; + int r = webview_init(&webview); + if (r != 0) { + return r; + } + while (webview_loop(&webview, 1) == 0) { + } + webview_exit(&webview); + return 0; +} + +WEBVIEW_API void webview_debug(const char *format, ...) { + char buf[4096]; + va_list ap; + va_start(ap, format); + vsnprintf(buf, sizeof(buf), format, ap); + webview_print_log(buf); + va_end(ap); +} + +static int webview_js_encode(const char *s, char *esc, size_t n) { + int r = 1; /* At least one byte for trailing zero */ + for (; *s; s++) { + const unsigned char c = *s; + if (c >= 0x20 && c < 0x80 && strchr("<>\\'\"", c) == NULL) { + if (n > 0) { + *esc++ = c; + n--; + } + r++; + } else { + if (n > 0) { + snprintf(esc, n, "\\x%02x", (int)c); + esc += 4; + n -= 4; + } + r += 4; + } + } + return r; +} + +WEBVIEW_API int webview_inject_css(struct webview *w, const char *css) { + int n = webview_js_encode(css, NULL, 0); + char *esc = (char *)calloc(1, sizeof(CSS_INJECT_FUNCTION) + n + 4); + if (esc == NULL) { + return -1; + } + char *js = (char *)calloc(1, n); + webview_js_encode(css, js, n); + snprintf(esc, sizeof(CSS_INJECT_FUNCTION) + n + 4, "%s(\"%s\")", + CSS_INJECT_FUNCTION, js); + int r = webview_eval(w, esc); + free(js); + free(esc); + return r; +} + +#if defined(WEBVIEW_GTK) +static void external_message_received_cb(WebKitUserContentManager *m, + WebKitJavascriptResult *r, + gpointer arg) { + (void)m; + struct webview *w = (struct webview *)arg; + if (w->external_invoke_cb == NULL) { + return; + } + JSGlobalContextRef context = webkit_javascript_result_get_global_context(r); + JSValueRef value = webkit_javascript_result_get_value(r); + JSStringRef js = JSValueToStringCopy(context, value, NULL); + size_t n = JSStringGetMaximumUTF8CStringSize(js); + char *s = g_new(char, n); + JSStringGetUTF8CString(js, s, n); + w->external_invoke_cb(w, s); + JSStringRelease(js); + g_free(s); +} + +static void webview_load_changed_cb(WebKitWebView *webview, + WebKitLoadEvent event, gpointer arg) { + (void)webview; + struct webview *w = (struct webview *)arg; + if (event == WEBKIT_LOAD_FINISHED) { + w->priv.ready = 1; + } +} + +static void webview_destroy_cb(GtkWidget *widget, gpointer arg) { + (void)widget; + struct webview *w = (struct webview *)arg; + webview_terminate(w); +} + +static gboolean webview_context_menu_cb(WebKitWebView *webview, + GtkWidget *default_menu, + WebKitHitTestResult *hit_test_result, + gboolean triggered_with_keyboard, + gpointer userdata) { + (void)webview; + (void)default_menu; + (void)hit_test_result; + (void)triggered_with_keyboard; + (void)userdata; + return TRUE; +} + +WEBVIEW_API int webview_init(struct webview *w) { + if (gtk_init_check(0, NULL) == FALSE) { + return -1; + } + + w->priv.ready = 0; + w->priv.should_exit = 0; + w->priv.queue = g_async_queue_new(); + w->priv.window = gtk_window_new(GTK_WINDOW_TOPLEVEL); + gtk_window_set_title(GTK_WINDOW(w->priv.window), w->title); + + if (w->resizable) { + gtk_window_set_default_size(GTK_WINDOW(w->priv.window), w->width, + w->height); + } else { + gtk_widget_set_size_request(w->priv.window, w->width, w->height); + } + gtk_window_set_resizable(GTK_WINDOW(w->priv.window), !!w->resizable); + gtk_window_set_position(GTK_WINDOW(w->priv.window), GTK_WIN_POS_CENTER); + + w->priv.scroller = gtk_scrolled_window_new(NULL, NULL); + gtk_container_add(GTK_CONTAINER(w->priv.window), w->priv.scroller); + + WebKitUserContentManager *m = webkit_user_content_manager_new(); + webkit_user_content_manager_register_script_message_handler(m, "external"); + g_signal_connect(m, "script-message-received::external", + G_CALLBACK(external_message_received_cb), w); + + w->priv.webview = webkit_web_view_new_with_user_content_manager(m); + webkit_web_view_load_uri(WEBKIT_WEB_VIEW(w->priv.webview), + webview_check_url(w->url)); + g_signal_connect(G_OBJECT(w->priv.webview), "load-changed", + G_CALLBACK(webview_load_changed_cb), w); + gtk_container_add(GTK_CONTAINER(w->priv.scroller), w->priv.webview); + + if (w->debug) { + WebKitSettings *settings = + webkit_web_view_get_settings(WEBKIT_WEB_VIEW(w->priv.webview)); + webkit_settings_set_enable_write_console_messages_to_stdout(settings, true); + webkit_settings_set_enable_developer_extras(settings, true); + } else { + g_signal_connect(G_OBJECT(w->priv.webview), "context-menu", + G_CALLBACK(webview_context_menu_cb), w); + } + + gtk_widget_show_all(w->priv.window); + + webkit_web_view_run_javascript( + WEBKIT_WEB_VIEW(w->priv.webview), + "window.external={invoke:function(x){" + "window.webkit.messageHandlers.external.postMessage(x);}}", + NULL, NULL, NULL); + + g_signal_connect(G_OBJECT(w->priv.window), "destroy", + G_CALLBACK(webview_destroy_cb), w); + return 0; +} + +WEBVIEW_API int webview_loop(struct webview *w, int blocking) { + gtk_main_iteration_do(blocking); + return w->priv.should_exit; +} + +WEBVIEW_API void webview_set_title(struct webview *w, const char *title) { + gtk_window_set_title(GTK_WINDOW(w->priv.window), title); +} + +WEBVIEW_API void webview_set_fullscreen(struct webview *w, int fullscreen) { + if (fullscreen) { + gtk_window_fullscreen(GTK_WINDOW(w->priv.window)); + } else { + gtk_window_unfullscreen(GTK_WINDOW(w->priv.window)); + } +} + +WEBVIEW_API void webview_set_color(struct webview *w, uint8_t r, uint8_t g, + uint8_t b, uint8_t a) { + GdkRGBA color = {r / 255.0, g / 255.0, b / 255.0, a / 255.0}; + webkit_web_view_set_background_color(WEBKIT_WEB_VIEW(w->priv.webview), + &color); +} + +WEBVIEW_API void webview_dialog(struct webview *w, + enum webview_dialog_type dlgtype, int flags, + const char *title, const char *arg, + char *result, size_t resultsz) { + GtkWidget *dlg; + if (result != NULL) { + result[0] = '\0'; + } + if (dlgtype == WEBVIEW_DIALOG_TYPE_OPEN || + dlgtype == WEBVIEW_DIALOG_TYPE_SAVE) { + dlg = gtk_file_chooser_dialog_new( + title, GTK_WINDOW(w->priv.window), + (dlgtype == WEBVIEW_DIALOG_TYPE_OPEN + ? (flags & WEBVIEW_DIALOG_FLAG_DIRECTORY + ? GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER + : GTK_FILE_CHOOSER_ACTION_OPEN) + : GTK_FILE_CHOOSER_ACTION_SAVE), + "_Cancel", GTK_RESPONSE_CANCEL, + (dlgtype == WEBVIEW_DIALOG_TYPE_OPEN ? "_Open" : "_Save"), + GTK_RESPONSE_ACCEPT, NULL); + gtk_file_chooser_set_local_only(GTK_FILE_CHOOSER(dlg), FALSE); + gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(dlg), FALSE); + gtk_file_chooser_set_show_hidden(GTK_FILE_CHOOSER(dlg), TRUE); + gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(dlg), TRUE); + gtk_file_chooser_set_create_folders(GTK_FILE_CHOOSER(dlg), TRUE); + gint response = gtk_dialog_run(GTK_DIALOG(dlg)); + if (response == GTK_RESPONSE_ACCEPT) { + gchar *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dlg)); + g_strlcpy(result, filename, resultsz); + g_free(filename); + } + gtk_widget_destroy(dlg); + } else if (dlgtype == WEBVIEW_DIALOG_TYPE_ALERT) { + GtkMessageType type = GTK_MESSAGE_OTHER; + switch (flags & WEBVIEW_DIALOG_FLAG_ALERT_MASK) { + case WEBVIEW_DIALOG_FLAG_INFO: + type = GTK_MESSAGE_INFO; + break; + case WEBVIEW_DIALOG_FLAG_WARNING: + type = GTK_MESSAGE_WARNING; + break; + case WEBVIEW_DIALOG_FLAG_ERROR: + type = GTK_MESSAGE_ERROR; + break; + } + dlg = gtk_message_dialog_new(GTK_WINDOW(w->priv.window), GTK_DIALOG_MODAL, + type, GTK_BUTTONS_OK, "%s", title); + gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(dlg), "%s", + arg); + gtk_dialog_run(GTK_DIALOG(dlg)); + gtk_widget_destroy(dlg); + } +} + +static void webview_eval_finished(GObject *object, GAsyncResult *result, + gpointer userdata) { + (void)object; + (void)result; + struct webview *w = (struct webview *)userdata; + w->priv.js_busy = 0; +} + +WEBVIEW_API int webview_eval(struct webview *w, const char *js) { + while (w->priv.ready == 0) { + g_main_context_iteration(NULL, TRUE); + } + w->priv.js_busy = 1; + webkit_web_view_run_javascript(WEBKIT_WEB_VIEW(w->priv.webview), js, NULL, + webview_eval_finished, w); + while (w->priv.js_busy) { + g_main_context_iteration(NULL, TRUE); + } + return 0; +} + +static gboolean webview_dispatch_wrapper(gpointer userdata) { + struct webview *w = (struct webview *)userdata; + for (;;) { + struct webview_dispatch_arg *arg = + (struct webview_dispatch_arg *)g_async_queue_try_pop(w->priv.queue); + if (arg == NULL) { + break; + } + (arg->fn)(w, arg->arg); + g_free(arg); + } + return FALSE; +} + +WEBVIEW_API void webview_dispatch(struct webview *w, webview_dispatch_fn fn, + void *arg) { + struct webview_dispatch_arg *context = + (struct webview_dispatch_arg *)g_new(struct webview_dispatch_arg, 1); + context->w = w; + context->arg = arg; + context->fn = fn; + g_async_queue_lock(w->priv.queue); + g_async_queue_push_unlocked(w->priv.queue, context); + if (g_async_queue_length_unlocked(w->priv.queue) == 1) { + gdk_threads_add_idle(webview_dispatch_wrapper, w); + } + g_async_queue_unlock(w->priv.queue); +} + +WEBVIEW_API void webview_terminate(struct webview *w) { + w->priv.should_exit = 1; +} + +WEBVIEW_API void webview_exit(struct webview *w) { (void)w; } +WEBVIEW_API void webview_print_log(const char *s) { + fprintf(stderr, "%s\n", s); +} + +#endif /* WEBVIEW_GTK */ + +#if defined(WEBVIEW_WINAPI) + +#pragma comment(lib, "user32.lib") +#pragma comment(lib, "ole32.lib") +#pragma comment(lib, "oleaut32.lib") + +#define WM_WEBVIEW_DISPATCH (WM_APP + 1) + +typedef struct { + IOleInPlaceFrame frame; + HWND window; +} _IOleInPlaceFrameEx; + +typedef struct { + IOleInPlaceSite inplace; + _IOleInPlaceFrameEx frame; +} _IOleInPlaceSiteEx; + +typedef struct { + IDocHostUIHandler ui; +} _IDocHostUIHandlerEx; + +typedef struct { + IInternetSecurityManager mgr; +} _IInternetSecurityManagerEx; + +typedef struct { + IServiceProvider provider; + _IInternetSecurityManagerEx mgr; +} _IServiceProviderEx; + +typedef struct { + IOleClientSite client; + _IOleInPlaceSiteEx inplace; + _IDocHostUIHandlerEx ui; + IDispatch external; + _IServiceProviderEx provider; +} _IOleClientSiteEx; + +#ifdef __cplusplus +#define iid_ref(x) &(x) +#define iid_unref(x) *(x) +#else +#define iid_ref(x) (x) +#define iid_unref(x) (x) +#endif + +static inline WCHAR *webview_to_utf16(const char *s) { + DWORD size = MultiByteToWideChar(CP_UTF8, 0, s, -1, 0, 0); + WCHAR *ws = (WCHAR *)GlobalAlloc(GMEM_FIXED, sizeof(WCHAR) * size); + if (ws == NULL) { + return NULL; + } + MultiByteToWideChar(CP_UTF8, 0, s, -1, ws, size); + return ws; +} + +static inline char *webview_from_utf16(WCHAR *ws) { + int n = WideCharToMultiByte(CP_UTF8, 0, ws, -1, NULL, 0, NULL, NULL); + char *s = (char *)GlobalAlloc(GMEM_FIXED, n); + if (s == NULL) { + return NULL; + } + WideCharToMultiByte(CP_UTF8, 0, ws, -1, s, n, NULL, NULL); + return s; +} + +static int iid_eq(REFIID a, const IID *b) { + return memcmp((const void *)iid_ref(a), (const void *)b, sizeof(GUID)) == 0; +} + +static HRESULT STDMETHODCALLTYPE JS_QueryInterface(IDispatch FAR *This, + REFIID riid, + LPVOID FAR *ppvObj) { + if (iid_eq(riid, &IID_IDispatch)) { + *ppvObj = This; + return S_OK; + } + *ppvObj = 0; + return E_NOINTERFACE; +} +static ULONG STDMETHODCALLTYPE JS_AddRef(IDispatch FAR *This) { return 1; } +static ULONG STDMETHODCALLTYPE JS_Release(IDispatch FAR *This) { return 1; } +static HRESULT STDMETHODCALLTYPE JS_GetTypeInfoCount(IDispatch FAR *This, + UINT *pctinfo) { + return S_OK; +} +static HRESULT STDMETHODCALLTYPE JS_GetTypeInfo(IDispatch FAR *This, + UINT iTInfo, LCID lcid, + ITypeInfo **ppTInfo) { + return S_OK; +} +#define WEBVIEW_JS_INVOKE_ID 0x1000 +static HRESULT STDMETHODCALLTYPE JS_GetIDsOfNames(IDispatch FAR *This, + REFIID riid, + LPOLESTR *rgszNames, + UINT cNames, LCID lcid, + DISPID *rgDispId) { + if (cNames != 1) { + return S_FALSE; + } + if (wcscmp(rgszNames[0], L"invoke") == 0) { + rgDispId[0] = WEBVIEW_JS_INVOKE_ID; + return S_OK; + } + return S_FALSE; +} + +static HRESULT STDMETHODCALLTYPE +JS_Invoke(IDispatch FAR *This, DISPID dispIdMember, REFIID riid, LCID lcid, + WORD wFlags, DISPPARAMS *pDispParams, VARIANT *pVarResult, + EXCEPINFO *pExcepInfo, UINT *puArgErr) { + size_t offset = (size_t) & ((_IOleClientSiteEx *)NULL)->external; + _IOleClientSiteEx *ex = (_IOleClientSiteEx *)((char *)(This)-offset); + struct webview *w = (struct webview *)GetWindowLongPtr( + ex->inplace.frame.window, GWLP_USERDATA); + if (pDispParams->cArgs == 1 && pDispParams->rgvarg[0].vt == VT_BSTR) { + BSTR bstr = pDispParams->rgvarg[0].bstrVal; + char *s = webview_from_utf16(bstr); + if (s != NULL) { + if (dispIdMember == WEBVIEW_JS_INVOKE_ID) { + if (w->external_invoke_cb != NULL) { + w->external_invoke_cb(w, s); + } + } else { + return S_FALSE; + } + GlobalFree(s); + } + } + return S_OK; +} + +static IDispatchVtbl ExternalDispatchTable = { + JS_QueryInterface, JS_AddRef, JS_Release, JS_GetTypeInfoCount, + JS_GetTypeInfo, JS_GetIDsOfNames, JS_Invoke}; + +static ULONG STDMETHODCALLTYPE Site_AddRef(IOleClientSite FAR *This) { + return 1; +} +static ULONG STDMETHODCALLTYPE Site_Release(IOleClientSite FAR *This) { + return 1; +} +static HRESULT STDMETHODCALLTYPE Site_SaveObject(IOleClientSite FAR *This) { + return E_NOTIMPL; +} +static HRESULT STDMETHODCALLTYPE Site_GetMoniker(IOleClientSite FAR *This, + DWORD dwAssign, + DWORD dwWhichMoniker, + IMoniker **ppmk) { + return E_NOTIMPL; +} +static HRESULT STDMETHODCALLTYPE +Site_GetContainer(IOleClientSite FAR *This, LPOLECONTAINER FAR *ppContainer) { + *ppContainer = 0; + return E_NOINTERFACE; +} +static HRESULT STDMETHODCALLTYPE Site_ShowObject(IOleClientSite FAR *This) { + return NOERROR; +} +static HRESULT STDMETHODCALLTYPE Site_OnShowWindow(IOleClientSite FAR *This, + BOOL fShow) { + return E_NOTIMPL; +} +static HRESULT STDMETHODCALLTYPE +Site_RequestNewObjectLayout(IOleClientSite FAR *This) { + return E_NOTIMPL; +} +static HRESULT STDMETHODCALLTYPE Site_QueryInterface(IOleClientSite FAR *This, + REFIID riid, + void **ppvObject) { + if (iid_eq(riid, &IID_IUnknown) || iid_eq(riid, &IID_IOleClientSite)) { + *ppvObject = &((_IOleClientSiteEx *)This)->client; + } else if (iid_eq(riid, &IID_IOleInPlaceSite)) { + *ppvObject = &((_IOleClientSiteEx *)This)->inplace; + } else if (iid_eq(riid, &IID_IDocHostUIHandler)) { + *ppvObject = &((_IOleClientSiteEx *)This)->ui; + } else if (iid_eq(riid, &IID_IServiceProvider)) { + *ppvObject = &((_IOleClientSiteEx *)This)->provider; + } else { + *ppvObject = 0; + return (E_NOINTERFACE); + } + return S_OK; +} +static HRESULT STDMETHODCALLTYPE InPlace_QueryInterface( + IOleInPlaceSite FAR *This, REFIID riid, LPVOID FAR *ppvObj) { + return (Site_QueryInterface( + (IOleClientSite *)((char *)This - sizeof(IOleClientSite)), riid, ppvObj)); +} +static ULONG STDMETHODCALLTYPE InPlace_AddRef(IOleInPlaceSite FAR *This) { + return 1; +} +static ULONG STDMETHODCALLTYPE InPlace_Release(IOleInPlaceSite FAR *This) { + return 1; +} +static HRESULT STDMETHODCALLTYPE InPlace_GetWindow(IOleInPlaceSite FAR *This, + HWND FAR *lphwnd) { + *lphwnd = ((_IOleInPlaceSiteEx FAR *)This)->frame.window; + return S_OK; +} +static HRESULT STDMETHODCALLTYPE +InPlace_ContextSensitiveHelp(IOleInPlaceSite FAR *This, BOOL fEnterMode) { + return E_NOTIMPL; +} +static HRESULT STDMETHODCALLTYPE +InPlace_CanInPlaceActivate(IOleInPlaceSite FAR *This) { + return S_OK; +} +static HRESULT STDMETHODCALLTYPE +InPlace_OnInPlaceActivate(IOleInPlaceSite FAR *This) { + return S_OK; +} +static HRESULT STDMETHODCALLTYPE +InPlace_OnUIActivate(IOleInPlaceSite FAR *This) { + return S_OK; +} +static HRESULT STDMETHODCALLTYPE InPlace_GetWindowContext( + IOleInPlaceSite FAR *This, LPOLEINPLACEFRAME FAR *lplpFrame, + LPOLEINPLACEUIWINDOW FAR *lplpDoc, LPRECT lprcPosRect, LPRECT lprcClipRect, + LPOLEINPLACEFRAMEINFO lpFrameInfo) { + *lplpFrame = (LPOLEINPLACEFRAME) & ((_IOleInPlaceSiteEx *)This)->frame; + *lplpDoc = 0; + lpFrameInfo->fMDIApp = FALSE; + lpFrameInfo->hwndFrame = ((_IOleInPlaceFrameEx *)*lplpFrame)->window; + lpFrameInfo->haccel = 0; + lpFrameInfo->cAccelEntries = 0; + return S_OK; +} +static HRESULT STDMETHODCALLTYPE InPlace_Scroll(IOleInPlaceSite FAR *This, + SIZE scrollExtent) { + return E_NOTIMPL; +} +static HRESULT STDMETHODCALLTYPE +InPlace_OnUIDeactivate(IOleInPlaceSite FAR *This, BOOL fUndoable) { + return S_OK; +} +static HRESULT STDMETHODCALLTYPE +InPlace_OnInPlaceDeactivate(IOleInPlaceSite FAR *This) { + return S_OK; +} +static HRESULT STDMETHODCALLTYPE +InPlace_DiscardUndoState(IOleInPlaceSite FAR *This) { + return E_NOTIMPL; +} +static HRESULT STDMETHODCALLTYPE +InPlace_DeactivateAndUndo(IOleInPlaceSite FAR *This) { + return E_NOTIMPL; +} +static HRESULT STDMETHODCALLTYPE +InPlace_OnPosRectChange(IOleInPlaceSite FAR *This, LPCRECT lprcPosRect) { + IOleObject *browserObject; + IOleInPlaceObject *inplace; + browserObject = *((IOleObject **)((char *)This - sizeof(IOleObject *) - + sizeof(IOleClientSite))); + if (!browserObject->lpVtbl->QueryInterface(browserObject, + iid_unref(&IID_IOleInPlaceObject), + (void **)&inplace)) { + inplace->lpVtbl->SetObjectRects(inplace, lprcPosRect, lprcPosRect); + inplace->lpVtbl->Release(inplace); + } + return S_OK; +} +static HRESULT STDMETHODCALLTYPE Frame_QueryInterface( + IOleInPlaceFrame FAR *This, REFIID riid, LPVOID FAR *ppvObj) { + return E_NOTIMPL; +} +static ULONG STDMETHODCALLTYPE Frame_AddRef(IOleInPlaceFrame FAR *This) { + return 1; +} +static ULONG STDMETHODCALLTYPE Frame_Release(IOleInPlaceFrame FAR *This) { + return 1; +} +static HRESULT STDMETHODCALLTYPE Frame_GetWindow(IOleInPlaceFrame FAR *This, + HWND FAR *lphwnd) { + *lphwnd = ((_IOleInPlaceFrameEx *)This)->window; + return S_OK; +} +static HRESULT STDMETHODCALLTYPE +Frame_ContextSensitiveHelp(IOleInPlaceFrame FAR *This, BOOL fEnterMode) { + return E_NOTIMPL; +} +static HRESULT STDMETHODCALLTYPE Frame_GetBorder(IOleInPlaceFrame FAR *This, + LPRECT lprectBorder) { + return E_NOTIMPL; +} +static HRESULT STDMETHODCALLTYPE Frame_RequestBorderSpace( + IOleInPlaceFrame FAR *This, LPCBORDERWIDTHS pborderwidths) { + return E_NOTIMPL; +} +static HRESULT STDMETHODCALLTYPE Frame_SetBorderSpace( + IOleInPlaceFrame FAR *This, LPCBORDERWIDTHS pborderwidths) { + return E_NOTIMPL; +} +static HRESULT STDMETHODCALLTYPE Frame_SetActiveObject( + IOleInPlaceFrame FAR *This, IOleInPlaceActiveObject *pActiveObject, + LPCOLESTR pszObjName) { + return S_OK; +} +static HRESULT STDMETHODCALLTYPE +Frame_InsertMenus(IOleInPlaceFrame FAR *This, HMENU hmenuShared, + LPOLEMENUGROUPWIDTHS lpMenuWidths) { + return E_NOTIMPL; +} +static HRESULT STDMETHODCALLTYPE Frame_SetMenu(IOleInPlaceFrame FAR *This, + HMENU hmenuShared, + HOLEMENU holemenu, + HWND hwndActiveObject) { + return S_OK; +} +static HRESULT STDMETHODCALLTYPE Frame_RemoveMenus(IOleInPlaceFrame FAR *This, + HMENU hmenuShared) { + return E_NOTIMPL; +} +static HRESULT STDMETHODCALLTYPE Frame_SetStatusText(IOleInPlaceFrame FAR *This, + LPCOLESTR pszStatusText) { + return S_OK; +} +static HRESULT STDMETHODCALLTYPE +Frame_EnableModeless(IOleInPlaceFrame FAR *This, BOOL fEnable) { + return S_OK; +} +static HRESULT STDMETHODCALLTYPE +Frame_TranslateAccelerator(IOleInPlaceFrame FAR *This, LPMSG lpmsg, WORD wID) { + return E_NOTIMPL; +} +static HRESULT STDMETHODCALLTYPE UI_QueryInterface(IDocHostUIHandler FAR *This, + REFIID riid, + LPVOID FAR *ppvObj) { + return (Site_QueryInterface((IOleClientSite *)((char *)This - + sizeof(IOleClientSite) - + sizeof(_IOleInPlaceSiteEx)), + riid, ppvObj)); +} +static ULONG STDMETHODCALLTYPE UI_AddRef(IDocHostUIHandler FAR *This) { + return 1; +} +static ULONG STDMETHODCALLTYPE UI_Release(IDocHostUIHandler FAR *This) { + return 1; +} +static HRESULT STDMETHODCALLTYPE UI_ShowContextMenu( + IDocHostUIHandler FAR *This, DWORD dwID, POINT __RPC_FAR *ppt, + IUnknown __RPC_FAR *pcmdtReserved, IDispatch __RPC_FAR *pdispReserved) { + return S_OK; +} +static HRESULT STDMETHODCALLTYPE +UI_GetHostInfo(IDocHostUIHandler FAR *This, DOCHOSTUIINFO __RPC_FAR *pInfo) { + pInfo->cbSize = sizeof(DOCHOSTUIINFO); + pInfo->dwFlags = DOCHOSTUIFLAG_NO3DBORDER; + pInfo->dwDoubleClick = DOCHOSTUIDBLCLK_DEFAULT; + return S_OK; +} +static HRESULT STDMETHODCALLTYPE UI_ShowUI( + IDocHostUIHandler FAR *This, DWORD dwID, + IOleInPlaceActiveObject __RPC_FAR *pActiveObject, + IOleCommandTarget __RPC_FAR *pCommandTarget, + IOleInPlaceFrame __RPC_FAR *pFrame, IOleInPlaceUIWindow __RPC_FAR *pDoc) { + return S_OK; +} +static HRESULT STDMETHODCALLTYPE UI_HideUI(IDocHostUIHandler FAR *This) { + return S_OK; +} +static HRESULT STDMETHODCALLTYPE UI_UpdateUI(IDocHostUIHandler FAR *This) { + return S_OK; +} +static HRESULT STDMETHODCALLTYPE UI_EnableModeless(IDocHostUIHandler FAR *This, + BOOL fEnable) { + return S_OK; +} +static HRESULT STDMETHODCALLTYPE +UI_OnDocWindowActivate(IDocHostUIHandler FAR *This, BOOL fActivate) { + return S_OK; +} +static HRESULT STDMETHODCALLTYPE +UI_OnFrameWindowActivate(IDocHostUIHandler FAR *This, BOOL fActivate) { + return S_OK; +} +static HRESULT STDMETHODCALLTYPE +UI_ResizeBorder(IDocHostUIHandler FAR *This, LPCRECT prcBorder, + IOleInPlaceUIWindow __RPC_FAR *pUIWindow, BOOL fRameWindow) { + return S_OK; +} +static HRESULT STDMETHODCALLTYPE +UI_TranslateAccelerator(IDocHostUIHandler FAR *This, LPMSG lpMsg, + const GUID __RPC_FAR *pguidCmdGroup, DWORD nCmdID) { + return S_FALSE; +} +static HRESULT STDMETHODCALLTYPE UI_GetOptionKeyPath( + IDocHostUIHandler FAR *This, LPOLESTR __RPC_FAR *pchKey, DWORD dw) { + return S_FALSE; +} +static HRESULT STDMETHODCALLTYPE UI_GetDropTarget( + IDocHostUIHandler FAR *This, IDropTarget __RPC_FAR *pDropTarget, + IDropTarget __RPC_FAR *__RPC_FAR *ppDropTarget) { + return S_FALSE; +} +static HRESULT STDMETHODCALLTYPE UI_GetExternal( + IDocHostUIHandler FAR *This, IDispatch __RPC_FAR *__RPC_FAR *ppDispatch) { + *ppDispatch = (IDispatch *)(This + 1); + return S_OK; +} +static HRESULT STDMETHODCALLTYPE UI_TranslateUrl( + IDocHostUIHandler FAR *This, DWORD dwTranslate, OLECHAR __RPC_FAR *pchURLIn, + OLECHAR __RPC_FAR *__RPC_FAR *ppchURLOut) { + *ppchURLOut = 0; + return S_FALSE; +} +static HRESULT STDMETHODCALLTYPE +UI_FilterDataObject(IDocHostUIHandler FAR *This, IDataObject __RPC_FAR *pDO, + IDataObject __RPC_FAR *__RPC_FAR *ppDORet) { + *ppDORet = 0; + return S_FALSE; +} + +static const TCHAR *classname = "WebView"; +static const SAFEARRAYBOUND ArrayBound = {1, 0}; + +static IOleClientSiteVtbl MyIOleClientSiteTable = { + Site_QueryInterface, Site_AddRef, Site_Release, + Site_SaveObject, Site_GetMoniker, Site_GetContainer, + Site_ShowObject, Site_OnShowWindow, Site_RequestNewObjectLayout}; +static IOleInPlaceSiteVtbl MyIOleInPlaceSiteTable = { + InPlace_QueryInterface, + InPlace_AddRef, + InPlace_Release, + InPlace_GetWindow, + InPlace_ContextSensitiveHelp, + InPlace_CanInPlaceActivate, + InPlace_OnInPlaceActivate, + InPlace_OnUIActivate, + InPlace_GetWindowContext, + InPlace_Scroll, + InPlace_OnUIDeactivate, + InPlace_OnInPlaceDeactivate, + InPlace_DiscardUndoState, + InPlace_DeactivateAndUndo, + InPlace_OnPosRectChange}; + +static IOleInPlaceFrameVtbl MyIOleInPlaceFrameTable = { + Frame_QueryInterface, + Frame_AddRef, + Frame_Release, + Frame_GetWindow, + Frame_ContextSensitiveHelp, + Frame_GetBorder, + Frame_RequestBorderSpace, + Frame_SetBorderSpace, + Frame_SetActiveObject, + Frame_InsertMenus, + Frame_SetMenu, + Frame_RemoveMenus, + Frame_SetStatusText, + Frame_EnableModeless, + Frame_TranslateAccelerator}; + +static IDocHostUIHandlerVtbl MyIDocHostUIHandlerTable = { + UI_QueryInterface, + UI_AddRef, + UI_Release, + UI_ShowContextMenu, + UI_GetHostInfo, + UI_ShowUI, + UI_HideUI, + UI_UpdateUI, + UI_EnableModeless, + UI_OnDocWindowActivate, + UI_OnFrameWindowActivate, + UI_ResizeBorder, + UI_TranslateAccelerator, + UI_GetOptionKeyPath, + UI_GetDropTarget, + UI_GetExternal, + UI_TranslateUrl, + UI_FilterDataObject}; + + + +static HRESULT STDMETHODCALLTYPE IS_QueryInterface(IInternetSecurityManager FAR *This, REFIID riid, void **ppvObject) { + return E_NOTIMPL; +} +static ULONG STDMETHODCALLTYPE IS_AddRef(IInternetSecurityManager FAR *This) { return 1; } +static ULONG STDMETHODCALLTYPE IS_Release(IInternetSecurityManager FAR *This) { return 1; } +static HRESULT STDMETHODCALLTYPE IS_SetSecuritySite(IInternetSecurityManager FAR *This, IInternetSecurityMgrSite *pSited) { + return INET_E_DEFAULT_ACTION; +} +static HRESULT STDMETHODCALLTYPE IS_GetSecuritySite(IInternetSecurityManager FAR *This, IInternetSecurityMgrSite **ppSite) { + return INET_E_DEFAULT_ACTION; +} +static HRESULT STDMETHODCALLTYPE IS_MapUrlToZone(IInternetSecurityManager FAR *This, LPCWSTR pwszUrl, DWORD *pdwZone, DWORD dwFlags) { + *pdwZone = URLZONE_LOCAL_MACHINE; + return S_OK; +} +static HRESULT STDMETHODCALLTYPE IS_GetSecurityId(IInternetSecurityManager FAR *This, LPCWSTR pwszUrl, BYTE *pbSecurityId, DWORD *pcbSecurityId, DWORD_PTR dwReserved) { + return INET_E_DEFAULT_ACTION; +} +static HRESULT STDMETHODCALLTYPE IS_ProcessUrlAction(IInternetSecurityManager FAR *This, LPCWSTR pwszUrl, DWORD dwAction, BYTE *pPolicy, DWORD cbPolicy, BYTE *pContext, DWORD cbContext, DWORD dwFlags, DWORD dwReserved) { + return INET_E_DEFAULT_ACTION; +} +static HRESULT STDMETHODCALLTYPE IS_QueryCustomPolicy(IInternetSecurityManager FAR *This, LPCWSTR pwszUrl, REFGUID guidKey, BYTE **ppPolicy, DWORD *pcbPolicy, BYTE *pContext, DWORD cbContext, DWORD dwReserved) { + return INET_E_DEFAULT_ACTION; +} +static HRESULT STDMETHODCALLTYPE IS_SetZoneMapping(IInternetSecurityManager FAR *This, DWORD dwZone, LPCWSTR lpszPattern, DWORD dwFlags) { + return INET_E_DEFAULT_ACTION; +} +static HRESULT STDMETHODCALLTYPE IS_GetZoneMappings(IInternetSecurityManager FAR *This, DWORD dwZone, IEnumString **ppenumString, DWORD dwFlags) { + return INET_E_DEFAULT_ACTION; +} +static IInternetSecurityManagerVtbl MyInternetSecurityManagerTable = {IS_QueryInterface, IS_AddRef, IS_Release, IS_SetSecuritySite, IS_GetSecuritySite, IS_MapUrlToZone, IS_GetSecurityId, IS_ProcessUrlAction, IS_QueryCustomPolicy, IS_SetZoneMapping, IS_GetZoneMappings}; + +static HRESULT STDMETHODCALLTYPE SP_QueryInterface(IServiceProvider FAR *This, REFIID riid, void **ppvObject) { + return (Site_QueryInterface( + (IOleClientSite *)((char *)This - sizeof(IOleClientSite) - sizeof(_IOleInPlaceSiteEx) - sizeof(_IDocHostUIHandlerEx) - sizeof(IDispatch)), riid, ppvObject)); +} +static ULONG STDMETHODCALLTYPE SP_AddRef(IServiceProvider FAR *This) { return 1; } +static ULONG STDMETHODCALLTYPE SP_Release(IServiceProvider FAR *This) { return 1; } +static HRESULT STDMETHODCALLTYPE SP_QueryService(IServiceProvider FAR *This, REFGUID siid, REFIID riid, void **ppvObject) { + if (iid_eq(siid, &IID_IInternetSecurityManager) && iid_eq(riid, &IID_IInternetSecurityManager)) { + *ppvObject = &((_IServiceProviderEx *)This)->mgr; + } else { + *ppvObject = 0; + return (E_NOINTERFACE); + } + return S_OK; +} +static IServiceProviderVtbl MyServiceProviderTable = {SP_QueryInterface, SP_AddRef, SP_Release, SP_QueryService}; + +static void UnEmbedBrowserObject(struct webview *w) { + if (w->priv.browser != NULL) { + (*w->priv.browser)->lpVtbl->Close(*w->priv.browser, OLECLOSE_NOSAVE); + (*w->priv.browser)->lpVtbl->Release(*w->priv.browser); + GlobalFree(w->priv.browser); + w->priv.browser = NULL; + } +} + +static int EmbedBrowserObject(struct webview *w) { + RECT rect; + IWebBrowser2 *webBrowser2 = NULL; + LPCLASSFACTORY pClassFactory = NULL; + _IOleClientSiteEx *_iOleClientSiteEx = NULL; + IOleObject **browser = (IOleObject **)GlobalAlloc( + GMEM_FIXED, sizeof(IOleObject *) + sizeof(_IOleClientSiteEx)); + if (browser == NULL) { + goto error; + } + w->priv.browser = browser; + + _iOleClientSiteEx = (_IOleClientSiteEx *)(browser + 1); + _iOleClientSiteEx->client.lpVtbl = &MyIOleClientSiteTable; + _iOleClientSiteEx->inplace.inplace.lpVtbl = &MyIOleInPlaceSiteTable; + _iOleClientSiteEx->inplace.frame.frame.lpVtbl = &MyIOleInPlaceFrameTable; + _iOleClientSiteEx->inplace.frame.window = w->priv.hwnd; + _iOleClientSiteEx->ui.ui.lpVtbl = &MyIDocHostUIHandlerTable; + _iOleClientSiteEx->external.lpVtbl = &ExternalDispatchTable; + _iOleClientSiteEx->provider.provider.lpVtbl = &MyServiceProviderTable; + _iOleClientSiteEx->provider.mgr.mgr.lpVtbl = &MyInternetSecurityManagerTable; + + if (CoGetClassObject(iid_unref(&CLSID_WebBrowser), + CLSCTX_INPROC_SERVER | CLSCTX_INPROC_HANDLER, NULL, + iid_unref(&IID_IClassFactory), + (void **)&pClassFactory) != S_OK) { + goto error; + } + + if (pClassFactory == NULL) { + goto error; + } + + if (pClassFactory->lpVtbl->CreateInstance(pClassFactory, 0, + iid_unref(&IID_IOleObject), + (void **)browser) != S_OK) { + goto error; + } + pClassFactory->lpVtbl->Release(pClassFactory); + if ((*browser)->lpVtbl->SetClientSite( + *browser, (IOleClientSite *)_iOleClientSiteEx) != S_OK) { + goto error; + } + (*browser)->lpVtbl->SetHostNames(*browser, L"My Host Name", 0); + + if (OleSetContainedObject((struct IUnknown *)(*browser), TRUE) != S_OK) { + goto error; + } + GetClientRect(w->priv.hwnd, &rect); + if ((*browser)->lpVtbl->DoVerb((*browser), OLEIVERB_SHOW, NULL, + (IOleClientSite *)_iOleClientSiteEx, -1, + w->priv.hwnd, &rect) != S_OK) { + goto error; + } + if ((*browser)->lpVtbl->QueryInterface((*browser), + iid_unref(&IID_IWebBrowser2), + (void **)&webBrowser2) != S_OK) { + goto error; + } + + webBrowser2->lpVtbl->put_Left(webBrowser2, 0); + webBrowser2->lpVtbl->put_Top(webBrowser2, 0); + webBrowser2->lpVtbl->put_Width(webBrowser2, rect.right); + webBrowser2->lpVtbl->put_Height(webBrowser2, rect.bottom); + webBrowser2->lpVtbl->Release(webBrowser2); + + return 0; +error: + UnEmbedBrowserObject(w); + if (pClassFactory != NULL) { + pClassFactory->lpVtbl->Release(pClassFactory); + } + if (browser != NULL) { + GlobalFree(browser); + } + return -1; +} + +#define WEBVIEW_DATA_URL_PREFIX "data:text/html," +static int DisplayHTMLPage(struct webview *w) { + IWebBrowser2 *webBrowser2; + VARIANT myURL; + LPDISPATCH lpDispatch; + IHTMLDocument2 *htmlDoc2; + BSTR bstr; + IOleObject *browserObject; + SAFEARRAY *sfArray; + VARIANT *pVar; + browserObject = *w->priv.browser; + int isDataURL = 0; + const char *webview_url = webview_check_url(w->url); + if (!browserObject->lpVtbl->QueryInterface( + browserObject, iid_unref(&IID_IWebBrowser2), (void **)&webBrowser2)) { + LPCSTR webPageName; + isDataURL = (strncmp(webview_url, WEBVIEW_DATA_URL_PREFIX, + strlen(WEBVIEW_DATA_URL_PREFIX)) == 0); + if (isDataURL) { + webPageName = "about:blank"; + } else { + webPageName = (LPCSTR)webview_url; + } + VariantInit(&myURL); + myURL.vt = VT_BSTR; +#ifndef UNICODE + { + wchar_t *buffer = webview_to_utf16(webPageName); + if (buffer == NULL) { + goto badalloc; + } + myURL.bstrVal = SysAllocString(buffer); + GlobalFree(buffer); + } +#else + myURL.bstrVal = SysAllocString(webPageName); +#endif + if (!myURL.bstrVal) { + badalloc: + webBrowser2->lpVtbl->Release(webBrowser2); + return (-6); + } + webBrowser2->lpVtbl->Navigate2(webBrowser2, &myURL, 0, 0, 0, 0); + VariantClear(&myURL); + if (!isDataURL) { + return 0; + } + + char *url = (char *)calloc(1, strlen(webview_url) + 1); + char *q = url; + for (const char *p = webview_url + strlen(WEBVIEW_DATA_URL_PREFIX); *q = *p; + p++, q++) { + if (*q == '%' && *(p + 1) && *(p + 2)) { + sscanf(p + 1, "%02x", q); + p = p + 2; + } + } + + if (webBrowser2->lpVtbl->get_Document(webBrowser2, &lpDispatch) == S_OK) { + if (lpDispatch->lpVtbl->QueryInterface(lpDispatch, + iid_unref(&IID_IHTMLDocument2), + (void **)&htmlDoc2) == S_OK) { + if ((sfArray = SafeArrayCreate(VT_VARIANT, 1, + (SAFEARRAYBOUND *)&ArrayBound))) { + if (!SafeArrayAccessData(sfArray, (void **)&pVar)) { + pVar->vt = VT_BSTR; +#ifndef UNICODE + { + wchar_t *buffer = webview_to_utf16(url); + if (buffer == NULL) { + goto release; + } + bstr = SysAllocString(buffer); + GlobalFree(buffer); + } +#else + bstr = SysAllocString(string); +#endif + if ((pVar->bstrVal = bstr)) { + htmlDoc2->lpVtbl->write(htmlDoc2, sfArray); + htmlDoc2->lpVtbl->close(htmlDoc2); + } + } + SafeArrayDestroy(sfArray); + } + release: + free(url); + htmlDoc2->lpVtbl->Release(htmlDoc2); + } + lpDispatch->lpVtbl->Release(lpDispatch); + } + webBrowser2->lpVtbl->Release(webBrowser2); + return (0); + } + return (-5); +} + +static LRESULT CALLBACK wndproc(HWND hwnd, UINT uMsg, WPARAM wParam, + LPARAM lParam) { + struct webview *w = (struct webview *)GetWindowLongPtr(hwnd, GWLP_USERDATA); + switch (uMsg) { + case WM_CREATE: + w = (struct webview *)((CREATESTRUCT *)lParam)->lpCreateParams; + w->priv.hwnd = hwnd; + return EmbedBrowserObject(w); + case WM_DESTROY: + UnEmbedBrowserObject(w); + PostQuitMessage(0); + return TRUE; + case WM_SIZE: { + IWebBrowser2 *webBrowser2; + IOleObject *browser = *w->priv.browser; + if (browser->lpVtbl->QueryInterface(browser, iid_unref(&IID_IWebBrowser2), + (void **)&webBrowser2) == S_OK) { + RECT rect; + GetClientRect(hwnd, &rect); + webBrowser2->lpVtbl->put_Width(webBrowser2, rect.right); + webBrowser2->lpVtbl->put_Height(webBrowser2, rect.bottom); + } + return TRUE; + } + case WM_WEBVIEW_DISPATCH: { + webview_dispatch_fn f = (webview_dispatch_fn)wParam; + void *arg = (void *)lParam; + (*f)(w, arg); + return TRUE; + } + } + return DefWindowProc(hwnd, uMsg, wParam, lParam); +} + +#define WEBVIEW_KEY_FEATURE_BROWSER_EMULATION \ + "Software\\Microsoft\\Internet " \ + "Explorer\\Main\\FeatureControl\\FEATURE_BROWSER_EMULATION" + +static int webview_fix_ie_compat_mode() { + HKEY hKey; + DWORD ie_version = 11000; + TCHAR appname[MAX_PATH + 1]; + TCHAR *p; + if (GetModuleFileName(NULL, appname, MAX_PATH + 1) == 0) { + return -1; + } + for (p = &appname[strlen(appname) - 1]; p != appname && *p != '\\'; p--) { + } + p++; + if (RegCreateKey(HKEY_CURRENT_USER, WEBVIEW_KEY_FEATURE_BROWSER_EMULATION, + &hKey) != ERROR_SUCCESS) { + return -1; + } + if (RegSetValueEx(hKey, p, 0, REG_DWORD, (BYTE *)&ie_version, + sizeof(ie_version)) != ERROR_SUCCESS) { + RegCloseKey(hKey); + return -1; + } + RegCloseKey(hKey); + return 0; +} + +WEBVIEW_API int webview_init(struct webview *w) { + WNDCLASSEX wc; + HINSTANCE hInstance; + DWORD style; + RECT clientRect; + RECT rect; + + if (webview_fix_ie_compat_mode() < 0) { + return -1; + } + + hInstance = GetModuleHandle(NULL); + if (hInstance == NULL) { + return -1; + } + if (OleInitialize(NULL) != S_OK) { + return -1; + } + ZeroMemory(&wc, sizeof(WNDCLASSEX)); + wc.cbSize = sizeof(WNDCLASSEX); + wc.hInstance = hInstance; + wc.lpfnWndProc = wndproc; + wc.lpszClassName = classname; + RegisterClassEx(&wc); + + style = WS_OVERLAPPEDWINDOW; + if (!w->resizable) { + style = WS_OVERLAPPED | WS_CAPTION | WS_MINIMIZEBOX | WS_SYSMENU; + } + + rect.left = 0; + rect.top = 0; + rect.right = w->width; + rect.bottom = w->height; + AdjustWindowRect(&rect, WS_OVERLAPPEDWINDOW, 0); + + GetClientRect(GetDesktopWindow(), &clientRect); + int left = (clientRect.right / 2) - ((rect.right - rect.left) / 2); + int top = (clientRect.bottom / 2) - ((rect.bottom - rect.top) / 2); + rect.right = rect.right - rect.left + left; + rect.left = left; + rect.bottom = rect.bottom - rect.top + top; + rect.top = top; + + w->priv.hwnd = + CreateWindowEx(0, classname, w->title, style, rect.left, rect.top, + rect.right - rect.left, rect.bottom - rect.top, + HWND_DESKTOP, NULL, hInstance, (void *)w); + if (w->priv.hwnd == 0) { + OleUninitialize(); + return -1; + } + + SetWindowLongPtr(w->priv.hwnd, GWLP_USERDATA, (LONG_PTR)w); + + DisplayHTMLPage(w); + + SetWindowText(w->priv.hwnd, w->title); + ShowWindow(w->priv.hwnd, SW_SHOWDEFAULT); + UpdateWindow(w->priv.hwnd); + SetFocus(w->priv.hwnd); + + return 0; +} + +WEBVIEW_API int webview_loop(struct webview *w, int blocking) { + MSG msg; + if (blocking) { + GetMessage(&msg, 0, 0, 0); + } else { + PeekMessage(&msg, 0, 0, 0, PM_REMOVE); + } + switch (msg.message) { + case WM_QUIT: + return -1; + case WM_COMMAND: + case WM_KEYDOWN: + case WM_KEYUP: { + HRESULT r = S_OK; + IWebBrowser2 *webBrowser2; + IOleObject *browser = *w->priv.browser; + if (browser->lpVtbl->QueryInterface(browser, iid_unref(&IID_IWebBrowser2), + (void **)&webBrowser2) == S_OK) { + IOleInPlaceActiveObject *pIOIPAO; + if (browser->lpVtbl->QueryInterface( + browser, iid_unref(&IID_IOleInPlaceActiveObject), + (void **)&pIOIPAO) == S_OK) { + r = pIOIPAO->lpVtbl->TranslateAccelerator(pIOIPAO, &msg); + pIOIPAO->lpVtbl->Release(pIOIPAO); + } + webBrowser2->lpVtbl->Release(webBrowser2); + } + if (r != S_FALSE) { + break; + } + } + default: + TranslateMessage(&msg); + DispatchMessage(&msg); + } + return 0; +} + +WEBVIEW_API int webview_eval(struct webview *w, const char *js) { + IWebBrowser2 *webBrowser2; + IHTMLDocument2 *htmlDoc2; + IDispatch *docDispatch; + IDispatch *scriptDispatch; + if ((*w->priv.browser) + ->lpVtbl->QueryInterface((*w->priv.browser), + iid_unref(&IID_IWebBrowser2), + (void **)&webBrowser2) != S_OK) { + return -1; + } + + if (webBrowser2->lpVtbl->get_Document(webBrowser2, &docDispatch) != S_OK) { + return -1; + } + if (docDispatch->lpVtbl->QueryInterface(docDispatch, + iid_unref(&IID_IHTMLDocument2), + (void **)&htmlDoc2) != S_OK) { + return -1; + } + if (htmlDoc2->lpVtbl->get_Script(htmlDoc2, &scriptDispatch) != S_OK) { + return -1; + } + DISPID dispid; + BSTR evalStr = SysAllocString(L"eval"); + if (scriptDispatch->lpVtbl->GetIDsOfNames( + scriptDispatch, iid_unref(&IID_NULL), &evalStr, 1, + LOCALE_SYSTEM_DEFAULT, &dispid) != S_OK) { + SysFreeString(evalStr); + return -1; + } + SysFreeString(evalStr); + + DISPPARAMS params; + VARIANT arg; + VARIANT result; + EXCEPINFO excepInfo; + UINT nArgErr = (UINT)-1; + params.cArgs = 1; + params.cNamedArgs = 0; + params.rgvarg = &arg; + arg.vt = VT_BSTR; + static const char *prologue = "(function(){"; + static const char *epilogue = ";})();"; + int n = strlen(prologue) + strlen(epilogue) + strlen(js) + 1; + char *eval = (char *)malloc(n); + snprintf(eval, n, "%s%s%s", prologue, js, epilogue); + wchar_t *buf = webview_to_utf16(eval); + if (buf == NULL) { + return -1; + } + arg.bstrVal = SysAllocString(buf); + if (scriptDispatch->lpVtbl->Invoke( + scriptDispatch, dispid, iid_unref(&IID_NULL), 0, DISPATCH_METHOD, + ¶ms, &result, &excepInfo, &nArgErr) != S_OK) { + return -1; + } + SysFreeString(arg.bstrVal); + free(eval); + scriptDispatch->lpVtbl->Release(scriptDispatch); + htmlDoc2->lpVtbl->Release(htmlDoc2); + docDispatch->lpVtbl->Release(docDispatch); + return 0; +} + +WEBVIEW_API void webview_dispatch(struct webview *w, webview_dispatch_fn fn, + void *arg) { + PostMessageW(w->priv.hwnd, WM_WEBVIEW_DISPATCH, (WPARAM)fn, (LPARAM)arg); +} + +WEBVIEW_API void webview_set_title(struct webview *w, const char *title) { + SetWindowText(w->priv.hwnd, title); +} + +WEBVIEW_API void webview_set_fullscreen(struct webview *w, int fullscreen) { + if (w->priv.is_fullscreen == !!fullscreen) { + return; + } + if (w->priv.is_fullscreen == 0) { + w->priv.saved_style = GetWindowLong(w->priv.hwnd, GWL_STYLE); + w->priv.saved_ex_style = GetWindowLong(w->priv.hwnd, GWL_EXSTYLE); + GetWindowRect(w->priv.hwnd, &w->priv.saved_rect); + } + w->priv.is_fullscreen = !!fullscreen; + if (fullscreen) { + MONITORINFO monitor_info; + SetWindowLong(w->priv.hwnd, GWL_STYLE, + w->priv.saved_style & ~(WS_CAPTION | WS_THICKFRAME)); + SetWindowLong(w->priv.hwnd, GWL_EXSTYLE, + w->priv.saved_ex_style & + ~(WS_EX_DLGMODALFRAME | WS_EX_WINDOWEDGE | + WS_EX_CLIENTEDGE | WS_EX_STATICEDGE)); + monitor_info.cbSize = sizeof(monitor_info); + GetMonitorInfo(MonitorFromWindow(w->priv.hwnd, MONITOR_DEFAULTTONEAREST), + &monitor_info); + RECT r; + r.left = monitor_info.rcMonitor.left; + r.top = monitor_info.rcMonitor.top; + r.right = monitor_info.rcMonitor.right; + r.bottom = monitor_info.rcMonitor.bottom; + SetWindowPos(w->priv.hwnd, NULL, r.left, r.top, r.right - r.left, + r.bottom - r.top, + SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED); + } else { + SetWindowLong(w->priv.hwnd, GWL_STYLE, w->priv.saved_style); + SetWindowLong(w->priv.hwnd, GWL_EXSTYLE, w->priv.saved_ex_style); + SetWindowPos(w->priv.hwnd, NULL, w->priv.saved_rect.left, + w->priv.saved_rect.top, + w->priv.saved_rect.right - w->priv.saved_rect.left, + w->priv.saved_rect.bottom - w->priv.saved_rect.top, + SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED); + } +} + +WEBVIEW_API void webview_set_color(struct webview *w, uint8_t r, uint8_t g, + uint8_t b, uint8_t a) { + HBRUSH brush = CreateSolidBrush(RGB(r, g, b)); + SetClassLongPtr(w->priv.hwnd, GCLP_HBRBACKGROUND, (LONG_PTR)brush); +} + +/* These are missing parts from MinGW */ +#ifndef __IFileDialog_INTERFACE_DEFINED__ +#define __IFileDialog_INTERFACE_DEFINED__ +enum _FILEOPENDIALOGOPTIONS { + FOS_OVERWRITEPROMPT = 0x2, + FOS_STRICTFILETYPES = 0x4, + FOS_NOCHANGEDIR = 0x8, + FOS_PICKFOLDERS = 0x20, + FOS_FORCEFILESYSTEM = 0x40, + FOS_ALLNONSTORAGEITEMS = 0x80, + FOS_NOVALIDATE = 0x100, + FOS_ALLOWMULTISELECT = 0x200, + FOS_PATHMUSTEXIST = 0x800, + FOS_FILEMUSTEXIST = 0x1000, + FOS_CREATEPROMPT = 0x2000, + FOS_SHAREAWARE = 0x4000, + FOS_NOREADONLYRETURN = 0x8000, + FOS_NOTESTFILECREATE = 0x10000, + FOS_HIDEMRUPLACES = 0x20000, + FOS_HIDEPINNEDPLACES = 0x40000, + FOS_NODEREFERENCELINKS = 0x100000, + FOS_DONTADDTORECENT = 0x2000000, + FOS_FORCESHOWHIDDEN = 0x10000000, + FOS_DEFAULTNOMINIMODE = 0x20000000, + FOS_FORCEPREVIEWPANEON = 0x40000000 +}; +typedef DWORD FILEOPENDIALOGOPTIONS; +typedef enum FDAP { FDAP_BOTTOM = 0, FDAP_TOP = 1 } FDAP; +DEFINE_GUID(IID_IFileDialog, 0x42f85136, 0xdb7e, 0x439c, 0x85, 0xf1, 0xe4, 0x07, + 0x5d, 0x13, 0x5f, 0xc8); +typedef struct IFileDialogVtbl { + BEGIN_INTERFACE + HRESULT(STDMETHODCALLTYPE *QueryInterface) + (IFileDialog *This, REFIID riid, void **ppvObject); + ULONG(STDMETHODCALLTYPE *AddRef)(IFileDialog *This); + ULONG(STDMETHODCALLTYPE *Release)(IFileDialog *This); + HRESULT(STDMETHODCALLTYPE *Show)(IFileDialog *This, HWND hwndOwner); + HRESULT(STDMETHODCALLTYPE *SetFileTypes) + (IFileDialog *This, UINT cFileTypes, const COMDLG_FILTERSPEC *rgFilterSpec); + HRESULT(STDMETHODCALLTYPE *SetFileTypeIndex) + (IFileDialog *This, UINT iFileType); + HRESULT(STDMETHODCALLTYPE *GetFileTypeIndex) + (IFileDialog *This, UINT *piFileType); + HRESULT(STDMETHODCALLTYPE *Advise) + (IFileDialog *This, IFileDialogEvents *pfde, DWORD *pdwCookie); + HRESULT(STDMETHODCALLTYPE *Unadvise)(IFileDialog *This, DWORD dwCookie); + HRESULT(STDMETHODCALLTYPE *SetOptions) + (IFileDialog *This, FILEOPENDIALOGOPTIONS fos); + HRESULT(STDMETHODCALLTYPE *GetOptions) + (IFileDialog *This, FILEOPENDIALOGOPTIONS *pfos); + HRESULT(STDMETHODCALLTYPE *SetDefaultFolder) + (IFileDialog *This, IShellItem *psi); + HRESULT(STDMETHODCALLTYPE *SetFolder)(IFileDialog *This, IShellItem *psi); + HRESULT(STDMETHODCALLTYPE *GetFolder)(IFileDialog *This, IShellItem **ppsi); + HRESULT(STDMETHODCALLTYPE *GetCurrentSelection) + (IFileDialog *This, IShellItem **ppsi); + HRESULT(STDMETHODCALLTYPE *SetFileName)(IFileDialog *This, LPCWSTR pszName); + HRESULT(STDMETHODCALLTYPE *GetFileName)(IFileDialog *This, LPWSTR *pszName); + HRESULT(STDMETHODCALLTYPE *SetTitle)(IFileDialog *This, LPCWSTR pszTitle); + HRESULT(STDMETHODCALLTYPE *SetOkButtonLabel) + (IFileDialog *This, LPCWSTR pszText); + HRESULT(STDMETHODCALLTYPE *SetFileNameLabel) + (IFileDialog *This, LPCWSTR pszLabel); + HRESULT(STDMETHODCALLTYPE *GetResult)(IFileDialog *This, IShellItem **ppsi); + HRESULT(STDMETHODCALLTYPE *AddPlace) + (IFileDialog *This, IShellItem *psi, FDAP fdap); + HRESULT(STDMETHODCALLTYPE *SetDefaultExtension) + (IFileDialog *This, LPCWSTR pszDefaultExtension); + HRESULT(STDMETHODCALLTYPE *Close)(IFileDialog *This, HRESULT hr); + HRESULT(STDMETHODCALLTYPE *SetClientGuid)(IFileDialog *This, REFGUID guid); + HRESULT(STDMETHODCALLTYPE *ClearClientData)(IFileDialog *This); + HRESULT(STDMETHODCALLTYPE *SetFilter) + (IFileDialog *This, IShellItemFilter *pFilter); + END_INTERFACE +} IFileDialogVtbl; +interface IFileDialog { + CONST_VTBL IFileDialogVtbl *lpVtbl; +}; +DEFINE_GUID(IID_IFileOpenDialog, 0xd57c7288, 0xd4ad, 0x4768, 0xbe, 0x02, 0x9d, + 0x96, 0x95, 0x32, 0xd9, 0x60); +DEFINE_GUID(IID_IFileSaveDialog, 0x84bccd23, 0x5fde, 0x4cdb, 0xae, 0xa4, 0xaf, + 0x64, 0xb8, 0x3d, 0x78, 0xab); +#endif + +WEBVIEW_API void webview_dialog(struct webview *w, + enum webview_dialog_type dlgtype, int flags, + const char *title, const char *arg, + char *result, size_t resultsz) { + if (dlgtype == WEBVIEW_DIALOG_TYPE_OPEN || + dlgtype == WEBVIEW_DIALOG_TYPE_SAVE) { + IFileDialog *dlg = NULL; + IShellItem *res = NULL; + WCHAR *ws = NULL; + char *s = NULL; + FILEOPENDIALOGOPTIONS opts, add_opts; + if (dlgtype == WEBVIEW_DIALOG_TYPE_OPEN) { + if (CoCreateInstance( + iid_unref(&CLSID_FileOpenDialog), NULL, CLSCTX_INPROC_SERVER, + iid_unref(&IID_IFileOpenDialog), (void **)&dlg) != S_OK) { + goto error_dlg; + } + if (flags & WEBVIEW_DIALOG_FLAG_DIRECTORY) { + add_opts |= FOS_PICKFOLDERS; + } + add_opts |= FOS_NOCHANGEDIR | FOS_ALLNONSTORAGEITEMS | FOS_NOVALIDATE | + FOS_PATHMUSTEXIST | FOS_FILEMUSTEXIST | FOS_SHAREAWARE | + FOS_NOTESTFILECREATE | FOS_NODEREFERENCELINKS | + FOS_FORCESHOWHIDDEN | FOS_DEFAULTNOMINIMODE; + } else { + if (CoCreateInstance( + iid_unref(&CLSID_FileSaveDialog), NULL, CLSCTX_INPROC_SERVER, + iid_unref(&IID_IFileSaveDialog), (void **)&dlg) != S_OK) { + goto error_dlg; + } + add_opts |= FOS_OVERWRITEPROMPT | FOS_NOCHANGEDIR | + FOS_ALLNONSTORAGEITEMS | FOS_NOVALIDATE | FOS_SHAREAWARE | + FOS_NOTESTFILECREATE | FOS_NODEREFERENCELINKS | + FOS_FORCESHOWHIDDEN | FOS_DEFAULTNOMINIMODE; + } + if (dlg->lpVtbl->GetOptions(dlg, &opts) != S_OK) { + goto error_dlg; + } + opts &= ~FOS_NOREADONLYRETURN; + opts |= add_opts; + if (dlg->lpVtbl->SetOptions(dlg, opts) != S_OK) { + goto error_dlg; + } + if (dlg->lpVtbl->Show(dlg, w->priv.hwnd) != S_OK) { + goto error_dlg; + } + if (dlg->lpVtbl->GetResult(dlg, &res) != S_OK) { + goto error_dlg; + } + if (res->lpVtbl->GetDisplayName(res, SIGDN_FILESYSPATH, &ws) != S_OK) { + goto error_result; + } + s = webview_from_utf16(ws); + strncpy(result, s, resultsz); + result[resultsz - 1] = '\0'; + CoTaskMemFree(ws); + error_result: + res->lpVtbl->Release(res); + error_dlg: + dlg->lpVtbl->Release(dlg); + return; + } else if (dlgtype == WEBVIEW_DIALOG_TYPE_ALERT) { +#if 0 + /* MinGW often doesn't contain TaskDialog, we'll use MessageBox for now */ + WCHAR *wtitle = webview_to_utf16(title); + WCHAR *warg = webview_to_utf16(arg); + TaskDialog(w->priv.hwnd, NULL, NULL, wtitle, warg, 0, NULL, NULL); + GlobalFree(warg); + GlobalFree(wtitle); +#else + UINT type = MB_OK; + switch (flags & WEBVIEW_DIALOG_FLAG_ALERT_MASK) { + case WEBVIEW_DIALOG_FLAG_INFO: + type |= MB_ICONINFORMATION; + break; + case WEBVIEW_DIALOG_FLAG_WARNING: + type |= MB_ICONWARNING; + break; + case WEBVIEW_DIALOG_FLAG_ERROR: + type |= MB_ICONERROR; + break; + } + MessageBox(w->priv.hwnd, arg, title, type); +#endif + } +} + +WEBVIEW_API void webview_terminate(struct webview *w) { PostQuitMessage(0); } + +WEBVIEW_API void webview_exit(struct webview *w) { + DestroyWindow(w->priv.hwnd); + OleUninitialize(); +} + +WEBVIEW_API void webview_print_log(const char *s) { OutputDebugString(s); } + +#endif /* WEBVIEW_WINAPI */ + +#if defined(WEBVIEW_COCOA) +#define NSAlertStyleWarning 0 +#define NSAlertStyleCritical 2 +#define NSWindowStyleMaskResizable 8 +#define NSWindowStyleMaskMiniaturizable 4 +#define NSWindowStyleMaskTitled 1 +#define NSWindowStyleMaskClosable 2 +#define NSWindowStyleMaskFullScreen (1 << 14) +#define NSViewWidthSizable 2 +#define NSViewHeightSizable 16 +#define NSBackingStoreBuffered 2 +#define NSEventMaskAny ULONG_MAX +#define NSEventModifierFlagCommand (1 << 20) +#define NSEventModifierFlagOption (1 << 19) +#define NSAlertStyleInformational 1 +#define NSAlertFirstButtonReturn 1000 +#define WKNavigationActionPolicyDownload 2 +#define NSModalResponseOK 1 +#define WKNavigationActionPolicyDownload 2 +#define WKNavigationResponsePolicyAllow 1 +#define WKUserScriptInjectionTimeAtDocumentStart 0 +#define NSApplicationActivationPolicyRegular 0 + +static id get_nsstring(const char *c_str) { + return objc_msgSend((id)objc_getClass("NSString"), + sel_registerName("stringWithUTF8String:"), c_str); +} + +static id create_menu_item(id title, const char *action, const char *key) { + id item = + objc_msgSend((id)objc_getClass("NSMenuItem"), sel_registerName("alloc")); + objc_msgSend(item, sel_registerName("initWithTitle:action:keyEquivalent:"), + title, sel_registerName(action), get_nsstring(key)); + objc_msgSend(item, sel_registerName("autorelease")); + + return item; +} + +static void webview_window_will_close(id self, SEL cmd, id notification) { + struct webview *w = + (struct webview *)objc_getAssociatedObject(self, "webview"); + webview_terminate(w); +} + +static void webview_external_invoke(id self, SEL cmd, id contentController, + id message) { + struct webview *w = + (struct webview *)objc_getAssociatedObject(contentController, "webview"); + if (w == NULL || w->external_invoke_cb == NULL) { + return; + } + + w->external_invoke_cb(w, (const char *)objc_msgSend( + objc_msgSend(message, sel_registerName("body")), + sel_registerName("UTF8String"))); +} + +static void run_open_panel(id self, SEL cmd, id webView, id parameters, + id frame, void (^completionHandler)(id)) { + + id openPanel = objc_msgSend((id)objc_getClass("NSOpenPanel"), + sel_registerName("openPanel")); + + objc_msgSend( + openPanel, sel_registerName("setAllowsMultipleSelection:"), + objc_msgSend(parameters, sel_registerName("allowsMultipleSelection"))); + + objc_msgSend(openPanel, sel_registerName("setCanChooseFiles:"), 1); + objc_msgSend( + openPanel, sel_registerName("beginWithCompletionHandler:"), ^(id result) { + if (result == (id)NSModalResponseOK) { + completionHandler(objc_msgSend(openPanel, sel_registerName("URLs"))); + } else { + completionHandler(nil); + } + }); +} + +static void run_save_panel(id self, SEL cmd, id download, id filename, + void (^completionHandler)(int allowOverwrite, + id destination)) { + id savePanel = objc_msgSend((id)objc_getClass("NSSavePanel"), + sel_registerName("savePanel")); + objc_msgSend(savePanel, sel_registerName("setCanCreateDirectories:"), 1); + objc_msgSend(savePanel, sel_registerName("setNameFieldStringValue:"), + filename); + objc_msgSend(savePanel, sel_registerName("beginWithCompletionHandler:"), + ^(id result) { + if (result == (id)NSModalResponseOK) { + id url = objc_msgSend(savePanel, sel_registerName("URL")); + id path = objc_msgSend(url, sel_registerName("path")); + completionHandler(1, path); + } else { + completionHandler(NO, nil); + } + }); +} + +static void run_confirmation_panel(id self, SEL cmd, id webView, id message, + id frame, void (^completionHandler)(bool)) { + + id alert = + objc_msgSend((id)objc_getClass("NSAlert"), sel_registerName("new")); + objc_msgSend(alert, sel_registerName("setIcon:"), + objc_msgSend((id)objc_getClass("NSImage"), + sel_registerName("imageNamed:"), + get_nsstring("NSCaution"))); + objc_msgSend(alert, sel_registerName("setShowsHelp:"), 0); + objc_msgSend(alert, sel_registerName("setInformativeText:"), message); + objc_msgSend(alert, sel_registerName("addButtonWithTitle:"), + get_nsstring("OK")); + objc_msgSend(alert, sel_registerName("addButtonWithTitle:"), + get_nsstring("Cancel")); + if (objc_msgSend(alert, sel_registerName("runModal")) == + (id)NSAlertFirstButtonReturn) { + completionHandler(true); + } else { + completionHandler(false); + } + objc_msgSend(alert, sel_registerName("release")); +} + +static void run_alert_panel(id self, SEL cmd, id webView, id message, id frame, + void (^completionHandler)(void)) { + id alert = + objc_msgSend((id)objc_getClass("NSAlert"), sel_registerName("new")); + objc_msgSend(alert, sel_registerName("setIcon:"), + objc_msgSend((id)objc_getClass("NSImage"), + sel_registerName("imageNamed:"), + get_nsstring("NSCaution"))); + objc_msgSend(alert, sel_registerName("setShowsHelp:"), 0); + objc_msgSend(alert, sel_registerName("setInformativeText:"), message); + objc_msgSend(alert, sel_registerName("addButtonWithTitle:"), + get_nsstring("OK")); + objc_msgSend(alert, sel_registerName("runModal")); + objc_msgSend(alert, sel_registerName("release")); + completionHandler(); +} + +static void download_failed(id self, SEL cmd, id download, id error) { + printf("%s", + (const char *)objc_msgSend( + objc_msgSend(error, sel_registerName("localizedDescription")), + sel_registerName("UTF8String"))); +} + +static void make_nav_policy_decision(id self, SEL cmd, id webView, id response, + void (^decisionHandler)(int)) { + if (objc_msgSend(response, sel_registerName("canShowMIMEType")) == 0) { + decisionHandler(WKNavigationActionPolicyDownload); + } else { + decisionHandler(WKNavigationResponsePolicyAllow); + } +} + +WEBVIEW_API int webview_init(struct webview *w) { + w->priv.pool = objc_msgSend((id)objc_getClass("NSAutoreleasePool"), + sel_registerName("new")); + objc_msgSend((id)objc_getClass("NSApplication"), + sel_registerName("sharedApplication")); + + Class __WKScriptMessageHandler = objc_allocateClassPair( + objc_getClass("NSObject"), "__WKScriptMessageHandler", 0); + class_addMethod( + __WKScriptMessageHandler, + sel_registerName("userContentController:didReceiveScriptMessage:"), + (IMP)webview_external_invoke, "v@:@@"); + objc_registerClassPair(__WKScriptMessageHandler); + + id scriptMessageHandler = + objc_msgSend((id)__WKScriptMessageHandler, sel_registerName("new")); + + /*** + _WKDownloadDelegate is an undocumented/private protocol with methods called + from WKNavigationDelegate + References: + https://github.com/WebKit/webkit/blob/master/Source/WebKit/UIProcess/API/Cocoa/_WKDownload.h + https://github.com/WebKit/webkit/blob/master/Source/WebKit/UIProcess/API/Cocoa/_WKDownloadDelegate.h + https://github.com/WebKit/webkit/blob/master/Tools/TestWebKitAPI/Tests/WebKitCocoa/Download.mm + ***/ + + Class __WKDownloadDelegate = objc_allocateClassPair( + objc_getClass("NSObject"), "__WKDownloadDelegate", 0); + class_addMethod( + __WKDownloadDelegate, + sel_registerName("_download:decideDestinationWithSuggestedFilename:" + "completionHandler:"), + (IMP)run_save_panel, "v@:@@?"); + class_addMethod(__WKDownloadDelegate, + sel_registerName("_download:didFailWithError:"), + (IMP)download_failed, "v@:@@"); + objc_registerClassPair(__WKDownloadDelegate); + id downloadDelegate = + objc_msgSend((id)__WKDownloadDelegate, sel_registerName("new")); + + Class __WKPreferences = objc_allocateClassPair(objc_getClass("WKPreferences"), + "__WKPreferences", 0); + objc_property_attribute_t type = {"T", "c"}; + objc_property_attribute_t ownership = {"N", ""}; + objc_property_attribute_t attrs[] = {type, ownership}; + class_replaceProperty(__WKPreferences, "developerExtrasEnabled", attrs, 2); + objc_registerClassPair(__WKPreferences); + id wkPref = objc_msgSend((id)__WKPreferences, sel_registerName("new")); + objc_msgSend(wkPref, sel_registerName("setValue:forKey:"), + objc_msgSend((id)objc_getClass("NSNumber"), + sel_registerName("numberWithBool:"), !!w->debug), + objc_msgSend((id)objc_getClass("NSString"), + sel_registerName("stringWithUTF8String:"), + "developerExtrasEnabled")); + + id userController = objc_msgSend((id)objc_getClass("WKUserContentController"), + sel_registerName("new")); + objc_setAssociatedObject(userController, "webview", (id)(w), + OBJC_ASSOCIATION_ASSIGN); + objc_msgSend( + userController, sel_registerName("addScriptMessageHandler:name:"), + scriptMessageHandler, + objc_msgSend((id)objc_getClass("NSString"), + sel_registerName("stringWithUTF8String:"), "invoke")); + + /*** + In order to maintain compatibility with the other 'webviews' we need to + override window.external.invoke to call + webkit.messageHandlers.invoke.postMessage + ***/ + + id windowExternalOverrideScript = objc_msgSend( + (id)objc_getClass("WKUserScript"), sel_registerName("alloc")); + objc_msgSend( + windowExternalOverrideScript, + sel_registerName("initWithSource:injectionTime:forMainFrameOnly:"), + get_nsstring("window.external = this; invoke = function(arg){ " + "webkit.messageHandlers.invoke.postMessage(arg); };"), + WKUserScriptInjectionTimeAtDocumentStart, 0); + + objc_msgSend(userController, sel_registerName("addUserScript:"), + windowExternalOverrideScript); + + id config = objc_msgSend((id)objc_getClass("WKWebViewConfiguration"), + sel_registerName("new")); + id processPool = objc_msgSend(config, sel_registerName("processPool")); + objc_msgSend(processPool, sel_registerName("_setDownloadDelegate:"), + downloadDelegate); + objc_msgSend(config, sel_registerName("setProcessPool:"), processPool); + objc_msgSend(config, sel_registerName("setUserContentController:"), + userController); + objc_msgSend(config, sel_registerName("setPreferences:"), wkPref); + + Class __NSWindowDelegate = objc_allocateClassPair(objc_getClass("NSObject"), + "__NSWindowDelegate", 0); + class_addProtocol(__NSWindowDelegate, objc_getProtocol("NSWindowDelegate")); + class_replaceMethod(__NSWindowDelegate, sel_registerName("windowWillClose:"), + (IMP)webview_window_will_close, "v@:@"); + objc_registerClassPair(__NSWindowDelegate); + + w->priv.windowDelegate = + objc_msgSend((id)__NSWindowDelegate, sel_registerName("new")); + + objc_setAssociatedObject(w->priv.windowDelegate, "webview", (id)(w), + OBJC_ASSOCIATION_ASSIGN); + + id nsTitle = + objc_msgSend((id)objc_getClass("NSString"), + sel_registerName("stringWithUTF8String:"), w->title); + + CGRect r = CGRectMake(0, 0, w->width, w->height); + + unsigned int style = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | + NSWindowStyleMaskMiniaturizable; + if (w->resizable) { + style = style | NSWindowStyleMaskResizable; + } + + w->priv.window = + objc_msgSend((id)objc_getClass("NSWindow"), sel_registerName("alloc")); + objc_msgSend(w->priv.window, + sel_registerName("initWithContentRect:styleMask:backing:defer:"), + r, style, NSBackingStoreBuffered, 0); + + objc_msgSend(w->priv.window, sel_registerName("autorelease")); + objc_msgSend(w->priv.window, sel_registerName("setTitle:"), nsTitle); + objc_msgSend(w->priv.window, sel_registerName("setDelegate:"), + w->priv.windowDelegate); + objc_msgSend(w->priv.window, sel_registerName("center")); + + Class __WKUIDelegate = + objc_allocateClassPair(objc_getClass("NSObject"), "__WKUIDelegate", 0); + class_addProtocol(__WKUIDelegate, objc_getProtocol("WKUIDelegate")); + class_addMethod(__WKUIDelegate, + sel_registerName("webView:runOpenPanelWithParameters:" + "initiatedByFrame:completionHandler:"), + (IMP)run_open_panel, "v@:@@@?"); + class_addMethod(__WKUIDelegate, + sel_registerName("webView:runJavaScriptAlertPanelWithMessage:" + "initiatedByFrame:completionHandler:"), + (IMP)run_alert_panel, "v@:@@@?"); + class_addMethod( + __WKUIDelegate, + sel_registerName("webView:runJavaScriptConfirmPanelWithMessage:" + "initiatedByFrame:completionHandler:"), + (IMP)run_confirmation_panel, "v@:@@@?"); + objc_registerClassPair(__WKUIDelegate); + id uiDel = objc_msgSend((id)__WKUIDelegate, sel_registerName("new")); + + Class __WKNavigationDelegate = objc_allocateClassPair( + objc_getClass("NSObject"), "__WKNavigationDelegate", 0); + class_addProtocol(__WKNavigationDelegate, + objc_getProtocol("WKNavigationDelegate")); + class_addMethod( + __WKNavigationDelegate, + sel_registerName( + "webView:decidePolicyForNavigationResponse:decisionHandler:"), + (IMP)make_nav_policy_decision, "v@:@@?"); + objc_registerClassPair(__WKNavigationDelegate); + id navDel = objc_msgSend((id)__WKNavigationDelegate, sel_registerName("new")); + + w->priv.webview = + objc_msgSend((id)objc_getClass("WKWebView"), sel_registerName("alloc")); + objc_msgSend(w->priv.webview, + sel_registerName("initWithFrame:configuration:"), r, config); + objc_msgSend(w->priv.webview, sel_registerName("setUIDelegate:"), uiDel); + objc_msgSend(w->priv.webview, sel_registerName("setNavigationDelegate:"), + navDel); + + id nsURL = objc_msgSend((id)objc_getClass("NSURL"), + sel_registerName("URLWithString:"), + get_nsstring(webview_check_url(w->url))); + + objc_msgSend(w->priv.webview, sel_registerName("loadRequest:"), + objc_msgSend((id)objc_getClass("NSURLRequest"), + sel_registerName("requestWithURL:"), nsURL)); + objc_msgSend(w->priv.webview, sel_registerName("setAutoresizesSubviews:"), 1); + objc_msgSend(w->priv.webview, sel_registerName("setAutoresizingMask:"), + (NSViewWidthSizable | NSViewHeightSizable)); + objc_msgSend(objc_msgSend(w->priv.window, sel_registerName("contentView")), + sel_registerName("addSubview:"), w->priv.webview); + objc_msgSend(w->priv.window, sel_registerName("orderFrontRegardless")); + + objc_msgSend(objc_msgSend((id)objc_getClass("NSApplication"), + sel_registerName("sharedApplication")), + sel_registerName("setActivationPolicy:"), + NSApplicationActivationPolicyRegular); + + objc_msgSend(objc_msgSend((id)objc_getClass("NSApplication"), + sel_registerName("sharedApplication")), + sel_registerName("finishLaunching")); + + objc_msgSend(objc_msgSend((id)objc_getClass("NSApplication"), + sel_registerName("sharedApplication")), + sel_registerName("activateIgnoringOtherApps:"), 1); + + id menubar = + objc_msgSend((id)objc_getClass("NSMenu"), sel_registerName("alloc")); + objc_msgSend(menubar, sel_registerName("initWithTitle:"), get_nsstring("")); + objc_msgSend(menubar, sel_registerName("autorelease")); + + id appName = objc_msgSend(objc_msgSend((id)objc_getClass("NSProcessInfo"), + sel_registerName("processInfo")), + sel_registerName("processName")); + + id appMenuItem = + objc_msgSend((id)objc_getClass("NSMenuItem"), sel_registerName("alloc")); + objc_msgSend(appMenuItem, + sel_registerName("initWithTitle:action:keyEquivalent:"), appName, + NULL, get_nsstring("")); + + id appMenu = + objc_msgSend((id)objc_getClass("NSMenu"), sel_registerName("alloc")); + objc_msgSend(appMenu, sel_registerName("initWithTitle:"), appName); + objc_msgSend(appMenu, sel_registerName("autorelease")); + + objc_msgSend(appMenuItem, sel_registerName("setSubmenu:"), appMenu); + objc_msgSend(menubar, sel_registerName("addItem:"), appMenuItem); + + id title = + objc_msgSend(get_nsstring("Hide "), + sel_registerName("stringByAppendingString:"), appName); + id item = create_menu_item(title, "hide:", "h"); + objc_msgSend(appMenu, sel_registerName("addItem:"), item); + + item = create_menu_item(get_nsstring("Hide Others"), + "hideOtherApplications:", "h"); + objc_msgSend(item, sel_registerName("setKeyEquivalentModifierMask:"), + (NSEventModifierFlagOption | NSEventModifierFlagCommand)); + objc_msgSend(appMenu, sel_registerName("addItem:"), item); + + item = + create_menu_item(get_nsstring("Show All"), "unhideAllApplications:", ""); + objc_msgSend(appMenu, sel_registerName("addItem:"), item); + + objc_msgSend(appMenu, sel_registerName("addItem:"), + objc_msgSend((id)objc_getClass("NSMenuItem"), + sel_registerName("separatorItem"))); + + title = objc_msgSend(get_nsstring("Quit "), + sel_registerName("stringByAppendingString:"), appName); + item = create_menu_item(title, "terminate:", "q"); + objc_msgSend(appMenu, sel_registerName("addItem:"), item); + + objc_msgSend(objc_msgSend((id)objc_getClass("NSApplication"), + sel_registerName("sharedApplication")), + sel_registerName("setMainMenu:"), menubar); + + w->priv.should_exit = 0; + return 0; +} + +WEBVIEW_API int webview_loop(struct webview *w, int blocking) { + id until = (blocking ? objc_msgSend((id)objc_getClass("NSDate"), + sel_registerName("distantFuture")) + : objc_msgSend((id)objc_getClass("NSDate"), + sel_registerName("distantPast"))); + + id event = objc_msgSend( + objc_msgSend((id)objc_getClass("NSApplication"), + sel_registerName("sharedApplication")), + sel_registerName("nextEventMatchingMask:untilDate:inMode:dequeue:"), + ULONG_MAX, until, + objc_msgSend((id)objc_getClass("NSString"), + sel_registerName("stringWithUTF8String:"), + "kCFRunLoopDefaultMode"), + true); + + if (event) { + objc_msgSend(objc_msgSend((id)objc_getClass("NSApplication"), + sel_registerName("sharedApplication")), + sel_registerName("sendEvent:"), event); + } + + return w->priv.should_exit; +} + +WEBVIEW_API int webview_eval(struct webview *w, const char *js) { + objc_msgSend(w->priv.webview, + sel_registerName("evaluateJavaScript:completionHandler:"), + get_nsstring(js), NULL); + + return 0; +} + +WEBVIEW_API void webview_set_title(struct webview *w, const char *title) { + objc_msgSend(w->priv.window, sel_registerName("setTitle"), + get_nsstring(title)); +} + +WEBVIEW_API void webview_set_fullscreen(struct webview *w, int fullscreen) { + unsigned long windowStyleMask = (unsigned long)objc_msgSend( + w->priv.window, sel_registerName("styleMask")); + int b = (((windowStyleMask & NSWindowStyleMaskFullScreen) == + NSWindowStyleMaskFullScreen) + ? 1 + : 0); + if (b != fullscreen) { + objc_msgSend(w->priv.window, sel_registerName("toggleFullScreen:"), NULL); + } +} + +WEBVIEW_API void webview_set_color(struct webview *w, uint8_t r, uint8_t g, + uint8_t b, uint8_t a) { + + id color = objc_msgSend((id)objc_getClass("NSColor"), + sel_registerName("colorWithRed:green:blue:alpha:"), + (float)r / 255.0, (float)g / 255.0, (float)b / 255.0, + (float)a / 255.0); + + objc_msgSend(w->priv.window, sel_registerName("setBackgroundColor:"), color); + + if (0.5 >= ((r / 255.0 * 299.0) + (g / 255.0 * 587.0) + (b / 255.0 * 114.0)) / + 1000.0) { + objc_msgSend(w->priv.window, sel_registerName("setAppearance:"), + objc_msgSend((id)objc_getClass("NSAppearance"), + sel_registerName("appearanceNamed:"), + get_nsstring("NSAppearanceNameVibrantDark"))); + } else { + objc_msgSend(w->priv.window, sel_registerName("setAppearance:"), + objc_msgSend((id)objc_getClass("NSAppearance"), + sel_registerName("appearanceNamed:"), + get_nsstring("NSAppearanceNameVibrantLight"))); + } + objc_msgSend(w->priv.window, sel_registerName("setOpaque:"), 0); + objc_msgSend(w->priv.window, + sel_registerName("setTitlebarAppearsTransparent:"), 1); +} + +WEBVIEW_API void webview_dialog(struct webview *w, + enum webview_dialog_type dlgtype, int flags, + const char *title, const char *arg, + char *result, size_t resultsz) { + if (dlgtype == WEBVIEW_DIALOG_TYPE_OPEN || + dlgtype == WEBVIEW_DIALOG_TYPE_SAVE) { + id panel = (id)objc_getClass("NSSavePanel"); + if (dlgtype == WEBVIEW_DIALOG_TYPE_OPEN) { + id openPanel = objc_msgSend((id)objc_getClass("NSOpenPanel"), + sel_registerName("openPanel")); + if (flags & WEBVIEW_DIALOG_FLAG_DIRECTORY) { + objc_msgSend(openPanel, sel_registerName("setCanChooseFiles:"), 0); + objc_msgSend(openPanel, sel_registerName("setCanChooseDirectories:"), + 1); + } else { + objc_msgSend(openPanel, sel_registerName("setCanChooseFiles:"), 1); + objc_msgSend(openPanel, sel_registerName("setCanChooseDirectories:"), + 0); + } + objc_msgSend(openPanel, sel_registerName("setResolvesAliases:"), 0); + objc_msgSend(openPanel, sel_registerName("setAllowsMultipleSelection:"), + 0); + panel = openPanel; + } else { + panel = objc_msgSend((id)objc_getClass("NSSavePanel"), + sel_registerName("savePanel")); + } + + objc_msgSend(panel, sel_registerName("setCanCreateDirectories:"), 1); + objc_msgSend(panel, sel_registerName("setShowsHiddenFiles:"), 1); + objc_msgSend(panel, sel_registerName("setExtensionHidden:"), 0); + objc_msgSend(panel, sel_registerName("setCanSelectHiddenExtension:"), 0); + objc_msgSend(panel, sel_registerName("setTreatsFilePackagesAsDirectories:"), + 1); + objc_msgSend( + panel, sel_registerName("beginSheetModalForWindow:completionHandler:"), + w->priv.window, ^(id result) { + objc_msgSend(objc_msgSend((id)objc_getClass("NSApplication"), + sel_registerName("sharedApplication")), + sel_registerName("stopModalWithCode:"), result); + }); + + if (objc_msgSend(objc_msgSend((id)objc_getClass("NSApplication"), + sel_registerName("sharedApplication")), + sel_registerName("runModalForWindow:"), + panel) == (id)NSModalResponseOK) { + id url = objc_msgSend(panel, sel_registerName("URL")); + id path = objc_msgSend(url, sel_registerName("path")); + const char *filename = + (const char *)objc_msgSend(path, sel_registerName("UTF8String")); + strlcpy(result, filename, resultsz); + } + } else if (dlgtype == WEBVIEW_DIALOG_TYPE_ALERT) { + id a = objc_msgSend((id)objc_getClass("NSAlert"), sel_registerName("new")); + switch (flags & WEBVIEW_DIALOG_FLAG_ALERT_MASK) { + case WEBVIEW_DIALOG_FLAG_INFO: + objc_msgSend(a, sel_registerName("setAlertStyle:"), + NSAlertStyleInformational); + break; + case WEBVIEW_DIALOG_FLAG_WARNING: + printf("Warning\n"); + objc_msgSend(a, sel_registerName("setAlertStyle:"), NSAlertStyleWarning); + break; + case WEBVIEW_DIALOG_FLAG_ERROR: + printf("Error\n"); + objc_msgSend(a, sel_registerName("setAlertStyle:"), NSAlertStyleCritical); + break; + } + objc_msgSend(a, sel_registerName("setShowsHelp:"), 0); + objc_msgSend(a, sel_registerName("setShowsSuppressionButton:"), 0); + objc_msgSend(a, sel_registerName("setMessageText:"), get_nsstring(title)); + objc_msgSend(a, sel_registerName("setInformativeText:"), get_nsstring(arg)); + objc_msgSend(a, sel_registerName("addButtonWithTitle:"), + get_nsstring("OK")); + objc_msgSend(a, sel_registerName("runModal")); + objc_msgSend(a, sel_registerName("release")); + } +} + +static void webview_dispatch_cb(void *arg) { + struct webview_dispatch_arg *context = (struct webview_dispatch_arg *)arg; + (context->fn)(context->w, context->arg); + free(context); +} + +WEBVIEW_API void webview_dispatch(struct webview *w, webview_dispatch_fn fn, + void *arg) { + struct webview_dispatch_arg *context = (struct webview_dispatch_arg *)malloc( + sizeof(struct webview_dispatch_arg)); + context->w = w; + context->arg = arg; + context->fn = fn; + dispatch_async_f(dispatch_get_main_queue(), context, webview_dispatch_cb); +} + +WEBVIEW_API void webview_terminate(struct webview *w) { + w->priv.should_exit = 1; +} + +WEBVIEW_API void webview_exit(struct webview *w) { + id app = objc_msgSend((id)objc_getClass("NSApplication"), + sel_registerName("sharedApplication")); + objc_msgSend(app, sel_registerName("terminate:"), app); +} + +WEBVIEW_API void webview_print_log(const char *s) { printf("%s\n", s); } + +#endif /* WEBVIEW_COCOA */ + +#endif /* WEBVIEW_IMPLEMENTATION */ + +#ifdef __cplusplus +} +#endif + +#endif /* WEBVIEW_H */ diff --git a/ui/proton_test.cc b/ui/proton_test.cc new file mode 100644 index 000000000..2074cc84a --- /dev/null +++ b/ui/proton_test.cc @@ -0,0 +1,176 @@ +// +build ignore + +#include +#include +#include +#include +#include +#include +#include +#include + +#define WEBVIEW_IMPLEMENTATION +#include "proton.h" + +extern "C" void webview_dispatch_proxy(struct webview *w, void *arg) { + (*static_cast *>(arg))(w); +} + +class runner { +public: + runner(struct webview *w) : w(w) { webview_init(this->w); } + ~runner() { webview_exit(this->w); } + runner &then(std::function fn) { + auto arg = new std::pair, 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, void *> *>( + arg); + dispatch_arg->first(w); + delete dispatch_arg; + }, + reinterpret_cast(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> 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 results; + std::cout << "TEST: window size" << std::endl; + w.width = 480; + w.height = 320; + w.resizable = 1; + w.userdata = static_cast(&results); + w.external_invoke_cb = [](struct webview *w, const char *arg) { + auto *v = static_cast *>(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 results; + std::cout << "TEST: inject JS" << std::endl; + w.width = 480; + w.height = 320; + w.userdata = static_cast(&results); + w.external_invoke_cb = [](struct webview *w, const char *arg) { + auto *v = static_cast *>(w->userdata); + v->push_back(std::string(arg)); + }; + runner(&w) + .then([](struct webview *w) { + webview_eval(w, + R"(document.body.innerHTML = '
Foo
';)"); + 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 results; + std::cout << "TEST: inject CSS" << std::endl; + w.width = 480; + w.height = 320; + w.userdata = static_cast(&results); + w.external_invoke_cb = [](struct webview *w, const char *arg) { + auto *v = static_cast *>(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; +}