diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/shoes.rb | 548 | ||||
-rw-r--r-- | lib/shoes/cache.rb | 54 | ||||
-rw-r--r-- | lib/shoes/chipmunk.rb | 35 | ||||
-rw-r--r-- | lib/shoes/data.rb | 39 | ||||
-rw-r--r-- | lib/shoes/help.rb | 468 | ||||
-rw-r--r-- | lib/shoes/image.rb | 25 | ||||
-rw-r--r-- | lib/shoes/inspect.rb | 128 | ||||
-rw-r--r-- | lib/shoes/log.rb | 48 | ||||
-rw-r--r-- | lib/shoes/minitar.rb | 986 | ||||
-rw-r--r-- | lib/shoes/override.rb | 38 | ||||
-rw-r--r-- | lib/shoes/pack.rb | 543 | ||||
-rw-r--r-- | lib/shoes/search.rb | 46 | ||||
-rw-r--r-- | lib/shoes/setup.rb | 329 | ||||
-rw-r--r-- | lib/shoes/shy.rb | 131 | ||||
-rw-r--r-- | lib/shoes/shybuilder.rb | 44 |
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(/&/, '&').gsub(/>/, '>').gsub(/>/, '<').gsub(/"/, '"'). + 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 |