Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/lib
diff options
context:
space:
mode:
authorIshan Bansal <ishan@seeta.in>2011-02-04 12:53:17 (GMT)
committer Ishan Bansal <ishan@seeta.in>2011-02-04 12:53:17 (GMT)
commitee852209b70c803e6ab1bb69b71c7dafcb1448e7 (patch)
treeac20673816893b398aed4c4da463b157d9e4899e /lib
Imported Upstream version 1HEADmaster
Diffstat (limited to 'lib')
-rw-r--r--lib/shoes.rb548
-rw-r--r--lib/shoes/cache.rb54
-rw-r--r--lib/shoes/chipmunk.rb35
-rw-r--r--lib/shoes/data.rb39
-rw-r--r--lib/shoes/help.rb468
-rw-r--r--lib/shoes/image.rb25
-rw-r--r--lib/shoes/inspect.rb128
-rw-r--r--lib/shoes/log.rb48
-rw-r--r--lib/shoes/minitar.rb986
-rw-r--r--lib/shoes/override.rb38
-rw-r--r--lib/shoes/pack.rb543
-rw-r--r--lib/shoes/search.rb46
-rw-r--r--lib/shoes/setup.rb329
-rw-r--r--lib/shoes/shy.rb131
-rw-r--r--lib/shoes/shybuilder.rb44
15 files changed, 3462 insertions, 0 deletions
diff --git a/lib/shoes.rb b/lib/shoes.rb
new file mode 100644
index 0000000..1df9589
--- /dev/null
+++ b/lib/shoes.rb
@@ -0,0 +1,548 @@
+# -*- encoding: utf-8 -*-
+#
+# lib/shoes.rb
+# The Shoes base app, both a demonstration and the learning tool for
+# using Shoes.
+#
+ARGV.delete_if { |x| x =~ /-psn_/ }
+
+require 'open-uri'
+require 'optparse'
+require 'resolv-replace' if RUBY_PLATFORM =~ /win/
+require 'shoes/inspect'
+require 'shoes/cache'
+if Object.const_defined? :Shoes
+ require 'shoes/image'
+end
+require 'shoes/shybuilder'
+
+def Shoes.hook; end
+
+class Encoding
+ %w[ASCII_8BIT UTF_16BE UTF_16LE UTF_32BE UTF_32LE US_ASCII].each do |ec|
+ eval "#{ec} = '#{ec.sub '_', '-'}'"
+ end unless RUBY_PLATFORM =~ /linux/
+end
+
+class Range
+ def rand
+ conv = (Integer === self.end && Integer === self.begin ? :to_i : :to_f)
+ ((Kernel.rand * (self.end - self.begin)) + self.begin).send(conv)
+ end
+end
+
+unless Time.respond_to? :today
+ def Time.today
+ t = Time.now
+ t - (t.to_i % 86400)
+ end
+end
+
+class Shoes
+ RELEASES = %w[Curious Raisins Policeman]
+
+ NotFound = proc do
+ para "404 NOT FOUND, GUYS!"
+ end
+
+ class << self; attr_accessor :locale, :language end
+ @locale = ENV["SHOES_LANG"] || ENV["LC_MESSAGES"] || ENV["LC_ALL"] || ENV["LANG"] || "C"
+ @language = @locale[/^(\w{2})_/, 1] || "en"
+
+ @mounts = []
+
+ OPTS = OptionParser.new do |opts|
+ opts.banner = "Usage: shoes [options] (app.rb or app.shy)"
+
+ opts.on("-m", "--manual",
+ "Open the built-in manual.") do
+ show_manual
+ end
+
+ opts.on("-p", "--package",
+ "Package a Shoes app for Windows, OS X and Linux.") do |s|
+ make_pack
+ end
+
+ opts.on("-g", "--gem",
+ "Passes commands to RubyGems.") do
+ require 'shoes/setup'
+ require 'rubygems/gem_runner'
+ Gem::GemRunner.new.run(ARGV)
+ raise SystemExit, ""
+ end
+
+ opts.on("--manual-html DIRECTORY", "Saves the manual to a directory as HTML.") do |dir|
+ manual_as :html, dir
+ raise SystemExit, "HTML manual in: #{dir}"
+ end
+
+ opts.on("--install MODE SRC DEST", "Installs a file.") do |mode|
+ src, dest = ARGV
+ FileUtils.install src, dest, :mode => mode.to_i(8), :preserve => true
+ raise SystemExit, ""
+ end
+
+ opts.on("--nolayered", "No WS_EX_LAYERED style option.") do
+ $NOLAYERED = 1
+ Shoes.args!
+ end
+
+ opts.on_tail("-v", "--version", "Display the version info.") do
+ raise SystemExit, File.read("#{DIR}/VERSION.txt").strip
+ end
+
+ opts.on_tail("-h", "--help", "Show this message") do
+ raise SystemExit, opts.to_s
+ end
+ end
+
+ class SettingUp < StandardError; end
+
+ @setups = {}
+
+ def self.setup &blk
+ require 'shoes/setup'
+ line = caller[0]
+ return if @setups[line]
+ script = line[/^(.+?):/, 1]
+ set = Shoes::Setup.new(script, &blk)
+ @setups[line] = true
+ unless set.no_steps?
+ raise SettingUp
+ end
+ end
+
+ def self.show_selector
+ fname = ask_open_file
+ Shoes.visit(fname) if fname
+ end
+
+ def self.package_app
+ fname = ask_open_file
+ return false unless fname
+ start_shy_builder fname
+ end
+
+ def self.splash
+ font "#{DIR}/fonts/Lacuna.ttf"
+ Shoes.app :width => 400, :height => 300, :resizable => false do
+ style(Para, :align => "center", :weight => "bold", :font => "Lacuna Regular", :size => 13)
+ style(Link, :stroke => yellow, :underline => nil)
+ style(LinkHover, :stroke => yellow, :fill => nil)
+
+ x1 = 77; y1 = 122
+ x2 = 148; y2 = -122
+ x3 = 245; y3 = 0
+
+ nofill
+ strokewidth 40.0
+
+ @waves = stack :top => 0, :left => 0
+
+ require 'shoes/search'
+ require 'shoes/help'
+
+ stack :margin => 18 do
+ para "Welcome to", :stroke => "#DFA", :margin => 0
+ para "SHOES", :size => 48, :stroke => "#DFA", :margin_top => 0
+ stack do
+ background black(0.2), :curve => 8
+ para link("Open an App.") { Shoes.show_selector and close }, :margin => 10, :margin_bottom => 4
+ #para link("Package an App.") { Shoes.package_app and close }, :margin => 10, :margin_bottom => 4
+ para link("Package an App.") { Shoes.make_pack and close }, :margin => 10, :margin_bottom => 4
+ para link("Read the Manual.") { Shoes.show_manual and close }, :margin => 10
+ end
+ inscription "Alt-Slash opens the console.", :stroke => "#DFA", :align => "center"
+ end
+
+ animate(10) do |ani|
+ a = Math.sin(ani * 0.02) * 20
+ @waves.clear do
+ background white
+ y = -30
+ 16.times do |i|
+ shape do
+ move_to x = (-300 - (i*(a*0.8))), y
+ c = (a + 14) * 0.01
+ stroke rgb(i * 0.06, c + 0.1, 0.1, 1.0 - (ani * 0.0003))
+ 4.times do
+ curve_to x1 + x, (y1-(i*a)) + y, x2 + x, (y2+(i*a)) + y, x3 + x, y3 + y
+ x += x3
+ end
+ end
+ y += 30
+ end
+ end
+ end
+ end
+ end
+
+ def self.make_pack
+ require 'shoes/pack'
+ Shoes.app(:width => 500, :height => 480, :resizable => true, &PackMake)
+ end
+
+ def self.manual_p(str, path)
+ str.gsub(/\n+\s*/, " ").
+ gsub(/&/, '&amp;').gsub(/>/, '&gt;').gsub(/>/, '&lt;').gsub(/"/, '&quot;').
+ gsub(/`(.+?)`/m, '<code>\1</code>').gsub(/\[\[BR\]\]/i, "<br />\n").
+ gsub(/\^(.+?)\^/m, '\1').
+ gsub(/'''(.+?)'''/m, '<strong>\1</strong>').gsub(/''(.+?)''/m, '<em>\1</em>').
+ gsub(/\[\[(http:\/\/\S+?)\]\]/m, '<a href="\1" target="_new">\1</a>').
+ gsub(/\[\[(http:\/\/\S+?) (.+?)\]\]/m, '<a href="\1" target="_new">\2</a>').
+ gsub(/\[\[(\S+?)\]\]/m) do
+ ms, mn = $1.split(".", 2)
+ if mn
+ '<a href="' + ms + '.html#' + mn + '">' + mn + '</a>'
+ else
+ '<a href="' + ms + '.html">' + ms + '</a>'
+ end
+ end.
+ gsub(/\[\[(\S+?) (.+?)\]\]/m, '<a href="\1.html">\2</a>').
+ gsub(/\!(\{[^}\n]+\})?([^!\n]+\.\w+)\!/) do
+ x = "static/#$2"
+ FileUtils.cp("#{DIR}/#{x}", "#{path}/#{x}") if File.exists? "#{DIR}/#{x}"
+ '<img src="' + x + '" />'
+ end
+ end
+
+ def self.manual_link(sect)
+ end
+
+ TITLES = {:title => :h1, :subtitle => :h2, :tagline => :h3, :caption => :h4}
+
+ def self.manual_as format, *args
+ require 'shoes/search'
+ require 'shoes/help'
+
+ case format
+ when :shoes
+ Shoes.app(:width => 720, :height => 640, &Shoes::Help)
+ else
+ extend Shoes::Manual
+ man = self
+ dir, = args
+ FileUtils.mkdir_p File.join(dir, 'static')
+ FileUtils.cp "#{DIR}/static/shoes-icon.png", "#{dir}/static"
+ %w[manual.css code_highlighter.js code_highlighter_ruby.js].
+ each { |x| FileUtils.cp "#{DIR}/static/#{x}", "#{dir}/static" }
+ html_bits = proc do
+ proc do |sym, text|
+ case sym when :intro
+ div.intro { p { self << man.manual_p(text, dir) } }
+ when :code
+ pre { code.rb text.gsub(/^\s*?\n/, '') }
+ when :colors
+ color_names = (Shoes::COLORS.keys*"\n").split("\n").sort
+ color_names.each do |color|
+ c = Shoes::COLORS[color.intern]
+ f = c.dark? ? "white" : "black"
+ div.color(:style => "background: #{c}; color: #{f}") { h3 color; p c }
+ end
+ when :index
+ tree = man.class_tree
+ shown = []
+ i = 0
+ index_p = proc do |k, subs|
+ unless shown.include? k
+ i += 1
+ p "â–¸ #{k}", :style => "margin-left: #{20*i}px"
+ subs.uniq.sort.each do |s|
+ index_p[s, tree[s]]
+ end if subs
+ i -= 1
+ shown << k
+ end
+ end
+ tree.sort.each &index_p
+ # index_page
+ when :list
+ ul { text.each { |x| li { self << man.manual_p(x, dir) } } }
+ when :samples
+ folder = File.join DIR, 'samples'
+ h = {}
+ Dir.glob(File.join folder, '*').each do |file|
+ if File.extname(file) == '.rb'
+ key = File.basename(file).split('-')[0]
+ h[key] ? h[key].push(file) : h[key] = [file]
+ end
+ end
+ h.each do |k, v|
+ p "<h4>#{k}</h4>"
+ samples = []
+ v.each do |file|
+ sample = File.basename(file).split('-')[1..-1].join('-')[0..-4]
+ samples << "<a href=\"http://github.com/shoes/shoes/raw/master/manual-snapshots/#{k}-#{sample}.png\">#{sample}</a>"
+ end
+ p samples.join ' '
+ end
+ else
+ send(TITLES[sym] || :p) { self << man.manual_p(text, dir) }
+ end
+ end
+ end
+
+ docs = load_docs(Shoes::Manual::path)
+ sections = docs.map { |x,| x }
+
+ docn = 1
+ docs.each do |title1, opt1|
+ subsect = opt1['sections'].map { |x,| x }
+ menu = sections.map do |x|
+ [x, (subsect if x == title1)]
+ end
+
+ path1 = File.join(dir, title1.gsub(/\W/, ''))
+ make_html("#{path1}.html", title1, menu) do
+ h2 "The Shoes Manual"
+ h1 title1
+ man.wiki_tokens opt1['description'], true, &instance_eval(&html_bits)
+ p.next { text "Next: "
+ a opt1['sections'].first[1]['title'], :href => "#{opt1['sections'].first[0]}.html" }
+ end
+
+ optn = 1
+ opt1['sections'].each do |title2, opt2|
+ path2 = File.join(dir, title2)
+ make_html("#{path2}.html", opt2['title'], menu) do
+ h2 "The Shoes Manual"
+ h1 opt2['title']
+ man.wiki_tokens opt2['description'], true, &instance_eval(&html_bits)
+ opt2['methods'].each do |title3, desc3|
+ sig, val = title3.split(/\s+»\s+/, 2)
+ aname = sig[/^[^(=]+=?/].gsub(/\s/, '').downcase
+ a :name => aname
+ div.method do
+ a sig, :href => "##{aname}"
+ text " » #{val}" if val
+ end
+ div.sample do
+ man.wiki_tokens desc3, &instance_eval(&html_bits)
+ end
+ end
+ if opt1['sections'][optn]
+ p.next { text "Next: "
+ a opt1['sections'][optn][1]['title'], :href => "#{opt1['sections'][optn][0]}.html" }
+ elsif docs[docn]
+ p.next { text "Next: "
+ a docs[docn][0], :href => "#{docs[docn][0].gsub(/\W/, '')}.html" }
+ end
+ optn += 1
+ end
+ end
+
+ docn += 1
+ end
+ end
+ end
+
+ def self.show_manual
+ manual_as :shoes
+ end
+
+ def self.show_log
+ require 'shoes/log'
+ return if @log_app and Shoes.APPS.include? @log_app
+ @log_app =
+ Shoes.app do
+ extend Shoes::LogWindow
+ setup
+ end
+ end
+
+ def self.mount(path, meth, &blk)
+ @mounts << [path, meth || blk]
+ end
+
+ SHOES_URL_RE = %r!^@([^/]+)(.*)$!
+
+ def self.run(path)
+ uri = URI(path)
+ @mounts.each do |mpath, rout|
+ m, *args = *path.match(/^#{mpath}$/)
+ if m
+ unless rout.is_a? Proc
+ rout = rout[0].instance_method(rout[1])
+ end
+ return [rout, args]
+ end
+ end
+ case uri.path when "/"
+ [nil]
+ when SHOES_URL_RE
+ [proc { eval(URI("http://#$1:53045#$2").read) }]
+ else
+ [NotFound]
+ end
+ end
+
+ def self.args!
+ if RUBY_PLATFORM !~ /darwin/ and ARGV.empty?
+ Shoes.splash
+ end
+ OPTS.parse! ARGV
+ ARGV[0] or true
+ end
+
+ def self.uri(str)
+ if str =~ SHOES_URL_RE
+ URI("http://#$1:53045#$2")
+ else
+ URI(str) rescue nil
+ end
+ end
+
+ def self.visit(path)
+ uri = Shoes.uri(path)
+
+ case uri
+ when URI::HTTP
+ str = uri.read
+ if str !~ /Shoes\.app/
+ Shoes.app do
+ eval(uri.read)
+ end
+ else
+ eval(uri.read)
+ end
+ else
+ path = File.expand_path(path.gsub(/\\/, "/"))
+ if path =~ /\.shy$/
+ @shy = true
+ require 'shoes/shy'
+ base = File.basename(path, ".shy")
+ tmpdir = "%s/shoes-%s.%d" % [Dir.tmpdir, base, $$]
+ shy = Shy.x(path, tmpdir)
+ Dir.chdir(tmpdir)
+ Shoes.debug "Loaded SHY: #{shy.name} #{shy.version} by #{shy.creator}"
+ path = shy.launch
+ else
+ @shy = false
+ Dir.chdir(File.dirname(path))
+ path = File.basename(path)
+ end
+
+ $0.replace path
+
+ code = read_file(path)
+ eval(code, TOPLEVEL_BINDING, path)
+ end
+ rescue SettingUp
+ rescue Object => e
+ error(e)
+ show_log
+ end
+
+ def self.read_file path
+ if RUBY_VERSION =~ /^1\.9/ and !@shy
+ #File.open(path, 'r:utf-8') { |f| f.read }
+ IO.read(path).force_encoding("UTF-8")
+ else
+ File.read(path)
+ end
+ end
+
+ def self.url(path, meth)
+ Shoes.mount(path, [self, meth])
+ end
+
+ module Basic
+ def tween opts, &blk
+ opts = opts.dup
+
+ if opts[:upward]
+ opts[:top] = self.top - opts.delete(:upward)
+ elsif opts[:downward]
+ opts[:top] = self.top + opts.delete(:downward)
+ end
+
+ if opts[:sideways]
+ opts[:left] = self.left + opts.delete(:sideways)
+ end
+
+ @TWEEN.remove if @TWEEN
+ @TWEEN = parent.animate(opts[:speed] || 20) do
+
+ # figure out a coordinate halfway between here and there
+ cont = opts.select do |k, v|
+ if self.respond_to? k
+ n, o = v, self.send(k)
+ if n != o
+ n = o + ((n - o) / 2)
+ n = v if o == n
+ self.send("#{k}=", n)
+ end
+ self.style[k] != v
+ end
+ end
+
+ # if we're there, get rid of the animation
+ if cont.empty?
+ @TWEEN.remove
+ @TWEEN = nil
+ blk.call if blk
+ end
+ end
+ end
+ end
+
+ # complete list of styles
+ BASIC_S = [:left, :top, :right, :bottom, :width, :height, :attach, :hidden,
+ :displace_left, :displace_top, :margin, :margin_left, :margin_top,
+ :margin_right, :margin_bottom]
+ TEXT_S = [:strikecolor, :undercolor, :font, :size, :family, :weight,
+ :rise, :kerning, :emphasis, :strikethrough, :stretch, :underline,
+ :variant]
+ MOUSE_S = [:click, :motion, :release, :hover, :leave]
+ KEY_S = [:keydown, :keypress, :keyup]
+ COLOR_S = [:stroke, :fill]
+
+ {Background => [:angle, :radius, :curve, *BASIC_S],
+ Border => [:angle, :radius, :curve, :strokewidth, *BASIC_S],
+ Canvas => [:scroll, :start, :finish, *(KEY_S|MOUSE_S|BASIC_S)],
+ Check => [:click, :checked, *BASIC_S],
+ Radio => [:click, :checked, :group, *BASIC_S],
+ EditLine => [:change, :secret, :text, *BASIC_S],
+ EditBox => [:change, :text, *BASIC_S],
+ Effect => [:radius, :distance, :inner, *(COLOR_S|BASIC_S)],
+ Image => MOUSE_S|BASIC_S,
+ ListBox => [:change, :items, :choose, *BASIC_S],
+ # Pattern => [:angle, :radius, *BASIC_S],
+ Progress => BASIC_S,
+ Shape => COLOR_S|MOUSE_S|BASIC_S,
+ TextBlock => [:justify, :align, :leading, *(COLOR_S|MOUSE_S|TEXT_S|BASIC_S)],
+ Text => COLOR_S|MOUSE_S|TEXT_S|BASIC_S}.
+ each do |klass, styles|
+ klass.class_eval do
+ include Basic
+ styles.each do |m|
+ case m when *MOUSE_S
+ else
+ define_method(m) { style[m] } unless klass.method_defined? m
+ define_method("#{m}=") { |v| style(m => v) } unless klass.method_defined? "#{m}="
+ end
+ end
+ end
+ end
+
+ class Types::Widget
+ @types = {}
+ def self.inherited subc
+ methc = subc.to_s[/(^|::)(\w+)$/, 2].
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').downcase
+ @types[methc] = subc
+ Shoes.class_eval %{
+ def #{methc}(*a, &b)
+ a.unshift Widget.instance_variable_get("@types")[#{methc.dump}]
+ widget(*a, &b)
+ end
+ }
+ end
+ end
+end
+
+def window(*a, &b)
+ Shoes.app(*a, &b)
+end
diff --git a/lib/shoes/cache.rb b/lib/shoes/cache.rb
new file mode 100644
index 0000000..b3a5583
--- /dev/null
+++ b/lib/shoes/cache.rb
@@ -0,0 +1,54 @@
+require 'fileutils'
+include FileUtils
+
+# locate ~/.shoes
+require 'tmpdir'
+
+lib_dir = nil
+homes = []
+homes << [ENV['HOME'], File.join( ENV['HOME'], '.shoes' )] if ENV['HOME']
+homes << [ENV['APPDATA'], File.join( ENV['APPDATA'], 'Shoes' )] if ENV['APPDATA']
+homes.each do |home_top, home_dir|
+ next unless home_top
+ if File.exists? home_top
+ lib_dir = home_dir
+ break
+ end
+end
+LIB_DIR = lib_dir || File.join(Dir::tmpdir, "shoes")
+LIB_DIR.gsub! /\\/, '/'
+SITE_LIB_DIR = File.join(LIB_DIR, '+lib')
+GEM_DIR = File.join(LIB_DIR, '+gem')
+CACHE_DIR = File.join(LIB_DIR, '+cache')
+
+mkdir_p(CACHE_DIR)
+$:.unshift SITE_LIB_DIR
+$:.unshift GEM_DIR
+ENV['GEM_HOME'] = GEM_DIR
+
+require 'rbconfig'
+config = {
+ 'ruby_install_name' => "shoes --ruby",
+ 'RUBY_INSTALL_NAME' => "shoes --ruby",
+ 'prefix' => "#{DIR}",
+ 'bindir' => "#{DIR}",
+ 'rubylibdir' => "#{DIR}/ruby/lib",
+ 'datarootdir' => "#{DIR}/share",
+ 'dvidir' => "#{DIR}/doc/${PACKAGE}",
+ 'psdir' => "#{DIR}/doc/${PACKAGE}",
+ 'htmldir' => "#{DIR}/doc/${PACKAGE}",
+ 'docdir' => "#{DIR}/doc/${PACKAGE}",
+ 'archdir' => "#{DIR}/ruby/lib/#{RUBY_PLATFORM}",
+ 'sitedir' => SITE_LIB_DIR,
+ 'sitelibdir' => SITE_LIB_DIR,
+ 'sitearchdir' => "#{SITE_LIB_DIR}/#{RUBY_PLATFORM}",
+ 'LIBRUBYARG_STATIC' => "",
+ 'libdir' => "#{DIR}",
+ 'LDFLAGS' => "-L. -L#{DIR}"
+}
+Config::CONFIG.merge! config
+Config::MAKEFILE_CONFIG.merge! config
+GEM_CENTRAL_DIR = File.join(DIR, 'ruby/gems/' + Config::CONFIG['ruby_version'])
+Dir[GEM_CENTRAL_DIR + "/gems/*"].each do |gdir|
+ $: << "#{gdir}/lib"
+end
diff --git a/lib/shoes/chipmunk.rb b/lib/shoes/chipmunk.rb
new file mode 100644
index 0000000..f56e583
--- /dev/null
+++ b/lib/shoes/chipmunk.rb
@@ -0,0 +1,35 @@
+require 'chipmunk'
+
+module ChipMunk
+ def cp_space
+ @space = CP::Space.new
+ @space.damping = 0.8
+ @space.gravity = vec2 0, 25
+ @space
+ end
+
+ def cp_oval l, t, r, opts = {}
+ b = CP::Body.new 1,1
+ b.p = vec2 l, t
+ @space.add_body b
+ @space.add_shape CP::Shape::Circle.new(b, r, vec2(0, 0))
+
+ img = image left: l-r-1, top: t-r-1, width: 2*r+2, height: 2*r+2, body: b, inflate: r-2 do
+ oval 1, 1, 2*r, opts
+ end
+ end
+
+ def cp_line x0, y0, x1, y1, opts = {}
+ opts[:strokewidth] = 5 unless opts[:strokewidth]
+ sb = CP::Body.new 1.0/0.0, 1.0/0.0
+ seg = CP::Shape::Segment.new sb, vec2(x0, y0), vec2(x1, y1), opts[:strokewidth]
+ @space.add_shape seg
+ line x0, y0, x1, y1, opts
+ end
+end
+
+Shoes::Image.class_eval do
+ def cp_move
+ move style[:body].p.x.to_i - style[:inflate], style[:body].p.y.to_i - style[:inflate]
+ end
+end
diff --git a/lib/shoes/data.rb b/lib/shoes/data.rb
new file mode 100644
index 0000000..1f05001
--- /dev/null
+++ b/lib/shoes/data.rb
@@ -0,0 +1,39 @@
+require 'sqlite3'
+
+data_file = File.join(LIB_DIR, "+data")
+data_init = !File.exists?(data_file)
+
+DATABASE = SQLite3::Database.new data_file
+DATABASE.type_translation = true
+
+class << DATABASE
+ DATABASE_VERSION = 1
+ def setup
+ DATABASE.execute_batch %{
+ CREATE TABLE cache (
+ url text primary key,
+ etag text,
+ hash varchar(40),
+ saved datetime
+ );
+ CREATE TABLE upgrades (
+ version int primary key
+ );
+ INSERT INTO upgrades VALUES (?);
+ }, DATABASE_VERSION
+ end
+ def check_cache_for url
+ etag, hash, saved =
+ DATABASE.get_first_row("SELECT etag, hash, saved FROM cache WHERE url = ?", url)
+ {:etag => etag, :hash => hash, :saved => saved.nil? ? 0 : Time.parse(saved.to_s).to_i}
+ end
+ def notify_cache_of url, etag, hash
+ DATABASE.query %{
+ REPLACE INTO cache (url, etag, hash, saved)
+ VALUES (?, ?, ?, datetime("now", "localtime"));
+ }, url, etag, hash
+ nil
+ end
+end
+
+DATABASE.setup if data_init
diff --git a/lib/shoes/help.rb b/lib/shoes/help.rb
new file mode 100644
index 0000000..836bbd7
--- /dev/null
+++ b/lib/shoes/help.rb
@@ -0,0 +1,468 @@
+# -*- encoding: utf-8 -*-
+module Shoes::Manual
+ PARA_RE = /\s*?(\{{3}(?:.+?)\}{3})|\n\n/m
+ CODE_RE = /\{{3}(?:\s*\#![^\n]+)?(.+?)\}{3}/m
+ IMAGE_RE = /\!(\{([^}\n]+)\})?([^!\n]+\.\w+)\!/
+ CODE_STYLE = {:size => 9, :margin => 12}
+ INTRO_STYLE = {:size => 16, :margin_bottom => 20, :stroke => "#000"}
+ SUB_STYLE = {:stroke => "#CCC", :margin_top => 10}
+ IMAGE_STYLE = {:margin => 8, :margin_left => 100}
+ COLON = ": "
+
+ [INTRO_STYLE, SUB_STYLE].each do |h|
+ h[:font] = "MS UI Gothic"
+ end if Shoes.language == 'ja'
+
+ def self.path
+ path = "#{DIR}/static/manual-#{Shoes.language}.txt"
+ unless File.exists? path
+ path = "#{DIR}/static/manual-en.txt"
+ end
+ path
+ end
+
+ def dewikify_hi(str, terms, intro = false)
+ if terms
+ code = []
+ str = str.
+ gsub(CODE_RE) { |x| code << x; "CODE#[#{code.length-1}]" }.
+ gsub(/#{Regexp::quote(terms)}/i, '@\0@').
+ gsub(/CODE#\[(\d+)\]/) { code[$1.to_i] }
+ end
+ dewikify(str, intro)
+ end
+
+ def dewikify_p(ele, str, *args)
+ str = str.gsub(/\n+\s*/, " ").dump.
+ gsub(/`(.+?)`/m, '", code("\1"), "').gsub(/\[\[BR\]\]/i, "\n").
+ gsub(/\^(.+?)\^/m, '\1').
+ gsub(/@(.+?)@/m, '", strong("\1", :fill => yellow), "').
+ gsub(/'''(.+?)'''/m, '", strong("\1"), "').gsub(/''(.+?)''/m, '", em("\1"), "').
+ gsub(/\[\[(\S+?)\]\]/m, '", link("\1".split(".", 2).last) { open_link("\1") }, "').
+ gsub(/\[\[(\S+?) (.+?)\]\]/m, '", link("\2") { open_link("\1") }, "').
+ gsub(IMAGE_RE, '", *args); stack(IMAGE_STYLE.merge({\2})) { image("#{DIR}/static/\3") }; #{ele}("')
+ #debug str if str =~ /The list of special keys/
+ a = str.split(', ", ", ')
+ if a.size == 1
+ eval("#{ele}(#{str}, *args)")
+ else
+ flow do
+ a[0...-1].each{|s| eval("#{ele}(#{s}, ',', *args)")}
+ eval("#{ele}(#{a[-1]}, *args)")
+ end
+ end
+ end
+
+ def dewikify_code(str)
+ str = str.gsub(/\A\n+/, '').chomp
+ stack :margin_bottom => 12 do
+ background rgb(210, 210, 210), :curve => 4
+ para code(str), CODE_STYLE
+ stack :top => 0, :right => 2, :width => 70 do
+ stack do
+ background "#8A7", :margin => [0, 2, 0, 2], :curve => 4
+ para link("Run this", :stroke => "#eee", :underline => "none") { run_code(str) },
+ :margin => 4, :align => 'center', :weight => 'bold', :size => 9
+ end
+ end
+ end
+ end
+
+ def wiki_tokens(str, intro = false)
+ paras = str.split(PARA_RE).reject { |x| x.empty? }
+ if intro
+ yield :intro, paras.shift
+ end
+ paras.map do |ps|
+ if ps =~ CODE_RE
+ yield :code, $1
+ else
+ case ps
+ when /\A\{COLORS\}/
+ yield :colors, nil
+ when /\A\{INDEX\}/
+ yield :index, nil
+ when /\A\{SAMPLES\}/
+ yield :samples, nil
+ when /\A \* (.+)/m
+ yield :list, $1.split(/^ \* /)
+ when /\A==== (.+) ====/
+ yield :caption, $1
+ when /\A=== (.+) ===/
+ yield :tagline, $1
+ when /\A== (.+) ==/
+ yield :subtitle, $1
+ when /\A= (.+) =/
+ yield :title, $1
+ else
+ yield :para, ps
+ end
+ end
+ end
+ end
+
+ def dewikify(str, intro = false)
+ proc do
+ wiki_tokens(str, intro) do |sym, text|
+ case sym when :intro
+ dewikify_p :para, text, INTRO_STYLE
+ when :code
+ dewikify_code(text)
+ when :colors
+ color_page
+ when :index
+ index_page
+ when :samples
+ sample_page
+ when :list
+ text.each { |t| stack(:margin_left => 30) {
+ fill black; oval -10, 7, 6; dewikify_p :para, t } }
+ else
+ dewikify_p sym, text
+ end
+ end
+ end
+ end
+
+ def sample_page
+ folder = File.join DIR, 'samples'
+ h = {}
+ Dir.glob(File.join folder, '*').each do |file|
+ if File.extname(file) == '.rb'
+ key = File.basename(file).split('-')[0]
+ h[key] ? h[key].push(file) : h[key] = [file]
+ end
+ end
+ stack do
+ h.each do |k, v|
+ subtitle k
+ flow do
+ v.each do |file|
+ para link(File.basename(file).split('-')[1..-1].join('-')[0..-4]){
+ Dir.chdir(folder){eval IO.read(file).force_encoding("UTF-8"), TOPLEVEL_BINDING}
+ }
+ end
+ end
+ end
+ end
+ end
+
+ def color_page
+ color_names = (Shoes::COLORS.keys*"\n").split("\n").sort
+ flow do
+ color_names.each do |color|
+ flow :width => 0.33 do
+ c = send(color)
+ background c
+ para strong(color), "\n", c, :stroke => (c.dark? ? white : black),
+ :margin => 4, :align => 'center'
+ end
+ end
+ end
+ end
+
+ def class_tree
+ tree = {}
+ Shoes.constants.each do |c|
+ k = Shoes.const_get(c)
+ next unless k.respond_to? :superclass
+
+ c = "Shoes::#{c}"
+ if k.superclass == Object
+ tree[c] ||= []
+ else
+ k.ancestors[1..-1].each do |sk|
+ break if [Object, Kernel].include? sk
+ next unless sk.is_a? Class #don't show mixins
+ (tree[sk.name] ||= []) << c
+ c = sk.name
+ end
+ end
+ end
+ tree
+ end
+
+ def index_page
+ tree = class_tree
+ shown = []
+ index_p = proc do |k, subs|
+ unless shown.include? k
+ stack :margin_left => 20 do
+ flow do
+ para "â–¸ ", :font => case RUBY_PLATFORM
+ when /mingw/; "MS UI Gothic"
+ when /darwin/; "AppleGothic, Arial"
+ else "Arial"
+ end
+ para k
+ end
+ subs.uniq.sort.each do |s|
+ index_p[s, tree[s]]
+ end if subs
+ end
+ shown << k
+ end
+ end
+ tree.sort.each &index_p
+ end
+
+ def run_code str
+ eval(str, TOPLEVEL_BINDING)
+ end
+
+ def load_docs path
+ return @docs if @docs
+ str = Shoes.read_file(path)
+ @search = Shoes::Search.new
+ @sections, @methods, @mindex = {}, {}, {}
+ @docs =
+ (str.split(/^= (.+?) =/)[1..-1]/2).map do |k,v|
+ sparts = v.split(/^== (.+?) ==/)
+
+ sections = (sparts[1..-1]/2).map do |k2,v2|
+ meth = v2.split(/^=== (.+?) ===/)
+ k2t = k2[/^(?:The )?([\-\w]+)/, 1]
+ meth_plain = meth[0].gsub(IMAGE_RE, '')
+ @search.add_document :uri => "T #{k2t}", :body => "#{k2}\n#{meth_plain}".downcase
+
+ hsh = {'title' => k2, 'section' => k,
+ 'description' => meth[0],
+ 'methods' => (meth[1..-1]/2).map { |_k,_v|
+ @search.add_document :uri => "M #{k}#{COLON}#{k2t}#{COLON}#{_k}", :body => "#{_k}\n#{_v}".downcase
+ @mindex["#{k2t}.#{_k[/[\w\.]+/]}"] = [k2t, _k]
+ [_k, _v]
+ }}
+ @methods[k2t] = hsh
+ [k2t, hsh]
+ end
+
+ @search.add_document :uri => "S #{k}", :body => "#{k}\n#{sparts[0]}".downcase
+ hsh = {'description' => sparts[0], 'sections' => sections,
+ 'class' => "toc" + k.downcase.gsub(/\W+/, '')}
+ @sections[k] = hsh
+ [k, hsh]
+ end
+ @search.finish!
+ @docs
+ end
+
+ def show_search
+ @toc.each { |k,v| v.hide }
+ @title.replace "Search"
+ @doc.clear do
+ dewikify_p :para, "Try method names (like `button` or `arrow`) or topics (like `slots`)", :align => 'center'
+ flow :margin_left => 60 do
+ edit_line :width => -60 do |terms|
+ @results.clear do
+ termd = terms.text.downcase
+ #found = termd.empty? ? [] : manual_search(termd)
+ found = (termd.empty? or termd[0] == 'z' or termd[0] == 'y') ? [] : manual_search(termd)
+ para "#{found.length} matches", :align => "center", :margin_bottom => 0
+ found.each do |typ, head|
+ flow :margin => 4 do
+ case typ
+ when "S"
+ background "#333", :curve => 4
+ caption strong(link(head, :stroke => white) { open_section(head, terms.text) })
+ para "Section header", Shoes::Manual::SUB_STYLE
+ when "T"
+ background "#777", :curve => 4
+ caption strong(link(head, :stroke => "#EEE") { open_methods(head, terms.text) })
+ hsh = @methods[head]
+ para "Sub-section under #{hsh['section']} (#{hsh['methods'].length} methods)", Shoes::Manual::SUB_STYLE
+ when "M"
+ background "#CCC", :curve => 4
+ sect, subhead, head = head.split(Shoes::Manual::COLON, 3)
+ para strong(sect, Shoes::Manual::COLON, subhead, Shoes::Manual::COLON, link(head) { open_methods(subhead, terms.text, head) })
+ end
+ end
+ end
+ end
+ end
+ end
+ @results = stack
+ end
+ app.slot.scroll_top = 0
+ end
+
+ def open_link(head)
+ if head == "Search"
+ show_search
+ elsif @sections.has_key? head
+ open_section(head)
+ elsif @methods.has_key? head
+ open_methods(head)
+ elsif @mindex.has_key? head
+ head, sub = @mindex[head]
+ open_methods(head, nil, sub)
+ elsif head =~ /^http:\/\//
+ debug head
+ visit head
+ end
+ end
+
+ def add_next_link(docn, optn)
+ opt1, optn = @docs[docn][1], optn + 1
+ if opt1['sections'][optn]
+ @doc.para "Next: ",
+ link(opt1['sections'][optn][1]['title']) { open_methods(opt1['sections'][optn][0]) },
+ :align => "right"
+ elsif @docs[docn + 1]
+ @doc.para "Next: ",
+ link(@docs[docn + 1][0]) { open_section(@docs[docn + 1][0].gsub(/\W/, '')) },
+ :align => "right"
+ end
+ end
+
+ def open_section(sect_s, terms = nil)
+ sect_h = @sections[sect_s]
+ sect_cls = sect_h['class']
+ @toc.each { |k,v| v.send(k == sect_cls ? :show : :hide) }
+ @title.replace sect_s
+ @doc.clear(&dewikify_hi(sect_h['description'], terms, true))
+ add_next_link(@docs.index { |x,| x == sect_s }, -1) rescue nil
+ app.slot.scroll_top = 0
+ end
+
+ def open_methods(meth_s, terms = nil, meth_a = nil)
+ meth_h = @methods[meth_s]
+ @title.replace meth_h['title']
+ @doc.clear do
+ unless meth_a
+ instance_eval &dewikify_hi(meth_h['description'], terms, true)
+ end
+ meth_h['methods'].each do |mname, expl|
+ if meth_a.nil? or meth_a == mname
+ sig, val = mname.split("»", 2)
+ stack(:margin_top => 8, :margin_bottom => 8) {
+ background "#333".."#666", :curve => 3, :angle => 90
+ tagline sig, (span("»", val, :stroke => "#BBB") if val), :margin => 4 }
+ instance_eval &dewikify_hi(expl, terms)
+ end
+ end
+ end
+ optn = nil
+ docn = @docs.index { |_,h| optn = h['sections'].index { |x,| x == meth_s } } rescue nil
+ add_next_link(docn, optn) if docn
+ app.slot.scroll_top = 0
+ end
+
+ def manual_search(terms)
+ terms += " " if terms.length == 1
+ @search.find_all(terms).map do |title, count|
+ title.split(" ", 2)
+ end
+ end
+
+ def make_html(path, title, menu, &blk)
+ require 'hpricot'
+ File.open(path, 'w') do |f|
+ f << Hpricot do
+ xhtml_transitional do
+ head do
+ meta :"http-equiv" => "Content-Type", "content" => "text/html; charset=utf-8"
+ title "The Shoes Manual // #{title}"
+ script :type => "text/javascript", :src => "static/code_highlighter.js"
+ script :type => "text/javascript", :src => "static/code_highlighter_ruby.js"
+ style :type => "text/css" do
+ text "@import 'static/manual.css';"
+ end
+ end
+ body do
+ div.main! do
+ div.manual! &blk
+ div.sidebar do
+ img :src => "static/shoes-icon.png"
+ ul do
+ li { a.prime "HELP", :href => "./" }
+ menu.each do |m, sm|
+ li do
+ a m, :href => "#{m[/^\w+/]}.html"
+ if sm
+ ul.sub do
+ sm.each { |smm| li { a smm, :href => "#{smm}.html" } }
+ end
+ end
+ end
+ end
+
+ end
+ end
+ end
+ end
+ end
+ end.to_html
+ end
+ end
+end
+
+def Shoes.make_help_page
+ font "#{DIR}/fonts/Coolvetica.ttf" unless Shoes::FONTS.include? "Coolvetica"
+ proc do
+ extend Shoes::Manual
+ docs = load_docs Shoes::Manual.path
+
+ style(Shoes::Image, :margin => 8, :margin_left => 100)
+ style(Shoes::Code, :stroke => "#C30")
+ style(Shoes::LinkHover, :stroke => green, :fill => nil)
+ style(Shoes::Para, :size => 12, :stroke => "#332")
+ style(Shoes::Tagline, :size => 12, :weight => "bold", :stroke => "#eee", :margin => 6)
+ style(Shoes::Caption, :size => 24)
+ background "#ddd".."#fff", :angle => 90
+
+ [Shoes::LinkHover, Shoes::Para, Shoes::Tagline, Shoes::Caption].each do |type|
+ style(type, :font => "MS UI Gothic")
+ end if Shoes.language == 'ja'
+
+ stack do
+ background black
+ stack :margin_left => 118 do
+ para "The Shoes Manual", :stroke => "#eee", :margin_top => 8, :margin_left => 17,
+ :margin_bottom => 0
+ @title = title docs[0][0], :stroke => white, :margin => 4, :margin_left => 14,
+ :margin_top => 0, :font => "Coolvetica"
+ end
+ background "rgb(66, 66, 66, 180)".."rgb(0, 0, 0, 0)", :height => 0.7
+ background "rgb(66, 66, 66, 100)".."rgb(255, 255, 255, 0)", :height => 20, :bottom => 0
+ end
+ @doc =
+ stack :margin_left => 130, :margin_top => 20, :margin_bottom => 50, :margin_right => 50 + gutter,
+ &dewikify(docs[0][-1]['description'], true)
+ add_next_link(0, -1)
+ stack :top => 80, :left => 0, :attach => Shoes::Window do
+ @toc = {}
+ stack :margin => 12, :width => 130, :margin_top => 20 do
+ docs.each do |sect_s, sect_h|
+ sect_cls = sect_h['class']
+ para strong(link(sect_s, :stroke => black) { open_section(sect_s) }),
+ :size => 11, :margin => 4, :margin_top => 0
+ @toc[sect_cls] =
+ stack :hidden => @toc.empty? ? false : true do
+ links = sect_h['sections'].map do |meth_s, meth_h|
+ [link(meth_s) { open_methods(meth_s) }, "\n"]
+ end.flatten
+ links[-1] = {:size => 9, :margin => 4, :margin_left => 10}
+ para *links
+ end
+ end
+ end
+ stack :margin => 12, :width => 118, :margin_top => 6 do
+ background "#330", :curve => 4
+ para "Not finding it? Try ", strong(link("Search", :stroke => white) { show_search }), "!", :stroke => "#ddd", :size => 9, :align => "center", :margin => 6
+ end
+ stack :margin => 12, :width => 118 do
+ inscription "Shoes #{Shoes::RELEASE_NAME}\nRevision: #{Shoes::REVISION}",
+ :size => 7, :align => "center", :stroke => "#999"
+ end
+ end
+ image :width => 120, :height => 120, :top => -18, :left => 6 do
+ image "#{DIR}/static/shoes-icon.png", :width => 100, :height => 100, :top => 10, :left => 10
+ glow 2
+ end
+ end
+rescue => e
+ p e.message
+ p e.class
+end
+
+Shoes::Help = Shoes.make_help_page
diff --git a/lib/shoes/image.rb b/lib/shoes/image.rb
new file mode 100644
index 0000000..c7dc3ef
--- /dev/null
+++ b/lib/shoes/image.rb
@@ -0,0 +1,25 @@
+require 'digest/sha1'
+
+class Shoes
+ def self.image_temp_path uri, uext
+ File.join(Dir::tmpdir, "#{uri.host}-#{Time.now.usec}" + uext)
+ end
+ def self.image_cache_path hash, ext
+ dir = File.join(CACHE_DIR, hash[0,2])
+ Dir.mkdir(dir) unless File.exists?(dir)
+ File.join(dir, hash[2..-1]) + ext.downcase
+ end
+ def snapshot(options = {}, &block)
+ options[:format] ||= :svg
+
+ options[:filename] ||= ( tf_path = ( require 'tempfile'
+ tf = Tempfile.new(File.basename(__FILE__)).path ))
+
+ _snapshot options do
+ block.call
+ end
+ return File.read(options[:filename])
+ ensure
+ File.unlink(tf_path) if tf_path
+ end
+end
diff --git a/lib/shoes/inspect.rb b/lib/shoes/inspect.rb
new file mode 100644
index 0000000..3a2c637
--- /dev/null
+++ b/lib/shoes/inspect.rb
@@ -0,0 +1,128 @@
+module Kernel
+ def inspect(hits = {})
+ return "(#{self.class} ...)" if hits[self]
+ hits[self] = true
+ if instance_variables.empty?
+ "(#{self.class})"
+ else
+ "(#{self.class} " +
+ instance_variables.map do |x|
+ v = instance_variable_get(x)
+ "#{x}=" + (v.method(:inspect).arity == 0 ? v.inspect : v.inspect(hits))
+ end.join(' ') +
+ ")"
+ end
+ end
+ #def to_html
+ # obj = self
+ # Web.Bit {
+ # h1 "A #{obj.class}"
+ # }
+ #end
+ def to_s; inspect end
+end
+
+class Array
+ def inspect(hits = {})
+ return "[...]" if hits[self]
+ hits[self] = true
+ "[" + map { |x| x.method(:inspect).arity == 0 ? x.inspect : x.inspect(hits) }.join(', ') + "]"
+ end
+ def to_html
+ ary = self
+ Web.Bit {
+ h5 "A List of Things"
+ h1 "An Array"
+ unless ary.empty?
+ ol {
+ ary.map { |x| li { self << HTML(x) } }
+ }
+ end
+ }
+ end
+ def / len
+ a = []
+ each_with_index do |x, i|
+ a << [] if i % len == 0
+ a.last << x
+ end
+ a
+ end
+end
+
+class Hash
+ def inspect(hits = {})
+ return "{...}" if hits[self]
+ hits[self] = true
+ mappings = map do |k,v|
+ key = k.method(:inspect).arity == 0 ? k.inspect : k.inspect(hits)
+ val = v.method(:inspect).arity == 0 ? v.inspect : v.inspect(hits)
+ "#{key} => #{val}"
+ end
+ "{ #{mappings.join(', ')} }"
+ end
+ def to_html
+ h = self
+ Web.Bit {
+ h5 "Pairs of Things"
+ h1 "A Hash"
+ unless h.empty?
+ ul {
+ h.each { |k, v| li { self << "<div class='hashkey'>#{HTML(k)}</div><div class='hashvalue'>#{HTML(v)}</div>" } }
+ }
+ end
+ }
+ end
+end
+
+class File
+ def inspect(hits = nil)
+ "(#{self.class} #{path})"
+ end
+end
+
+class Proc
+ def inspect(hits = nil)
+ v = "a"
+ pvars = []
+ (arity < 0 ? -(arity+1) : arity).times do |i|
+ pvars << v
+ v = v.succ
+ end
+ pvars << "*#{v}" if arity < 0
+ "(Proc |#{pvars.join(',')}|)"
+ end
+end
+
+class Class
+ def make_inspect m = :inspect
+ alias_method :the_original_inspect, m
+ class_eval %{
+ def inspect(hits = nil)
+ the_original_inspect
+ end
+ }
+ #def to_html(hits = nil)
+ # #{m} + " <div class='classname'>" + self.class.name + "</div>"
+ #end
+ end
+end
+
+class Module; make_inspect :name end
+class Regexp; make_inspect end
+class String; make_inspect end
+class Symbol; make_inspect end
+class Time; make_inspect end
+class Numeric; make_inspect :to_s end
+class Bignum; make_inspect :to_s end
+class Fixnum; make_inspect :to_s end
+class Float; make_inspect :to_s end
+class TrueClass; make_inspect :to_s end
+class FalseClass; make_inspect :to_s end
+class NilClass; make_inspect end
+
+class Shoes::Types::App
+ def inspect(hits = nil)
+ "(#{self.class} #{name.dump})"
+ end
+end
diff --git a/lib/shoes/log.rb b/lib/shoes/log.rb
new file mode 100644
index 0000000..602e688
--- /dev/null
+++ b/lib/shoes/log.rb
@@ -0,0 +1,48 @@
+module Shoes::LogWindow
+ def setup
+ stack do
+ flow do
+ background black
+ stack :width => -100 do
+ tagline "Shoes Console", :stroke => white
+ end
+ button "Clear", :margin => 6, :width => 80, :height => 40 do
+ Shoes.log.clear
+ end
+ end
+ @log, @hash = stack, nil
+ update
+ every(0.2) do
+ update
+ end
+ end
+ end
+ def update
+ if @hash != Shoes.log.hash
+ @hash = Shoes.log.hash
+ @log.clear do
+ i = 0
+ Shoes.log.each do |typ, msg, at, mid, rbf, rbl|
+ stack do
+ background "#f1f5e1" if i % 2 == 0
+ inscription strong(typ.to_s.capitalize, :stroke => "#05C"), " in ",
+ span(rbf, " line ", rbl, :stroke => "#335"), " | ",
+ span(at, :stroke => "#777"),
+ :stroke => "#059", :margin => 4, :margin_bottom => 0
+ flow do
+ stack :margin => 6, :width => 20 do
+ image "#{DIR}/static/icon-#{typ}.png"
+ end
+ stack :margin => 4, :width => -20 do
+ s = msg.to_s.force_encoding "UTF-8"
+ s << "\n#{msg.backtrace.join("\n")}" if msg.kind_of?(Exception)
+ para s, :margin => 4, :margin_top => 0
+ end
+ end
+ end
+ i += 1
+ end
+ end
+ end
+ end
+end
diff --git a/lib/shoes/minitar.rb b/lib/shoes/minitar.rb
new file mode 100644
index 0000000..87617f9
--- /dev/null
+++ b/lib/shoes/minitar.rb
@@ -0,0 +1,986 @@
+#!/usr/bin/env ruby
+#--
+# Archive::Tar::Minitar 0.5.1
+# Copyright © 2004 Mauricio Julio Fernández Pradier and Austin Ziegler
+#
+# This program is based on and incorporates parts of RPA::Package from
+# rpa-base (lib/rpa/package.rb and lib/rpa/util.rb) by Mauricio and has been
+# adapted to be more generic by Austin.
+#
+# It is licensed under the GNU General Public Licence or Ruby's licence.
+#
+# $Id: minitar.rb,v 1.3 2004/09/22 17:47:43 austin Exp $
+#++
+
+module Archive; end
+module Archive::Tar; end
+
+ # = Archive::Tar::PosixHeader
+ # Implements the POSIX tar header as a Ruby class. The structure of
+ # the POSIX tar header is:
+ #
+ # struct tarfile_entry_posix
+ # { // pack/unpack
+ # char name[100]; // ASCII (+ Z unless filled) a100/Z100
+ # char mode[8]; // 0 padded, octal, null a8 /A8
+ # char uid[8]; // ditto a8 /A8
+ # char gid[8]; // ditto a8 /A8
+ # char size[12]; // 0 padded, octal, null a12 /A12
+ # char mtime[12]; // 0 padded, octal, null a12 /A12
+ # char checksum[8]; // 0 padded, octal, null, space a8 /A8
+ # char typeflag[1]; // see below a /a
+ # char linkname[100]; // ASCII + (Z unless filled) a100/Z100
+ # char magic[6]; // "ustar\0" a6 /A6
+ # char version[2]; // "00" a2 /A2
+ # char uname[32]; // ASCIIZ a32 /Z32
+ # char gname[32]; // ASCIIZ a32 /Z32
+ # char devmajor[8]; // 0 padded, octal, null a8 /A8
+ # char devminor[8]; // 0 padded, octal, null a8 /A8
+ # char prefix[155]; // ASCII (+ Z unless filled) a155/Z155
+ # };
+ #
+ # The +typeflag+ may be one of the following known values:
+ #
+ # <tt>"0"</tt>:: Regular file. NULL should be treated as a synonym, for
+ # compatibility purposes.
+ # <tt>"1"</tt>:: Hard link.
+ # <tt>"2"</tt>:: Symbolic link.
+ # <tt>"3"</tt>:: Character device node.
+ # <tt>"4"</tt>:: Block device node.
+ # <tt>"5"</tt>:: Directory.
+ # <tt>"6"</tt>:: FIFO node.
+ # <tt>"7"</tt>:: Reserved.
+ #
+ # POSIX indicates that "A POSIX-compliant implementation must treat any
+ # unrecognized typeflag value as a regular file."
+class Archive::Tar::PosixHeader
+ FIELDS = %w(name mode uid gid size mtime checksum typeflag linkname) +
+ %w(magic version uname gname devmajor devminor prefix)
+
+ FIELDS.each { |field| attr_reader field.intern }
+
+ HEADER_PACK_FORMAT = "a100a8a8a8a12a12a7aaa100a6a2a32a32a8a8a155"
+ HEADER_UNPACK_FORMAT = "Z100A8A8A8A12A12A8aZ100A6A2Z32Z32A8A8Z155"
+
+ # Creates a new PosixHeader from a data stream.
+ def self.new_from_stream(stream)
+ data = stream.read(512)
+ fields = data.unpack(HEADER_UNPACK_FORMAT)
+ name = fields.shift
+ mode = fields.shift.oct
+ uid = fields.shift.oct
+ gid = fields.shift.oct
+ size = fields.shift.oct
+ mtime = fields.shift.oct
+ checksum = fields.shift.oct
+ typeflag = fields.shift
+ linkname = fields.shift
+ magic = fields.shift
+ version = fields.shift.oct
+ uname = fields.shift
+ gname = fields.shift
+ devmajor = fields.shift.oct
+ devminor = fields.shift.oct
+ prefix = fields.shift
+
+ empty = (data == "\0" * 512)
+
+ new(:name => name, :mode => mode, :uid => uid, :gid => gid,
+ :size => size, :mtime => mtime, :checksum => checksum,
+ :typeflag => typeflag, :magic => magic, :version => version,
+ :uname => uname, :gname => gname, :devmajor => devmajor,
+ :devminor => devminor, :prefix => prefix, :empty => empty)
+ end
+
+ # Creates a new PosixHeader. A PosixHeader cannot be created unless the
+ # #name, #size, #prefix, and #mode are provided.
+ def initialize(vals)
+ unless vals[:name] && vals[:size] && vals[:prefix] && vals[:mode]
+ raise ArgumentError
+ end
+
+ vals[:mtime] ||= 0
+ vals[:checksum] ||= ""
+ vals[:typeflag] ||= "0"
+ vals[:magic] ||= "ustar"
+ vals[:version] ||= "00"
+
+ FIELDS.each do |field|
+ instance_variable_set("@#{field}", vals[field.intern])
+ end
+ @empty = vals[:empty]
+ end
+
+ def empty?
+ @empty
+ end
+
+ def to_s
+ update_checksum
+ header(@checksum)
+ end
+
+ # Update the checksum field.
+ def update_checksum
+ hh = header(" " * 8)
+ @checksum = oct(calculate_checksum(hh), 6)
+ end
+
+ private
+ def oct(num, len)
+ if num.nil?
+ "\0" * (len + 1)
+ else
+ "%0#{len}o" % num
+ end
+ end
+
+ def calculate_checksum(hdr)
+ hdr.unpack("C*").inject { |aa, bb| aa + bb }
+ end
+
+ def header(chksum)
+ arr = [name, oct(mode, 7), oct(uid, 7), oct(gid, 7), oct(size, 11),
+ oct(mtime, 11), chksum, " ", typeflag, linkname, magic, version,
+ uname, gname, oct(devmajor, 7), oct(devminor, 7), prefix]
+ str = arr.pack(HEADER_PACK_FORMAT)
+ str + "\0" * ((512 - str.size) % 512)
+ end
+end
+
+require 'fileutils'
+require 'find'
+
+ # = Archive::Tar::Minitar 0.5.1
+ # Archive::Tar::Minitar is a pure-Ruby library and command-line
+ # utility that provides the ability to deal with POSIX tar(1) archive
+ # files. The implementation is based heavily on Mauricio Fernández's
+ # implementation in rpa-base, but has been reorganised to promote
+ # reuse in other projects.
+ #
+ # This tar class performs a subset of all tar (POSIX tape archive)
+ # operations. We can only deal with typeflags 0, 1, 2, and 5 (see
+ # Archive::Tar::PosixHeader). All other typeflags will be treated as
+ # normal files.
+ #
+ # NOTE::: support for typeflags 1 and 2 is not yet implemented in this
+ # version.
+ #
+ # This release is version 0.5.1. The library can only handle files and
+ # directories at this point. A future version will be expanded to
+ # handle symbolic links and hard links in a portable manner. The
+ # command line utility, minitar, can only create archives, extract
+ # from archives, and list archive contents.
+ #
+ # == Synopsis
+ # Using this library is easy. The simplest case is:
+ #
+ # require 'zlib'
+ # require 'archive/tar/minitar'
+ # include Archive::Tar
+ #
+ # # Packs everything that matches Find.find('tests')
+ # File.open('test.tar', 'wb') { |tar| Minitar.pack('tests', tar) }
+ # # Unpacks 'test.tar' to 'x', creating 'x' if necessary.
+ # Minitar.unpack('test.tar', 'x')
+ #
+ # A gzipped tar can be written with:
+ #
+ # tgz = Zlib::GzipWriter.new(File.open('test.tgz', 'wb'))
+ # # Warning: tgz will be closed!
+ # Minitar.pack('tests', tgz)
+ #
+ # tgz = Zlib::GzipReader.new(File.open('test.tgz', 'rb'))
+ # # Warning: tgz will be closed!
+ # Minitar.unpack(tgz, 'x')
+ #
+ # As the case above shows, one need not write to a file. However, it
+ # will sometimes require that one dive a little deeper into the API,
+ # as in the case of StringIO objects. Note that I'm not providing a
+ # block with Minitar::Output, as Minitar::Output#close automatically
+ # closes both the Output object and the wrapped data stream object.
+ #
+ # begin
+ # sgz = Zlib::GzipWriter.new(StringIO.new(""))
+ # tar = Output.new(sgz)
+ # Find.find('tests') do |entry|
+ # Minitar.pack_file(entry, tar)
+ # end
+ # ensure
+ # # Closes both tar and sgz.
+ # tar.close
+ # end
+ #
+ # == Copyright
+ # Copyright 2004 Mauricio Julio Fernández Pradier and Austin Ziegler
+ #
+ # This program is based on and incorporates parts of RPA::Package from
+ # rpa-base (lib/rpa/package.rb and lib/rpa/util.rb) by Mauricio and
+ # has been adapted to be more generic by Austin.
+ #
+ # 'minitar' contains an adaptation of Ruby/ProgressBar by Satoru
+ # Takabayashi <satoru@namazu.org>, copyright © 2001 - 2004.
+ #
+ # This program is free software. It may be redistributed and/or
+ # modified under the terms of the GPL version 2 (or later) or Ruby's
+ # licence.
+module Archive::Tar::Minitar
+ VERSION = "0.5.1"
+
+ # The exception raised when a wrapped data stream class is expected to
+ # respond to #rewind or #pos but does not.
+ class NonSeekableStream < StandardError; end
+ # The exception raised when a block is required for proper operation of
+ # the method.
+ class BlockRequired < ArgumentError; end
+ # The exception raised when operations are performed on a stream that has
+ # previously been closed.
+ class ClosedStream < StandardError; end
+ # The exception raised when a filename exceeds 256 bytes in length,
+ # the maximum supported by the standard Tar format.
+ class FileNameTooLong < StandardError; end
+ # The exception raised when a data stream ends before the amount of data
+ # expected in the archive's PosixHeader.
+ class UnexpectedEOF < StandardError; end
+
+ # The class that writes a tar format archive to a data stream.
+ class Writer
+ # A stream wrapper that can only be written to. Any attempt to read
+ # from this restricted stream will result in a NameError being thrown.
+ class RestrictedStream
+ def initialize(anIO)
+ @io = anIO
+ end
+
+ def write(data)
+ @io.write(data)
+ end
+ end
+
+ # A RestrictedStream that also has a size limit.
+ class BoundedStream < Archive::Tar::Minitar::Writer::RestrictedStream
+ # The exception raised when the user attempts to write more data to
+ # a BoundedStream than has been allocated.
+ class FileOverflow < RuntimeError; end
+
+ # The maximum number of bytes that may be written to this data
+ # stream.
+ attr_reader :limit
+ # The current total number of bytes written to this data stream.
+ attr_reader :written
+
+ def initialize(io, limit)
+ @io = io
+ @limit = limit
+ @written = 0
+ end
+
+ def write(data)
+ raise FileOverflow if (data.size + @written) > @limit
+ @io.write(data)
+ @written += data.size
+ data.size
+ end
+ end
+
+ # With no associated block, +Writer::open+ is a synonym for
+ # +Writer::new+. If the optional code block is given, it will be
+ # passed the new _writer_ as an argument and the Writer object will
+ # automatically be closed when the block terminates. In this instance,
+ # +Writer::open+ returns the value of the block.
+ def self.open(anIO)
+ writer = Writer.new(anIO)
+
+ return writer unless block_given?
+
+ begin
+ res = yield writer
+ ensure
+ writer.close
+ end
+
+ res
+ end
+
+ # Creates and returns a new Writer object.
+ def initialize(anIO)
+ @io = anIO
+ @closed = false
+ end
+
+ # Adds a file to the archive as +name+. +opts+ must contain the
+ # following values:
+ #
+ # <tt>:mode</tt>:: The Unix file permissions mode value.
+ # <tt>:size</tt>:: The size, in bytes.
+ #
+ # +opts+ may contain the following values:
+ #
+ # <tt>:uid</tt>: The Unix file owner user ID number.
+ # <tt>:gid</tt>: The Unix file owner group ID number.
+ # <tt>:mtime</tt>:: The *integer* modification time value.
+ #
+ # It will not be possible to add more than <tt>opts[:size]</tt> bytes
+ # to the file.
+ def add_file_simple(name, opts = {}) # :yields BoundedStream:
+ raise Archive::Tar::Minitar::BlockRequired unless block_given?
+ raise Archive::Tar::ClosedStream if @closed
+
+ name, prefix = split_name(name)
+
+ header = { :name => name, :mode => opts[:mode], :mtime => opts[:mtime],
+ :size => opts[:size], :gid => opts[:gid], :uid => opts[:uid],
+ :prefix => prefix }
+ header = Archive::Tar::PosixHeader.new(header).to_s
+ @io.write(header)
+
+ os = BoundedStream.new(@io, opts[:size])
+ yield os
+ # FIXME: what if an exception is raised in the block?
+
+ min_padding = opts[:size] - os.written
+ @io.write("\0" * min_padding)
+ remainder = (512 - (opts[:size] % 512)) % 512
+ @io.write("\0" * remainder)
+ end
+
+ # Adds a file to the archive as +name+. +opts+ must contain the
+ # following value:
+ #
+ # <tt>:mode</tt>:: The Unix file permissions mode value.
+ #
+ # +opts+ may contain the following values:
+ #
+ # <tt>:uid</tt>: The Unix file owner user ID number.
+ # <tt>:gid</tt>: The Unix file owner group ID number.
+ # <tt>:mtime</tt>:: The *integer* modification time value.
+ #
+ # The file's size will be determined from the amount of data written
+ # to the stream.
+ #
+ # For #add_file to be used, the Archive::Tar::Minitar::Writer must be
+ # wrapping a stream object that is seekable (e.g., it responds to
+ # #pos=). Otherwise, #add_file_simple must be used.
+ #
+ # +opts+ may be modified during the writing to the stream.
+ def add_file(name, opts = {}) # :yields RestrictedStream, +opts+:
+ raise Archive::Tar::Minitar::BlockRequired unless block_given?
+ raise Archive::Tar::Minitar::ClosedStream if @closed
+ raise Archive::Tar::Minitar::NonSeekableStream unless @io.respond_to?(:pos=)
+
+ name, prefix = split_name(name)
+ init_pos = @io.pos
+ @io.write("\0" * 512) # placeholder for the header
+
+ yield RestrictedStream.new(@io), opts
+ # FIXME: what if an exception is raised in the block?
+
+ size = @io.pos - (init_pos + 512)
+ remainder = (512 - (size % 512)) % 512
+ @io.write("\0" * remainder)
+
+ final_pos = @io.pos
+ @io.pos = init_pos
+
+ header = { :name => name, :mode => opts[:mode], :mtime => opts[:mtime],
+ :size => size, :gid => opts[:gid], :uid => opts[:uid],
+ :prefix => prefix }
+ header = Archive::Tar::PosixHeader.new(header).to_s
+ @io.write(header)
+ @io.pos = final_pos
+ end
+
+ # Creates a directory in the tar.
+ def mkdir(name, opts = {})
+ raise ClosedStream if @closed
+ name, prefix = split_name(name)
+ header = { :name => name, :mode => opts[:mode], :typeflag => "5",
+ :size => 0, :gid => opts[:gid], :uid => opts[:uid],
+ :mtime => opts[:mtime], :prefix => prefix }
+ header = Archive::Tar::PosixHeader.new(header).to_s
+ @io.write(header)
+ nil
+ end
+
+ # Passes the #flush method to the wrapped stream, used for buffered
+ # streams.
+ def flush
+ raise ClosedStream if @closed
+ @io.flush if @io.respond_to?(:flush)
+ end
+
+ # Closes the Writer.
+ def close
+ return if @closed
+ @io.write("\0" * 1024)
+ @closed = true
+ end
+
+ private
+ def split_name(name)
+ raise FileNameTooLong if name.size > 256
+ if name.size <= 100
+ prefix = ""
+ else
+ parts = name.split(/\//)
+ newname = parts.pop
+
+ nxt = ""
+
+ loop do
+ nxt = parts.pop
+ break if newname.size + 1 + nxt.size > 100
+ newname = "#{nxt}/#{newname}"
+ end
+
+ prefix = (parts + [nxt]).join("/")
+
+ name = newname
+
+ raise FileNameTooLong if name.size > 100 || prefix.size > 155
+ end
+ return name, prefix
+ end
+ end
+
+ # The class that reads a tar format archive from a data stream. The data
+ # stream may be sequential or random access, but certain features only work
+ # with random access data streams.
+ class Reader
+ # This marks the EntryStream closed for reading without closing the
+ # actual data stream.
+ module InvalidEntryStream
+ def read(len = nil); raise ClosedStream; end
+ def getc; raise ClosedStream; end
+ def rewind; raise ClosedStream; end
+ end
+
+ # EntryStreams are pseudo-streams on top of the main data stream.
+ class EntryStream
+ Archive::Tar::PosixHeader::FIELDS.each do |field|
+ attr_reader field.intern
+ end
+
+ def initialize(header, anIO)
+ @io = anIO
+ @name = header.name
+ @mode = header.mode
+ @uid = header.uid
+ @gid = header.gid
+ @size = header.size
+ @mtime = header.mtime
+ @checksum = header.checksum
+ @typeflag = header.typeflag
+ @linkname = header.linkname
+ @magic = header.magic
+ @version = header.version
+ @uname = header.uname
+ @gname = header.gname
+ @devmajor = header.devmajor
+ @devminor = header.devminor
+ @prefix = header.prefix
+ @read = 0
+ @orig_pos = @io.pos
+ end
+
+ # Reads +len+ bytes (or all remaining data) from the entry. Returns
+ # +nil+ if there is no more data to read.
+ def read(len = nil)
+ return nil if @read >= @size
+ len ||= @size - @read
+ max_read = [len, @size - @read].min
+ ret = @io.read(max_read)
+ @read += ret.size
+ ret
+ end
+
+ # Reads one byte from the entry. Returns +nil+ if there is no more data
+ # to read.
+ def getc
+ return nil if @read >= @size
+ ret = @io.getc
+ @read += 1 if ret
+ ret
+ end
+
+ # Returns +true+ if the entry represents a directory.
+ def directory?
+ @typeflag == "5"
+ end
+ alias_method :directory, :directory?
+
+ # Returns +true+ if the entry represents a plain file.
+ def file?
+ @typeflag == "0"
+ end
+ alias_method :file, :file?
+
+ # Returns +true+ if the current read pointer is at the end of the
+ # EntryStream data.
+ def eof?
+ @read >= @size
+ end
+
+ # Returns the current read pointer in the EntryStream.
+ def pos
+ @read
+ end
+
+ # Sets the current read pointer to the beginning of the EntryStream.
+ def rewind
+ raise NonSeekableStream unless @io.respond_to?(:pos=)
+ @io.pos = @orig_pos
+ @read = 0
+ end
+
+ def orig_pos
+ @orig_pos
+ end
+
+ def bytes_read
+ @read
+ end
+
+ # Returns the full and proper name of the entry.
+ def full_name
+ if @prefix != ""
+ File.join(@prefix, @name)
+ else
+ @name
+ end
+ end
+
+ # Closes the entry.
+ def close
+ invalidate
+ end
+
+ private
+ def invalidate
+ extend InvalidEntryStream
+ end
+ end
+
+ # With no associated block, +Reader::open+ is a synonym for
+ # +Reader::new+. If the optional code block is given, it will be passed
+ # the new _writer_ as an argument and the Reader object will
+ # automatically be closed when the block terminates. In this instance,
+ # +Reader::open+ returns the value of the block.
+ def self.open(anIO)
+ reader = Reader.new(anIO)
+
+ return reader unless block_given?
+
+ begin
+ res = yield reader
+ ensure
+ reader.close
+ end
+
+ res
+ end
+
+ # Creates and returns a new Reader object.
+ def initialize(anIO)
+ @io = anIO
+ @init_pos = anIO.pos
+ end
+
+ # Iterates through each entry in the data stream.
+ def each(&block)
+ each_entry(&block)
+ end
+
+ # Resets the read pointer to the beginning of data stream. Do not call
+ # this during a #each or #each_entry iteration. This only works with
+ # random access data streams that respond to #rewind and #pos.
+ def rewind
+ if @init_pos == 0
+ raise NonSeekableStream unless @io.respond_to?(:rewind)
+ @io.rewind
+ else
+ raise NonSeekableStream unless @io.respond_to?(:pos=)
+ @io.pos = @init_pos
+ end
+ end
+
+ # Iterates through each entry in the data stream.
+ def each_entry
+ loop do
+ return if @io.eof?
+
+ header = Archive::Tar::PosixHeader.new_from_stream(@io)
+ return if header.empty?
+
+ entry = EntryStream.new(header, @io)
+ size = entry.size
+
+ yield entry
+
+ skip = (512 - (size % 512)) % 512
+
+ if @io.respond_to?(:seek)
+ # avoid reading...
+ @io.seek(size - entry.bytes_read, IO::SEEK_CUR)
+ else
+ pending = size - entry.bytes_read
+ while pending > 0
+ bread = @io.read([pending, 4096].min).size
+ raise UnexpectedEOF if @io.eof?
+ pending -= bread
+ end
+ end
+ @io.read(skip) # discard trailing zeros
+ # make sure nobody can use #read, #getc or #rewind anymore
+ entry.close
+ end
+ end
+
+ def close
+ end
+ end
+
+ # Wraps a Archive::Tar::Minitar::Reader with convenience methods and
+ # wrapped stream management; Input only works with random access data
+ # streams. See Input::new for details.
+ class Input
+ include Enumerable
+
+ # With no associated block, +Input::open+ is a synonym for
+ # +Input::new+. If the optional code block is given, it will be passed
+ # the new _writer_ as an argument and the Input object will
+ # automatically be closed when the block terminates. In this instance,
+ # +Input::open+ returns the value of the block.
+ def self.open(input)
+ stream = Input.new(input)
+ return stream unless block_given?
+
+ begin
+ res = yield stream
+ ensure
+ stream.close
+ end
+
+ res
+ end
+
+ # Creates a new Input object. If +input+ is a stream object that responds
+ # to #read), then it will simply be wrapped. Otherwise, one will be
+ # created and opened using Kernel#open. When Input#close is called, the
+ # stream object wrapped will be closed.
+ def initialize(input)
+ if input.respond_to?(:read)
+ @io = input
+ else
+ @io = open(input, "rb")
+ end
+ @tarreader = Archive::Tar::Minitar::Reader.new(@io)
+ end
+
+ # Iterates through each entry and rewinds to the beginning of the stream
+ # when finished.
+ def each(&block)
+ @tarreader.each { |entry| yield entry }
+ ensure
+ @tarreader.rewind
+ end
+
+ # Extracts the current +entry+ to +destdir+. If a block is provided, it
+ # yields an +action+ Symbol, the full name of the file being extracted
+ # (+name+), and a Hash of statistical information (+stats+).
+ #
+ # The +action+ will be one of:
+ # <tt>:dir</tt>:: The +entry+ is a directory.
+ # <tt>:file_start</tt>:: The +entry+ is a file; the extract of the
+ # file is just beginning.
+ # <tt>:file_progress</tt>:: Yielded every 4096 bytes during the extract
+ # of the +entry+.
+ # <tt>:file_done</tt>:: Yielded when the +entry+ is completed.
+ #
+ # The +stats+ hash contains the following keys:
+ # <tt>:current</tt>:: The current total number of bytes read in the
+ # +entry+.
+ # <tt>:currinc</tt>:: The current number of bytes read in this read
+ # cycle.
+ # <tt>:entry</tt>:: The entry being extracted; this is a
+ # Reader::EntryStream, with all methods thereof.
+ def extract_entry(destdir, entry) # :yields action, name, stats:
+ stats = {
+ :current => 0,
+ :currinc => 0,
+ :entry => entry
+ }
+
+ if entry.directory?
+ dest = File.join(destdir, entry.full_name)
+
+ yield :dir, entry.full_name, stats if block_given?
+
+ if Archive::Tar::Minitar.dir?(dest)
+ begin
+ FileUtils.chmod(entry.mode, dest)
+ rescue Exception
+ nil
+ end
+ else
+ FileUtils.mkdir_p(dest, :mode => entry.mode)
+ FileUtils.chmod(entry.mode, dest)
+ end
+
+ fsync_dir(dest)
+ fsync_dir(File.join(dest, ".."))
+ return
+ else # it's a file
+ destdir = File.join(destdir, File.dirname(entry.full_name))
+ FileUtils.mkdir_p(destdir, :mode => 0755)
+
+ destfile = File.join(destdir, File.basename(entry.full_name))
+ FileUtils.chmod(0600, destfile) rescue nil # Errno::ENOENT
+
+ yield :file_start, entry.full_name, stats if block_given?
+
+ File.open(destfile, "wb", entry.mode) do |os|
+ loop do
+ data = entry.read(4096)
+ break unless data
+
+ stats[:currinc] = os.write(data)
+ stats[:current] += stats[:currinc]
+
+ yield :file_progress, entry.full_name, stats if block_given?
+ end
+ os.fsync
+ end
+
+ FileUtils.chmod(entry.mode, destfile)
+ fsync_dir(File.dirname(destfile))
+ fsync_dir(File.join(File.dirname(destfile), ".."))
+
+ yield :file_done, entry.full_name, stats if block_given?
+ end
+ end
+
+ # Returns the Reader object for direct access.
+ def tar
+ @tarreader
+ end
+
+ # Closes the Reader object and the wrapped data stream.
+ def close
+ @io.close
+ @tarreader.close
+ end
+
+ private
+ def fsync_dir(dirname)
+ # make sure this hits the disc
+ dir = open(dirname, 'rb')
+ dir.fsync
+ rescue # ignore IOError if it's an unpatched (old) Ruby
+ nil
+ ensure
+ dir.close if dir rescue nil
+ end
+ end
+
+ # Wraps a Archive::Tar::Minitar::Writer with convenience methods and
+ # wrapped stream management; Output only works with random access data
+ # streams. See Output::new for details.
+ class Output
+ # With no associated block, +Output::open+ is a synonym for
+ # +Output::new+. If the optional code block is given, it will be passed
+ # the new _writer_ as an argument and the Output object will
+ # automatically be closed when the block terminates. In this instance,
+ # +Output::open+ returns the value of the block.
+ def self.open(output)
+ stream = Output.new(output)
+ return stream unless block_given?
+
+ begin
+ res = yield stream
+ ensure
+ stream.close
+ end
+
+ res
+ end
+
+ # Creates a new Output object. If +output+ is a stream object that
+ # responds to #read), then it will simply be wrapped. Otherwise, one will
+ # be created and opened using Kernel#open. When Output#close is called,
+ # the stream object wrapped will be closed.
+ def initialize(output)
+ if output.respond_to?(:write)
+ @io = output
+ else
+ @io = ::File.open(output, "wb")
+ end
+ @tarwriter = Archive::Tar::Minitar::Writer.new(@io)
+ end
+
+ # Returns the Writer object for direct access.
+ def tar
+ @tarwriter
+ end
+
+ # Closes the Writer object and the wrapped data stream.
+ def close
+ @tarwriter.close
+ @io.close
+ end
+ end
+
+ class << self
+ # Tests if +path+ refers to a directory. Fixes an apparently
+ # corrupted <tt>stat()</tt> call on Windows.
+ def dir?(path)
+ File.directory?((path[-1] == ?/) ? path : "#{path}/")
+ end
+
+ # A convenience method for wrapping Archive::Tar::Minitar::Input.open
+ # (mode +r+) and Archive::Tar::Minitar::Output.open (mode +w+). No other
+ # modes are currently supported.
+ def open(dest, mode = "r", &block)
+ case mode
+ when "r"
+ Input.open(dest, &block)
+ when "w"
+ Output.open(dest, &block)
+ else
+ raise "Unknown open mode for Archive::Tar::Minitar.open."
+ end
+ end
+
+ # A convenience method to packs the file provided. +entry+ may either be
+ # a filename (in which case various values for the file (see below) will
+ # be obtained from <tt>File#stat(entry)</tt> or a Hash with the fields:
+ #
+ # <tt>:name</tt>:: The filename to be packed into the tarchive.
+ # *REQUIRED*.
+ # <tt>:mode</tt>:: The mode to be applied.
+ # <tt>:uid</tt>:: The user owner of the file. (Ignored on Windows.)
+ # <tt>:gid</tt>:: The group owner of the file. (Ignored on Windows.)
+ # <tt>:mtime</tt>:: The modification Time of the file.
+ #
+ # During packing, if a block is provided, #pack_file yields an +action+
+ # Symol, the full name of the file being packed, and a Hash of
+ # statistical information, just as with
+ # Archive::Tar::Minitar::Input#extract_entry.
+ #
+ # The +action+ will be one of:
+ # <tt>:dir</tt>:: The +entry+ is a directory.
+ # <tt>:file_start</tt>:: The +entry+ is a file; the extract of the
+ # file is just beginning.
+ # <tt>:file_progress</tt>:: Yielded every 4096 bytes during the extract
+ # of the +entry+.
+ # <tt>:file_done</tt>:: Yielded when the +entry+ is completed.
+ #
+ # The +stats+ hash contains the following keys:
+ # <tt>:current</tt>:: The current total number of bytes read in the
+ # +entry+.
+ # <tt>:currinc</tt>:: The current number of bytes read in this read
+ # cycle.
+ # <tt>:name</tt>:: The filename to be packed into the tarchive.
+ # *REQUIRED*.
+ # <tt>:mode</tt>:: The mode to be applied.
+ # <tt>:uid</tt>:: The user owner of the file. (+nil+ on Windows.)
+ # <tt>:gid</tt>:: The group owner of the file. (+nil+ on Windows.)
+ # <tt>:mtime</tt>:: The modification Time of the file.
+ def pack_file(entry, outputter) #:yields action, name, stats:
+ outputter = outputter.tar if outputter.kind_of?(Archive::Tar::Minitar::Output)
+
+ stats = {}
+
+ if entry.kind_of?(Hash)
+ name = entry[:name]
+
+ entry.each { |kk, vv| stats[kk] = vv unless vv.nil? }
+ else
+ name = entry
+ end
+
+ name = name.sub(%r{\./}, '')
+ stat = File.stat(name)
+ stats[:mode] ||= stat.mode
+ stats[:mtime] ||= stat.mtime
+ stats[:size] = stat.size
+ if name == "sh-install" # an ugly shoes-specific hack, to
+ stats[:mode] = 0755 # get the file to be 0755 on windows
+ end
+
+ if RUBY_PLATFORM =~ /win32/
+ stats[:uid] = nil
+ stats[:gid] = nil
+ else
+ stats[:uid] ||= stat.uid
+ stats[:gid] ||= stat.gid
+ end
+
+ case
+ when File.file?(name)
+ outputter.add_file_simple(name, stats) do |os|
+ stats[:current] = 0
+ yield :file_start, name, stats if block_given?
+ File.open(name, "rb") do |ff|
+ until ff.eof?
+ stats[:currinc] = os.write(ff.read(4096))
+ stats[:current] += stats[:currinc]
+ yield :file_progress, name, stats if block_given?
+ end
+ end
+ yield :file_done, name, stats if block_given?
+ end
+ when dir?(name)
+ yield :dir, name, stats if block_given?
+ outputter.mkdir(name, stats)
+ else
+ raise "Don't yet know how to pack this type of file."
+ end
+ end
+
+ # A convenience method to pack files specified by +src+ into +dest+. If
+ # +src+ is an Array, then each file detailed therein will be packed into
+ # the resulting Archive::Tar::Minitar::Output stream; if +recurse_dirs+
+ # is true, then directories will be recursed.
+ #
+ # If +src+ is an Array, it will be treated as the argument to Find.find;
+ # all files matching will be packed.
+ def pack(src, dest, recurse_dirs = true, &block)
+ Output.open(dest) do |outp|
+ if src.kind_of?(Array)
+ src.each do |entry|
+ pack_file(entry, outp, &block)
+ if dir?(entry) and recurse_dirs
+ Dir["#{entry}/**/**"].each do |ee|
+ pack_file(ee, outp, &block)
+ end
+ end
+ end
+ else
+ Find.find(src) do |entry|
+ pack_file(entry, outp, &block)
+ end
+ end
+ end
+ end
+
+ # A convenience method to unpack files from +src+ into the directory
+ # specified by +dest+. Only those files named explicitly in +files+
+ # will be extracted.
+ def unpack(src, dest, files = [], &block)
+ Input.open(src) do |inp|
+ if File.exist?(dest) and (not dir?(dest))
+ raise "Can't unpack to a non-directory."
+ elsif not File.exist?(dest)
+ FileUtils.mkdir_p(dest)
+ end
+
+ inp.each do |entry|
+ if files.empty? or files.include?(entry.full_name)
+ inp.extract_entry(dest, entry, &block)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/shoes/override.rb b/lib/shoes/override.rb
new file mode 100644
index 0000000..8fab47f
--- /dev/null
+++ b/lib/shoes/override.rb
@@ -0,0 +1,38 @@
+module Override
+ def self.extended mod
+ def mod.list_box args = {}
+ l, t, w, h = args[:left], args[:top], args[:width], 20
+ w ||= 200
+ bcolor = rgb(123, 158, 189)
+ selected, fimg, bimg = [], nil, nil
+
+ f = flow :left => l, :top => t, :width => w, :height => h do
+ border bcolor
+ selected[0] = inscription
+ fimg = image "#{DIR}/static/listbox_button1.png", :left => w-17, :top => 2
+ bimg = image("#{DIR}/static/listbox_button2.png", :left => w-17, :top => 2).hide
+ fimg.hover{bimg.show}
+ bimg.leave{bimg.hide}
+ bimg.click{bimg.show}
+ end
+
+ rects, inscs = [], []
+ args[:items].length.times do |i|
+ x, y = l, t+(i+1)*h
+ r = rect(x, y, w-1, h, :stroke => bcolor, :fill => white).hide
+ s = inscription(args[:items][i], :left => x, :top => y).hide
+ r.hover{r.style :fill => blue}
+ r.leave{r.style :fill => white}
+ r.click{selected[0].text = s.text; selected[1] = r}
+ rects << r
+ inscs << s
+ end
+
+ f.click do
+ rects.each{|r| r.toggle; r.style(:fill => blue) if r == selected[1]}
+ inscs.each{|i| i.toggle}
+ end
+ end
+ end
+end
+
diff --git a/lib/shoes/pack.rb b/lib/shoes/pack.rb
new file mode 100644
index 0000000..bb1b4d0
--- /dev/null
+++ b/lib/shoes/pack.rb
@@ -0,0 +1,543 @@
+#
+# lib/shoes/pack.rb
+# Packing apps into Windows, OS X and Linux binaries
+#
+require 'shoes/shy'
+require 'binject'
+require 'open-uri'
+
+class Shoes
+ module Pack
+ def self.rewrite a, before, hsh
+ File.open(before) do |b|
+ b.each do |line|
+ a << line.gsub(/\#\{(\w+)\}/) { hsh[$1] }
+ end
+ end
+ end
+
+ def self.pkg(platform, opt)
+ $stderr.puts "#{platform} #{opt}"
+ extension = case platform
+ when "win32" then
+ "exe"
+ when "linux" then
+ "run"
+ when "osx" then
+ "dmg"
+ else
+ raise "Unknown platform"
+ end
+
+ case opt
+ when Shoes::I_YES then
+ url = "http://shoes.heroku.com/pkg/#{Shoes::RELEASE_NAME.downcase}/#{platform}/shoes"
+ local_file_path = File.join(LIB_DIR, Shoes::RELEASE_NAME.downcase, platform, "latest_shoes.#{extension}")
+ when Shoes::I_NOV then
+ url = "http://shoes.heroku.com/pkg/#{Shoes::RELEASE_NAME.downcase}/#{platform}/shoes-novideo"
+ local_file_path = File.join(LIB_DIR, Shoes::RELEASE_NAME.downcase, platform, "latest_shoes-novideo.#{extension}")
+ when I_NET then
+ url = false
+ else
+ raise "missing download option #{opt}"
+ end
+
+ FileUtils.makedirs File.join(LIB_DIR, Shoes::RELEASE_NAME.downcase, platform)
+
+ if url then
+ begin
+ url = open(url).read.strip
+ debug url
+ internet_ok = true
+ rescue Exception => e
+ error e
+ internet_ok = false
+ end
+
+ if File.exists? local_file_path
+ return open(local_file_path)
+ elsif internet_ok then
+ begin
+ debug "Downloading #{url}..."
+ downloaded = open(url)
+ debug "Download of #{url} finished"
+ rescue Exception => e
+ error "Could not download from the internet at #{url}\n" + e
+ internet_ok = false
+ end
+ if internet_ok then
+ begin
+ File.open(local_file_path, "wb") do |f|
+ f.write(downloaded.read)
+ end
+ return open(local_file_path)
+ rescue Exception => e
+ raise "The download failed from\n#{url}\nor could not write to local files" + e
+ end
+ end
+ else
+ noHopeMsg = "Failed to find an existing Shoes at:\n#{local_file_path}\nor download from\n#{url} to include with your script."
+ raise noHopeMsg
+ end
+ end
+ end
+
+ def self.exe(script, opt, &blk)
+ size = File.size(script)
+ f = File.open(script, 'rb')
+ exe = Binject::EXE.new(File.join(DIR, "static", "stubs", "blank.exe"))
+ size += script.length
+ exe.inject("SHOES_FILENAME", File.basename(script))
+ size += File.size(script)
+ exe.inject("SHOES_PAYLOAD", f)
+ f2 = pkg("win32", opt)
+ if f2
+ size += File.size(f2.path)
+ f3 = File.open(f2.path, 'rb')
+ exe.inject("SHOES_SETUP", f3)
+
+ count, last = 0, 0.0
+ exe.save(script.gsub(/\.\w+$/, '') + ".exe") do |len|
+ count += len
+ prg = count.to_f / size.to_f
+ blk[last = prg] if blk and prg - last > 0.02 and prg < 1.0
+ end
+
+ f.close
+ f2.close
+ f3.close
+ else
+ Dir.chdir DIR + '/static/stubs' do
+ `.\\shoes-stub-inject.exe #{script.gsub('/', "\\")}`
+ end
+ end
+ blk[1.0] if blk
+ end
+
+ def self.dmg(script, opt, &blk)
+ name = File.basename(script).gsub(/\.\w+$/, '')
+ app_name = name.capitalize.gsub(/[-_](\w)/) { $1.capitalize }
+ vol_name = name.capitalize.gsub(/[-_](\w)/) { " " + $1.capitalize }
+ app_app = "#{app_name}.app"
+ vers = [1, 0]
+
+ tmp_dir = File.join(LIB_DIR, "+dmg")
+ FileUtils.rm_rf(tmp_dir)
+ FileUtils.mkdir_p(tmp_dir)
+ FileUtils.cp(File.join(DIR, "static", "stubs", "blank.hfz"),
+ File.join(tmp_dir, "blank.hfz"))
+ app_dir = File.join(tmp_dir, app_app)
+ res_dir = File.join(tmp_dir, app_app, "Contents", "Resources")
+ mac_dir = File.join(tmp_dir, app_app, "Contents", "MacOS")
+ [res_dir, mac_dir].map { |x| FileUtils.mkdir_p(x) }
+ FileUtils.cp(File.join(DIR, "static", "Shoes.icns"), app_dir)
+ FileUtils.cp(File.join(DIR, "static", "Shoes.icns"), res_dir)
+ File.open(File.join(app_dir, "Contents", "PkgInfo"), 'w') do |f|
+ f << "APPL????"
+ end
+ File.open(File.join(app_dir, "Contents", "Info.plist"), 'w') do |f|
+ f << <<END
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+<key>CFBundleGetInfoString</key>
+<string>#{app_name} #{vers.join(".")}</string>
+<key>CFBundleExecutable</key>
+<string>#{name}-launch</string>
+<key>CFBundleIdentifier</key>
+<string>org.hackety.#{name}</string>
+<key>CFBundleName</key>
+<string>#{app_name}</string>
+<key>CFBundleIconFile</key>
+<string>Shoes.icns</string>
+<key>CFBundleShortVersionString</key>
+<string>#{vers.join(".")}</string>
+<key>CFBundleInfoDictionaryVersion</key>
+<string>6.0</string>
+<key>CFBundlePackageType</key>
+<string>APPL</string>
+<key>IFMajorVersion</key>
+<integer>#{vers[0]}</integer>
+<key>IFMinorVersion</key>
+<integer>#{vers[1]}</integer>
+</dict>
+</plist>
+END
+ end
+ File.open(File.join(app_dir, "Contents", "version.plist"), 'w') do |f|
+ f << <<END
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+<key>BuildVersion</key>
+<string>1</string>
+<key>CFBundleVersion</key>
+<string>#{vers.join(".")}</string>
+<key>ProjectName</key>
+<string>#{app_name}</string>
+<key>SourceVersion</key>
+<string>#{Time.now.strftime("%Y%m%d")}</string>
+</dict>
+</plist>
+END
+ end
+ File.open(File.join(mac_dir, "#{name}-launch"), 'w') do |f|
+ f << <<END
+#!/bin/bash
+SHOESPATH=/Applications/Shoes.app/Contents/MacOS
+APPPATH="${0%/*}"
+unset DYLD_LIBRARY_PATH
+cd "$APPPATH"
+echo "[Pango]" > /tmp/pangorc
+echo "ModuleFiles=$SHOESPATH/pango.modules" >> /tmp/pangorc
+if [ ! -d /Applications/Shoes.app ]
+ then ./cocoa-install
+ fi
+ open -a /Applications/Shoes.app "#{File.basename(script)}"
+ # DYLD_LIBRARY_PATH=$SHOESPATH PANGO_RC_FILE="$APPPATH/pangorc" $SHOESPATH/shoes-bin "#{File.basename(script)}"
+END
+ end
+ FileUtils.cp(script, File.join(mac_dir, File.basename(script)))
+ FileUtils.cp(File.join(DIR, "static", "stubs", "cocoa-install"),
+ File.join(mac_dir, "cocoa-install"))
+
+ dmg = Binject::DMG.new(File.join(tmp_dir, "blank.hfz"), vol_name)
+ f2 = pkg("osx", opt)
+ if f2
+ dmg.grow(10)
+ dmg.inject_file("setup.dmg", f2.path)
+ end
+ dmg.inject_dir(app_app, app_dir)
+ dmg.chmod_file(0755, "#{app_app}/Contents/MacOS/#{name}-launch")
+ dmg.chmod_file(0755, "#{app_app}/Contents/MacOS/cocoa-install")
+ dmg.save(script.gsub(/\.\w+$/, '') + ".dmg") do |perc|
+ blk[perc * 0.01] if blk
+ end
+ FileUtils.rm_rf(tmp_dir)
+ blk[1.0] if blk
+ end
+
+ def self.linux(script, opt, &blk)
+ name = File.basename(script).gsub(/\.\w+$/, '')
+ app_name = name.capitalize.gsub(/[-_](\w)/) { $1.capitalize }
+ run_path = script.gsub(/\.\w+$/, '') + ".run"
+ tgz_path = script.gsub(/\.\w+$/, '') + ".tgz"
+ tmp_dir = File.join(LIB_DIR, "+run")
+ FileUtils.mkdir_p(tmp_dir)
+ pkgf = pkg("linux", opt)
+ prog = 1.0
+ if pkgf
+ size = Shy.hrun(pkgf)
+ pblk = Shy.progress(size) do |name, perc, left|
+ blk[perc * 0.5]
+ end if blk
+ Shy.xzf(pkgf, tmp_dir, &pblk)
+ prog -= 0.5
+ end
+
+ FileUtils.cp(script, File.join(tmp_dir, File.basename(script)))
+ File.open(File.join(tmp_dir, "sh-install"), 'wb') do |a|
+ rewrite a, File.join(DIR, "static", "stubs", "sh-install"),
+ 'SCRIPT' => "./#{File.basename(script)}"
+ end
+ FileUtils.chmod 0755, File.join(tmp_dir, "sh-install")
+
+ raw = Shy.du(tmp_dir)
+ File.open(tgz_path, 'wb') do |f|
+ pblk = Shy.progress(raw) do |name, perc, left|
+ blk[prog + (perc * prog)]
+ end if blk
+ Shy.czf(f, tmp_dir, &pblk)
+ end
+
+ md5, fsize = Shy.md5sum(tgz_path), File.size(tgz_path)
+ File.open(run_path, 'wb') do |f|
+ rewrite f, File.join(DIR, "static", "stubs", "blank.run"),
+ 'CRC' => '0000000000', 'MD5' => md5, 'LABEL' => app_name, 'NAME' => name,
+ 'SIZE' => fsize, 'RAWSIZE' => (raw / 1024) + 1, 'TIME' => Time.now, 'FULLSIZE' => raw
+ File.open(tgz_path, 'rb') do |f2|
+ f.write f2.read(8192) until f2.eof
+ end
+ end
+ FileUtils.chmod 0755, run_path
+ FileUtils.rm_rf(tgz_path)
+ FileUtils.rm_rf(tmp_dir)
+ blk[1.0] if blk
+ end
+ end
+
+ Shoes::I_NET = "No, download Shoes if it's absent."
+ Shoes::I_YES = "Yes, I want Shoes included."
+ Shoes::I_NOV = "Yes, include Shoes, but without video support."
+ PackMake = proc do
+ background "#DDD"
+
+ @page1 = stack do
+ stack do
+ background white
+ background "#FFF".."#EEE", :height => 50, :bottom => 50
+ border "#CCC", :height => 2, :bottom => 0
+ stack :margin => 20 do
+ selt = proc { @sel1.toggle; @sel2.toggle }
+ @path = ""
+ @shy_path = nil
+ @sel1 =
+ flow do
+ para "File to package:"
+ inscription " (or a ", link("directory", &selt), ")"
+ edit1 = edit_line :width => -120
+ @bb = button "Browse...", :width => 100 do
+ @path = edit1.text = ask_open_file
+ #est_recount
+ end
+ end
+ @sel2 =
+ flow :hidden => true do
+ para "Directory:"
+ inscription " (or a ", link("single file", &selt), ")"
+ edit2 = edit_line :width => -120
+ @bf = button "Folder...", :width => 100 do
+ @path = edit2.text = ask_open_folder
+ #est_recount
+ end
+ end
+
+ para "Packaging options"
+ para "Should Shoes be included with your script or should the script ",
+ "download Shoes when the user runs it? Not all options are available on all ",
+ "systems. The defaults work."
+ flow :margin_left => 20 do
+ @shy = check
+ para "Shoes (.shy) for users who have Shoes already", :margin_right => 20
+ end
+ items = [Shoes::I_NET, Shoes::I_YES, Shoes::I_NOV]
+ items.shift unless ::RUBY_PLATFORM =~ /mswin|mingw/
+ flow :margin_left => 20 do
+ flow :width => 0.25 do
+ @exe = check
+ para "Windows"
+ end
+ @incWin = list_box :items => items, :width => 0.6, :height => 30, do
+ @downOpt = @incWin.text
+ est_recount
+ end
+ @incWin.choose items[0]
+ end
+ flow :margin_left => 20 do
+ flow :width => 0.25 do
+ @dmg = check
+ para "OS X", :margin_right => 47
+ end
+ osxop = [Shoes::I_NET, Shoes::I_NOV]
+ @incOSX = list_box :items => osxop, :width => 0.6, :height => 30 do
+ @downOpt = @incOSX.text
+ est_recount
+ end
+ @incOSX.choose(Shoes::I_NOV)
+ end
+ flow :margin_left => 20 do
+ flow :width => 0.25 do
+ @run = check
+ para "Linux", :margin_right => 49
+ end
+ @incLinux = list_box :items => [Shoes::I_NET], :width => 0.6,
+ :height => 30 do
+ est_recount
+ end
+ @incLinux.choose(Shoes::I_NET)
+ end
+ end
+ end
+
+ stack :margin => 20 do
+ @est = para "Estimated size of your choice: ", strong("0k"), :margin => 0, :margin_bottom => 4
+ def est_recount
+ base =
+ case @downOpt
+ when Shoes::I_NET; 98
+ when Shoes::I_YES; 11600
+ when Shoes::I_NOV; 7000
+ end
+ base += ((File.directory?(@path) ? Shy.du(@path) : File.size(@path)) rescue 0) / 1024
+ @est.replace "Estimated size of each app: ", strong(base > 1024 ?
+ "%0.1fM" % [base / 1024.0] : "#{base}K")
+ end
+ def build_thread
+ @shy_path = nil
+ if File.directory? @path
+ @shy_path = @path.gsub(%r![\\/]+$!, '') + ".shy"
+ elsif @shy.style[:checked]
+ @shy_path = @path.gsub(/\.\w+$/, '') + ".shy"
+ end
+ if @shy_path and not @shy_meta
+ @page_shy.show
+ @shy_para.text = File.basename(@shy_path)
+ @shy_launch.items = Shy.launchable(@path)
+ return
+ end
+ @page2.show
+ @path2.replace File.basename(@path)
+ #inc_text = @inc.text
+ inc_win_text, inc_osx_text, inc_linux_text = @incWin.text, @incOSX.text, @incLinux.text
+ Thread.start do
+ begin
+ sofar, stage = 0.0, 1.0 / [@shy.style[:checked], @exe.style[:checked], @dmg.style[:checked], @run.style[:checked]].
+ select { |x| x }.size
+ blk = proc do |frac|
+ @prog.style(:width => sofar + (frac * stage))
+ end
+
+ if @shy_path
+ @status.replace "Compressing the script's folder."
+ pblk = Shy.progress(Shy.du(@path)) do |name, perc, left|
+ blk[perc]
+ end
+ Shy.c(@shy_path, @shy_meta, @path, &pblk)
+ @path = @shy_path
+ @prog.style(:width => sofar += stage)
+ end
+ if @exe.style[:checked]
+ @status.replace "Working on an .exe for Windows."
+ Shoes::Pack.exe(@path, inc_win_text, &blk)
+ @prog.style(:width => sofar += stage)
+ end
+ if @dmg.style[:checked]
+ @status.replace "Working on a .dmg for Mac OS X."
+ Shoes::Pack.dmg(@path, inc_osx_text, &blk)
+ @prog.style(:width => sofar += stage)
+ end
+ if @run.style[:checked]
+ @status.replace "Working on a .run for Linux."
+ Shoes::Pack.linux(@path, inc_linux_text, &blk)
+ @prog.style(:width => sofar += stage)
+ end
+ if @shy_path and not @shy.style[:checked]
+ FileUtils.rm_rf(@shy_path)
+ end
+
+ every do
+ if @prog.style[:width] == 1.0
+ @page2.hide
+ @page3.show
+ @path3.replace File.basename(@path)
+ end
+ end
+ rescue => e
+ @packErrMsg = e
+ # weirdness begins
+ @page2.hide
+ @path3.style :font => 'italic', :size => 12
+ @page3.show
+ @path3.replace @packErrMsg
+ end
+ end
+ end
+
+ inscription "Using the latest Shoes build (0.r#{Shoes::REVISION})", :margin => 0
+ flow :margin_top => 10, :margin_left => 310 do
+ button "OK", :margin_right => 4 do
+ @page1.hide; @bb.hide; @bf.hide
+ @packErrMsg = nil
+ build_thread
+ end
+ button "Cancel" do
+ close
+ end
+ end
+ end
+ end
+
+ @page_shy = stack :hidden => true do
+ stack do
+ background white
+ border "#DDD", :height => 2, :bottom => 0
+ stack :margin => 20 do
+ para "Details for:", :margin => 4
+ @shy_para = para "", :size => 20, :margin => 4
+ flow do
+ stack :margin => 10, :width => 0.4 do
+ para "Name of app:"
+ @shy_name = edit_line :width => 1.0
+ end
+ stack :margin => 10, :width => 0.4 do
+ para "Version:"
+ @shy_version = edit_line :width => 120
+ end
+ stack :margin => 10, :width => 0.4 do
+ para "Creator"
+ @shy_creator = edit_line :width => 1.0
+ end
+ stack :margin => 10, :width => 0.5 do
+ para "Launch"
+ @shy_launch = list_box :height => 30
+ end
+ end
+ end
+ end
+
+ stack :margin => 20 do
+ flow :margin_top => 10, :margin_left => 310 do
+ button "OK", :margin_right => 4 do
+ @shy_meta = Shy.new
+ @shy_meta.name = @shy_name.text
+ @shy_meta.creator = @shy_creator.text
+ @shy_meta.version = @shy_version.text
+ @shy_meta.launch = @shy_launch.text
+ @page_shy.hide
+ build_thread
+ end
+ button "Cancel" do
+ close
+ end
+ end
+ end
+ end
+
+ @page2 = stack :hidden => true do
+ stack do
+ background white
+ border "#DDD", :height => 2, :bottom => 0
+ stack :margin => 20 do
+ para "Packaging:", :margin => 4
+ @path2 = para "", :size => 20, :margin => 4
+ @status = para "", :margin => 4
+ end
+ end
+
+ stack :margin => 20 do
+ stack :width => -20, :height => 24 do
+ @prog = background "#{DIR}/static/stripe.png", :curve => 7
+ background "rgb(0, 0, 0, 100)".."rgb(120, 120, 120, 0)", :curve => 6, :height => 16
+ background "rgb(120, 120, 120, 0)".."rgb(0, 0, 0, 100)", :curve => 6,
+ :height => 16, :top => 8
+ border "rgb(60, 60, 60, 80)", :curve => 7, :strokewidth => 2
+ end
+ end
+ end
+
+ @page3 = stack :hidden => true do
+ stack do
+ background white
+ border "#DDD", :height => 2, :bottom => 0
+ stack :margin => 20 do
+ para "Completed:", :margin => 4
+ @path3 = para "", :size => 20, :margin => 4
+ para "Your files are done, you may close this window.", :margin => 4
+ button "Quit" do
+ exit
+ end
+ end
+ end
+ end
+
+ start do
+ @exe.checked = false
+ @dmg.checked = false
+ @run.checked = false
+ @shy.checked = true
+ #@inc.choose( ::RUBY_PLATFORM =~ /mswin|mingw/ ? Shoes::I_NET : Shoes::I_NOV )
+ end
+ end
+end
diff --git a/lib/shoes/search.rb b/lib/shoes/search.rb
new file mode 100644
index 0000000..98c480c
--- /dev/null
+++ b/lib/shoes/search.rb
@@ -0,0 +1,46 @@
+require 'ftsearch/fragment_writer'
+require 'ftsearch/analysis/simple_identifier_analyzer'
+#require 'ftsearchrt'
+
+class Shoes::Search
+ include FTSearch
+ attr_reader :index
+ def initialize fields = [:uri, :body]
+ field_infos = FTSearch::FieldInfos.new
+ fields.each do |name|
+ field_infos.add_field :name => name,
+ :analyzer => FTSearch::Analysis::SimpleIdentifierAnalyzer.new
+ end
+ @index = FTSearch::FragmentWriter.new :path => nil, :field_infos => field_infos
+ end
+ def add_document hsh
+ @index.add_document hsh
+ end
+ def finish!
+ @index.finish!
+
+ @ft = FulltextReader.new :io => StringIO.new(@index.fulltext_writer.data)
+ @sa = SuffixArrayReader.new @ft, nil, :io => StringIO.new(@index.suffix_array_writer.data)
+ @dm = DocumentMapReader.new :io => StringIO.new(@index.doc_map_writer.data)
+ end
+ def find_all terms, show = 20, prob_sort = false
+ h = Hash.new{|h,k| h[k] = 0}
+ weights = Hash.new(1.0)
+ weights[0] = 10000000 # :uri
+ weights[1] = 10000000 # :body
+ hits = @sa.find_all terms
+ size = hits.size
+ if prob_sort && size > 10000
+ iterations = 50 * Math.sqrt(size)
+ offsets = @sa.lazyhits_to_offsets(hits)
+ weight_arr = weights.sort_by{|id,w| id}.map{|_,v| v}
+ sorted = @dm.rank_offsets_probabilistic(offsets, weight_arr, iterations)
+ else
+ offsets = @sa.lazyhits_to_offsets(hits)
+ sorted = @dm.rank_offsets(offsets, weights.sort_by{|id,w| id}.map{|_,v| v})
+ end
+ sorted[0..show].map do |doc_id, count|
+ [@dm.document_id_to_uri(doc_id), count]
+ end
+ end
+end
diff --git a/lib/shoes/setup.rb b/lib/shoes/setup.rb
new file mode 100644
index 0000000..834847a
--- /dev/null
+++ b/lib/shoes/setup.rb
@@ -0,0 +1,329 @@
+require 'rubygems'
+require 'rubygems/dependency_installer'
+module Gem
+ @ruby = (File.join(Config::CONFIG['bindir'], 'shoes') + Config::CONFIG['EXEEXT']).
+ sub(/.*\s.*/m, '"\&"') + " --ruby"
+end
+class << Gem::Ext::ExtConfBuilder
+ alias_method :make__, :make
+ def make(dest_path, results)
+ raise unless File.exist?('Makefile')
+ mf = File.read('Makefile')
+ mf = mf.gsub(/^INSTALL\s*=\s*.*$/, "INSTALL = $(RUBY) -run -e install -- -vp")
+ mf = mf.gsub(/^INSTALL_PROG\s*=\s*.*$/, "INSTALL_PROG = $(INSTALL) -m 0755")
+ mf = mf.gsub(/^INSTALL_DATA\s*=\s*.*$/, "INSTALL_DATA = $(INSTALL) -m 0644")
+ File.open('Makefile', 'wb') {|f| f.print mf}
+ make__(dest_path, results)
+ end
+end
+
+# STDIN.reopen("/dev/tty") if STDIN.eof?
+class NotSupportedByShoes < Exception; end
+
+class Shoes::Setup
+
+ def self.init
+ gem_reset
+ install_sources if Gem.source_index.find_name('sources').empty?
+ end
+
+ def self.gem_reset
+ Gem.use_paths(GEM_DIR, [GEM_DIR, GEM_CENTRAL_DIR])
+ Gem.source_index.refresh!
+ end
+
+ def self.setup_app(setup)
+ appt = "Setting up for #{setup.script}"
+ Shoes.app :width => 370, :height => 158, :resizable => false, :title => appt do
+ background "#EEE".."#9AA"
+ image :top => 0, :left => 0 do
+ stroke "#FFF"; strokewidth 0.1
+ (0..158).step(3) { |i| line 0, i, 370, i }
+ end
+ @pulse = stack :top => 0, :left => 0
+ @logo = image "#{DIR}/static/shoes-icon-blue.png", :top => -20, :right => -20
+ stack :margin => 18 do
+ title "Shoes Setup", :size => 12, :weight => "bold", :margin => 0
+ para "Preparing #{setup.script}", :size => 8, :margin => 0, :margin_top => 8, :width => 220
+ @pr = progress :width => 1.0, :top => 70, :height => 20
+ button "Cancel", :top => 98, :left => 0.4 do
+ self.close
+ end
+
+ start do
+ @th =
+ Thread.start(self.app) do |app|
+ begin
+ setup.start(app)
+ rescue => e
+ puts e.message
+ end
+ end
+ end
+ end
+
+ animate 10 do |i|
+ i %= 10
+ @pulse.clear do
+ fill black(0.2 - (i * 0.02))
+ strokewidth(3.0 - (i * 0.2))
+ stroke rgb(0.7, 0.7, 0.9, 1.0 - (i * 0.1))
+ oval(@logo.left - i, @logo.top - i, @logo.width + (i * 2))
+ end
+ @pr.fraction = $fraction
+ if @script
+ Shoes.visit(@script)
+ close
+ end
+ end
+ end
+ end
+
+ attr_accessor :steps, :script
+
+ def initialize(script, &blk)
+ @steps = []
+ @script = script
+ instance_eval &blk
+ unless no_steps?
+ app = self.class.setup_app(self)
+ end
+ end
+
+ def no_steps?
+ (@steps.map { |s| s[0] }.uniq - [:source]).empty?
+ end
+
+ def gem name, version = nil
+ arg = "#{name} #{version}".strip
+ name, version = arg.split(/\s+/, 2)
+ if Gem.source_index.find_name(name, version).empty?
+ @steps << [:gem, arg]
+ else
+ activate_gem(name, version)
+ end
+ end
+
+ def source uri
+ @steps << [:source, uri]
+ end
+
+ def activate_gem(name, version)
+ gem = Gem.source_index.find_name(name, version).first
+ Gem.activate(gem.name, "= #{gem.version}")
+ end
+
+ def start(app)
+ old_ui = Gem::DefaultUserInteraction.ui
+ ui = Gem::DefaultUserInteraction.ui = Gem::ShoesFace.new(app)
+ count, total = 0.5, @steps.length
+ ui.progress count, total
+
+ steps.each do |act, arg|
+ case act
+ when :gem
+ name, version = arg.split(/\s+/, 2)
+ count += 1
+ ui.say "Looking for #{name}"
+ if Gem.source_index.find_name(name, version).empty?
+ ui.title "Installing #{name}"
+ installer = Gem::DependencyInstaller.new
+ begin
+ installer.install(name, version || Gem::Requirement.default)
+ self.class.gem_reset
+ activate_gem(name, version)
+ ui.say "Finished installing #{name}"
+ rescue Object => e
+ ui.error "while installing #{name}", e
+ raise e
+ end
+ end
+ when :source
+ ui.title "Switching Gem servers"
+ ui.say "Pulling from #{arg}"
+ Gem.sources.clear << arg
+ self.class.gem_reset
+ end
+ ui.progress count, total
+ end
+ Gem::DefaultUserInteraction.ui = old_ui
+ app.instance_variable_set("@script", @script)
+ end
+
+ def svn(dir, save_as = nil, &blk)
+ dir.gsub! /(.)\/*$/, '\1/'
+ if save_as.nil? or save_as.empty?
+ save_as = File.join(GEM_DIR, 'svn', '1')
+ save_as.succ! while File.exists? save_as
+ elsif save_as.index(GEM_DIR) != 0
+ save_as = File.join(GEM_DIR, 'svn', save_as)
+ end
+ mkdir_p(save_as)
+ puts "** Pulling down #{dir}..."
+ svnuri = URI.parse(dir)
+ case svnuri.scheme
+ when "http", "https"
+ REXML::Document.new(svnuri.open { |f| f.read }).
+ each_element("/svn/index/*") do |ele|
+ fname, href = ele.attributes['name'], ele.attributes['href']
+ case ele.name
+ when "file"
+ puts "- #{dir}#{href}"
+ URI.parse("#{dir}#{href}").open do |f|
+ File.open(File.join(save_as, fname), 'wb') do |f2|
+ f2 << f.read(16384) until f.eof?
+ end
+ end
+ when "dir"
+ svn("#{dir}#{href}", File.join(save_as, fname))
+ end
+ end
+ else
+ raise NotSupportedByShoes, "Only HTTP addresses are supported by Shoes's Subversion module."
+ end
+ if blk
+ Dir.chdir(save_as, &blk)
+ end
+ end
+
+ def self.install_sources
+ require 'base64'
+ sources_gem = File.join(LIB_DIR, "sources-0.0.1.gem")
+ File.open(sources_gem, "wb") do |f|
+ f << Base64.decode64( <<-GEM.gsub(/^ +/, '') )
+ ZGF0YS50YXIuZ3oAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAADAwMDA2NDQAMDAwMDAwMAAwMDAwMDAwADAwMDAwMDAwMjYw
+ ADAwMDAwMDAwMDAwADAxMzMxNQAgMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB1c3RhcgAwMHdoZWVs
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAd2hlZWwAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAwMDAwMDAwADAwMDAwMDAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAfiwgAAKBzRAADyslM0i/OLy1KTi3WK0pioAkw
+ AAIzMxMwDQTotIGhCYINFgcKGBsxKBjQxjmooLS4JLEIaH15RmpqDh51hOTR
+ PTdEQG5+SmlOqoJ7ai6XgoIDNCUo2CpEK2WUlBRY6eunp+YCU0ZpUmVaflF6
+ qh6QUIoFKk1JTVMoTs1J04NqAQoh9AM5qXkpXCA80P4bBaNgFIyCUYAdAAAA
+ AP//AwBOIUx0AAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAG1ldGFkYXRhLmd6
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAw
+ MDAwNjQ0ADAwMDAwMDAAMDAwMDAwMAAwMDAwMDAwMDYzMAAwMDAwMDAwMDAw
+ MAAwMTM0MDAAIDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAdXN0YXIAMDB3aGVlbAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAHdoZWVsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAMDAwMDAwMAAwMDAwMDAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAH4sIAACgc0QAA4SRyW7cMAyG73wKNndPNCkaFDrkmntb9FIEgizR
+ thItLiVneftKHk88QYHWMqCF5E/yY9d1+ImX/u069Y9kirynIOX3mYwbnNHF
+ pYjQ7COFrJ6Jc32RKA5fD8cj5Eu/3XqEqANJzGlhQxneDX9n+nkyISBeiIvD
+ EawuVeJGiNtOfOluPqMQcv2xE7d1g7yEoPlN4o/JZZy1edIj4czp2VnKaNNL
+ 9EnbcxU4JEamkAphbQZdzEV7v5YOTL8Xx6RmXaYsETr0rgcK2vl6m1KguYrL
+ E4oqNFZXTmsbCDWbYTeXtXjQS0mb3E7A0qAXXxS9klmK7n3T6l20jiXWHSad
+ FdtkJA7aZzoXZFVLqP4PUMpvp4hAsTSavF9bQ4hdXVd3V/XUzv+cRPs+TENc
+ jgfmCq0yCBKbCGQ3RhdH9UR1FmCIizKTdhuLKXHN/+sBYHCe3tleb2QO3EOh
+ XNRmbY6Ng0orz+2FXgvrlc+l3w5zd6OY97CPDNqLpZmipWjcOeYPAAAA//8D
+ ADLcAkIBAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAA
+ GEM
+ end
+ Gem::Installer.new(sources_gem).install()
+ end
+end
+
+class Gem::ShoesFace
+ class ProgressReporter
+ attr_reader :count
+
+ def initialize(prog, status, size, initial_message,
+ terminal_message = "complete")
+ @prog = prog
+ (@status = status).replace initial_message
+ @total = size
+ @count = 0.0
+ end
+
+ def updated(message)
+ @count += 1.0
+ @prog.fraction = (@count / @total.to_f) * 0.5
+ end
+
+ def done
+ end
+ end
+
+ def initialize app
+ @title, @status, @prog, = app.slot.contents[-1].contents
+ end
+ def title msg
+ @title.replace msg
+ end
+ def progress count, total
+ #@prog.fraction = count.to_f / total.to_f
+ $fraction = count.to_f / total.to_f
+ end
+ def ask_yes_no msg
+ Kernel.confirm(msg)
+ end
+ def ask msg
+ Kernel.ask(msg)
+ end
+ def error msg, e
+ stat = @status
+ stat.app do
+ error(e)
+ stat.replace link("Error") { Shoes.show_log }, " ", msg
+ end
+ end
+ def say msg
+ @status.replace msg
+ end
+ def alert msg, quiz=nil
+ say(msg)
+ ask(quiz) if quiz
+ end
+ def progress_reporter(*args)
+ ProgressReporter.new(@prog, @status, *args)
+ end
+ def method_missing(*args)
+ p args
+ nil
+ end
+end
+
+Shoes::Setup.init
diff --git a/lib/shoes/shy.rb b/lib/shoes/shy.rb
new file mode 100644
index 0000000..96f6de3
--- /dev/null
+++ b/lib/shoes/shy.rb
@@ -0,0 +1,131 @@
+#
+# lib/shoes/shy.rb
+# Shy, the Shoes YAML archive format
+#
+require 'digest/md5'
+require 'zlib'
+require 'shoes/minitar'
+require 'find'
+require 'tmpdir'
+require 'yaml'
+
+class Shy
+ VERSION = 0x0001
+ MAGIC = "_shy".freeze
+ LAYOUT = "A4vV".freeze #Force to Little Endian for all platforms
+
+ yaml_as 'tag:hackety.org,2007:shy'
+ attr_accessor :name, :creator, :version, :launch
+
+ def self.launchable(d)
+ if File.directory? d
+ Dir["#{d}/**/*.rb"].map do |path|
+ path.gsub(%r!#{Regexp::quote(d)}/!, '')
+ end
+ else
+ [File.basename(d)]
+ end
+ end
+
+ def self.__hdr__(f)
+ hdr = f.read(10).unpack(LAYOUT)
+ raise IOError, "Invalid header" if hdr[0] != MAGIC and hdr[1] > VERSION
+ YAML.load(f.read(hdr[2]))
+ end
+
+
+ def self.du(root)
+ size = 0
+ Find.find(root) do |path|
+ if FileTest.directory?(path)
+ if File.basename(path)[0] == ?.
+ Find.prune
+ else
+ next
+ end
+ else
+ size += FileTest.size(path)
+ end
+ end
+ size
+ end
+
+ def self.meta(path, d = ".")
+ File.open(path, 'rb') do |f|
+ shy = __hdr__(f)
+ end
+ end
+
+ def self.x(path, d = ".")
+ File.open(path, 'rb') do |f|
+ shy = __hdr__(f)
+ inp = Zlib::GzipReader.new(f)
+ Archive::Tar::Minitar.unpack(inp, d)
+ shy
+ end
+ end
+
+ def self.c(path, shy, d, &blk)
+ path = File.expand_path(path)
+ meta = shy.to_yaml
+ File.open(path, "wb") do |f|
+ f << [MAGIC, VERSION, meta.length].pack(LAYOUT)
+ f << meta
+ self.czf(f, d, &blk)
+ end
+ end
+
+ def self.progress(total, &blk)
+ if blk
+ last, left = 0.0, total
+ proc do |action, name, stats|
+ if action == :file_progress
+ left -= stats[:currinc]
+ prg = 1.0 - (left.to_f / total.to_f)
+ blk[name, (last = prg), left] if prg - last > 0.02
+ end
+ end
+ end
+ end
+
+ def self.czf(f, d, &blk)
+ total = du(d)
+ out = Zlib::GzipWriter.new(f)
+ files = ["."]
+ unless File.directory? d
+ files = [File.basename(d)]
+ d = File.dirname(d)
+ end
+ Dir.chdir(d) do
+ Archive::Tar::Minitar.pack(files, out, &blk)
+ end
+ end
+
+ def self.xzf(f, d, &blk)
+ gz = Zlib::GzipReader.new(f)
+ Archive::Tar::Minitar.unpack(gz, d, &blk)
+ end
+
+ def self.md5sum(path)
+ digest = Digest::MD5.new
+ File.open(path, "rb") do |f|
+ digest.update f.read(8192) until f.eof
+ end
+ digest.hexdigest
+ end
+
+ def self.hrun(f)
+ b, i = 0, 1
+ last = 65535
+ while i < last
+ case f.readline
+ when /OLDSKIP=(\d+)/
+ last = $1.to_i
+ when /FULLSIZE=(\d+)/
+ b = $1.to_i
+ end
+ i += 1
+ end
+ b
+ end
+end
diff --git a/lib/shoes/shybuilder.rb b/lib/shoes/shybuilder.rb
new file mode 100644
index 0000000..f0f09c6
--- /dev/null
+++ b/lib/shoes/shybuilder.rb
@@ -0,0 +1,44 @@
+# -*- encoding: utf-8 -*-
+# crude shy-building UI; inspired by Jesse's "Shy Makey Thing"
+
+require 'shoes/shy'
+
+class Shoes
+ def self.start_shy_builder(launch_script)
+ launch_script = File.expand_path(launch_script)
+ top_dir = File.dirname(launch_script)
+ launch_script = File.basename(launch_script)
+ shy_name = "#{top_dir}.shy"
+ Shoes.app do
+ background white
+ stack do
+ para "Almost ready to make #{shy_name}"
+ fields = {}
+ for label, name in [["Project Name", "name"],
+ ["Version", "version"],
+ ["Your Name", "creator"]]
+ flow :width => 1.0 do
+ para "#{label}: "
+ fields[name] = edit_line ''
+ end
+ end
+ button "Build .shy" do
+ shy_desc = Shy.new
+ for name in fields.keys
+ shy_desc.send("#{name}=".intern, fields[name].text)
+ end
+ shy_desc.launch = launch_script
+ Shy.c(shy_name, shy_desc, top_dir)
+ clear
+ background white
+ stack do
+ para "Built #{shy_name}"
+ button "Ok" do
+ close
+ end
+ end
+ end
+ end
+ end
+ end
+end