├── .gitignore ├── Gemfile ├── LICENCE ├── README.md ├── bin └── proton ├── build.rb ├── lib └── proton.rb ├── login.html ├── opal ├── node │ ├── buffer.rb │ ├── net.rb │ ├── readable_stream.rb │ └── writable_stream.rb ├── proton.rb ├── proton │ ├── browser_window.rb │ ├── client_request.rb │ ├── incoming_message.rb │ ├── ipc_main.rb │ ├── ipc_renderer.rb │ ├── net.rb │ ├── remote.rb │ └── web_contents.rb ├── proton_app.rb ├── proton_global.rb └── proton_process.rb └── proton.gemspec /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | Gemfile.lock 4 | 5 | *.gem 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | source "https://rubygems.org" 3 | 4 | gem "opal" 5 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 - Guillaume Hivert 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Proton — Ruby Electron — Desktop app using Ruby and Node! 2 | 3 | Ruby Electron is an experimental work. As an open source project, it should provide a full port of [Electron](https://electronjs.org), the perfect web desktop app framework. Some parts are currently working, but everything needs more work. As time is passing, more and more features will be added, and examples will be provided in the future. [They can be found here](https://github.com/ghivert/proton-sample-apps). 4 | 5 | Right now, Proton can be bundled as a gem, but is not purposefully. It is still at a really early stage, and eveything can change or break previous code at any moment. When stable, Proton will be released on RubyGems. 6 | 7 | ## How does it work ? 8 | 9 | Ruby Electron makes its magic with help of Node.js and [Opal](http://opalrb.com). Opal is a transpiler from Ruby to JavaScript. It has few limitations like non mutable string. Otherwise it provides a full transpiler. More details can be found on the page of the project. 10 | Providing Electron to Ruby users, Proton makes extensive use of Electron, by providing a Ruby-like library. 11 | 12 | ## How to compile ? 13 | 14 | ``` 15 | git clone https://github.com/ghivert/proton.git 16 | cd proton 17 | gem build proton.gemspec 18 | gem install ./proton-0.1.0.gem 19 | ``` 20 | 21 | ## How can I use it ? 22 | 23 | Once you cloned and locally installed the gem, you can simple use `proton main.rb` on a correct file (see the examples or sources). Everything is automated to give the perfect Proton experience ! 24 | 25 | ## Remarks ? 26 | ### PR ? Contributing ? 27 | PR are welcome. :) You can else mail me, or come and contribute if you like the project and want to help ! Any help is welcome, even docs ! 28 | 29 | ### Tests 30 | "Hey guy, you're not writing tests!" I know. Proton is mainly a POC as for now, and shouldn't be used into production. If the project goes on, Spectron will probably be used. 31 | 32 | ### Issues 33 | Any issues ? Let me know, I'll fix it as soon as I can ! 34 | 35 | ## Licence 36 | 37 | MIT Licence. Enjoy the work. 38 | 39 | ## Why ? 40 | 41 | Because it's both fun and useful. Electron rocks. Ruby rocks. 42 | 43 | Ruby + Electron = <3 44 | 45 | ## Support on Beerpay 46 | Hey dude! Help me out for a couple of :beers:! 47 | 48 | [![Beerpay](https://beerpay.io/ghivert/proton/badge.svg?style=beer-square)](https://beerpay.io/ghivert/proton) [![Beerpay](https://beerpay.io/ghivert/proton/make-wish.svg?style=flat-square)](https://beerpay.io/ghivert/proton?focus=wish) 49 | -------------------------------------------------------------------------------- /bin/proton: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'opal' 3 | require 'getoptlong' 4 | 5 | # Parsing class. Ease code reading. 6 | 7 | class CliArgs 8 | @help = <<-HELP 9 | proton [OPTIONS] main.rb 10 | -h, --help: Show help. 11 | -s, --start: Launch electron right after compiling! 12 | -o, --out: Specify file name of output. 13 | HELP 14 | 15 | def self.parse 16 | start = false 17 | output = nil 18 | opts = GetoptLong.new( 19 | [ '--start', '-s', GetoptLong::NO_ARGUMENT ], 20 | [ '--help', '-h', GetoptLong::NO_ARGUMENT ], 21 | [ '--out', '-o', GetoptLong::REQUIRED_ARGUMENT ] 22 | ) 23 | 24 | opts.each do |opt, arg| 25 | case opt 26 | when '--help' then puts @help; exit 0 27 | when '--start' then start = true 28 | when '--out' then output = arg 29 | end 30 | end 31 | 32 | if ARGV.length != 1 33 | puts "No file provided. (Call proton --help to see the full list of commands.)" 34 | exit 0 35 | end 36 | 37 | input = ARGV.shift 38 | output = input.gsub(/.rb/, '.js') if output.nil? 39 | 40 | { 41 | input: input, 42 | output: output, 43 | start: start 44 | } 45 | end 46 | end 47 | 48 | 49 | Opal.use_gem 'proton' 50 | Opal.append_path '.' 51 | 52 | options = CliArgs.parse 53 | 54 | requirements = [ 55 | 'var electron = require(\'electron\');', 56 | 'var {BrowserWindow, webContents, net} = electron;', 57 | 'var nodeNet = require(\'net\');' 58 | ].join "\n" 59 | 60 | IO.binwrite options[:output], requirements + Opal::Builder.build(options[:input]).to_s 61 | `electron #{options[:output]}` if options[:start] 62 | -------------------------------------------------------------------------------- /build.rb: -------------------------------------------------------------------------------- 1 | require 'opal' 2 | 3 | Opal.append_path 'lib' 4 | Opal.append_path 'test_app' 5 | File.binwrite "main.js", "var electron = require('electron');\nvar {BrowserWindow, webContents, net} = electron;\n" + Opal::Builder.build('main').to_s 6 | -------------------------------------------------------------------------------- /lib/proton.rb: -------------------------------------------------------------------------------- 1 | require 'opal' 2 | 3 | Opal.append_path File.expand_path('../../opal', __FILE__).untaint 4 | -------------------------------------------------------------------------------- /login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Ricochet Robot 6 | 7 | 8 | 9 |

Please, enter your username !

10 |
11 | 12 | 13 |
14 | 15 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /opal/node/buffer.rb: -------------------------------------------------------------------------------- 1 | module Node 2 | class Buffer 3 | attr_reader :buffer 4 | 5 | def initialize(buffer) 6 | @buffer = buffer 7 | end 8 | 9 | def to_s 10 | `#{buffer}.toString()` 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /opal/node/net.rb: -------------------------------------------------------------------------------- 1 | require 'native' 2 | require 'node/buffer' 3 | 4 | module Node 5 | class Net 6 | attr_reader :connection 7 | 8 | def initialize(connection) 9 | @connection = connection 10 | end 11 | 12 | # Events 13 | 14 | def on_data 15 | apply_to_chunk = -> (chunk) do 16 | if `typeof chunk` == 'string' 17 | yield chunk 18 | else 19 | yield Node::Buffer.new(chunk) 20 | end 21 | end 22 | `#{connection}.on('data', #{apply_to_chunk})` 23 | end 24 | 25 | # Instance Methods 26 | 27 | def write(data, encoding = 'utf-8', &callback) 28 | if callback.nil? 29 | `#{connection}.write(#{data}, #{encoding})` 30 | else 31 | `#{connection}.write(#{data}, #{encoding}, #{callback})` 32 | end 33 | end 34 | 35 | def finish 36 | `#{connection}.end()` 37 | end 38 | 39 | # Class Methods 40 | 41 | def self.connect(options={}) 42 | Net.new(`nodeNet.connect(#{options.to_n})`) 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /opal/node/readable_stream.rb: -------------------------------------------------------------------------------- 1 | module Node 2 | module ReadableStream 3 | # Events 4 | 5 | def on_close 6 | close_ = -> () { yield } 7 | `#{stream}.on('close', #{close_})` 8 | end 9 | 10 | def on_data 11 | apply_to_chunk = -> (chunk) { yield Node::Buffer.new(chunk) } 12 | `#{stream}.on('data', #{apply_to_chunk})` 13 | end 14 | 15 | def on_end 16 | end_ = -> () { yield } 17 | `#{stream}.on('end', #{end_})` 18 | end 19 | 20 | def on_error 21 | apply_to_error = -> (error) { yield error } 22 | `#{stream}.on('error', #{apply_to_error})` 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /opal/node/writable_stream.rb: -------------------------------------------------------------------------------- 1 | module Node 2 | module WritableStream 3 | # Events 4 | 5 | def on_close 6 | close_ = -> () { yield } 7 | `#{stream}.on('close', #{close_})` 8 | end 9 | 10 | def on_drain 11 | drain_ = -> () { yield } 12 | `#{stream}.on('drain', #{drain_})` 13 | end 14 | 15 | def on_error 16 | apply_to_error = -> (error) { yield error } 17 | `#{stream}.on('error', #{apply_to_error})` 18 | end 19 | 20 | def on_finish 21 | finish_ = -> () { yield } 22 | `#{stream}.on('finish', #{finish_})` 23 | end 24 | 25 | # Instance Methods 26 | 27 | def write(data, encoding = 'utf-8', &callback) 28 | if callback.nil? 29 | `#{stream}.write(#{data}, #{encoding})` 30 | else 31 | `#{stream}.write(#{data}, #{encoding}, #{callback})` 32 | end 33 | end 34 | 35 | def finish(chunk = nil, encoding = 'utf-8', &callback) 36 | if chunk.nil? 37 | if callback.nil? 38 | `#{stream}.end()` 39 | else 40 | `#{stream}.end(#{callback})` 41 | end 42 | else 43 | if callback.nil? 44 | `#{stream}.end(#{chunk}, #{encoding})` 45 | else 46 | `#{stream}.end(#{chunk}, #{encoding}, #{callback})` 47 | end 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /opal/proton.rb: -------------------------------------------------------------------------------- 1 | require 'opal' 2 | require 'proton_app' 3 | require 'proton_global' 4 | require 'proton_process' 5 | 6 | module Proton 7 | def self.app 8 | Proton::App.new 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /opal/proton/browser_window.rb: -------------------------------------------------------------------------------- 1 | require 'native' 2 | require 'proton/web_contents' 3 | 4 | class FromWindowAssignementError < Exception 5 | end 6 | 7 | 8 | module Proton 9 | class BrowserWindow 10 | class << self 11 | def from_window(window) 12 | if window.is_a? Proton::BrowserWindow 13 | alloc(window.to_js) 14 | else 15 | raise FromWindowAssignementError 16 | end 17 | end 18 | 19 | def new(options) 20 | alloc(`new BrowserWindow(#{options.to_n})`) 21 | end 22 | 23 | def alloc(var) 24 | browser_window = self.allocate 25 | browser_window.initialize var 26 | browser_window 27 | end 28 | end 29 | 30 | def initialize(window) 31 | @window = window 32 | @web_contents = WebContents.new(`#{@window}.webContents`) 33 | end 34 | 35 | # Instance Properties 36 | 37 | def web_contents 38 | @web_contents 39 | end 40 | 41 | # Events 42 | 43 | def on_closed 44 | closed_ = -> (event) { yield event } 45 | `#{@window}.on('closed', #{closed_})` 46 | end 47 | 48 | # Instance Methods 49 | 50 | def load_url(url) 51 | `#{@window}.loadURL(#{url})` 52 | end 53 | 54 | def close 55 | `#{@window}.close()` 56 | end 57 | 58 | def toggle_dev_tools 59 | `#{@window}.toggleDevTools()` 60 | end 61 | 62 | def to_js 63 | @window 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /opal/proton/client_request.rb: -------------------------------------------------------------------------------- 1 | require 'proton/incoming_message' 2 | require 'node/writable_stream' 3 | 4 | module Proton 5 | class ClientRequest 6 | include WritableStream 7 | attr_reader :client_request 8 | alias_method :client_request, :stream 9 | 10 | def initialize(options) 11 | @client_request = `net.request(#{options.to_n})` 12 | end 13 | 14 | # Events 15 | 16 | def on_response 17 | apply_to_message = -> (incoming_message) do 18 | yield Proton::IncomingMessage.new(incoming_message) 19 | end 20 | `#{client_request}.on('response', #{apply_to_message})` 21 | end 22 | 23 | def on_login 24 | apply_to_auth_info = -> (auth_info, callback) do 25 | yield Proton::AuthInfo.new(auth_info), callback 26 | end 27 | `#{client_request}.on('login', #{apply_to_auth_info})` 28 | end 29 | 30 | def on_abort 31 | abort_ = -> () { yield } 32 | `#{client_request}.on('login', #{abort_})` 33 | end 34 | 35 | # Instance Properties 36 | 37 | def chunked_encoding 38 | `#{client_request}.chunkedEncoding` 39 | end 40 | 41 | def chunked_encoding=(obj) 42 | `#{client_request}.chunkedEncoding = #{obj}` 43 | end 44 | 45 | # Instance Methods 46 | 47 | def set_header(name, value) 48 | `#{client_request}.setHeader(#{name}, #{value})` 49 | end 50 | 51 | def get_header(name) 52 | `#{client_request}.getHeader(#{name})` 53 | end 54 | 55 | def remove_header(name) 56 | `#{client_request}.removeHeader(#{name})` 57 | end 58 | 59 | def abort 60 | `#{client_request}.abort()` 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /opal/proton/incoming_message.rb: -------------------------------------------------------------------------------- 1 | require 'node/buffer' 2 | require 'node/readable_stream' 3 | 4 | module Proton 5 | class IncomingMessage 6 | attr_reader :incoming_message 7 | alias_method :incoming_message, :stream 8 | include Node::ReadableStream 9 | 10 | def initialize(incoming_message) 11 | @incoming_message = incoming_message 12 | end 13 | 14 | # Events 15 | 16 | def on_aborted 17 | aborted_ = -> () { yield } 18 | `#{incoming_message}.on('aborted', #{aborted_})` 19 | end 20 | 21 | # Instance Properties 22 | 23 | def status_code 24 | `#{incoming_message}.statusCode` 25 | end 26 | 27 | def status_message 28 | `#{incoming_message}.statusMessage` 29 | end 30 | 31 | def headers 32 | `#{incoming_message}.headers` 33 | end 34 | 35 | def http_version 36 | `#{incoming_message}.httpVersion` 37 | end 38 | 39 | def http_version_major 40 | `#{incoming_message}.httpVersionMajor` 41 | end 42 | 43 | def http_version_minor 44 | `#{incoming_message}.httpVersionMinor` 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /opal/proton/ipc_main.rb: -------------------------------------------------------------------------------- 1 | module Proton 2 | class IpcMain 3 | @ipc = `electron.ipcMain` 4 | class << self 5 | # Events 6 | 7 | def on(channel, &callback) 8 | `#{@ipc}.on(#{channel}, #{callback})` 9 | end 10 | 11 | def once(channel, &callback) 12 | `#{@ipc}.once(#{channel}, #{callback})` 13 | end 14 | 15 | def remove_listener(channel, &callback) 16 | `#{@ipc}.removeListener(#{channel}, #{callback})` 17 | end 18 | 19 | def remove_all_listeners(*channels) 20 | `#{@ipc}.removeAllListeners(#{channel})` 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /opal/proton/ipc_renderer.rb: -------------------------------------------------------------------------------- 1 | module Proton 2 | class IpcRenderer 3 | @ipc = `electron.ipcRenderer` 4 | class << self 5 | # Instance Methods 6 | 7 | def send(channel) 8 | `#{@ipc}.send(#{channel})` 9 | end 10 | 11 | def on(event, &callback) 12 | `#{@ipc}.on(#{event}, #{callback})` 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /opal/proton/net.rb: -------------------------------------------------------------------------------- 1 | require 'proton/client_request' 2 | 3 | module Proton 4 | class Net 5 | class << self 6 | # Class Methods 7 | 8 | def request(options) 9 | Proton::ClientRequest.new(options) 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /opal/proton/remote.rb: -------------------------------------------------------------------------------- 1 | require 'proton/web_contents' 2 | 3 | module Proton 4 | class Remote 5 | @remote = `electron.remote` 6 | # Class Methods 7 | 8 | def self.access(global_value) 9 | access = [] 10 | if global_value[0] == '$' 11 | access << :gvars 12 | global_value = global_value.delete '$' 13 | access << global_value.to_sym 14 | else 15 | values = global_value.split('.') 16 | values.each { |val| access << val.to_sym } 17 | end 18 | opal = `#{@remote}.require("./main.js").Opal` 19 | access.each { |name| opal = opal.JS[name]} 20 | opal 21 | end 22 | 23 | def self.get_current_web_contents 24 | WebContents.new `#{@remote}.getCurrentWebContents` 25 | end 26 | 27 | def self.ready! 28 | `#{@remote}.getGlobal("ready").ready = true` 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /opal/proton/web_contents.rb: -------------------------------------------------------------------------------- 1 | require 'native' 2 | 3 | module Proton 4 | class BrowserWindow 5 | class WebContents 6 | attr_reader :content 7 | # Class Methods 8 | 9 | class << self 10 | def get_all_web_contents 11 | arr, wcs = [], `webContents.getAllWebContents()` 12 | (0...`wcs.length`).each do |i| 13 | arr << WebContents.new(wcs.JS[i]) 14 | end 15 | arr 16 | end 17 | 18 | def get_focused_web_contents 19 | wc = `webContents.getFocusedWebContents()` 20 | if wc 21 | WebContents.new(wc) 22 | else 23 | nil 24 | end 25 | end 26 | 27 | def from_id(id) 28 | wc = `webContents.fromId(id)` 29 | if wc 30 | WebContents.new(wc) 31 | else 32 | nil 33 | end 34 | end 35 | end 36 | 37 | # Instance Properties 38 | 39 | def id 40 | `#{content}.id` 41 | end 42 | 43 | def session 44 | `#{content}.session` 45 | end 46 | 47 | def host_web_contents 48 | `#{content}.hostWebContents` 49 | end 50 | 51 | def dev_tools_web_contents 52 | `#{content}.devToolsWebContents` 53 | end 54 | 55 | def debugger 56 | `#{content}.debugger` 57 | end 58 | 59 | # Instance Methods 60 | 61 | def initialize(content) 62 | @content = content 63 | end 64 | 65 | def send(chan, *rest) 66 | if rest.size == 0 67 | `#{content}.send(#{chan})` 68 | elsif rest.size == 1 69 | `#{content}.send(#{chan}, #{rest.shift})` 70 | else 71 | `#{content}.send(#{chan}, #{rest})` 72 | end 73 | end 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /opal/proton_app.rb: -------------------------------------------------------------------------------- 1 | module Proton 2 | class App 3 | def initialize 4 | @app = `electron.app` 5 | end 6 | 7 | def on(event, &callback) 8 | `#{@app}.on(#{event}, #{callback})` 9 | end 10 | 11 | def on_window_all_closed 12 | closed_ = -> () { yield } 13 | `#{@app}.on('window-all-closed', #{closed_})` 14 | end 15 | 16 | def on_ready 17 | ready_ = -> (launch_info) { yield launch_info } 18 | `#{@app}.on('ready', #{ready_})` 19 | end 20 | 21 | def quit 22 | `#{@app}.quit()` 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /opal/proton_global.rb: -------------------------------------------------------------------------------- 1 | require 'native' 2 | 3 | class MainWindowAssignementError < Exception 4 | end 5 | 6 | class Global 7 | class << self 8 | def main_window 9 | Proton::BrowserWindow.from_window `global.mainWindow` 10 | end 11 | 12 | def main_window=(obj) 13 | unless obj 14 | `global.mainWindow = #{obj}` 15 | else 16 | if obj.is_a? Proton::BrowserWindow 17 | `global.mainWindow = #{obj.to_js}` 18 | else 19 | raise MainWindowAssignementError 20 | end 21 | end 22 | end 23 | 24 | def ready=(obj) 25 | `global.ready = #{obj.to_n}` 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /opal/proton_process.rb: -------------------------------------------------------------------------------- 1 | class Process 2 | class << self 3 | def on(event, &callback) 4 | `process.on(#{event}, #{callback})` 5 | end 6 | 7 | def platform 8 | `process.platform` 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /proton.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = 'proton' 3 | s.version = '0.1.0' 4 | s.date = '2017-03-18' 5 | s.summary = 'Ruby Electron !' 6 | s.description = 'Wrapper for Electron in Ruby.' 7 | s.authors = ['Guillaume Hivert'] 8 | s.email = 'guillaume.hivert@outlook.com' 9 | s.license = 'MIT' 10 | s.homepage = 'https://github.com/ghivert/proton' 11 | 12 | s.files = `git ls-files`.split("\n") 13 | s.require_paths = ['opal'] 14 | s.executables = ['proton'] 15 | 16 | s.add_dependency 'opal', '~> 0.10.3' 17 | end 18 | --------------------------------------------------------------------------------