This commit is contained in:
pjht 2023-10-06 17:02:48 -05:00
parent 0224abf215
commit 1b1598e668
Signed by: pjht
GPG Key ID: CA239FC6934E6F3A
13 changed files with 466 additions and 431 deletions

12
Gemfile
View File

@ -1,6 +1,8 @@
source "https://rubygems.org" # frozen_string_literal: true
gem "pry" source 'https://rubygems.org'
gem "web_gui", :path => "."
gem "dry-struct" gem 'dry-events'
gem "dry-events" gem 'dry-struct'
gem 'pry'
gem 'web_gui', path: '.'

View File

@ -1,5 +1,7 @@
require "bundler/gem_tasks" # frozen_string_literal: true
task :default => :build
require 'bundler/gem_tasks'
task default: :build
task :geminabox do task :geminabox do
`gem inabox -o pkg/web_gui-*.gem` `gem inabox -o pkg/web_gui-*.gem`
`rm pkg/web_gui-*.gem` `rm pkg/web_gui-*.gem`

View File

@ -1,11 +1,12 @@
#!/usr/bin/env ruby #!/usr/bin/env ruby
# frozen_string_literal: true
require "bundler/setup" require 'bundler/setup'
require "web_gui" require 'web_gui'
# You can add fixtures and/or initialization code here to make experimenting # You can add fixtures and/or initialization code here to make experimenting
# with your gem easier. You can also use a different console, if you like. # with your gem easier. You can also use a different console, if you like.
# (If you use this, don't forget to add pry to your Gemfile!) # (If you use this, don't forget to add pry to your Gemfile!)
require "pry" require 'pry'
Pry.start Pry.start

40
calc.rb
View File

@ -1,28 +1,30 @@
require "bundler/setup" # frozen_string_literal: true
require 'bundler/setup'
Bundler.setup Bundler.setup
require "web_gui" require 'web_gui'
$op=nil $op = nil
$num1=nil $num1 = nil
$num2=nil $num2 = nil
app=WebGui::App.new("Calculator") { app = WebGui::App.new('Calculator') do
opthash={:add=>"Add",:sub=>"Subtract",:mult=>"Multiply",:div=>"Divide"} opthash = { add: 'Add', sub: 'Subtract', mult: 'Multiply', div: 'Divide' }
add_element(WebGui::TextField.new {|val| $num1=val.to_f}) add_element(WebGui::TextField.new { |val| $num1 = val.to_f })
add_element(WebGui::Menu.new(opthash) {|val| $op=val}) add_element(WebGui::Menu.new(opthash) { |val| $op = val })
add_element(WebGui::TextField.new {|val| $num2=val.to_f}) add_element(WebGui::TextField.new { |val| $num2 = val.to_f })
res=nil res = nil
add_element(WebGui::ActionButton.new("Calculate") { add_element(WebGui::ActionButton.new('Calculate') do
case $op case $op
when :add when :add
result=$num1+$num2 result = $num1 + $num2
when :sub when :sub
result=$num1-$num2 result = $num1 - $num2
when :mult when :mult
result=$num1*$num2 result = $num1 * $num2
when :div when :div
result=$num1.to_f/$num2 result = $num1.to_f / $num2
end end
res.settext("Result:#{result}") res.settext("Result:#{result}")
}) end)
res=add_element(WebGui::Text.new("Result:")) res = add_element(WebGui::Text.new('Result:'))
} end
app.run app.run

View File

@ -1,69 +1,71 @@
# frozen_string_literal: true
class Object class Object
def descendants def descendants
ObjectSpace.each_object(::Class).select {|klass| klass < self } ObjectSpace.each_object(::Class).select { |klass| klass < self }
end end
end end
module WebGui; end module WebGui; end
require "dry-struct" require 'dry-struct'
module Types module Types
include Dry::Types.module include Dry::Types.module
end end
require "dry-events" require 'dry-events'
require_relative "web_gui/element" require_relative 'web_gui/element'
require_relative "web_gui/event_manager" require_relative 'web_gui/event_manager'
require_relative "web_gui/app" require_relative 'web_gui/app'
require_relative "web_gui/window" require_relative 'web_gui/window'
require_relative "web_gui/serv" require_relative 'web_gui/serv'
require_relative "web_gui/wsserv" require_relative 'web_gui/wsserv'
module WebGui module WebGui
class Text < Element class Text < Element
attribute :text, Types::Coercible::String attribute :text, Types::Coercible::String
def render() def render
return "<p id=#{id}>#{text}</p>" "<p id=#{id}>#{text}</p>"
end end
def settext(text) def settext(text)
$app.update("paragraph",id,text) $app.update('paragraph', id, text)
end end
end end
class Link < Element class Link < Element
attribute :page, Types::Symbol attribute :page, Types::Symbol
attribute :text, Types::Coercible::String.optional attribute :text, Types::Coercible::String.optional
def render() def render
if text if text
return "<a href=\"#{page}\">#{text}</a>" "<a href=\"#{page}\">#{text}</a>"
else else
return "<a href=\"#{page}\">#{page.to_s}</a>" "<a href=\"#{page}\">#{page}</a>"
end end
end end
end end
class Button < Link class Button < Link
@css="a.button{-webkit-appearance: button;-moz-appearance: button;appearance: button;text-decoration: none;color: initial;padding: 0px 10px;}" @css = 'a.button{-webkit-appearance: button;-moz-appearance: button;appearance: button;text-decoration: none;color: initial;padding: 0px 10px;}'
def render() def render
return "<a href=\"#{page}\" class=\"button\">#{text}</a>" "<a href=\"#{page}\" class=\"button\">#{text}</a>"
end end
def self.css() class << self
return @css attr_reader :css
end end
end end
class Image < Element class Image < Element
attribute :name, Types::Coercible::String attribute :name, Types::Coercible::String
def render() def render
return "<img src=\"#{name}.jpg\"></img>" "<img src=\"#{name}.jpg\"></img>"
end end
end end
class Video < Element class Video < Element
attribute :name, Types::Coercible::String attribute :name, Types::Coercible::String
def render() def render
return "<video controls width=320 height=300><source src=\"#{name}.webm\" type=\"video/webm\"><source src=\"#{name}.mp4\" type=\"video/mp4\"></video>" "<video controls width=320 height=300><source src=\"#{name}.webm\" type=\"video/webm\"><source src=\"#{name}.mp4\" type=\"video/mp4\"></video>"
end end
end end
@ -71,53 +73,51 @@ module WebGui
attribute :text, Types::Coercible::String attribute :text, Types::Coercible::String
def initialize(opthash, &block) def initialize(opthash, &block)
super(opthash) super(opthash)
@block=block @block = block
end end
def render() def render
return "<button id=#{id}>#{text}</button>" "<button id=#{id}>#{text}</button>"
end end
def on_button_pushed(event) def on_button_pushed(event)
@block.call if event[:id]==id @block.call if event[:id] == id
end end
end end
class Menu < Element class Menu < Element
attribute :opts, Types::Hash attribute :opts, Types::Hash
def initialize(opthash, &block) def initialize(opthash, &block)
super(opthash) super(opthash)
@block=block @block = block
end end
def render() def render
html="<select id=#{id}>" html = "<select id=#{id}>"
opts.each do |val,text| opts.each do |val, text|
html+="<option value=\"#{val.to_s}\">#{text}</option>" html += "<option value=\"#{val}\">#{text}</option>"
end end
html+="</select>" html += '</select>'
return html html
end end
def on_menu_updated(event) def on_menu_updated(event)
@block.call(event[:val]) if event[:id]==id @block.call(event[:val]) if event[:id] == id
end end
end end
class TextField < Element class TextField < Element
def initialize(opthash, &block) def initialize(opthash, &block)
super(opthash) super(opthash)
@block=block @block = block
end end
def render() def render
html="<input id=#{id}>" html = "<input id=#{id}>"
end end
def on_textfield_updated(event) def on_textfield_updated(event)
@block.call(event[:val]) if event[:id]==id @block.call(event[:val]) if event[:id] == id
end end
end end
@ -125,40 +125,39 @@ module WebGui
attribute :buttons, Types::Coercible::Hash attribute :buttons, Types::Coercible::Hash
def initialize(opthash, &block) def initialize(opthash, &block)
super(opthash) super(opthash)
@block=block @block = block
end end
def render() def render
html="" html = ''
buttons.each do |value,text| buttons.each do |value, text|
html+="<input type=\"radio\" id=#{id} name=\"#{id}\" value=\"#{value.to_s}\">#{text}<br>" html += "<input type=\"radio\" id=#{id} name=\"#{id}\" value=\"#{value}\">#{text}<br>"
end end
return html html
end end
def on_radiobutton_updated(event) def on_radiobutton_updated(event)
@block.call(event[:val]) if event[:id]==id @block.call(event[:val]) if event[:id] == id
end end
end end
class CheckBox < Element class CheckBox < Element
attribute :boxes, Types::Coercible::Hash attribute :boxes, Types::Coercible::Hash
def initialize(opthash, &block) def initialize(opthash, &block)
super(opthash) super(opthash)
@block=block @block = block
end end
def render() def render
html="" html = ''
boxes.each do |value,text| boxes.each do |value, text|
html+="<input type=\"checkbox\" id=#{id} name=\"#{id}\" value=\"#{value.to_s}\">#{text}<br>" html += "<input type=\"checkbox\" id=#{id} name=\"#{id}\" value=\"#{value}\">#{text}<br>"
end end
return html html
end end
def on_checkbox_updated(event) def on_checkbox_updated(event)
@block.call(event[:val]) if event[:id]==id @block.call(event[:val]) if event[:id] == id
end end
end end
end end

View File

@ -1,110 +1,110 @@
$forcenochrome=false # frozen_string_literal: true
$stdout.sync=true
$eventmanager=EventManager.new $forcenochrome = false
class WebGui::App $stdout.sync = true
attr_reader :windows $eventmanager = EventManager.new
def initialize(title,&block) module WebGui
$app=self class App
if ARGV.length>0 attr_reader :windows
if ARGV[0]=="--forcenochrome"
$forcenochrome=true def initialize(title, &block)
$app = self
$forcenochrome = true if ARGV.length.positive? && (ARGV[0] == '--forcenochrome')
@windows = {}
@windows[:main] = WebGui::Window.new(name: :main, title: title)
@windows[:main].instance_eval(&block)
end
def render_document(wname)
window = windows[wname]
html = window.render
css = window.render_css
<<~ENDDOC
<!DOCTYPE html>
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src=\"main.js\"></script>
<style>
#{css}
</style>
<title>#{window.title}</title>
</head>
<body>
#{html}
</body>
</html>
ENDDOC
end
def add_window(wname, title, &block)
@windows[wname] = WebGui::Window.new(name: wname, title: title)
@windows[wname].instance_eval(&block)
end
def run
servthread = Thread.new do
WebGui::Server.server(self)
end
startwsserv
if File.exist?("/Applications/Google\ Chrome.app") && !$forcenochrome
`"/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome" --app="http://localhost:2000"`
else
puts 'Chrome is not on your system.'
puts 'Please install Chrome to use this framework properly.'
puts 'If Chrome is installed, please make sure it is called Google Chrome.app and is in the root Applications folder.'
puts 'The app will open in your default browser as a regular webpage instead.'
sleep(5)
`open http://localhost:2000`
servthread.join
end end
end end
@windows={}
@windows[:main]=WebGui::Window.new(name: :main,title: title)
@windows[:main].instance_eval(&block)
end
def render_document(wname) def startwsserv
@serv = WebGui::WebSocketServer.new
window=windows[wname] Thread.new(self, @serv) do |parent, server|
html=window.render server.accept
css=window.render_css loop do
doc=<<-ENDDOC message = server.recv
<!DOCTYPE html> if message == false
<html> server.accept
<head> next
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script> end
<script src=\"main.js\"></script> parent.handlemessage(message)
<style>
#{css}
</style>
<title>#{window.title}</title>
</head>
<body>
#{html}
</body>
</html>
ENDDOC
return doc
end
def add_window(wname,title,&block)
@windows[wname]=WebGui::Window.new(name: wname,title: title)
@windows[wname].instance_eval(&block)
end
def run()
servthread=Thread.new do
WebGui::Server.server(self)
end
startwsserv()
if File.exists? "/Applications/Google\ Chrome.app" and !$forcenochrome
`"/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome" --app="http://localhost:2000"`
else
puts "Chrome is not on your system."
puts "Please install Chrome to use this framework properly."
puts "If Chrome is installed, please make sure it is called Google Chrome.app and is in the root Applications folder."
puts "The app will open in your default browser as a regular webpage instead."
sleep(5)
`open http://localhost:2000`
servthread.join
end
end
def startwsserv()
@serv=WebGui::WebSocketServer.new
Thread.new(self,@serv) do |parent,server|
server.accept
while true
message=server.recv
if message==false
server.accept
next
end end
parent.handlemessage(message)
end end
end end
end
def handlemessage(message) def handlemessage(message)
puts "Got message #{message}" puts "Got message #{message}"
part1,val=message.split("=") part1, val = message.split('=')
type=part1.match /[a-zA-Z]+/.to_s type = part1.match(/[a-zA-Z]+/.to_s)
type=type[0] type = type[0]
id=part1.match /\d+/ id = part1.match(/\d+/)
id=id[0].to_i id = id[0].to_i
if val if val
case type case type
when "menu" when 'menu'
val=val.to_sym val = val.to_sym
when "radiobutton" when 'radiobutton'
val=val.to_sym val = val.to_sym
when "checkbox" when 'checkbox'
vals=val.split("&") vals = val.split('&')
temp=[] temp = []
vals.each do |val| vals.each do |val|
temp.push(val.to_sym) temp.push(val.to_sym)
end
val = temp
end end
val=temp $eventmanager.publish("#{type}.updated", val: val, id: id)
else
$eventmanager.publish("#{type}.pushed", id: id)
end end
$eventmanager.publish("#{type}.updated",val: val,id: id)
else
$eventmanager.publish("#{type}.pushed",id: id)
end end
end
def update(type,id,val) def update(type, id, val)
puts "Sent message #{type}#{id}=#{val}" puts "Sent message #{type}#{id}=#{val}"
@serv.send("#{type}#{id}=#{val}") @serv.send("#{type}#{id}=#{val}")
end
end end
end end

View File

@ -1,24 +1,31 @@
$id=0 # frozen_string_literal: true
$idtoel={}
def get_id() $id = 0
id=$id $idtoel = {}
$id+=1 def get_id
return id id = $id
$id += 1
id
end end
class WebGui::Element < Dry::Struct
constructor_type :strict_with_defaults module WebGui
attribute :id, Types::Coercible::Int.default { get_id() } class Element < Dry::Struct
def initialize(opthash=nil) constructor_type :strict_with_defaults
if opthash attribute :id, Types::Coercible::Int.default { get_id }
super(opthash) def initialize(opthash = nil)
@block=nil if opthash
super(opthash)
@block = nil
end
$idtoel[id] = self
end
def render
''
end
def self.css
@ccs
end end
$idtoel[id]=self
end
def render()
return ""
end
def self.css()
return @ccs
end end
end end

View File

@ -1,8 +1,10 @@
# frozen_string_literal: true
class EventManager class EventManager
include Dry::Events::Publisher[:event_manager] include Dry::Events::Publisher[:event_manager]
register_event("button.pushed") register_event('button.pushed')
register_event("menu.updated") register_event('menu.updated')
register_event("textfield.updated") register_event('textfield.updated')
register_event("checkbox.updated") register_event('checkbox.updated')
register_event("radiobutton.updated") register_event('radiobutton.updated')
end end

View File

@ -1,136 +1,144 @@
require "socket" # frozen_string_literal: true
class WebGui::Server
def self.sfile(file,headers,type,client,url)
total=file.length
range=headers["Range"]
if range!=nil
positions=range.split("=")[1].split("-")
start=positions[0].to_i(10)
m_end=positions[1] ? positions[1].to_i(10) : total - 1;
chunksize=(m_end-start)+1
chunk=file[start, m_end+1]
if type=="mp4"
r_headers={"Content-Range"=>"bytes #{start}-#{m_end}/#{total}","Accept-Ranges"=>"bytes","Content-Length"=>chunksize,"Content-Type"=>"video/mp4"}
elsif type=="webm"
r_headers={"Content-Range"=>"bytes #{start}-#{m_end}/#{total}","Accept-Ranges"=>"bytes","Content-Length"=>chunksize,"Content-Type"=>"video/webm"}
elsif type=="mpeg"
r_headers={"Content-Range"=>"bytes #{start}-#{m_end}/#{total}","Accept-Ranges"=>"bytes","Content-Length"=>chunksize,"Content-Type"=>"audio/mpeg"}
elsif type=="ogg"
r_headers={"Content-Range"=>"bytes #{start}-#{m_end}/#{total}","Accept-Ranges"=>"bytes","Content-Length"=>chunksize,"Content-Type"=>"audio/ogg"}
end
header=""
r_headers.each do |key,value|
header+="#{key}: #{value}\n"
end
client.puts "HTTP/1.1 206 Partial Content"
client.print "#{header}"
client.print "\n"
client.print "#{chunk}"
else
if type=="mp4"
r_headers={"Content-Type"=>"video/mp4"}
elsif type=="webm"
r_headers={"Content-Type"=>"video/webm"}
elsif type=="mpeg"
r_headers={"Content-Type"=>"audio/mpeg"}
elsif type=="ogg"
r_headers={"Content-Type"=>"audio/ogg"}
end
header=""
r_headers.each do |key,value|
header+="#{key}: #{value}\n"
end
client.puts "HTTP/1.1 200 OK"
client.print "#{header}"
client.print "\n"
client.print "#{file}"
end
client.close
end
def self.server(app) require 'socket'
Thread::abort_on_exception=true module WebGui
server = TCPServer.new(2000) class Server
$debug = true def self.sfile(file, headers, type, client, _url)
igfiles=["favicon.ico"] total = file.length
loop do range = headers['Range']
if !range.nil?
positions = range.split('=')[1].split('-')
start = positions[0].to_i(10)
m_end = positions[1] ? positions[1].to_i(10) : total - 1
chunksize = (m_end - start) + 1
chunk = file[start, m_end + 1]
case type
when 'mp4'
r_headers = { 'Content-Range' => "bytes #{start}-#{m_end}/#{total}", 'Accept-Ranges' => 'bytes',
'Content-Length' => chunksize, 'Content-Type' => 'video/mp4' }
when 'webm'
r_headers = { 'Content-Range' => "bytes #{start}-#{m_end}/#{total}", 'Accept-Ranges' => 'bytes',
'Content-Length' => chunksize, 'Content-Type' => 'video/webm' }
when 'mpeg'
r_headers = { 'Content-Range' => "bytes #{start}-#{m_end}/#{total}", 'Accept-Ranges' => 'bytes',
'Content-Length' => chunksize, 'Content-Type' => 'audio/mpeg' }
when 'ogg'
r_headers = { 'Content-Range' => "bytes #{start}-#{m_end}/#{total}", 'Accept-Ranges' => 'bytes',
'Content-Length' => chunksize, 'Content-Type' => 'audio/ogg' }
end
header = ''
r_headers.each do |key, value|
header += "#{key}: #{value}\n"
end
client.puts 'HTTP/1.1 206 Partial Content'
client.print header.to_s
client.print "\n"
client.print chunk.to_s
else
case type
when 'mp4'
r_headers = { 'Content-Type' => 'video/mp4' }
when 'webm'
r_headers = { 'Content-Type' => 'video/webm' }
when 'mpeg'
r_headers = { 'Content-Type' => 'audio/mpeg' }
when 'ogg'
r_headers = { 'Content-Type' => 'audio/ogg' }
end
header = ''
r_headers.each do |key, value|
header += "#{key}: #{value}\n"
end
client.puts 'HTTP/1.1 200 OK'
client.print header.to_s
client.print "\n"
client.print file.to_s
end
client.close
end
def self.server(app)
Thread.abort_on_exception = true
server = TCPServer.new(2000)
$debug = true
igfiles = ['favicon.ico']
loop do
Thread.start(server.accept) do |client| Thread.start(server.accept) do |client|
begin begin
lines=[] lines = []
line=client.gets line = client.gets
while line != "\r\n" while line != "\r\n"
if line==nil if line.nil?
lines=[""] lines = ['']
runreq=false runreq = false
break break
end
lines << line
line = client.gets
end end
lines<<line i = 0
line=client.gets lines.each do |value|
end lines[i] = value.chomp
i=0 i += 1
lines.each do |value| end
lines[i]=value.chomp temp = lines.shift
i=i+1 method = temp.split(' ')[0]
end url = temp.split(' ')[1]
temp=lines.shift headers = {}
method=temp.split(" ")[0] lines.each do |value|
url=temp.split(" ")[1] temp = value.split(': ')
headers={} headers[temp[0]] = temp[1]
lines.each do |value| end
temp=value.split(": ") wname = url.gsub('/', '')
headers[temp[0]]=temp[1] if igfiles.include? wname
end client.puts 'HTTP/1.1 404 Not Found'
wname=url.gsub("/","") client.puts 'Content-Type:text/html'
if igfiles.include? wname client.puts
client.puts "HTTP/1.1 404 Not Found" client.print ''
client.puts "Content-Type:text/html" elsif wname.include? '.jpg'
client.puts img = File.read(wname)
client.print "" client.puts 'HTTP/1.1 200 OK'
else client.puts 'Content-Type:image/jpeg'
if wname.include? ".jpg"
img=File.read(wname)
client.puts "HTTP/1.1 200 OK"
client.puts "Content-Type:image/jpeg"
client.puts client.puts
client.print img client.print img
elsif wname.include? ".mp4" elsif wname.include? '.mp4'
sfile(File.open(wname, "rb") {|io| io.read},headers,"mp4",client,url) sfile(File.open(wname, 'rb', &:read), headers, 'mp4', client, url)
elsif wname.include? ".webm" elsif wname.include? '.webm'
sfile(File.open(wname, "rb") {|io| io.read},headers,"webm",client,url) sfile(File.open(wname, 'rb', &:read), headers, 'webm', client, url)
elsif wname.include? ".js" elsif wname.include? '.js'
if wname=="main.js" if wname == 'main.js'
jspath=`gem which web_gui`.split("/") jspath = `gem which web_gui`.split('/')
jspath.pop jspath.pop
jspath.push "main.js" jspath.push 'main.js'
jspath=jspath.join("/") jspath = jspath.join('/')
script=File.read(jspath) script = File.read(jspath)
else else
script=File.read(wname) script = File.read(wname)
end end
client.puts "HTTP/1.1 200 OK" client.puts 'HTTP/1.1 200 OK'
client.puts "Content-Type:application/javascript" client.puts 'Content-Type:application/javascript'
client.puts client.puts
client.print script client.print script
else else
wname="main" if wname=="" wname = 'main' if wname == ''
wname=wname.to_sym wname = wname.to_sym
doc=app.render_document(wname) doc = app.render_document(wname)
client.puts "HTTP/1.1 200 OK" client.puts 'HTTP/1.1 200 OK'
client.puts "Content-Type:text/html" client.puts 'Content-Type:text/html'
client.puts client.puts
client.print doc client.print doc
end end
client.close
rescue StandardError => e
puts e
puts e.backtrace
client.puts 'HTTP/1.1 500 Internal Server Error'
client.puts 'Content-Type:text/plain'
client.puts
client.puts "Server error: #{e}"
client.print e.backtrace.join("\n")
client.close
end end
client.close
rescue StandardError=>e
puts e
puts e.backtrace
client.puts "HTTP/1.1 500 Internal Server Error"
client.puts "Content-Type:text/plain"
client.puts
client.puts "Server error: #{e}"
client.print e.backtrace.join("\n")
client.close
end end
end end
end end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
module WebGui module WebGui
VERSION = "1.0.0" VERSION = '1.0.0'
end end

View File

@ -1,34 +1,37 @@
class WebGui::Window < Dry::Struct # frozen_string_literal: true
attribute :title, Types::Coercible::String.default("")
attribute :name, Types::Coercible::String
def initialize(opthash)
super(opthash)
@elements=[]
end
def add_element(element) module WebGui
raise ArgumentError, "The element must be of type Element." unless element.is_a? WebGui::Element class Window < Dry::Struct
@elements.push element attribute :title, Types::Coercible::String.default('')
$eventmanager.subscribe(element) attribute :name, Types::Coercible::String
return element def initialize(opthash)
end super(opthash)
@elements = []
def render()
html=""
@elements.each do |el|
html+="<p>#{el.render()}</p>"
end end
return html
end
def render_css() def add_element(element)
css="" raise ArgumentError, 'The element must be of type Element.' unless element.is_a? WebGui::Element
WebGui::Element.descendants.each do |el|
elcss=el.css @elements.push element
if elcss!=nil $eventmanager.subscribe(element)
css+=elcss element
end
def render
html = ''
@elements.each do |el|
html += "<p>#{el.render}</p>"
end end
html
end
def render_css
css = ''
WebGui::Element.descendants.each do |el|
elcss = el.css
css += elcss unless elcss.nil?
end
css
end end
return css
end end
end end

View File

@ -1,76 +1,82 @@
# frozen_string_literal: true
require 'socket' # Provides TCPServer and TCPSocket classes require 'socket' # Provides TCPServer and TCPSocket classes
require 'digest/sha1' require 'digest/sha1'
class WebGui::WebSocketServer module WebGui
def initialize(host="localhost",port=2345) class WebSocketServer
@server=TCPServer.new(host,port) def initialize(host = 'localhost', port = 2345)
end @server = TCPServer.new(host, port)
def accept()
# Wait for a connection
@socket = @server.accept
# Read the HTTP request. We know it's finished when we see a line with nothing but \r\n
http_request = ""
while (line = @socket.gets) && (line != "\r\n")
http_request += line
end end
# Grab the security key from the headers. If one isn't present, close the connection. def accept
if matches = http_request.match(/^Sec-WebSocket-Key: (\S+)/) # Wait for a connection
websocket_key = matches[1] @socket = @server.accept
else
socket.close # Read the HTTP request. We know it's finished when we see a line with nothing but \r\n
return http_request = ''
while (line = @socket.gets) && (line != "\r\n")
http_request += line
end
# Grab the security key from the headers. If one isn't present, close the connection.
if matches = http_request.match(/^Sec-WebSocket-Key: (\S+)/)
websocket_key = matches[1]
else
socket.close
return
end
response_key = Digest::SHA1.base64digest([websocket_key, '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'].join)
@socket.write <<~EOS
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: #{response_key}
#{' '}
EOS
end end
response_key = Digest::SHA1.base64digest([websocket_key, "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"].join)
@socket.write <<-eos
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: #{response_key}
eos def recv
end first_byte = @socket.getbyte
fin = first_byte & 0b10000000
opcode = first_byte & 0b00001111
fin = fin >> 7
fin = (fin ? true : false)
raise "We don't support continuations" unless fin
def recv() unless (opcode == 1) || (opcode == 8)
first_byte = @socket.getbyte raise 'We only support text data and close frame'
fin = first_byte & 0b10000000 send('', 3)
opcode = first_byte & 0b00001111 end
fin = fin >> 7 case opcode
fin = (fin ? true : false) when 1
raise "We don't support continuations" unless fin second_byte = @socket.getbyte
unless opcode == 1 or opcode == 8 is_masked = second_byte & 0b10000000
raise "We only support text data and close frame" payload_size = second_byte & 0b01111111
send("",3)
raise 'All incoming frames should be masked according to the websocket spec' unless is_masked
raise 'We only support payloads < 126 bytes in length' unless payload_size < 126
mask = 4.times.map { @socket.getbyte }
data = payload_size.times.map { @socket.getbyte }
unmasked_data = data.each_with_index.map { |byte, i| byte ^ mask[i % 4] }
unmasked_data.pack('C*').force_encoding('utf-8')
when 8
send('', 0)
false
end
end end
if opcode == 1
second_byte = @socket.getbyte
is_masked = second_byte & 0b10000000
payload_size = second_byte & 0b01111111
raise "All incoming frames should be masked according to the websocket spec" unless is_masked def send(message, closetype = false)
raise "We only support payloads < 126 bytes in length" unless payload_size < 126 if closetype
mask = 4.times.map {@socket.getbyte} output = [0x88, 2, 3, 0xe8 + closetype]
data = payload_size.times.map {@socket.getbyte} @socket.write output.pack('C*')
unmasked_data = data.each_with_index.map { |byte, i| byte ^ mask[i % 4] } @socket.close
string=unmasked_data.pack('C*').force_encoding('utf-8') else
return string output = [0x81, message.size, message]
elsif opcode == 8 @socket.write output.pack("CCA#{message.size}")
send("",0) end
return false
end
end
def send(message,closetype=false)
if closetype
output=[0x88,2,3,0xe8+closetype]
@socket.write output.pack("C*")
@socket.close
else
output = [0x81, message.size, message]
@socket.write output.pack("CCA#{message.size}")
end end
end end
end end

View File

@ -1,17 +1,18 @@
# coding: utf-8 # frozen_string_literal: true
lib = File.expand_path("../lib", __FILE__)
lib = File.expand_path('lib', __dir__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require "web_gui/version" require 'web_gui/version'
Gem::Specification.new do |spec| Gem::Specification.new do |spec|
spec.name="web_gui" spec.name = 'web_gui'
spec.version=WebGui::VERSION spec.version = WebGui::VERSION
spec.authors=["pjht"] spec.authors = ['pjht']
spec.email=["pjht@users.noreply.github.com"] spec.email = ['pjht@users.noreply.github.com']
spec.summary= "A GUI framework for ruby based around the web browser" spec.summary = 'A GUI framework for ruby based around the web browser'
spec.homepage="https://github.com/pjht/web_gui" spec.homepage = 'https://github.com/pjht/web_gui'
spec.files=["lib/web_gui.rb","lib/main.js"]+Dir.glob("lib/web_gui/*.rb") spec.files = ['lib/web_gui.rb', 'lib/main.js'] + Dir.glob('lib/web_gui/*.rb')
spec.bindir="exe" spec.bindir = 'exe'
spec.executables=spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
spec.require_paths=["lib"] spec.require_paths = ['lib']
end end