Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/app
diff options
context:
space:
mode:
authorIshan Bansal <ishan@seeta.in>2011-02-04 12:53:17 (GMT)
committer Ishan Bansal <ishan@seeta.in>2011-02-04 12:53:17 (GMT)
commitee852209b70c803e6ab1bb69b71c7dafcb1448e7 (patch)
treeac20673816893b398aed4c4da463b157d9e4899e /app
Imported Upstream version 1HEADmaster
Diffstat (limited to 'app')
-rw-r--r--app/.gitignore4
-rw-r--r--app/LICENSE19
-rw-r--r--app/README.textile94
-rw-r--r--app/app.yaml13
-rw-r--r--app/app/boot.rb32
-rw-r--r--app/app/db/connection_pool.rb65
-rw-r--r--app/app/db/core_ext.rb9
-rw-r--r--app/app/db/database.rb119
-rw-r--r--app/app/db/dataset.rb354
-rw-r--r--app/app/db/http.rb78
-rw-r--r--app/app/db/model.rb235
-rw-r--r--app/app/db/schema.rb161
-rw-r--r--app/app/db/sequel.rb21
-rw-r--r--app/app/db/sqlite.rb112
-rw-r--r--app/app/db/table.rb231
-rw-r--r--app/app/syntax/common.rb197
-rw-r--r--app/app/syntax/convertors/abstract.rb27
-rw-r--r--app/app/syntax/convertors/html.rb51
-rw-r--r--app/app/syntax/lang/ruby.rb317
-rw-r--r--app/app/syntax/lang/xml.rb108
-rw-r--r--app/app/syntax/lang/yaml.rb105
-rw-r--r--app/app/syntax/markup.rb214
-rw-r--r--app/app/syntax/version.rb11
-rw-r--r--app/app/ui/completion.rb183
-rw-r--r--app/app/ui/editor/editor.rb448
-rw-r--r--app/app/ui/lessons.rb319
-rwxr-xr-xapp/app/ui/mainwindow.rb130
-rw-r--r--app/app/ui/tabs/editor.rb2
-rw-r--r--app/app/ui/tabs/home.rb175
-rw-r--r--app/app/ui/tabs/lessons.rb32
-rw-r--r--app/app/ui/tabs/prefs.rb68
-rw-r--r--app/app/ui/tabs/sidetabs.rb185
-rw-r--r--app/app/ui/widgets.rb296
-rwxr-xr-xapp/fonts/Animals.ttfbin0 -> 73516 bytes
-rwxr-xr-xapp/fonts/Arcade.ttfbin0 -> 57860 bytes
-rwxr-xr-xapp/fonts/Bruegheliana.ttfbin0 -> 45648 bytes
-rwxr-xr-xapp/fonts/Carr Space.ttfbin0 -> 25472 bytes
-rwxr-xr-xapp/fonts/Chess Utrecht.ttfbin0 -> 14396 bytes
-rwxr-xr-xapp/fonts/DayRoman-X.ttfbin0 -> 30088 bytes
-rwxr-xr-xapp/fonts/DayRoman.ttfbin0 -> 83160 bytes
-rwxr-xr-xapp/fonts/Delicious-Bold.otfbin0 -> 24648 bytes
-rwxr-xr-xapp/fonts/Delicious-BoldItalic.otfbin0 -> 25424 bytes
-rwxr-xr-xapp/fonts/Delicious-Heavy.otfbin0 -> 25264 bytes
-rwxr-xr-xapp/fonts/Delicious-Italic.otfbin0 -> 25036 bytes
-rwxr-xr-xapp/fonts/Delicious-Roman.otfbin0 -> 24700 bytes
-rwxr-xr-xapp/fonts/Delicious-SmallCaps.otfbin0 -> 25532 bytes
-rwxr-xr-xapp/fonts/Even More Dings JL.ttfbin0 -> 98608 bytes
-rwxr-xr-xapp/fonts/Fontalicious.ttfbin0 -> 62452 bytes
-rwxr-xr-xapp/fonts/Fontin-Bold.otfbin0 -> 30460 bytes
-rwxr-xr-xapp/fonts/Fontin-Italic.otfbin0 -> 30636 bytes
-rwxr-xr-xapp/fonts/Fontin-Regular.otfbin0 -> 30396 bytes
-rwxr-xr-xapp/fonts/Fontin-SmallCaps.otfbin0 -> 29308 bytes
-rwxr-xr-xapp/fonts/Free.ttfbin0 -> 91148 bytes
-rwxr-xr-xapp/fonts/Illustries.ttfbin0 -> 58816 bytes
-rwxr-xr-xapp/fonts/JustOldFashion.ttfbin0 -> 79480 bytes
-rw-r--r--app/fonts/Lacuna.ttfbin0 -> 56784 bytes
-rwxr-xr-xapp/fonts/LiberationMono-Bold.ttfbin0 -> 104980 bytes
-rw-r--r--app/fonts/LiberationMono-Regular.ttfbin0 -> 107920 bytes
-rwxr-xr-xapp/fonts/LiberationSans-Bold.ttfbin0 -> 133000 bytes
-rwxr-xr-xapp/fonts/LiberationSans-BoldItalic.ttfbin0 -> 128828 bytes
-rwxr-xr-xapp/fonts/LiberationSans-Italic.ttfbin0 -> 155304 bytes
-rwxr-xr-xapp/fonts/LiberationSans-Regular.ttfbin0 -> 133088 bytes
-rwxr-xr-xapp/fonts/LiberationSerif-Bold.ttfbin0 -> 141132 bytes
-rwxr-xr-xapp/fonts/LiberationSerif-BoldItalic.ttfbin0 -> 144184 bytes
-rwxr-xr-xapp/fonts/LiberationSerif-Italic.ttfbin0 -> 138328 bytes
-rwxr-xr-xapp/fonts/LiberationSerif-Regular.ttfbin0 -> 146036 bytes
-rwxr-xr-xapp/fonts/Outer Space JL.ttfbin0 -> 67752 bytes
-rwxr-xr-xapp/fonts/Oxygene1.ttfbin0 -> 23096 bytes
-rwxr-xr-xapp/fonts/Phonetica.ttfbin0 -> 20232 bytes
-rw-r--r--app/fonts/Pixelpoiiz.ttfbin0 -> 27028 bytes
-rwxr-xr-xapp/fonts/Playing Cards.ttfbin0 -> 42064 bytes
-rwxr-xr-xapp/fonts/Silhouette.ttfbin0 -> 35164 bytes
-rw-r--r--app/fonts/TakaoGothic.otfbin0 -> 3615184 bytes
-rwxr-xr-xapp/fonts/YanoneKaffeesatz-Bold.otfbin0 -> 55568 bytes
-rwxr-xr-xapp/fonts/YanoneKaffeesatz-Light.otfbin0 -> 58328 bytes
-rwxr-xr-xapp/fonts/YanoneKaffeesatz-Regular.otfbin0 -> 58528 bytes
-rwxr-xr-xapp/fonts/YanoneKaffeesatz-Thin.otfbin0 -> 58292 bytes
-rw-r--r--app/h-ety-h.rb5
-rwxr-xr-xapp/installer/HackFolder.ini49
-rwxr-xr-xapp/installer/base.nsi252
-rwxr-xr-xapp/installer/installer-1.bmpbin0 -> 154544 bytes
-rwxr-xr-xapp/installer/installer-2.bmpbin0 -> 25820 bytes
-rwxr-xr-xapp/installer/setup.icobin0 -> 10134 bytes
-rw-r--r--app/lessons/basic_programming.rb302
-rw-r--r--app/lessons/basic_ruby.rb281
-rw-r--r--app/lessons/basic_shoes.rb183
-rw-r--r--app/lessons/tour.rb180
-rw-r--r--app/lib/all.rb9
-rw-r--r--app/lib/art/turtle.rb349
-rw-r--r--app/lib/dev/errors.rb153
-rw-r--r--app/lib/dev/events.rb112
-rw-r--r--app/lib/dev/init.rb48
-rw-r--r--app/lib/dev/stdout.rb9
-rw-r--r--app/lib/dev/win32.rb29
-rw-r--r--app/lib/enhancements.rb158
-rw-r--r--app/lib/web/all.rb2
-rw-r--r--app/lib/web/hacker.rb37
-rw-r--r--app/lib/web/web.rb318
-rw-r--r--app/lib/web/yaml.rb60
-rw-r--r--app/platform/mac/App.icnsbin0 -> 43392 bytes
-rw-r--r--app/platform/mac/Cheat.icnsbin0 -> 49066 bytes
-rw-r--r--app/platform/mac/Help.icnsbin0 -> 49908 bytes
-rw-r--r--app/platform/mac/dmg_ds_storebin0 -> 12292 bytes
-rwxr-xr-xapp/platform/msw/App.icobin0 -> 302430 bytes
-rwxr-xr-xapp/platform/msw/Cheat.icobin0 -> 302430 bytes
-rwxr-xr-xapp/platform/msw/Help.icobin0 -> 302430 bytes
-rw-r--r--app/platform/nix/app.pngbin0 -> 5591 bytes
-rw-r--r--app/root/Home/.gitignore0
-rwxr-xr-xapp/root/comics.txt4
-rwxr-xr-xapp/spec/all.rb5
-rwxr-xr-xapp/spec/enhancements.rb452
-rwxr-xr-xapp/spec/events.rb217
-rwxr-xr-xapp/spec/rspec.rb30
-rwxr-xr-xapp/spec/stdout.rb40
-rw-r--r--app/static/hacketyhack-dmg.jpgbin0 -> 18298 bytes
-rw-r--r--app/static/hhabout.pngbin0 -> 92278 bytes
-rwxr-xr-xapp/static/hhcheat.pngbin0 -> 120296 bytes
-rwxr-xr-xapp/static/hhconsole.pngbin0 -> 5988 bytes
-rw-r--r--app/static/hhhello.pngbin0 -> 301160 bytes
-rw-r--r--app/static/icon-art.pngbin0 -> 450 bytes
-rw-r--r--app/static/icon-dingbat.pngbin0 -> 676 bytes
-rw-r--r--app/static/icon-email.pngbin0 -> 673 bytes
-rwxr-xr-xapp/static/icon-file.pngbin0 -> 294 bytes
-rw-r--r--app/static/icon-sound.pngbin0 -> 732 bytes
-rw-r--r--app/static/icon-table.pngbin0 -> 726 bytes
-rw-r--r--app/static/matz.jpgbin0 -> 17047 bytes
-rw-r--r--app/static/splash-hand.pngbin0 -> 78698 bytes
-rw-r--r--app/static/tab-cheat.pngbin0 -> 918 bytes
-rw-r--r--app/static/tab-email.pngbin0 -> 641 bytes
-rw-r--r--app/static/tab-hand.pngbin0 -> 959 bytes
-rw-r--r--app/static/tab-help.pngbin0 -> 978 bytes
-rw-r--r--app/static/tab-home.pngbin0 -> 806 bytes
-rw-r--r--app/static/tab-new.pngbin0 -> 342 bytes
-rw-r--r--app/static/tab-properties.pngbin0 -> 464 bytes
-rw-r--r--app/static/tab-quit.pngbin0 -> 688 bytes
-rw-r--r--app/static/tab-tour.pngbin0 -> 782 bytes
-rw-r--r--app/static/tab-try.pngbin0 -> 525 bytes
-rw-r--r--app/static/turtle.pngbin0 -> 814 bytes
138 files changed, 7699 insertions, 0 deletions
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 0000000..d8e4748
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1,4 @@
+*~
+root/Home/*
+test*
+.*
diff --git a/app/LICENSE b/app/LICENSE
new file mode 100644
index 0000000..43f3180
--- /dev/null
+++ b/app/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2010 Steve Klabnik
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. \ No newline at end of file
diff --git a/app/README.textile b/app/README.textile
new file mode 100644
index 0000000..3e4583c
--- /dev/null
+++ b/app/README.textile
@@ -0,0 +1,94 @@
+h1. Hackety Hack (for Mac OS X, Windows, and Linux)
+
+ Hackety Hack is a programming starter kit. It's an editor
+ with helpful coding tools and built-in messaging (so you can
+ pass scripts to friends easily.)
+
+h2. This is 1.0!
+
+All the major pieces are in place. Hooray! There are still some kinks to work out, though. Nobody's perfect! Please "file an Issue":http://github.com/hacketyhack/hacketyhack/issues if you find something.
+
+h2. Building Hackety Hack
+
+h3. Shoooes
+
+H-ety H is built on "Shoes":http://github.com/shoes/shoes. So, you gotta get Shoes first.
+
+*IMPORTANT NOTE*
+
+bq. Hackety Hack depends on features that are only in the latest version of Shoes, "Policeman." This is the third version of Shoes, there's also "Rasins," which was version 2. If you download Shoes 2, it won't work!
+
+Now back to your regularly scheduled instructions.
+
+There are two options to getting Shoes: download a pre-built version, or build it yourself!
+
+h4. Pre-assembled Shoes
+
+You can try downloading the latest version of Shoes from the "Recent Builds Page":http://wiki.github.com/shoes/shoes/recentbuilds on the Shoes Wiki.
+
+h4. Some-assembly-required Shoes
+
+If you like living on the bleeding edge, or there isn't a Shoes made for your platform, you can check out the "Building Shoes":http://wiki.github.com/shoes/shoes/buildingshoes page to find out how to build Shoes on your platform.
+
+h3. 'Got Shoes Strapped on my Feet
+
+Once you've got yourself a pair of Shoes, you'll want to fork me, then clone your repo:
+
+bq. $ git clone git@github.com:YOURUSER/hacketyhack.git
+
+If you've got your 'shoes' environment variable set, you can just run Hackety directly:
+
+bq. $ cd hacketyhack
+ $ ./h-ety-h.rb
+
+Otherwise, pick 'h-ety-h.rb' from the "Open an App." menu in Shoes.
+
+You can also run 'shoes h-ety-h.rb' or if you're on a Mac, something like ' /Users/steveklabnik/Documents/src/shoes/Shoes.app/Contents/MacOS/shoes h-ety-h.rb' from the terminal.
+
+h2. Building an installer
+
+If you want to build Hackety Hack as a standalone app with the installer for your platform, you need to have your own Shoes built. Then, get your directories lined up...
+
+bq. $ ls
+ shoes hacketyhack
+
+And rebuild shoes, while pointing the APP flag at your Hackety directory:
+
+bq. $ cd shoes
+ $ rake APP=../hacketyhack
+ $ rake APP=../hacketyhack installer
+
+That's it!
+
+h2. Acknowledgements
+
+ Beneath my wings are many winds.
+
+* _why, who was quite the lucky stiff. Without his work and
+ vision, Hackety Hack would have never been born. Hopefully
+ he'll be proud of how his child lives out its life...
+
+* Yukihiro Matsumoto, whose Ruby language
+ is the heart of Hackety Hack. I adore
+ this language. Ruby's shared lib and stdlib
+ are included under the terms of the Ruby
+ license.
+
+* Sharon Rosner for the Sequel lib
+ (http://sequel.rubyforge.org)
+ I use a fork from an old version.
+
+* Jamis Buck for the Syntax lib.
+ (http://syntax.rubyforge.org)
+ Live syntax highlighting.
+
+* Alex Brem for help on bloopsaphone.
+ He just started hacking away. I like that!
+
+* Numerous font authors whose free
+ offerings are included.
+
+* Fela Winkelmolen, for devoting an entire summer to get Hackety to v1.0!
+
+* Everybody who's been putting hard work into Shoes. You guys are awesome.
+
diff --git a/app/app.yaml b/app/app.yaml
new file mode 100644
index 0000000..5c74a30
--- /dev/null
+++ b/app/app.yaml
@@ -0,0 +1,13 @@
+name: Hackety Hack
+version: 1
+release: Material
+icons:
+ win32: platform/msw/App.ico
+ osx: platform/mac/App.icns
+ gtk: platform/nix/app.png
+dmg:
+ ds_store: platform/mac/dmg_ds_store
+ background: static/hacketyhack-dmg.jpg
+run: app/h-ety-h.rb
+clone: git checkout-index --prefix=dist/app/ -a
+ignore: [samples]
diff --git a/app/app/boot.rb b/app/app/boot.rb
new file mode 100644
index 0000000..40b98d4
--- /dev/null
+++ b/app/app/boot.rb
@@ -0,0 +1,32 @@
+# requires and initializations needed for /h-ety-h.rb
+# more initializations are in h-ety-h/init.rb
+
+require 'hpricot'
+
+module ::HH end
+
+def HH.anonymous_binding
+ bind = ::TOPLEVEL_BINDING
+ obj = eval("self", bind)
+ obj.instance_variable_set("@binding", bind)
+ bind
+end
+
+require 'lib/all'
+require 'app/syntax/markup'
+
+require 'app/db/sequel'
+
+require 'app/ui/lessons'
+require 'app/ui/widgets'
+require 'app/ui/completion'
+require 'app/ui/tabs/sidetabs'
+
+#let's give them a simple program to start off with!
+if HH::PREFS['first_run'].nil?
+ File.open(File.join(HH::USER, "Hello World.rb"), "w") do |f|
+ f << 'alert "Hello, world!"'
+ end
+
+ #the first_run pref will get set by the tour notice in app/ui/mainwindow
+end
diff --git a/app/app/db/connection_pool.rb b/app/app/db/connection_pool.rb
new file mode 100644
index 0000000..72e47e1
--- /dev/null
+++ b/app/app/db/connection_pool.rb
@@ -0,0 +1,65 @@
+require 'thread'
+
+module HH::Sequel
+ class ConnectionPool
+ attr_reader :max_size, :mutex, :conn_maker
+ attr_reader :available_connections, :allocated, :created_count
+
+ def initialize(max_size = 4, &block)
+ @max_size = max_size
+ @mutex = Mutex.new
+ @conn_maker = block
+
+ @available_connections = []
+ @allocated = {}
+ @created_count = 0
+ end
+
+ def size
+ @created_count
+ end
+
+ def hold
+ t = Thread.current
+ if (conn = owned_connection(t))
+ return yield(conn)
+ end
+ while !(conn = acquire(t))
+ sleep 0.001
+ end
+ begin
+ yield conn
+ ensure
+ release(t)
+ end
+ end
+
+ def owned_connection(thread)
+ @mutex.synchronize {@allocated[thread]}
+ end
+
+ def acquire(thread)
+ @mutex.synchronize do
+ @allocated[thread] ||= available
+ end
+ end
+
+ def available
+ @available_connections.pop || make_new
+ end
+
+ def make_new
+ if @created_count < @max_size
+ @created_count += 1
+ @conn_maker.call
+ end
+ end
+
+ def release(thread)
+ @mutex.synchronize do
+ @available_connections << @allocated[thread]
+ @allocated.delete(thread)
+ end
+ end
+ end
+end
diff --git a/app/app/db/core_ext.rb b/app/app/db/core_ext.rb
new file mode 100644
index 0000000..81046a0
--- /dev/null
+++ b/app/app/db/core_ext.rb
@@ -0,0 +1,9 @@
+# Time extensions.
+class Time
+ SQL_FORMAT = "TIMESTAMP '%Y-%m-%d %H:%M:%S'".freeze
+
+ # Formats the Time object as an SQL TIMESTAMP.
+ def to_sql_timestamp
+ strftime(SQL_FORMAT)
+ end
+end
diff --git a/app/app/db/database.rb b/app/app/db/database.rb
new file mode 100644
index 0000000..ce5679d
--- /dev/null
+++ b/app/app/db/database.rb
@@ -0,0 +1,119 @@
+require 'uri'
+
+require 'app/db/schema'
+
+module HH::Sequel
+ # A Database object represents a virtual connection to a database.
+ # The Database class is meant to be subclassed by database adapters in order
+ # to provide the functionality needed for executing queries.
+ class Database
+ # Constructs a new instance of a database connection with the specified
+ # options hash.
+ #
+ # Sequel::Database is an abstract class that is not useful by itself.
+ def initialize(opts = {})
+ @opts = opts
+ end
+
+ # Returns a new dataset with the from method invoked.
+ def from(*args); dataset.from(*args); end
+
+ # Returns a new dataset with the select method invoked.
+ def select(*args); dataset.select(*args); end
+
+ # Returns a new dataset with the from parameter set. For example,
+ # db[:posts].each {|p| alert p[:title]}
+ def [](table)
+ dataset.from(table)
+ end
+
+ # call-seq:
+ # db.execute(sql)
+ # db << sql
+ #
+ # Executes an sql query.
+ def <<(sql)
+ execute(sql)
+ end
+
+ # Returns a literal SQL representation of a value. This method is usually
+ # overriden in database adapters.
+ def literal(v)
+ case v
+ when String then "'%s'" % v
+ else v.to_s
+ end
+ end
+
+ # Creates a table. The easiest way to use this method is to provide a
+ # block:
+ # DB.create_table :posts do
+ # primary_key :id, :serial
+ # column :title, :text
+ # column :content, :text
+ # index :title
+ # end
+ def create_table(name, columns = nil, indexes = nil, &block)
+ if block
+ schema = Schema.new
+ schema.create_table(name, &block)
+ schema.create(self)
+ else
+ execute Schema.create_table_sql(name, columns, indexes)
+ end
+ end
+
+ # Drops a table.
+ def drop_table(name)
+ execute Schema.drop_table_sql(name)
+ end
+
+ # Performs a brute-force check for the existance of a table. This method is
+ # usually overriden in descendants.
+ def table_exists?(name)
+ from(name).first && true
+ rescue
+ false
+ end
+
+ @@adapters = Hash.new
+
+ # Sets the adapter scheme for the database class. Call this method in
+ # descendnants of Database to allow connection using a URL. For example:
+ # class DB2::Database < Sequel::Database
+ # set_adapter_scheme :db2
+ # ...
+ # end
+ def self.set_adapter_scheme(scheme)
+ @@adapters[scheme.to_sym] = self
+ end
+
+ # Converts a uri to an options hash. These options are then passed
+ # to a newly created database object.
+ def self.uri_to_options(uri)
+ {
+ :user => uri.user,
+ :password => uri.password,
+ :host => uri.host,
+ :port => uri.port,
+ :database => (uri.path =~ /\/(.*)/) && ($1)
+ }
+ end
+
+ # call-seq:
+ # Sequel::Database.connect(conn_string)
+ # Sequel.connect(conn_string)
+ #
+ # Creates a new database object based on the supplied connection string.
+ # The specified scheme determines the database class used, and the rest
+ # of the string specifies the connection options. For example:
+ # DB = Sequel.connect('sqlite:///blog.db')
+ def self.connect(conn_string)
+ uri = URI.parse(conn_string)
+ c = @@adapters[uri.scheme.to_sym]
+ raise "Invalid database scheme" unless c
+ c.new(c.uri_to_options(uri))
+ end
+ end
+end
+
diff --git a/app/app/db/dataset.rb b/app/app/db/dataset.rb
new file mode 100644
index 0000000..ca653ab
--- /dev/null
+++ b/app/app/db/dataset.rb
@@ -0,0 +1,354 @@
+module HH::Sequel
+ # A Dataset represents a view of a the data in a database, constrained by
+ # specific parameters such as filtering conditions, order, etc. Datasets
+ # can be used to create, retrieve, update and delete records.
+ #
+ # Query results are always retrieved on demand, so a dataset can be kept
+ # around and reused indefinitely:
+ # my_posts = DB[:posts].filter(:author => 'david') # no records are retrieved
+ # p my_posts.all # records are now retrieved
+ # ...
+ # p my_posts.all # records are retrieved again
+ #
+ # In order to provide this functionality, dataset methods such as where,
+ # select, order, etc. return modified copies of the dataset, so you can
+ # use different datasets to access data:
+ # posts = DB[:posts]
+ # davids_posts = posts.filter(:author => 'david')
+ # old_posts = posts.filter('stamp < ?', 1.week.ago)
+ #
+ # Datasets are Enumerable objects, so they can be manipulated using any
+ # of the Enumerable methods, such as map, inject, etc.
+ class Dataset
+ include Enumerable
+
+ attr_reader :db
+ attr_accessor :record_class
+
+ # Constructs a new instance of a dataset with a database instance, initial
+ # options and an optional record class. Datasets are usually constructed by
+ # invoking Database methods:
+ # DB[:posts]
+ # Or:
+ # DB.dataset # the returned dataset is blank
+ #
+ # Sequel::Dataset is an abstract class that is not useful by itself. Each
+ # database adaptor should provide a descendant class of Sequel::Dataset.
+ def initialize(db, opts = {}, record_class = nil)
+ @db = db
+ @opts = opts || {}
+ @record_class = record_class
+ end
+
+ # Returns a new instance of the dataset with its options
+ def dup_merge(opts)
+ self.class.new(@db, @opts.merge(opts), @record_class)
+ end
+
+ AS_REGEXP = /(.*)___(.*)/.freeze
+ AS_FORMAT = "%s AS %s".freeze
+ DOUBLE_UNDERSCORE = '__'.freeze
+ PERIOD = '.'.freeze
+
+ # Returns a valid SQL fieldname as a string. Field names specified as
+ # symbols can include double underscores to denote a dot separator, e.g.
+ # :posts__id will be converted into posts.id.
+ def field_name(field)
+ field.is_a?(Symbol) ? field.to_field_name : field
+ end
+
+ QUALIFIED_REGEXP = /(.*)\.(.*)/.freeze
+ QUALIFIED_FORMAT = "%s.%s".freeze
+
+ # Returns a qualified field name (including a table name) if the field
+ # name isn't already qualified.
+ def qualified_field_name(field, table)
+ fn = field_name(field)
+ fn = QUALIFIED_FORMAT % [table, fn] unless fn =~ QUALIFIED_REGEXP
+ end
+
+ WILDCARD = '*'.freeze
+ COMMA_SEPARATOR = ", ".freeze
+
+ # Converts a field list into a comma seperated string of field names.
+ def field_list(fields)
+ case fields
+ when Array then
+ if fields.empty?
+ WILDCARD
+ else
+ fields.map {|i| field_name(i)}.join(COMMA_SEPARATOR)
+ end
+ when Symbol then
+ fields.to_field_name
+ else
+ fields
+ end
+ end
+
+ # Converts an array of sources into a comma separated list.
+ def source_list(source)
+ case source
+ when Array then source.join(COMMA_SEPARATOR)
+ else source
+ end
+ end
+
+ # Returns a literal representation of a value to be used as part
+ # of an SQL expression. This method is overriden in descendants.
+ def literal(v)
+ "'%s'" % SQLite3::Database.quote(v.to_s)
+ end
+
+ AND_SEPARATOR = " AND ".freeze
+ EQUAL_COND = "(%s = %s)".freeze
+
+ # Formats an equality condition SQL expression.
+ def where_equal_condition(left, right)
+ EQUAL_COND % [field_name(left), literal(right)]
+ end
+
+ # Formats a where clause.
+ def where_list(where)
+ case where
+ when Hash then
+ where.map {|kv| where_equal_condition(kv[0], kv[1])}.join(AND_SEPARATOR)
+ when Array then
+ fmt = where.shift
+ fmt.gsub('?') {|i| literal(where.shift)}
+ else
+ where
+ end
+ end
+
+ # Formats a join condition.
+ def join_cond_list(cond, join_table)
+ cond.map do |kv|
+ EQUAL_COND % [
+ qualified_field_name(kv[0], join_table),
+ qualified_field_name(kv[1], @opts[:from])]
+ end.join(AND_SEPARATOR)
+ end
+
+ # Returns a copy of the dataset with the source changed.
+ def from(source)
+ dup_merge(:from => source)
+ end
+
+ # Returns a copy of the dataset with the selected fields changed.
+ def select(*fields)
+ fields = fields.first if fields.size == 1
+ dup_merge(:select => fields)
+ end
+
+ # Returns a copy of the dataset with the order changed.
+ def order(*order)
+ dup_merge(:order => order)
+ end
+
+ DESC_ORDER_REGEXP = /(.*)\sDESC/.freeze
+
+ def reverse_order(order)
+ order.map do |f|
+ if f.to_s =~ DESC_ORDER_REGEXP
+ $1
+ else
+ f.DESC
+ end
+ end
+ end
+
+ # Returns a copy of the dataset with the where conditions changed.
+ def where(*where)
+ if where.size == 1
+ where = where.first
+ if @opts[:where] && @opts[:where].is_a?(Hash) && where.is_a?(Hash)
+ where = @opts[:where].merge(where)
+ end
+ end
+ dup_merge(:where => where)
+ end
+
+ LEFT_OUTER_JOIN = 'LEFT OUTER JOIN'.freeze
+ INNER_JOIN = 'INNER JOIN'.freeze
+ RIGHT_OUTER_JOIN = 'RIGHT OUTER JOIN'.freeze
+ FULL_OUTER_JOIN = 'FULL OUTER JOIN'.freeze
+
+ def join(table, cond)
+ dup_merge(:join_type => LEFT_OUTER_JOIN, :join_table => table,
+ :join_cond => cond)
+ end
+
+ alias_method :filter, :where
+ alias_method :all, :to_a
+ alias_method :enum_map, :map
+
+ #
+ def map(field_name = nil, &block)
+ if block
+ enum_map(&block)
+ elsif field_name
+ enum_map {|r| r[field_name]}
+ else
+ []
+ end
+ end
+
+ def hash_column(key_column, value_column)
+ inject({}) do |m, r|
+ m[r[key_column]] = r[value_column]
+ m
+ end
+ end
+
+ def <<(values)
+ insert(values)
+ end
+
+ def insert_multiple(array, &block)
+ if block
+ array.each {|i| insert(block[i])}
+ else
+ array.each {|i| insert(i)}
+ end
+ end
+
+ SELECT = "SELECT %s FROM %s".freeze
+ LIMIT = " LIMIT %s".freeze
+ ORDER = " ORDER BY %s".freeze
+ WHERE = " WHERE %s".freeze
+ JOIN_CLAUSE = " %s %s ON %s".freeze
+
+ EMPTY = ''.freeze
+
+ SPACE = ' '.freeze
+
+ def select_sql(opts = nil)
+ opts = opts ? @opts.merge(opts) : @opts
+
+ fields = opts[:select]
+ select_fields = fields ? field_list(fields) : WILDCARD
+ select_source = source_list(opts[:from])
+ sql = SELECT % [select_fields, select_source]
+
+ if join_type = opts[:join_type]
+ join_table = opts[:join_table]
+ join_cond = join_cond_list(opts[:join_cond], join_table)
+ sql << (JOIN_CLAUSE % [join_type, join_table, join_cond])
+ end
+
+ if where = opts[:where]
+ sql << (WHERE % where_list(where))
+ end
+
+ if order = opts[:order]
+ sql << (ORDER % order.join(COMMA_SEPARATOR))
+ end
+
+ if limit = opts[:limit]
+ sql << (LIMIT % limit)
+ end
+
+ sql
+ end
+
+ INSERT = "INSERT INTO %s (%s) VALUES (%s)".freeze
+ INSERT_EMPTY = "INSERT INTO %s DEFAULT VALUES".freeze
+
+ def insert_sql(values, opts = nil)
+ opts = opts ? @opts.merge(opts) : @opts
+
+ if values.nil? || values.empty?
+ INSERT_EMPTY % opts[:from]
+ else
+ field_list = []
+ value_list = []
+ values.each do |k, v|
+ field_list << k
+ value_list << literal(v)
+ end
+
+ INSERT % [
+ opts[:from],
+ field_list.join(COMMA_SEPARATOR),
+ value_list.join(COMMA_SEPARATOR)]
+ end
+ end
+
+ UPDATE = "UPDATE %s SET %s".freeze
+ SET_FORMAT = "%s = %s".freeze
+
+ def update_sql(values, opts = nil)
+ opts = opts ? @opts.merge(opts) : @opts
+
+ set_list = values.map {|kv| SET_FORMAT % [kv[0], literal(kv[1])]}.
+ join(COMMA_SEPARATOR)
+ update_clause = UPDATE % [opts[:from], set_list]
+
+ where = opts[:where]
+ where_clause = where ? WHERE % where_list(where) : EMPTY
+
+ [update_clause, where_clause].join(SPACE)
+ end
+
+ DELETE = "DELETE FROM %s".freeze
+
+ def delete_sql(opts = nil)
+ opts = opts ? @opts.merge(opts) : @opts
+
+ delete_source = opts[:from]
+
+ where = opts[:where]
+ where_clause = where ? WHERE % where_list(where) : EMPTY
+
+ [DELETE % delete_source, where_clause].join(SPACE)
+ end
+
+ COUNT = "COUNT(*)".freeze
+ SELECT_COUNT = {:select => COUNT, :order => nil}.freeze
+
+ def count_sql(opts = nil)
+ select_sql(opts ? opts.merge(SELECT_COUNT) : SELECT_COUNT)
+ end
+
+ # aggregates
+ def min(field)
+ select(field.MIN).first[:min]
+ end
+
+ def max(field)
+ select(field.MAX).first[:max]
+ end
+ end
+end
+
+class Symbol
+ def DESC
+ "#{to_s} DESC"
+ end
+
+ def AS(target)
+ "#{field_name} AS #{target}"
+ end
+
+ def MIN; "MIN(#{to_field_name})"; end
+ def MAX; "MAX(#{to_field_name})"; end
+
+ AS_REGEXP = /(.*)___(.*)/.freeze
+ AS_FORMAT = "%s AS %s".freeze
+ DOUBLE_UNDERSCORE = '__'.freeze
+ PERIOD = '.'.freeze
+
+ def to_field_name
+ s = to_s
+ if s =~ AS_REGEXP
+ s = AS_FORMAT % [$1, $2]
+ end
+ s.split(DOUBLE_UNDERSCORE).join(PERIOD)
+ end
+
+ def ALL
+ "#{to_s}.*"
+ end
+end
+
diff --git a/app/app/db/http.rb b/app/app/db/http.rb
new file mode 100644
index 0000000..96bdf97
--- /dev/null
+++ b/app/app/db/http.rb
@@ -0,0 +1,78 @@
+require 'lib/web/yaml'
+
+module HH::Sequel
+ module HTTP
+ class Database < HH::Sequel::Database
+ set_adapter_scheme :http
+ attr_reader :url
+
+ include HH::YAML
+
+ def initialize(opts = {})
+ super
+ end
+
+ def dataset(opts = nil)
+ Dataset.new(self, opts)
+ end
+
+ def tables
+ fetch_uri(:Get)
+ end
+
+ def drop_table(name)
+ fetch_uri(:Delete, name)
+ end
+ end
+
+ class Dataset < HH::Sequel::Dataset
+ def each(opts = nil, &block)
+ res = @db.fetch_uri(:Get, @opts[:from], opts)
+ res.each(&block)
+ self
+ end
+
+ LIMIT_1 = {:limit => 1}.freeze
+
+ def first(opts = nil)
+ opts = opts ? opts.merge(LIMIT_1) : LIMIT_1
+ @db.fetch_uri(:Get, @opts[:from], opts).first
+ end
+
+ def last(opts = nil)
+ raise RuntimeError, 'No order specified' unless
+ @opts[:order] || (opts && opts[:order])
+
+ opts = {:order => reverse_order(@opts[:order])}.
+ merge(opts ? opts.merge(LIMIT_1) : LIMIT_1)
+ @db.fetch_uri(:Get, @opts[:from], opts).first
+ end
+
+ def count(opts = nil)
+ @db.fetch_uri(:Get, "#{@opts[:from]}/count", opts)
+ end
+
+ def insert(values = nil, opts = nil)
+ @db.fetch_uri(:Post, @opts[:from], values)
+ end
+
+ def save(values = nil, opts = nil)
+ @db.fetch_uri(:Post, @opts[:from], values)
+ end
+
+ def bulk_insert(values = nil, opts = nil)
+ @db.fetch_uri(:Put, "#{@opts[:from]}/new", YAML.dump(values))
+ end
+
+ def update(values, opts = nil)
+ @db.fetch_uri(:Post, @opts[:from], values)
+ self
+ end
+
+ def delete(opts = nil)
+ @db.fetch_uri(:Delete, @opts[:from])
+ self
+ end
+ end
+ end
+end
diff --git a/app/app/db/model.rb b/app/app/db/model.rb
new file mode 100644
index 0000000..db1b2c9
--- /dev/null
+++ b/app/app/db/model.rb
@@ -0,0 +1,235 @@
+
+module HH::Sequel
+ class Model
+ @@db = nil
+
+ def self.db; @@db; end
+ def self.db=(db); @@db = db; end
+
+ def self.table_name; @table_name; end
+ def self.set_table_name(t); @table_name = t; end
+
+ def self.dataset
+ return @dataset if @dataset
+ if !table_name
+ raise RuntimeError, "Table name not specified for class #{self}."
+ elsif !db
+ raise RuntimeError, "No database connected."
+ end
+ @dataset = db[table_name]
+ @dataset.record_class = self
+ @dataset
+ end
+ def self.set_dataset(ds); @dataset = ds; @dataset.record_class = self; end
+
+ def self.cache_by(column, expiration)
+ @cache_column = column
+
+ prefix = "#{name}.#{column}."
+ define_method(:cache_key) do
+ prefix + @values[column].to_s
+ end
+
+ define_method("find_by_#{column}".to_sym) do |arg|
+ key = cache_key
+ rec = CACHE[key]
+ if !rec
+ rec = find(column => arg)
+ CACHE.set(key, rec, expiration)
+ end
+ rec
+ end
+
+ alias_method :delete, :delete_and_invalidate_cache
+ alias_method :set, :set_and_update_cache
+ end
+
+ def self.cache_column
+ @cache_column
+ end
+
+ def self.primary_key; @primary_key ||= :id; end
+ def self.set_primary_key(k); @primary_key = k; end
+
+ def self.schema(name = nil, &block)
+ name ||= table_name
+ @schema = Schema::Generator.new(name, &block)
+ set_table_name name
+ if @schema.primary_key_name
+ set_primary_key @schema.primary_key_name
+ end
+ end
+
+ def self.table_exists?
+ db.table_exists?(table_name)
+ end
+
+ def self.create_table
+ db.execute get_schema.create_sql
+ end
+
+ def self.drop_table
+ db.execute get_schema.drop_sql
+ end
+
+ def self.recreate_table
+ drop_table if table_exists?
+ create_table
+ end
+
+ def self.get_schema
+ @schema
+ end
+
+ ONE_TO_ONE_PROC = "proc {i = @values[:%s]; %s[i] if i}".freeze
+ ID_POSTFIX = "_id".freeze
+ FROM_DATASET = "db[%s]".freeze
+
+ def self.one_to_one(name, opts)
+ klass = opts[:class] ? opts[:class] : (FROM_DATASET % name.inspect)
+ key = opts[:key] || (name.to_s + ID_POSTFIX)
+ define_method name, &eval(ONE_TO_ONE_PROC % [key, klass])
+ end
+
+ ONE_TO_MANY_PROC = "proc {%s.filter(:%s => @pkey)}".freeze
+ ONE_TO_MANY_ORDER_PROC = "proc {%s.filter(:%s => @pkey).order(%s)}".freeze
+ def self.one_to_many(name, opts)
+ klass = opts[:class] ? opts[:class] :
+ (FROM_DATASET % (opts[:table] || name.inspect))
+ key = opts[:on]
+ order = opts[:order]
+ define_method name, &eval(
+ (order ? ONE_TO_MANY_ORDER_PROC : ONE_TO_MANY_PROC) %
+ [klass, key, order.inspect]
+ )
+ end
+
+ def self.get_hooks(key)
+ @hooks ||= {}
+ @hooks[key] ||= []
+ end
+
+ def self.has_hooks?(key)
+ !get_hooks(key).empty?
+ end
+
+ def run_hooks(key)
+ self.class.get_hooks(key).each {|h| instance_eval(&h)}
+ end
+
+ def self.before_delete(&block)
+ get_hooks(:before_delete).unshift(block)
+ end
+
+ def self.after_create(&block)
+ get_hooks(:after_create) << block
+ end
+
+ ############################################################################
+
+ attr_reader :values, :pkey
+
+ def model
+ self.class
+ end
+
+ def primary_key
+ model.primary_key
+ end
+
+ def initialize(values)
+ @values = values
+ @pkey = values[self.class.primary_key]
+ end
+
+ def exists?
+ model.filter(primary_key => @pkey).count == 1
+ end
+
+ def refresh
+ record = self.class.find(primary_key => @pkey)
+ record ? (@values = record.values) :
+ (raise RuntimeError, "Record not found")
+ self
+ end
+
+ def self.find(cond)
+ dataset.filter(cond).first # || (raise RuntimeError, "Record not found.")
+ end
+
+ def self.each(&block); dataset.each(&block); end
+ def self.all; dataset.all; end
+ def self.filter(*arg); dataset.filter(*arg); end
+ def self.first; dataset.first; end
+ def self.count; dataset.count; end
+ def self.map(column); dataset.map(column); end
+ def self.hash_column(column); dataset.hash_column(primary_key, column); end
+ def self.join(*args); dataset.join(*args); end
+ def self.lock(mode, &block); dataset.lock(mode, &block); end
+ def self.delete_all
+ if has_hooks?(:before_delete)
+ db.transaction {dataset.all.each {|r| r.delete}}
+ else
+ dataset.delete
+ end
+ end
+
+ def self.[](key)
+ find key.is_a?(Hash) ? key : {primary_key => key}
+ end
+
+ def self.create(values = nil)
+ db.transaction do
+ obj = find(primary_key => dataset.insert(values))
+ obj.run_hooks(:after_create)
+ obj
+ end
+ end
+
+ def delete
+ db.transaction do
+ run_hooks(:before_delete)
+ model.dataset.filter(primary_key => @pkey).delete
+ end
+ end
+
+ FIND_BY_REGEXP = /^find_by_(.*)/.freeze
+ FILTER_BY_REGEXP = /^filter_by_(.*)/.freeze
+
+ def self.method_missing(m, *args)
+ Thread.exclusive do
+ method_name = m.to_s
+ if method_name =~ FIND_BY_REGEXP
+ c = $1
+ meta_def(method_name) {|arg| find(c => arg)}
+ send(m, *args) if respond_to?(m)
+ elsif method_name =~ FILTER_BY_REGEXP
+ c = $1
+ meta_def(method_name) {|arg| filter(c => arg)}
+ send(m, *args) if respond_to?(m)
+ else
+ super
+ end
+ end
+ end
+
+ def db; @@db; end
+
+ def [](field); @values[field]; end
+
+ def ==(obj)
+ (obj.class == model) && (obj.pkey == @pkey)
+ end
+
+ def set(values)
+ model.dataset.filter(primary_key => @pkey).update(values)
+ @values.merge!(values)
+ end
+ end
+
+ def self.Model(table_name)
+ Class.new(Sequel::Model) do
+ meta_def(:inherited) {|c| c.set_table_name(table_name)}
+ end
+ end
+end
diff --git a/app/app/db/schema.rb b/app/app/db/schema.rb
new file mode 100644
index 0000000..3315385
--- /dev/null
+++ b/app/app/db/schema.rb
@@ -0,0 +1,161 @@
+
+module HH::Sequel
+ class Schema
+ COMMA_SEPARATOR = ', '.freeze
+ COLUMN_DEF = '%s %s'.freeze
+ UNIQUE = ' UNIQUE'.freeze
+ NOT_NULL = ' NOT NULL'.freeze
+ DEFAULT = ' DEFAULT %s'.freeze
+ PRIMARY_KEY = ' PRIMARY KEY'.freeze
+ REFERENCES = ' REFERENCES %s'.freeze
+ ON_DELETE = ' ON DELETE %s'.freeze
+
+ RESTRICT = 'RESTRICT'.freeze
+ CASCADE = 'CASCADE'.freeze
+ NO_ACTION = 'NO ACTION'.freeze
+ SET_NULL = 'SET NULL'.freeze
+ SET_DEFAULT = 'SET DEFAULT'.freeze
+
+ TYPES = Hash.new {|h, k| k}
+ TYPES[:double] = 'double precision'
+
+ def self.on_delete_action(action)
+ case action
+ when restrict then RESTRICT
+ when cascade then CASCADE
+ when set_null then SET_NULL
+ when set_default then SET_DEFAULT
+ else NO_ACTION
+ end
+ end
+
+ def self.column_definition(column)
+ c = COLUMN_DEF % [column[:name], TYPES[column[:type]]]
+ c << UNIQUE if column[:unique]
+ c << NOT_NULL if column[:null] == false
+ c << DEFAULT % SQLite::Database.quote(column[:default]) if column.include?(:default)
+ c << PRIMARY_KEY if column[:primary_key]
+ c << REFERENCES % column[:table] if column[:table]
+ c << ON_DELETE % on_delete_action(column[:on_delete]) if
+ column[:on_delete]
+ c
+ end
+
+ def self.create_table_column_list(columns)
+ columns.map {|c| column_definition(c)}.join(COMMA_SEPARATOR)
+ end
+
+ CREATE_INDEX = 'CREATE INDEX %s ON %s (%s);'.freeze
+ CREATE_UNIQUE_INDEX = 'CREATE UNIQUE INDEX %s ON %s (%s);'.freeze
+ INDEX_NAME = '%s_%s_index'.freeze
+ UNDERSCORE = '_'.freeze
+
+ def self.index_definition(table_name, index)
+ fields = index[:columns].join(COMMA_SEPARATOR)
+ index_name = index[:name] || INDEX_NAME %
+ [table_name, index[:columns].join(UNDERSCORE)]
+ (index[:unique] ? CREATE_UNIQUE_INDEX : CREATE_INDEX) %
+ [index_name, table_name, fields]
+ end
+
+ def self.create_indexes_sql(table_name, indexes)
+ indexes.map {|i| index_definition(table_name, i)}.join
+ end
+
+ CREATE_TABLE = "CREATE TABLE %s (%s);".freeze
+
+ def self.create_table_sql(name, columns, indexes = nil)
+ sql = CREATE_TABLE % [name, create_table_column_list(columns)]
+ sql << create_indexes_sql(name, indexes) if indexes && !indexes.empty?
+ sql
+ end
+
+ DROP_TABLE = "DROP TABLE %s CASCADE;".freeze
+
+ def self.drop_table_sql(name)
+ DROP_TABLE % name
+ end
+
+ class Generator
+ attr_reader :table_name
+
+ def initialize(table_name, &block)
+ @table_name = table_name
+ @primary_key = {:name => :id, :type => :serial, :primary_key => true}
+ @columns = []
+ @indexes = []
+ instance_eval(&block)
+ end
+
+ def primary_key(name, type = nil, opts = nil)
+ @primary_key = {
+ :name => name,
+ :type => type || :serial,
+ :primary_key => true
+ }.merge(opts || {})
+ end
+
+ def primary_key_name
+ @primary_key && @primary_key[:name]
+ end
+
+ def column(name, type, opts = nil)
+ @columns << {:name => name, :type => type}.merge(opts || {})
+ end
+
+ def foreign_key(name, opts)
+ @columns << {:name => name, :type => :integer}.merge(opts || {})
+ end
+
+ def has_column?(name)
+ @columns.each {|c| return true if c[:name] == name}
+ false
+ end
+
+ def index(columns, opts = nil)
+ columns = [columns] unless columns.is_a?(Array)
+ @indexes << {:columns => columns}.merge(opts || {})
+ end
+
+ def create_sql
+ if @primary_key && !has_column?(@primary_key[:name])
+ @columns.unshift(@primary_key)
+ end
+ Schema.create_table_sql(@table_name, @columns, @indexes)
+ end
+
+ def drop_sql
+ Schema.drop_table_sql(@table_name)
+ end
+ end
+
+ attr_reader :instructions
+
+ def initialize(&block)
+ @instructions = []
+ instance_eval(&block) if block
+ end
+
+ def create_table(table_name, &block)
+ @instructions << Generator.new(table_name, &block)
+ end
+
+ def create(db)
+ @instructions.each do |s|
+ db.execute(s.create_sql)
+ end
+ end
+
+ def drop(db)
+ @instructions.reverse_each do |s|
+ db.execute(s.drop_sql) if db.table_exists?(s.table_name)
+ end
+ end
+
+ def recreate(db)
+ drop(db)
+ create(db)
+ end
+ end
+end
+
diff --git a/app/app/db/sequel.rb b/app/app/db/sequel.rb
new file mode 100644
index 0000000..ec23298
--- /dev/null
+++ b/app/app/db/sequel.rb
@@ -0,0 +1,21 @@
+require 'app/db/core_ext'
+require 'app/db/database'
+require 'app/db/connection_pool'
+require 'app/db/schema'
+require 'app/db/dataset'
+require 'app/db/model'
+require 'app/db/sqlite'
+require 'app/db/http'
+
+module HH::Sequel #:nodoc:
+ def self.connect(url)
+ Database.connect(url)
+ end
+end
+
+require 'app/db/table'
+
+# some constant initialization
+HH::DB = HH::Sequel::SQLite::Database.new(:database => File.join(HH::USER, "+TABLES"))
+HH::DB.extend HH::DbMixin
+HH::DB.init \ No newline at end of file
diff --git a/app/app/db/sqlite.rb b/app/app/db/sqlite.rb
new file mode 100644
index 0000000..2267c2f
--- /dev/null
+++ b/app/app/db/sqlite.rb
@@ -0,0 +1,112 @@
+require 'sqlite3'
+
+module HH::Sequel
+ module SQLite
+ class Database < HH::Sequel::Database
+ set_adapter_scheme :sqlite
+ attr_reader :pool
+
+ def initialize(opts = {})
+ super
+ @pool = ConnectionPool.new(@opts[:max_connections] || 4) do
+ db = SQLite3::Database.new(@opts[:database])
+ db.type_translation = true
+ db
+ end
+ end
+
+ def dataset(opts = nil)
+ SQLite::Dataset.new(self, opts)
+ end
+
+ def tables
+ execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name").map { |name,| name }
+ end
+
+ def execute(sql)
+ @pool.hold {|conn| conn.execute(sql)}
+ end
+
+ def execute_insert(sql)
+ @pool.hold {|conn| conn.execute(sql); conn.last_insert_row_id}
+ end
+
+ def single_value(sql)
+ @pool.hold {|conn| conn.get_first_value(sql)}
+ end
+
+ def result_set(sql, record_class, &block)
+ @pool.hold do |conn|
+ conn.query(sql) do |result|
+ columns = result.columns
+ column_count = columns.size
+ result.each do |values|
+ row = {}
+ column_count.times {|i| row[columns[i].to_sym] = values[i]}
+ block.call(record_class ? record_class.new(row) : row)
+ end
+ end
+ end
+ end
+
+ def synchronize(&block)
+ @pool.hold(&block)
+ end
+
+ def transaction(&block)
+ @pool.hold {|conn| conn.transaction(&block)}
+ end
+
+ def table_exists?(name)
+ execute("PRAGMA table_info('%s')" % SQLite3::Database.quote(name.to_s)).any?
+ end
+ end
+
+ class Dataset < HH::Sequel::Dataset
+ def each(opts = nil, &block)
+ @db.result_set(select_sql(opts), @record_class, &block)
+ self
+ end
+
+ LIMIT_1 = {:limit => 1}.freeze
+
+ def first(opts = nil)
+ opts = opts ? opts.merge(LIMIT_1) : LIMIT_1
+ @db.result_set(select_sql(opts), @record_class) {|r| return r}
+ end
+
+ def last(opts = nil)
+ raise RuntimeError, 'No order specified' unless
+ @opts[:order] || (opts && opts[:order])
+
+ opts = {:order => reverse_order(@opts[:order])}.
+ merge(opts ? opts.merge(LIMIT_1) : LIMIT_1)
+ @db.result_set(select_sql(opts), @record_class) {|r| return r}
+ end
+
+ def count(opts = nil)
+ @db.single_value(count_sql(opts)).to_i
+ end
+
+ def insert(values = nil, opts = nil)
+ @db.synchronize do
+ @db.execute_insert insert_sql(values, opts)
+ end
+ end
+
+ def update(values, opts = nil)
+ @db.synchronize do
+ @db.execute update_sql(values, opts)
+ end
+ self
+ end
+
+ def delete(opts = nil)
+ @db.synchronize do
+ @db.execute delete_sql(opts)
+ end
+ self
+ end
+ end
+ end
+end
diff --git a/app/app/db/table.rb b/app/app/db/table.rb
new file mode 100644
index 0000000..25563a3
--- /dev/null
+++ b/app/app/db/table.rb
@@ -0,0 +1,231 @@
+class HH::Sequel::SQLite::Dataset
+ def table_name
+ @opts[:from]
+ end
+ def widget(slot)
+ slot.stack(:margin => 18).tap do |s|
+ s.title "The #{table_name} Table"
+ set.each do |item|
+ s.para s.link(item[:title], :size => 18, :stroke => "#777"),
+ " Table::Item", :stroke => "#999"
+ s.para "at #{item[:created]}"
+ s.para item[:editbox]
+ end
+ end
+ end
+end
+
+class HH::Sequel::Dataset
+ def only(id)
+ first(:where => ['id = ?', id])
+ end
+ def limit(num)
+ dup_merge(:limit => num)
+ end
+ def recent(num)
+ order("created DESC").limit(num)
+ end
+ def save(data)
+ @db.save(@opts[:from], data)
+ end
+end
+
+module HH::DbMixin
+ SPECIAL_FIELDS = ['id', 'created', 'updated']
+ def tables
+ execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name").
+ map { |name,| name if name !~ /^HETYH_/ }.compact
+ end
+ def save(table, obj)
+ table = table.to_s
+ fields = get_fields(table)
+ if fields.empty?
+ startup(table, obj.keys)
+ else
+ missing = obj.keys - fields
+ unless missing.empty?
+ missing.each do |name|
+ add_column(table, name)
+ end
+ end
+ end
+ if obj['id']
+ from(table).only(obj['id']).update(obj.merge(:updated => Time.now))
+ else
+ from(table).insert(obj.merge(:created => Time.now, :updated => Time.now))
+ end
+ end
+ def init
+ unless table_exists? "HETYH_PREFS"
+ create_table "HETYH_PREFS" do
+ primary_key :id, :integer
+ column :name, :text
+ column :value, :text
+ index :name
+ end
+ end
+ HH.load_prefs
+ unless table_exists? "HETYH_SHARES"
+ create_table "HETYH_SHARES" do
+ primary_key :id, :integer
+ column :title, :text
+ column :klass, :text
+ column :active, :integer
+ index :title
+ end
+ end
+ HH.load_shares
+ end
+ def startup(table, fields)
+ SPECIAL_FIELDS.each do |x|
+ fields.each do |y|
+ raise ArgumentError, "Can't have a field called #{y}!" if y.downcase == x
+ end
+ end
+ create_table table do
+ primary_key :id, :integer
+ column :created, :datetime
+ column :updated, :datetime
+ fields.each do |name|
+ column name, :text
+ if [:title, :name].include? name
+ index name
+ end
+ end
+ end
+ true
+ rescue SQLite3::SQLException
+ false
+ end
+ def drop_table(table)
+ raise ArgumentError, "Table name must be letters, numbers, underscores only." if table !~ /^\w+$/
+ execute("DROP TABLE #{table}")
+ end
+ def get_fields(table)
+ raise ArgumentError, "Table name must be letters, numbers, underscores only." if table !~ /^\w+$/
+ execute("PRAGMA table_info(#{table})").map { |id, name,| name }
+ end
+ def add_column(table, column)
+ raise ArgumentError, "Table name must be letters, numbers, underscores only." if table !~ /^\w+$/
+ execute("ALTER TABLE #{table} ADD COLUMN #{HH::Sequel::Schema.column_definition(:name => column, :type => :text)}")
+ end
+end
+
+def Table(t)
+ raise ArgumentError, "Table name must be letters, numbers, underscores only. No spaces!" if t !~ /^\w+$/
+ if HH.check_share(t, 'Table')
+ Web.table(t)
+ else
+ HH::DB[t]
+ end
+end
+
+module HH
+ PREFS = {}
+ SHARES = {}
+
+ class << self
+ def tutor_on?
+ PREFS['tutor'] == 'on'
+ end
+
+ def tutor=(state)
+ PREFS['tutor'] = state
+ save_prefs
+ end
+
+ def tutor_lesson
+ (PREFS['tut_lesson'] || 0).to_i
+ end
+
+ def tutor_lesson=(n)
+ PREFS['tut_lesson']=n
+ save_prefs
+ end
+
+ def tutor_page
+ PREFS['tut_page'] || '/start'
+ end
+
+ def tutor_page=(p)
+ PREFS['tut_page']=p
+ save_prefs
+ end
+
+ def save_prefs
+ preft = HH::DB["HETYH_PREFS"]
+ preft.delete
+ PREFS.each do |k, v|
+ preft.insert(:name => k, :value => v)
+ end
+ nil
+ end
+
+ def load_prefs
+ HH::DB["HETYH_PREFS"].each do |row|
+ PREFS[row[:name]] = row[:value] unless row[:value].strip.empty?
+ end
+ PREFS['tutor'] = 'off'
+ end
+
+ def load_shares
+ SHARES.clear
+ HH::DB["HETYH_SHARES"].each do |row|
+ SHARES["#{row[:title]}:#{row[:klass]}"] = row[:active]
+ end
+ end
+
+ def add_share(title, klass)
+ share = {:title => title, :klass => klass, :active => 1}
+ HH::DB["HETYH_SHARES"].insert(share)
+ SHARES["#{title}:#{klass}"] = 1
+ end
+
+ def check_share(title, klass)
+ SHARES["#{title}:#{klass}"]
+ end
+
+ def script_exists?(name)
+ File.exists?(HH::USER + "/" + name + ".rb")
+ end
+
+ def save_script(name, code)
+ APP.emit :save, :name => name, :code => code
+ File.open(HH::USER + "/" + name + ".rb", "w") do |f|
+ f << code
+ end
+ return if PREFS['username'].blank?
+ end
+
+ def get_script(path)
+ app = {:name => File.basename(path, '.rb'), :script => File.read(path)}
+ m, = *app[:script].match(/\A(([ \t]*#.+)(\r?\n|$))+/)
+ app[:mtime] = File.mtime(path)
+ app[:desc] = m.gsub(/^[ \t]*#+[ \t]*/, '').strip.gsub(/\n+/, ' ') if m
+ app
+ end
+
+ def scripts
+ Dir["#{HH::USER}/*.rb"].map { |path| get_script(path) }.
+ sort_by { |script| Time.now - script[:mtime] }
+ end
+
+ def samples
+ Dir["#{HH::HOME}/samples/*.rb"].map do |path|
+ s = get_script(path)
+ # set the creation time to nil
+ s[:mtime] = nil
+ s[:sample] = true
+ s
+ end. sort_by { |script| script[:name] }
+ end
+
+ def user
+ return if PREFS['username'].blank?
+ unless @user and @user.name == PREFS['username']
+ @user = Hacker(PREFS)
+ end
+ @user
+ end
+ end
+end
diff --git a/app/app/syntax/common.rb b/app/app/syntax/common.rb
new file mode 100644
index 0000000..be961c1
--- /dev/null
+++ b/app/app/syntax/common.rb
@@ -0,0 +1,197 @@
+require 'strscan'
+
+module HH::Syntax
+
+ # A single token extracted by a tokenizer. It is simply the lexeme
+ # itself, decorated with a 'group' attribute to identify the type of the
+ # lexeme.
+ class Token < String
+
+ # the type of the lexeme that was extracted.
+ attr_reader :group
+
+ # the instruction associated with this token (:none, :region_open, or
+ # :region_close)
+ attr_reader :instruction
+
+ # Create a new Token representing the given text, and belonging to the
+ # given group.
+ def initialize( text, group, instruction = :none )
+ super text
+ @group = group
+ @instruction = instruction
+ end
+
+ end
+
+ # The base class of all tokenizers. It sets up the scanner and manages the
+ # looping until all tokens have been extracted. It also provides convenience
+ # methods to make sure adjacent tokens of identical groups are returned as
+ # a single token.
+ class Tokenizer
+
+ # The current group being processed by the tokenizer
+ attr_reader :group
+
+ # The current chunk of text being accumulated
+ attr_reader :chunk
+
+ # Start tokenizing. This sets up the state in preparation for tokenization,
+ # such as creating a new scanner for the text and saving the callback block.
+ # The block will be invoked for each token extracted.
+ def start( text, &block )
+ @chunk = ""
+ @group = :normal
+ @callback = block
+ @text = StringScanner.new( text )
+ setup
+ end
+
+ # Subclasses may override this method to provide implementation-specific
+ # setup logic.
+ def setup
+ end
+
+ # Finish tokenizing. This flushes the buffer, yielding any remaining text
+ # to the client.
+ def finish
+ start_group nil
+ teardown
+ end
+
+ # Subclasses may override this method to provide implementation-specific
+ # teardown logic.
+ def teardown
+ end
+
+ # Subclasses must implement this method, which is called for each iteration
+ # of the tokenization process. This method may extract multiple tokens.
+ def step
+ raise NotImplementedError, "subclasses must implement #step"
+ end
+
+ # Begins tokenizing the given text, calling #step until the text has been
+ # exhausted.
+ def tokenize( text, &block )
+ start text, &block
+ step until @text.eos?
+ finish
+ end
+
+ # Specify a set of tokenizer-specific options. Each tokenizer may (or may
+ # not) publish any options, but if a tokenizer does those options may be
+ # used to specify optional behavior.
+ def set( opts={} )
+ ( @options ||= Hash.new ).update opts
+ end
+
+ # Get the value of the specified option.
+ def option(opt)
+ @options ? @options[opt] : nil
+ end
+
+ private
+
+ EOL = /(?=\r\n?|\n|$)/
+
+ # A convenience for delegating method calls to the scanner.
+ def self.delegate( sym )
+ define_method( sym ) { |*a| @text.__send__( sym, *a ) }
+ end
+
+ delegate :bol?
+ delegate :eos?
+ delegate :scan
+ delegate :scan_until
+ delegate :check
+ delegate :check_until
+ delegate :getch
+ delegate :matched
+ delegate :pre_match
+ delegate :peek
+ delegate :pos
+
+ # Access the n-th subgroup from the most recent match.
+ def subgroup(n)
+ @text[n]
+ end
+
+ # Append the given data to the currently active chunk.
+ def append( data )
+ @chunk << data
+ end
+
+ # Request that a new group be started. If the current group is the same
+ # as the group being requested, a new group will not be created. If a new
+ # group is created and the current chunk is not empty, the chunk's
+ # contents will be yielded to the client as a token, and then cleared.
+ #
+ # After the new group is started, if +data+ is non-nil it will be appended
+ # to the chunk.
+ def start_group( gr, data=nil )
+ flush_chunk if gr != @group
+ @group = gr
+ @chunk << data if data
+ end
+
+ def start_region( gr, data=nil )
+ flush_chunk
+ @group = gr
+ @callback.call( Token.new( data||"", @group, :region_open ) )
+ end
+
+ def end_region( gr, data=nil )
+ flush_chunk
+ @group = gr
+ @callback.call( Token.new( data||"", @group, :region_close ) )
+ end
+
+ def flush_chunk
+ @callback.call( Token.new( @chunk, @group ) ) unless @chunk.empty?
+ @chunk = ""
+ end
+
+ def subtokenize( syntax, text )
+ tokenizer = Syntax.load( syntax )
+ tokenizer.set @options if @options
+ flush_chunk
+ tokenizer.tokenize( text, &@callback )
+ end
+ end
+
+
+ # A default tokenizer for handling syntaxes that are not explicitly handled
+ # elsewhere. It simply yields the given text as a single token.
+ class Default
+
+ # Yield the given text as a single token.
+ def tokenize( text )
+ yield Token.new( text, :normal )
+ end
+
+ end
+
+ # A hash for registering syntax implementations.
+ SYNTAX = Hash.new( Default )
+
+ # Load the implementation of the requested syntax. If the syntax cannot be
+ # found, or if it cannot be loaded for whatever reason, the Default syntax
+ # handler will be returned.
+ def load( syntax )
+ begin
+ require "app/syntax/lang/#{syntax}"
+ rescue LoadError
+ end
+ SYNTAX[ syntax ].new
+ end
+ module_function :load
+
+ # Return an array of the names of supported syntaxes.
+ def all
+ lang_dir = File.join(File.dirname(__FILE__), "syntax", "lang")
+ Dir["#{lang_dir}/*.rb"].map { |path| File.basename(path, ".rb") }
+ end
+ module_function :all
+
+
+end
diff --git a/app/app/syntax/convertors/abstract.rb b/app/app/syntax/convertors/abstract.rb
new file mode 100644
index 0000000..461e2db
--- /dev/null
+++ b/app/app/syntax/convertors/abstract.rb
@@ -0,0 +1,27 @@
+require 'app/syntax/common'
+
+module HH::Syntax
+ module Convertors
+
+ # The abstract ancestor class for all convertors. It implements a few
+ # convenience methods to provide a common interface for all convertors.
+ class Abstract
+
+ # A reference to the tokenizer used by this convertor.
+ attr_reader :tokenizer
+
+ # A convenience method for instantiating a new convertor for a
+ # specific syntax.
+ def self.for_syntax( syntax )
+ new( Syntax.load( syntax ) )
+ end
+
+ # Creates a new convertor that uses the given tokenizer.
+ def initialize( tokenizer )
+ @tokenizer = tokenizer
+ end
+
+ end
+
+ end
+end
diff --git a/app/app/syntax/convertors/html.rb b/app/app/syntax/convertors/html.rb
new file mode 100644
index 0000000..bbea262
--- /dev/null
+++ b/app/app/syntax/convertors/html.rb
@@ -0,0 +1,51 @@
+require 'app/syntax/convertors/abstract'
+
+module HH::Syntax
+ module Convertors
+
+ # A simple class for converting a text into HTML.
+ class HTML < Abstract
+
+ # Converts the given text to HTML, using spans to represent token groups
+ # of any type but <tt>:normal</tt> (which is always unhighlighted). If
+ # +pre+ is +true+, the html is automatically wrapped in pre tags.
+ def convert( text, pre=true )
+ html = ""
+ html << "<pre>" if pre
+ regions = []
+ @tokenizer.tokenize( text ) do |tok|
+ value = html_escape(tok)
+ case tok.instruction
+ when :region_close then
+ regions.pop
+ html << "</span>"
+ when :region_open then
+ regions.push tok.group
+ html << "<span class=\"#{tok.group}\">#{value}"
+ else
+ if tok.group == ( regions.last || :normal )
+ html << value
+ else
+ html << "<span class=\"#{tok.group}\">#{value}</span>"
+ end
+ end
+ end
+ html << "</span>" while regions.pop
+ html << "</pre>" if pre
+ html
+ end
+
+ private
+
+ # Replaces some characters with their corresponding HTML entities.
+ def html_escape( string )
+ string.gsub( /&/, "&amp;" ).
+ gsub( /</, "&lt;" ).
+ gsub( />/, "&gt;" ).
+ gsub( /"/, "&quot;" )
+ end
+
+ end
+
+ end
+end
diff --git a/app/app/syntax/lang/ruby.rb b/app/app/syntax/lang/ruby.rb
new file mode 100644
index 0000000..407db20
--- /dev/null
+++ b/app/app/syntax/lang/ruby.rb
@@ -0,0 +1,317 @@
+require 'app/syntax/common'
+
+module HH::Syntax
+
+ # A tokenizer for the Ruby language. It recognizes all common syntax
+ # (and some less common syntax) but because it is not a true lexer, it
+ # will make mistakes on some ambiguous cases.
+ class Ruby < Tokenizer
+
+ # The list of all identifiers recognized as keywords.
+ KEYWORDS =
+ %w{if then elsif else end begin do rescue ensure while for
+ class module def yield raise until unless and or not when
+ case super undef break next redo retry in return alias
+ defined?}
+
+ # Perform ruby-specific setup
+ def setup
+ @selector = false
+ @allow_operator = false
+ @heredocs = []
+ end
+
+ # Step through a single iteration of the tokenization process.
+ def step
+ case
+ when bol? && check( /=begin/ )
+ start_group( :comment, scan_until( /^=end#{EOL}/ ) )
+ when bol? && check( /__END__#{EOL}/ )
+ start_group( :comment, scan_until( /\Z/ ) )
+ else
+ case
+ when check( /def\s+/ )
+ start_group :keyword, scan( /def\s+/ )
+ start_group :method, scan_until( /(?=[;(\s]|#{EOL})/ )
+ when check( /class\s+/ )
+ start_group :keyword, scan( /class\s+/ )
+ start_group :class, scan_until( /(?=[;\s<]|#{EOL})/ )
+ when check( /module\s+/ )
+ start_group :keyword, scan( /module\s+/ )
+ start_group :module, scan_until( /(?=[;\s]|#{EOL})/ )
+ when check( /::/ )
+ start_group :punct, scan(/::/)
+ when check( /:"/ )
+ start_group :symbol, scan(/:/)
+ scan_delimited_region :symbol, :symbol, "", true
+ @allow_operator = true
+ when check( /:'/ )
+ start_group :symbol, scan(/:/)
+ scan_delimited_region :symbol, :symbol, "", false
+ @allow_operator = true
+ when scan( /:[_a-zA-Z@$][$@\w]*[=!?]?/ )
+ start_group :symbol, matched
+ @allow_operator = true
+ when scan( /\?(\\[^\n\r]|[^\\\n\r\s])/ )
+ start_group :char, matched
+ @allow_operator = true
+ when check( /(__FILE__|__LINE__|true|false|nil|self)[?!]?/ )
+ if @selector || matched[-1] == ?? || matched[-1] == ?!
+ start_group :ident,
+ scan(/(__FILE__|__LINE__|true|false|nil|self)[?!]?/)
+ else
+ start_group :constant,
+ scan(/(__FILE__|__LINE__|true|false|nil|self)/)
+ end
+ @selector = false
+ @allow_operator = true
+ when scan(/0([bB][01]+|[oO][0-7]+|[dD][0-9]+|[xX][0-9a-fA-F]+)/)
+ start_group :number, matched
+ @allow_operator = true
+ else
+ case peek(2)
+ when "%r"
+ scan_delimited_region :punct, :regex, scan( /../ ), true
+ @allow_operator = true
+ when "%w", "%q"
+ scan_delimited_region :punct, :string, scan( /../ ), false
+ @allow_operator = true
+ when "%s"
+ scan_delimited_region :punct, :symbol, scan( /../ ), false
+ @allow_operator = true
+ when "%W", "%Q", "%x"
+ scan_delimited_region :punct, :string, scan( /../ ), true
+ @allow_operator = true
+ when /%[^\sa-zA-Z0-9]/
+ scan_delimited_region :punct, :string, scan( /./ ), true
+ @allow_operator = true
+ when "<<"
+ saw_word = ( chunk[-1,1] =~ /[\w!?]/ )
+ start_group :punct, scan( /<</ )
+ if saw_word
+ @allow_operator = false
+ return
+ end
+
+ float_right = scan( /-/ )
+ append "-" if float_right
+ if ( type = scan( /['"]/ ) )
+ append type
+ delim = scan_until( /(?=#{type})/ )
+ if delim.nil?
+ append scan_until( /\Z/ )
+ return
+ end
+ else
+ delim = scan( /\w+/ ) or return
+ end
+ start_group :constant, delim
+ start_group :punct, scan( /#{type}/ ) if type
+ @heredocs << [ float_right, type, delim ]
+ @allow_operator = true
+ else
+ case peek(1)
+ when /[\n\r]/
+ unless @heredocs.empty?
+ scan_heredoc(*@heredocs.shift)
+ else
+ start_group :normal, scan( /\s+/ )
+ end
+ @allow_operator = false
+ when /\s/
+ start_group :normal, scan( /\s+/ )
+ when "#"
+ start_group :comment, scan( /#[^\n\r]*/ )
+ when /[A-Z]/
+ start_group @selector ? :ident : :constant, scan( /\w+/ )
+ @allow_operator = true
+ when /[a-z_]/
+ word = scan( /\w+[?!]?/ )
+ if !@selector && KEYWORDS.include?( word )
+ start_group :keyword, word
+ @allow_operator = false
+ elsif
+ start_group :ident, word
+ @allow_operator = true
+ end
+ @selector = false
+ when /\d/
+ start_group :number,
+ scan( /[\d_]+(\.[\d_]+)?([eE][\d_]+)?/ )
+ @allow_operator = true
+ when '"'
+ scan_delimited_region :punct, :string, "", true
+ @allow_operator = true
+ when '/'
+ if @allow_operator
+ start_group :punct, scan(%r{/})
+ @allow_operator = false
+ else
+ scan_delimited_region :punct, :regex, "", true
+ @allow_operator = true
+ end
+ when "'"
+ scan_delimited_region :punct, :string, "", false
+ @allow_operator = true
+ when "."
+ dots = scan( /\.{1,3}/ )
+ start_group :punct, dots
+ @selector = ( dots.length == 1 )
+ when /[@]/
+ start_group :attribute, scan( /@{1,2}\w*/ )
+ @allow_operator = true
+ when /[$]/
+ start_group :global, scan(/\$/)
+ start_group :global, scan( /\w+|./ ) if check(/./)
+ @allow_operator = true
+ when /[-!?*\/+=<>(\[\{}:;,&|%]/
+ start_group :punct, scan(/./)
+ @allow_operator = false
+ when /[)\]]/
+ start_group :punct, scan(/./)
+ @allow_operator = true
+ else
+ # all else just falls through this, to prevent
+ # infinite loops...
+ append getch
+ end
+ end
+ end
+ end
+ end
+
+ private
+
+ # Scan a delimited region of text. This handles the simple cases (strings
+ # delimited with quotes) as well as the more complex cases of %-strings
+ # and here-documents.
+ #
+ # * +delim_group+ is the group to use to classify the delimiters of the
+ # region
+ # * +inner_group+ is the group to use to classify the contents of the
+ # region
+ # * +starter+ is the text to use as the starting delimiter
+ # * +exprs+ is a boolean flag indicating whether the region is an
+ # interpolated string or not
+ # * +delim+ is the text to use as the delimiter of the region. If +nil+,
+ # the next character will be treated as the delimiter.
+ # * +heredoc+ is either +false+, meaning the region is not a heredoc, or
+ # <tt>:flush</tt> (meaning the delimiter must be flushed left), or
+ # <tt>:float</tt> (meaning the delimiter doens't have to be flush left).
+ def scan_delimited_region( delim_group, inner_group, starter, exprs,
+ delim=nil, heredoc=false )
+ # begin
+ if !delim
+ start_group delim_group, starter
+ delim = scan( /./ )
+ append delim
+
+ delim = case delim
+ when '{' then '}'
+ when '(' then ')'
+ when '[' then ']'
+ when '<' then '>'
+ else delim
+ end
+ end
+
+ start_region inner_group
+
+ items = "\\\\|"
+ if heredoc
+ items << "(^"
+ items << '\s*' if heredoc == :float
+ items << "#{Regexp.escape(delim)}\s*?)#{EOL}"
+ else
+ items << "#{Regexp.escape(delim)}"
+ end
+ items << "|#(\\$|@@?|\\{)" if exprs
+ items = Regexp.new( items )
+
+ loop do
+ p = pos
+ match = scan_until( items )
+ if match.nil?
+ start_group inner_group, scan_until( /\Z/ )
+ break
+ else
+ text = pre_match[p..-1]
+ start_group inner_group, text if text.length > 0
+ case matched.strip
+ when "\\"
+ unless exprs
+ case peek(1)
+ when "'"
+ scan(/./)
+ start_group :escape, "\\'"
+ when "\\"
+ scan(/./)
+ start_group :escape, "\\\\"
+ else
+ start_group inner_group, "\\"
+ end
+ else
+ start_group :escape, "\\"
+ c = getch
+ append c
+ case c
+ when 'x'
+ append scan( /[a-fA-F0-9]{1,2}/ )
+ when /[0-7]/
+ append scan( /[0-7]{0,2}/ )
+ end
+ end
+ when delim
+ end_region inner_group
+ start_group delim_group, matched
+ break
+ when /^#/
+ do_highlight = (option(:expressions) == :highlight)
+ start_region :expr if do_highlight
+ start_group :expr, matched
+ case matched[1]
+ when ?{
+ depth = 1
+ content = ""
+ while depth > 0
+ p = pos
+ c = scan_until( /[\{}]/ )
+ if c.nil?
+ content << scan_until( /\Z/ )
+ break
+ else
+ depth += ( matched == "{" ? 1 : -1 )
+ content << pre_match[p..-1]
+ content << matched if depth > 0
+ end
+ end
+ if do_highlight
+ subtokenize "ruby", content
+ start_group :expr, "}"
+ else
+ append content + "}"
+ end
+ when ?$, ?@
+ append scan( /\w+/ )
+ end
+ end_region :expr if do_highlight
+ else raise "unexpected match on #{matched}"
+ end
+ end
+ end
+ end
+
+ # Scan a heredoc beginning at the current position.
+ #
+ # * +float+ indicates whether the delimiter may be floated to the right
+ # * +type+ is +nil+, a single quote, or a double quote
+ # * +delim+ is the delimiter to look for
+ def scan_heredoc(float, type, delim)
+ scan_delimited_region( :constant, :string, "", type != "'",
+ delim, float ? :float : :flush )
+ end
+ end
+
+ SYNTAX["ruby"] = Ruby
+
+end
diff --git a/app/app/syntax/lang/xml.rb b/app/app/syntax/lang/xml.rb
new file mode 100644
index 0000000..02ba798
--- /dev/null
+++ b/app/app/syntax/lang/xml.rb
@@ -0,0 +1,108 @@
+require 'app/syntax/common'
+
+module HH::Syntax
+
+ # A simple implementation of an XML lexer. It handles most cases. It is
+ # not a validating lexer, meaning it will happily process invalid XML without
+ # complaining.
+ class XML < Tokenizer
+
+ # Initialize the lexer.
+ def setup
+ @in_tag = false
+ end
+
+ # Step through a single iteration of the tokenization process. This will
+ # yield (potentially) many tokens, and possibly zero tokens.
+ def step
+ start_group :normal, matched if scan( /\s+/ )
+ if @in_tag
+ case
+ when scan( /([-\w]+):([-\w]+)/ )
+ start_group :namespace, subgroup(1)
+ start_group :punct, ":"
+ start_group :attribute, subgroup(2)
+ when scan( /\d+/ )
+ start_group :number, matched
+ when scan( /[-\w]+/ )
+ start_group :attribute, matched
+ when scan( %r{[/?]?>} )
+ @in_tag = false
+ start_group :punct, matched
+ when scan( /=/ )
+ start_group :punct, matched
+ when scan( /["']/ )
+ scan_string matched
+ else
+ append getch
+ end
+ elsif ( text = scan_until( /(?=[<&])/ ) )
+ start_group :normal, text unless text.empty?
+ if scan(/<!--.*?(-->|\Z)/m)
+ start_group :comment, matched
+ else
+ case peek(1)
+ when "<"
+ start_group :punct, getch
+ case peek(1)
+ when "?"
+ append getch
+ when "/"
+ append getch
+ when "!"
+ append getch
+ end
+ start_group :normal, matched if scan( /\s+/ )
+ if scan( /([-\w]+):([-\w]+)/ )
+ start_group :namespace, subgroup(1)
+ start_group :punct, ":"
+ start_group :tag, subgroup(2)
+ elsif scan( /[-\w]+/ )
+ start_group :tag, matched
+ end
+ @in_tag = true
+ when "&"
+ if scan( /&\S{1,10};/ )
+ start_group :entity, matched
+ else
+ start_group :normal, scan( /&/ )
+ end
+ end
+ end
+ else
+ append scan_until( /\Z/ )
+ end
+ end
+
+ private
+
+ # Scan the string starting at the current position, with the given
+ # delimiter character.
+ def scan_string( delim )
+ start_group :punct, delim
+ match = /(?=[&\\]|#{delim})/
+ loop do
+ break unless ( text = scan_until( match ) )
+ start_group :string, text unless text.empty?
+ case peek(1)
+ when "&"
+ if scan( /&\S{1,10};/ )
+ start_group :entity, matched
+ else
+ start_group :string, getch
+ end
+ when "\\"
+ start_group :string, getch
+ append getch || ""
+ when delim
+ start_group :punct, getch
+ break
+ end
+ end
+ end
+
+ end
+
+ SYNTAX["xml"] = XML
+
+end
diff --git a/app/app/syntax/lang/yaml.rb b/app/app/syntax/lang/yaml.rb
new file mode 100644
index 0000000..f6d44ad
--- /dev/null
+++ b/app/app/syntax/lang/yaml.rb
@@ -0,0 +1,105 @@
+require 'app/syntax/common'
+
+module HH::Syntax
+
+ # A simple implementation of an YAML lexer. It handles most cases. It is
+ # not a validating lexer.
+ class YAML < Tokenizer
+
+ # Step through a single iteration of the tokenization process. This will
+ # yield (potentially) many tokens, and possibly zero tokens.
+ def step
+ if bol?
+ case
+ when scan(/---(\s*.+)?$/)
+ start_group :document, matched
+ when scan(/(\s*)([a-zA-Z][-\w]*)(\s*):/)
+ start_group :normal, subgroup(1)
+ start_group :key, subgroup(2)
+ start_group :normal, subgroup(3)
+ start_group :punct, ":"
+ when scan(/(\s*)-/)
+ start_group :normal, subgroup(1)
+ start_group :punct, "-"
+ when scan(/\s*$/)
+ start_group :normal, matched
+ when scan(/#.*$/)
+ start_group :comment, matched
+ else
+ append getch
+ end
+ else
+ case
+ when scan(/[\n\r]+/)
+ start_group :normal, matched
+ when scan(/[ \t]+/)
+ start_group :normal, matched
+ when scan(/!+(.*?^)?\S+/)
+ start_group :type, matched
+ when scan(/&\S+/)
+ start_group :anchor, matched
+ when scan(/\*\S+/)
+ start_group :ref, matched
+ when scan(/\d\d:\d\d:\d\d/)
+ start_group :time, matched
+ when scan(/\d\d\d\d-\d\d-\d\d\s\d\d:\d\d:\d\d(\.\d+)? [-+]\d\d:\d\d/)
+ start_group :date, matched
+ when scan(/['"]/)
+ start_group :punct, matched
+ scan_string matched
+ when scan(/:\w+/)
+ start_group :symbol, matched
+ when scan(/[:]/)
+ start_group :punct, matched
+ when scan(/#.*$/)
+ start_group :comment, matched
+ when scan(/>-?/)
+ start_group :punct, matched
+ start_group :normal, scan(/.*$/)
+ append getch until eos? || bol?
+ return if eos?
+ indent = check(/ */)
+ start_group :string
+ loop do
+ line = check_until(/[\n\r]|\Z/)
+ break if line.nil?
+ if line.chomp.length > 0
+ this_indent = line.chomp.match( /^\s*/ )[0]
+ break if this_indent.length < indent.length
+ end
+ append scan_until(/[\n\r]|\Z/)
+ end
+ else
+ start_group :normal, scan_until(/(?=$|#)/)
+ end
+ end
+ end
+
+ private
+
+ def scan_string( delim )
+ regex = /(?=[#{delim=="'" ? "" : "\\\\"}#{delim}])/
+ loop do
+ text = scan_until( regex )
+ if text.nil?
+ start_group :string, scan_until( /\Z/ )
+ break
+ else
+ start_group :string, text unless text.empty?
+ end
+
+ case peek(1)
+ when "\\"
+ start_group :expr, scan(/../)
+ else
+ start_group :punct, getch
+ break
+ end
+ end
+ end
+
+ end
+
+ SYNTAX["yaml"] = YAML
+
+end
diff --git a/app/app/syntax/markup.rb b/app/app/syntax/markup.rb
new file mode 100644
index 0000000..f0a5207
--- /dev/null
+++ b/app/app/syntax/markup.rb
@@ -0,0 +1,214 @@
+# syntax highlighting
+
+require 'app/syntax/common'
+
+module HH::Markup
+
+ TOKENIZER = HH::Syntax.load "ruby"
+ COLORS = {
+ :comment => {:stroke => "#887"},
+ :keyword => {:stroke => "#111"},
+ :method => {:stroke => "#C09", :weight => "bold"},
+ # :class => {:stroke => "#0c4", :weight => "bold"},
+ # :module => {:stroke => "#050"},
+ # :punct => {:stroke => "#668", :weight => "bold"},
+ :symbol => {:stroke => "#C30"},
+ :string => {:stroke => "#C90"},
+ :number => {:stroke => "#396" },
+ :regex => {:stroke => "#000", :fill => "#FFC" },
+ # :char => {:stroke => "#f07"},
+ :attribute => {:stroke => "#369" },
+ # :global => {:stroke => "#7FB" },
+ :expr => {:stroke => "#722" },
+ # :escape => {:stroke => "#277" }
+ :ident => {:stroke => "#A79"},
+ :constant => {:stroke => "#630", :weight => "bold"},
+ :class => {:stroke => "#630", :weight => "bold"},
+ :matching => {:stroke => "#ff0", :weight => "bold"},
+ }
+
+
+ def highlight str, pos=nil, colors=COLORS
+ tokens = []
+ TOKENIZER.tokenize(str) do |t|
+ if t.group == :punct
+ # split punctuation into single characters tokens
+ # TODO: to it in the parser
+ tokens += t.split('').map{|s| HH::Syntax::Token.new(s, :punct)}
+ else
+ # add token as is
+ tokens << t
+ end
+ end
+
+ res = []
+ tokens.each do |token|
+ res <<
+ if colors[token.group]
+ span(token, colors[token.group])
+ elsif colors[:any]
+ span(token, colors[:any])
+ else
+ token
+ end
+ end
+
+ if pos.nil? or pos < 0
+ return res
+ end
+
+ token_index, matching_index = matching_token(tokens, pos)
+
+ if token_index
+ res[token_index] = span(tokens[token_index], colors[:matching])
+ if matching_index
+ res[matching_index] = span(tokens[matching_index], colors[:matching])
+ end
+ end
+
+ res
+ end
+
+private
+ def matching_token(tokens, pos)
+ curr_pos = 0
+ token_index = nil
+ tokens.each_with_index do |t, i|
+ curr_pos += t.size
+ if token_index.nil? and curr_pos >= pos
+ token_index = i
+ break
+ end
+ end
+ if token_index.nil? then return nil end
+
+ match = matching_token_at_index(tokens, token_index);
+ if match.nil? and curr_pos == pos and token_index < tokens.size-1
+ # try the token before the cursor, instead of the one after
+ token_index += 1
+ match = matching_token_at_index(tokens, token_index)
+ end
+
+ if match
+ [token_index, match]
+ else
+ nil
+ end
+ end
+
+
+ def matching_token_at_index(tokens, index)
+ starts, ends, direction = matching_tokens(tokens, index)
+ if starts.nil?
+ return nil
+ end
+
+ stack_level = 1
+ index += direction
+ while index >= 0 and index < tokens.size
+ # TODO separate space in the tokenizer
+ t = tokens[index].gsub(/\s/, '')
+ if ends.include?(t) and not as_modifier?(tokens, index)
+ stack_level -= 1
+ return index if stack_level == 0
+ elsif starts.include?(t) and not as_modifier?(tokens, index)
+ stack_level += 1
+ end
+ index += direction
+ end
+ # no matching token found
+ return nil
+ end
+
+ # returns an array of tokens matching and the direction
+ def matching_tokens(tokens, index)
+ # TODO separate space in the tokenizer
+ token = tokens[index].gsub(/\s/, '')
+ starts = [token]
+ if OPEN_BRACKETS[token]
+ direction = 1
+ ends = [OPEN_BRACKETS[token]]
+ elsif CLOSE_BRACKETS[token]
+ direction = -1
+ ends = [CLOSE_BRACKETS[token]]
+ elsif OPEN_BLOCK.include?(token)
+ if as_modifier?(tokens, index)
+ return nil
+ end
+ direction = 1
+ ends = ['end']
+ starts = OPEN_BLOCK
+ elsif token == 'end'
+ direction = -1
+ ends = OPEN_BLOCK
+ else
+ return nil
+ end
+
+ [starts, ends, direction]
+ end
+
+ def as_modifier?(tokens, index)
+
+ if not MODIFIERS.include? tokens[index].gsub(/\s/, '')
+ return false
+ end
+
+ index -= 1
+ # find last index before the token that is no space
+ index -= 1 while index >= 0 and tokens[index] =~ /\A[ \t]*\z/
+
+ if index < 0
+ # first character of the string
+ false
+ elsif tokens[index] =~ /\n[ \t]*\Z/
+ # first token of the line
+ false
+ elsif tokens[index].group == :punct
+ # preceded by a punctuation token on the same line
+ i = tokens[index].rindex(/\S/)
+ punc = tokens[index][i, 1]
+ # true if the preceeding statement was terminating
+ not NON_TERMINATING.include?(punc)
+ else
+ # preceded by a non punctuation token on the same line
+ true
+ end
+ end
+
+
+ OPEN_BRACKETS = {
+ '{' => '}',
+ '(' => ')',
+ '[' => ']',
+ }
+
+ #close_bracket = {}
+ #OPEN_BRACKETS.each{|open, close| opens_bracket[close] = open}
+ #CLOSE_BRACKETS = opens_bracket
+ # the following is more readable :)
+ CLOSE_BRACKETS = {
+ '}' => '{',
+ ')' => '(',
+ ']' => '[',
+ }
+
+ BRACKETS = CLOSE_BRACKETS.keys + OPEN_BRACKETS.keys
+
+ OPEN_BLOCK = [
+ 'def',
+ 'class',
+ 'module',
+ 'do',
+ 'if',
+ 'unless',
+ 'while',
+ 'until',
+ 'begin',
+ 'for'
+ ]
+
+ MODIFIERS = %w[if unless while until]
+
+ NON_TERMINATING = %w{+ - * / , . = ~ < > ( [}
+end
diff --git a/app/app/syntax/version.rb b/app/app/syntax/version.rb
new file mode 100644
index 0000000..72d93a6
--- /dev/null
+++ b/app/app/syntax/version.rb
@@ -0,0 +1,11 @@
+# not used anywhere
+
+#module HH::Syntax
+# module Version
+# MAJOR=1
+# MINOR=0
+# TINY=0
+#
+# STRING=[MAJOR,MINOR,TINY].join('.')
+# end
+#end
diff --git a/app/app/ui/completion.rb b/app/app/ui/completion.rb
new file mode 100644
index 0000000..91bacf9
--- /dev/null
+++ b/app/app/ui/completion.rb
@@ -0,0 +1,183 @@
+# from the irb source code
+
+module HH::InputCompletor
+
+ ReservedWords = [
+ "BEGIN", "END",
+ "alias", "and",
+ "begin", "break",
+ "case", "class",
+ "def", "defined", "do",
+ "else", "elsif", "end", "ensure",
+ "false", "for",
+ "if", "in",
+ "module",
+ "next", "nil", "not",
+ "or",
+ "redo", "rescue", "retry", "return",
+ "self", "super",
+ "then", "true",
+ "undef", "unless", "until",
+ "when", "while",
+ "yield",
+ ]
+
+ def self.complete input, bind
+ case input
+ when /^(\/[^\/]*\/)\.([^.]*)$/
+ # Regexp
+ receiver = $1
+ message = Regexp.quote($2)
+
+ candidates = Regexp.instance_methods(true)
+ select_message(receiver, message, candidates)
+
+ when /^([^\]]*\])\.([^.]*)$/
+ # Array
+ receiver = $1
+ message = Regexp.quote($2)
+
+ candidates = Array.instance_methods(true)
+ select_message(receiver, message, candidates)
+
+ when /^([^\}]*\})\.([^.]*)$/
+ # Proc or Hash
+ receiver = $1
+ message = Regexp.quote($2)
+
+ candidates = Proc.instance_methods(true) | Hash.instance_methods(true)
+ select_message(receiver, message, candidates)
+
+ when /^(:[^:.]*)$/
+ # Symbol
+ if Symbol.respond_to?(:all_symbols)
+ sym = $1
+ candidates = Symbol.all_symbols.collect{|s| ":" + s.id2name}
+ candidates.grep(/^#{sym}/)
+ else
+ []
+ end
+
+ when /^::([A-Z][^:\.\(]*)$/
+ # Absolute Constant or class methods
+ receiver = $1
+ candidates = Object.constants
+ candidates.grep(/^#{receiver}/).collect{|e| "::" + e}
+
+ when /^(((::)?[A-Z][^:.\(]*)+)::?([^:.]*)$/
+ # Constant or class methods
+ receiver = $1
+ message = Regexp.quote($4)
+ begin
+ candidates = eval("#{receiver}.constants | #{receiver}.methods", bind)
+ rescue Exception
+ candidates = []
+ end
+ candidates.grep(/^#{message}/).collect{|e| receiver + "::" + e}
+
+ when /^(:[^:.]+)\.([^.]*)$/
+ # Symbol
+ receiver = $1
+ message = Regexp.quote($2)
+
+ candidates = Symbol.instance_methods(true)
+ select_message(receiver, message, candidates)
+
+ when /^(-?(0[dbo])?[0-9_]+(\.[0-9_]+)?([eE]-?[0-9]+)?)\.([^.]*)$/
+ # Numeric
+ receiver = $1
+ message = Regexp.quote($5)
+
+ begin
+ candidates = eval(receiver, bind).methods
+ rescue Exception
+ candidates = []
+ end
+ select_message(receiver, message, candidates)
+
+ when /^(-?0x[0-9a-fA-F_]+)\.([^.]*)$/
+ # Numeric(0xFFFF)
+ receiver = $1
+ message = Regexp.quote($2)
+
+ begin
+ candidates = eval(receiver, bind).methods
+ rescue Exception
+ candidates = []
+ end
+ select_message(receiver, message, candidates)
+
+ when /^(\$[^.]*)$/
+ candidates = global_variables.grep(Regexp.new(Regexp.quote($1)))
+
+ # when /^(\$?(\.?[^.]+)+)\.([^.]*)$/
+ when /^((\.?[^.]+)+)\.([^.]*)$/
+ # variable
+ receiver = $1
+ message = Regexp.quote($3)
+
+ gv = eval("global_variables", bind)
+ lv = eval("local_variables", bind)
+ cv = eval("self.class.constants", bind)
+
+ if (gv | lv | cv).include?(receiver)
+ # foo.func and foo is local var.
+ candidates = eval("#{receiver}.methods", bind)
+ elsif /^[A-Z]/ =~ receiver and /\./ !~ receiver
+ # Foo::Bar.func
+ begin
+ candidates = eval("#{receiver}.methods", bind)
+ rescue Exception
+ candidates = []
+ end
+ else
+ # func1.func2
+ candidates = []
+ ObjectSpace.each_object(Module){|m|
+ begin
+ name = m.name
+ rescue Exception
+ name = ""
+ end
+ next if name != "IRB::Context" and
+ /^(IRB|SLex|RubyLex|RubyToken)/ =~ name
+ candidates.concat m.instance_methods(false)
+ }
+ candidates.sort!
+ candidates.uniq!
+ end
+ select_message(receiver, message, candidates).select{|x| x}
+
+ when /^\.([^.]*)$/
+ # unknown(maybe String)
+
+ receiver = ""
+ message = Regexp.quote($1)
+
+ candidates = String.instance_methods(true)
+ select_message(receiver, message, candidates)
+
+ else
+ candidates = eval("methods | private_methods | local_variables | self.class.constants", bind)
+
+ (candidates|ReservedWords).grep(/^#{Regexp.quote(input)}/)
+ end
+ end
+
+ Operators = ["%", "&", "*", "**", "+", "-", "/",
+ "<", "<<", "<=", "<=>", "==", "===", "=~", ">", ">=", ">>",
+ "[]", "[]=", "^",]
+
+ def self.select_message(receiver, message, candidates)
+ candidates.grep(/^#{message}/).collect do |e|
+ case e
+ when /^[a-zA-Z_]/
+ receiver + "." + e
+ when /^[0-9]/
+ when *Operators
+ #receiver + " " + e
+ end
+ end
+ end
+
+end
diff --git a/app/app/ui/editor/editor.rb b/app/app/ui/editor/editor.rb
new file mode 100644
index 0000000..450b027
--- /dev/null
+++ b/app/app/ui/editor/editor.rb
@@ -0,0 +1,448 @@
+# the code editor tab contents
+
+class HH::SideTabs::Editor < HH::SideTab
+ # common code between InsertionAction and DeletionAction
+ # on_insert_text and on_delete_text should be called before any subclass
+ # can be used
+ class InsertionDeletionCommand
+
+ def self.on_insert_text &block
+ @@insert_text = block
+ end
+ def self.on_delete_text &block
+ @@delete_text = block
+ end
+
+ # action to insert/delete str to text at position pos
+ def initialize pos, str
+ @position, @string = pos, str
+ end
+ def insert
+ @@insert_text.call(@position, @string)
+ end
+ def delete
+ @@delete_text.call(@position, @string.size)
+ end
+
+ protected
+ attr_accessor :position, :string
+ end
+
+ class InsertionCommand < InsertionDeletionCommand
+ alias execute insert
+ alias unexecute delete
+
+ # returns nil if not mergeble
+ def merge_with second
+ if second.class != self.class
+ nil
+ elsif second.position != self.position + self.string.size
+ nil
+ elsif second.string == "\n"
+ nil # newlines always start a new command
+ else
+ self.string += second.string
+ self
+ end
+ end
+ end
+
+ class DeletionCommand < InsertionDeletionCommand
+ alias execute delete
+ alias unexecute insert
+
+ def merge_with second
+ if second.class != self.class
+ nil
+ elsif second.string == "\n"
+ nil
+ elsif second.position == self.position
+ # probably the delete key
+ self.string += second.string
+ self
+ elsif self.position == second.position + second.string.size
+ # probably the backspace key
+ self.position = second.position
+ self.string = second.string + self.string
+ self
+ else
+ nil
+ end
+ end
+ end
+
+ module UndoRedo
+
+ def reset_undo_redo
+ @command_stack = [] # array of actions
+ @stack_position = 0;
+ @last_position = nil
+ end
+
+ # _act was added for consistency with redo_act
+ def undo_command
+ return if @stack_position == 0
+ @stack_position -= 1;
+ @command_stack[@stack_position].unexecute;
+ end
+
+ # _act was added because redo is a keyword
+ def redo_command
+ return if @stack_position == @command_stack.size
+ @command_stack[@stack_position].execute
+ @stack_position += 1;
+ end
+
+ def add_command cmd
+ # all redos get removed
+ @command_stack[@stack_position..-1] = nil
+ last = @command_stack.last
+ if last.nil? or not last.merge_with(cmd)
+ # create new command
+ @command_stack[@stack_position] = cmd
+ @stack_position += 1
+ end
+ end
+ end
+end # module HH::Editor
+
+class HH::SideTabs::Editor
+ include HH::Markup
+ include UndoRedo
+
+ def content
+ draw_content
+ end
+
+ def load script
+ if not @save_button.hidden
+ # current script is unsaved
+ name = @script[:name] || "An unnamed program"
+ if not confirm("#{name} has not been saved, if you continue \n" +
+ " all unsaved modifications will be lost")
+ return false
+ end
+ end
+ clear {draw_content script}
+ true
+ end
+
+ # asks confirmation and then saves (or not if save is)
+ def save_if_confirmed
+ if not @save_button.hidden
+ name = @script[:name] || "unnamed program"
+ question = "Do you want to save modifications to \"#{name}\"?\n" +
+ "Warning: by pressing \"Cancel\" you will lose any \n" +
+ "unsaved modifications."
+ if confirm(question)
+ save @script[:name]
+ true
+ else
+ false
+ end
+ end
+ end
+
+ def draw_content(script = {})
+ @str = script[:script] || ""
+ name = script[:name] || "A New Program"
+ @script = script
+
+ reset_undo_redo
+ InsertionDeletionCommand.on_insert_text {|pos, str| insert_text(pos, str)}
+ InsertionDeletionCommand.on_delete_text {|pos, len| delete_text(pos, len)}
+ @editor = stack :margin_left => 10, :margin_top => 10, :width => 1.0, :height => 92 do
+ @sname = subtitle name, :font => "Lacuna Regular", :size => 22,
+ :margin => 0, :wrap => "trim"
+ @stale = para(script[:mtime] ? "Last saved #{script[:mtime].since} ago." :
+ "Not yet saved.", :margin => 0, :stroke => "#39C")
+ glossb "New Program", :top => 0, :right => 0, :width => 160 do
+ load({})
+ end
+ end
+ stack :margin_left => 0, :width => 1.0, :height => -92 do
+ background white(0.4), :width => 38
+ @scroll =
+ flow :width => 1.0, :height => 1.0, :margin => 2, :scroll => true do
+ stack :width => 37, :margin_right => 6 do
+ @ln = para "1", :font => "Liberation Mono", :size => 10, :stroke => "#777", :align => "right"
+ end
+ stack :width => -37, :margin_left => 6, :margin_bottom => 60 do
+ @t = para "", :font => "Liberation Mono", :size => 10, :stroke => "#662",
+ :wrap => "trim", :margin_right => 28
+ @t.cursor = 0
+ def @t.hit_sloppy(x, y)
+ x -= 6
+ c = hit(x, y)
+ if c
+ c + 1
+ elsif x <= 48
+ hit(48, y)
+ end
+ end
+ end
+ motion do |x, y|
+ c = @t.hit_sloppy(x, y)
+ if c
+ if self.cursor == :arrow
+ self.cursor = :text
+ end
+ if self.mouse[0] == 1 and @clicked
+ if @t.marker.nil?
+ @t.marker = c
+ else
+ @t.cursor = c
+ end
+ end
+ elsif self.cursor == :text
+ self.cursor = :arrow
+ end
+ end
+ release do
+ @clicked = false
+ end
+ click do |_, x, y|
+ c = @t.hit_sloppy(x, y)
+ if c
+ @clicked = true
+ @t.marker = nil
+ @t.cursor = c
+ end
+ update_text
+ end
+ leave { self.cursor = :arrow }
+ end
+ end
+
+ stack :height => 40, :width => 182, :bottom => -3, :right => 0 do
+
+ @copy_button =
+ glossb "Copy", :width => 60, :top => 2, :left => 70 do
+ save(nil)
+ end
+ @save_button =
+ glossb "Save", :width => 60, :top => 2, :left => 70, :hidden => true do
+ if save(script[:name])
+ timer 0.1 do
+ @save_button.hide
+ @copy_button.show
+ @save_to_cloud_button.show
+ end
+ end
+ end
+ @save_to_cloud_button =
+ glossb "Upload", :width => 70, :top => 2, :left => 0 do
+ hacker = Hacker.new :username => HH::PREFS['username'], :password => HH::PREFS['password']
+ hacker.save_program_to_the_cloud script[:name].to_slug, @str
+ alert("Uploaded!")
+ end
+ glossb "Run", :width => 52, :top => 2, :left => 130 do
+ eval(@str, HH.anonymous_binding)
+ end
+ end
+
+ every 20 do
+ if script[:mtime]
+ @stale.text = "Last saved #{script[:mtime].since} ago."
+ end
+ end
+
+ def onkey(k)
+ case k when :shift_home, :shift_end, :shift_up, :shift_left, :shift_down, :shift_right
+ @t.marker = @t.cursor unless @t.marker
+ when :home, :end, :up, :left, :down, :right
+ @t.marker = nil
+ end
+
+ case k
+ when String
+ if k == "\n"
+ # handle indentation
+ ind = indentation_size
+ handle_text_insertion(k)
+ handle_text_insertion(" " * ind) if ind > 0
+ else
+ # usual case
+ handle_text_insertion(k)
+ end
+ when :backspace, :shift_backspace, :control_backspace
+ if @t.cursor > 0 and @t.marker.nil?
+ @t.marker = @t.cursor - 1 # make highlight length at least 1
+ end
+ sel = @t.highlight
+ if sel[0] > 0 or sel[1] > 0
+ handle_text_deletion(*sel)
+ end
+ when :delete
+ sel = @t.highlight
+ sel[1] = 1 if sel[1] == 0
+ handle_text_deletion(*sel)
+ when :tab
+ handle_text_insertion(" ")
+# when :alt_q
+# @action.clear { home }
+ when :control_a, :alt_a
+ @t.marker = 0
+ @t.cursor = @str.length
+ when :control_x, :alt_x
+ if @t.marker
+ sel = @t.highlight
+ self.clipboard = @str[*sel]
+ if sel[1] == 0
+ sel[1] = 1
+ raise "why did this happen??"
+ end
+ handle_text_deletion(*sel)
+ end
+ when :control_c, :alt_c, :control_insertadd_characte
+ if @t.marker
+ self.clipboard = @str[*@t.highlight]
+ end
+ when :control_v, :alt_v, :shift_insert
+ handle_text_insertion(self.clipboard) if self.clipboard
+ when :control_z
+ debug("undo!")
+ undo_command
+ when :control_y, :alt_Z, :shift_alt_z
+ redo_command
+ when :shift_home, :home
+ nl = @str.rindex("\n", @t.cursor - 1) || -1
+ @t.cursor = nl + 1
+ when :shift_end, :end
+ nl = @str.index("\n", @t.cursor) || @str.length
+ @t.cursor = nl
+ when :shift_up, :up
+ if @t.cursor > 0
+ nl = @str.rindex("\n", @t.cursor - 1)
+ if nl
+ horz = @t.cursor - nl
+ upnl = @str.rindex("\n", nl - 1) || -1
+ @t.cursor = upnl + horz
+ @t.cursor = nl if @t.cursor > nl
+ end
+ end
+ when :shift_down, :down
+ nl = @str.index("\n", @t.cursor)
+ if nl
+ if @t.cursor > 0
+ horz = @t.cursor - (@str.rindex("\n", @t.cursor - 1) || -1)
+ else
+ horz = 1
+ end
+ dnl = @str.index("\n", nl + 1) || @str.length
+ @t.cursor = nl + horz
+ @t.cursor = dnl if @t.cursor > dnl
+ end
+ when :shift_right, :right
+ @t.cursor += 1 if @t.cursor < @str.length
+ when :shift_left, :left
+ @t.cursor -= 1 if @t.cursor > 0
+ end
+ if k
+ text_changed
+ end
+
+ update_text
+ end
+
+ spaces = [?\t, ?\s, ?\n]
+
+ keypress do |k|
+ onkey(k)
+ if @t.cursor_top < @scroll.scroll_top
+ @scroll.scroll_top = @t.cursor_top
+ elsif @t.cursor_top + 92 > @scroll.scroll_top + @scroll.height
+ @scroll.scroll_top = (@t.cursor_top + 92) - @scroll.height
+ end
+
+ end
+
+ # for samples do not allow to upload to cloud when just opened
+ @save_to_cloud_button.hide if script[:sample]
+ update_text
+ end
+
+ # saves the file, asks for a new name if a nil argument is passed
+ def save name
+ if name.nil?
+ msg = ""
+ while true
+ name = ask(msg + "Give your program a name.")
+ break if name.nil? or not HH.script_exists?(name)
+ msg = "You already have a program named '" + name + "'.\n"
+ end
+ end
+ if name
+ @script[:name] = name
+ HH.save_script(@script[:name], @str)
+ @script[:mtime] = Time.now
+ @sname.text = @script[:name]
+ @stale.text = "Last saved #{@script[:mtime].since} ago."
+ true
+ else
+ false
+ end
+ end
+
+ def update_text
+ @t.replace *highlight(@str, @t.cursor)
+ @ln.replace [*1..(@str.count("\n")+1)].join("\n")
+ end
+
+ def text_changed
+ if @save_button.hidden
+ @copy_button.hide
+ @save_button.show
+ @save_to_cloud_button.hide
+ end
+ end
+
+ # find the indentation level at the current cursor or marker
+ # whatever occurs first
+ # the result is the number of spaces
+ def indentation_size
+ # TODO marker
+ pos = @str.rindex("\n", @t.cursor-1)
+ return 0 if pos.nil?
+
+ pos += 1
+
+ ind_size = 0
+ while @str[pos, 1] == ' '
+ ind_size += 1
+ pos += 1
+ end
+ ind_size
+ end
+
+ # called when the user wants to insert text
+ def handle_text_insertion str
+ pos, len = @t.highlight;
+ handle_text_deletion(pos, len) if len > 0
+
+ add_command InsertionCommand.new(pos, str)
+ insert_text(pos, str)
+ end
+
+ # called when the user wants to delete text
+ def handle_text_deletion pos, len
+ str = @str[pos, len]
+ return if str.empty? # happens if len == 0 or pos to big
+ add_command DeletionCommand.new(pos, str)
+ delete_text(pos, len)
+ end
+
+ def insert_text pos, text
+ @str.insert(pos, text)
+ @t.cursor = pos + text.size
+ @t.cursor = :marker # XXX ???
+ #update_text
+ end
+
+ def delete_text pos, len
+ @str[pos, len] = "" # TODO use slice?
+ @t.cursor = pos
+ @t.cursor = :marker
+ #update_text
+ end
+end
diff --git a/app/app/ui/lessons.rb b/app/app/ui/lessons.rb
new file mode 100644
index 0000000..14f7081
--- /dev/null
+++ b/app/app/ui/lessons.rb
@@ -0,0 +1,319 @@
+
+# redefines methods, like title and para, for use in the lessons
+# included in HH::LessonContainer
+module HH::LessonContainerText
+ TITLES = {:font => "Lacuna Regular", :stroke => "#e06", :margin => 4}
+ PARAS = {:stroke => "#eec", :size => 11, :margin_bottom => 6}
+ LIST = {:margin_left => 20, :margin => 4, :size => 10}
+ CODE_STYLE = {:size => 9, :margin => 8, :font => "Liberation Mono",
+ :stroke => "#000"}
+ COLORS = {
+ :comment => {:stroke => "#bba"},
+ :keyword => {:stroke => "#FCF90F"},
+ :method => {:stroke => "#C09", :weight => "bold"},
+ :symbol => {:stroke => "#9DF3C6"},
+ :string => {:stroke => "#C9F5A5"},
+ :number => {:stroke => "#C9F5A5"},
+ :regex => {:stroke => "#000", :fill => "#FFC" },
+ :attribute => {:stroke => "#C9F5A5"},
+ :expr => {:stroke => "#f33" },
+ :ident => {:stroke => "#6e7"},
+ :any => {:stroke => "#FFF"},
+ :constant => {:stroke => "#55f", :weight => "bold"},
+ :class => {:stroke => "#55f", :weight => "bold"},
+ :matching => {:stroke => "#f00", :weight => "bold"},
+ }
+
+ # merges options +opts+ with those of +args+ if any
+ def merge_opts(args, opts)
+ res = args.dup
+ if res.last.is_a? Hash
+ # there are already options
+ # keep them
+ orig_opts = res.last
+ orig_opts.replace(opts.merge(orig_opts))
+ else
+ res << opts
+ end
+ res
+ end
+
+ def title *args
+ super *merge_opts( args, TITLES.merge(:size => 22) )
+ end
+ def subtitle *args
+ super *merge_opts( args, TITLES.merge(:size => 14) )
+ end
+ def item *args
+ para *merge_opts( args, LIST )
+ end
+ def para *args
+ super *merge_opts( args, PARAS )
+ end
+ # FileUtils.link gets precedence else, i don't quite understand why that
+ # module is included at all but it is...
+ def link *a, &b; app.link *a, &b end
+ def code *args
+ super *merge_opts( args, {:stroke => "#9de", :fill => "#237"} )
+ end
+ def prompt *args
+ code *merge_opts( args, {:stroke => "#EEE", :fill => "#602"} )
+ end
+
+ include HH::Markup
+ def embed_code str, opts={}
+ stack :margin_bottom => 12 do
+ background "#602", :curve => 4
+ para highlight(str, nil, COLORS), CODE_STYLE
+ if opts[:run_button]
+ stack :top => 0, :right => 2, :width => 70 do
+ stack do
+ background "#8A7", :margin => [0, 2, 0, 2], :curve => 4
+ l = link("Run this", :stroke => "#eee", :underline => "none") do
+ eval(str, TOPLEVEL_BINDING)
+ end
+ para l, :margin => 4, :align => 'center',
+ :weight => 'bold', :size => 9
+ end
+ end
+ end
+ end
+ end
+end
+
+
+
+# the code in the +page+ blocks in the lessons is executed with +self+
+# being a LessonEnvironment, methods of the main app (and thus of shoes):
+# method_missing propagates all calls
+class HH::LessonContainer
+ include HH::LessonContainerText
+
+ # the Shoes slot that contains the lesson
+ attr_accessor :slot
+
+ def initialize lesson_set
+ @lesson_set = lesson_set
+ @event_connections = []
+ end
+
+ # convenience method the access the main shoes application
+ def app
+ @slot.app
+ end
+
+ def method_missing(symbol, *args, &blk)
+ app.send symbol, *args, &blk
+ end
+
+ # part of the lesson DSL, executes the page block
+ def set_content &blk
+ delete_event_connections
+ @slot.clear { instance_eval &blk }
+ end
+
+ def on_event *args, &blk
+ conn = app.on_event(*args, &blk)
+ @event_connections << conn
+ conn
+ end
+
+ # on the event specified in +args+ goes to the next page
+ # if a block is specified it is used as additional condition
+ # the event arguments are passed to the block
+ def next_when *args, &blk
+ if blk
+ unless args.size == 1
+ raise ArgumentError, "if a block is passed there should be no arguments"
+ end
+ cond = HH::EventCondition.new &blk
+ on_event(args[0], cond) {next_page}
+ else
+ on_event(*args) {next_page}
+ end
+ end
+
+ def delete_event_connections
+ @event_connections.each do |ec|
+ app.delete_event_connection ec
+ end
+ @event_connections = []
+ end
+
+ def next_page
+ @lesson_set.next_page
+ end
+end
+
+
+# class to load and execute the level sets
+class HH::LessonSet
+ include HH::Observable
+
+ def initialize name, blk
+ # content of @lessons:
+ # name, pages = @lessons[lesson_n]
+ # title, block = pages[page_n]
+ @lessons = []
+ @name = name
+ @container = HH::LessonContainer.new self
+ instance_eval &blk
+ end
+
+ def init &blk
+ @container.instance_eval &blk
+ end
+
+ # returns only when close gets called
+ def execute_in slot
+ # loads saved lesson and page, of 0, 0, by default
+ # differently from what is displayed in the UI,
+ # internally @lesson and @page start at 0
+ @lesson = (HH::PREFS["tut_lesson_#@name"] || "0").to_i
+ @page = (HH::PREFS["tut_page_#@name"] || "0").to_i
+ @container.slot = slot
+ slot.extend HH::Tooltip
+
+ execute_page
+ @@open_lesson = self
+ end
+
+ # finalization in case of an open lesson
+ def self.close_open_lesson
+ if (defined? @@open_lesson) && @@open_lesson
+ @@open_lesson.save_lesson
+ end
+ end
+
+ def show_menu
+ name, lessons = @name, @lessons
+ lesson_set = self
+ @container.set_content do
+ background gray(0.1)
+ stack :margin => 10, :height => -32, :scroll => true do
+ title name
+
+ lesson_i = 0
+ lessons.each do |name, pages|
+ lesson = lesson_i
+ lesson_i += 1
+
+ subtitle "#{lesson_i} #{name}"#, :stroke => gray(0.9)
+ page_i = 0
+ pages.each do |title, _proc|
+ page = page_i
+ page_i += 1
+ open_page = proc do lesson_set.instance_eval do
+ @lesson, @page = lesson, page
+ execute_page
+ end end
+ para link("#{title}", :stroke => gray(0.9), :click => open_page),
+ :margin_left => 10
+ end
+ end
+ end
+ flow :height => 32, :bottom => 0, :right => 0 do
+ icon_button :x, :right => 10 do
+ lesson_set.close_lesson
+ end
+ end
+ end
+ end
+
+ # displays the page @page of lesson @lesson
+ def execute_page
+ lessons = @lessons
+ lesson, page = @lesson, @page
+ lesson_set = self
+
+ @container.set_content do
+ background gray(0.1)
+
+ lesson_name, pages = lessons[lesson]
+ page_title, page_block = pages[page]
+
+ stack :margin => 10, :height => -32, :scroll => true do
+ # if first page of a lesson display the lesson name
+ if page == 0
+ title "#{lesson+1}. #{lesson_name}"
+ end
+
+ # if first page of a lesson do not display page number
+ page_num = page == 0 ? "" : "#{lesson+1}.#{page+1} "
+ subtitle "#{page_num}#{page_title}"
+
+ instance_eval &page_block
+ end
+
+ flow :height => 32, :bottom => 0, :right => 0 do
+ icon_button :arrow_left, "Previous", :left => 10 do
+ lesson_set.previous_page
+ end
+ icon_button :arrow_right, "Next", :left => 100 do
+ lesson_set.next_page
+ end
+ icon_button :menu, "Index", :left => 55 do
+ lesson_set.show_menu
+ end
+ icon_button :x, "Close", :right => 10 do
+ lesson_set.close_lesson
+ end
+ end
+ end
+ end
+
+ def next_page
+ @page += 1
+ _name, pages = @lessons[@lesson]
+ if @page >= pages.size
+ @page = 0
+ @lesson += 1
+ if @lesson >= @lessons.size
+ @lesson = 0
+ end
+ end
+ execute_page
+ end
+
+ def previous_page
+ @page -= 1
+ if @page < 0
+ @lesson -= 1
+ if @lesson < 0
+ @lesson = @lessons.size-1
+ end
+ _name, pages = @lessons[@lesson]
+ @page = pages.size-1
+ end
+ execute_page
+ end
+
+ # calls finalization
+ def close_lesson
+ save_lesson
+ @container.delete_event_connections
+ @@open_lesson = nil
+ emit :close
+ end
+
+ # called on close to save the current lesson and page
+ def save_lesson
+ HH::PREFS["tut_lesson_#@name"] = @lesson
+ HH::PREFS["tut_page_#@name"] = @page
+ HH.save_prefs
+ end
+
+ # lesson DSL method
+ def lesson name
+ @lessons << [name, []]
+ end
+
+ # lesson DSL method
+ def page title, &blk
+ if @lessons.empty?
+ lesson << "Lesson"
+ end
+ _name, pages = @lessons.last
+ pages << [title, blk]
+ end
+end
diff --git a/app/app/ui/mainwindow.rb b/app/app/ui/mainwindow.rb
new file mode 100755
index 0000000..d06cf84
--- /dev/null
+++ b/app/app/ui/mainwindow.rb
@@ -0,0 +1,130 @@
+require 'app/boot'
+
+
+# methods for the main app
+module HH::App
+ # starts a lesson
+ # returns only once the lesson gets closed
+ include HH::Markup
+ def start_lessons name, blk
+ @main_content.style(:width => -400)
+ @lesson_stack.show
+ l = HH::LessonSet.new(name, blk).execute_in @lesson_stack
+ l.on_event :close do hide_lesson end
+ end
+
+ def hide_lesson
+ @lesson_stack.hide
+ @main_content.style(:width => 1.0)
+ end
+
+ def load_file name={}
+ if gettab(:Editor).load(name)
+ opentab :Editor
+ end
+ end
+
+ # replaces the "Running..." message of the currently running program
+ def say arg
+ # FIXME TODO: DECOMMENT TO REPRODUCE A SEGMENTATION FAULT: para (para "abc")
+ if @program_running
+ txt = case arg
+ when String
+ arg
+ else
+ highlight(txt.inspect)
+ end
+ @program_running.clear{para txt}
+ end
+ end
+
+ def finalization
+ # this method gets called on close
+ HH::LessonSet.close_lesson
+ gettab(:Editor).save_if_confirmed
+
+ HH::PREFS['width'] = width
+ HH::PREFS['height'] = height
+ HH::save_prefs
+ end
+end
+
+w = (HH::PREFS['width'] || '790').to_i
+h = (HH::PREFS['height'] || '550').to_i
+window :title => "Hackety Hack", :width => w, :height => h do
+ HH::APP = self
+ extend HH::App, HH::Widgets, HH::Observable
+ style(Shoes::LinkHover, :fill => nil, :stroke => "#C66")
+ style(Shoes::Link, :stroke => "#377")
+
+ @main_content = flow :width => 1.0, :height => -1 do
+ background "#e9efe0"
+ background "#e9efe0".."#c1c5d0", :height => 150, :bottom => 150
+ end
+ @lesson_stack = stack :hidden => true, :width => 400
+ @lesson_stack.finish do
+ finalization
+ end
+
+ extend HH::HasSideTabs
+ init_tabs @main_content
+
+ addtab :Home, :icon => "tab-home.png"
+ addtab :Editor, :icon => "tab-new.png"
+ addtab :Lessons, :icon => "tab-tour.png"
+ addtab :Help, :icon => "tab-help.png" do
+ Shoes.show_manual
+ end
+ addtab :Cheat, :icon => "tab-cheat.png" do
+ dialog :title => "Hackety Hack - Cheat Sheet", :width => 496 do
+ image "#{HH::STATIC}/hhcheat.png"
+ end
+ end
+ addtab :About, :icon => "tab-hand.png" do
+ about = app.slot.stack :top => 0, :left => 0,
+ :width => 1.0, :height => 1.0 do
+ background white
+ image("#{HH::STATIC}/hhabout.png", :top => 30, :left => 100).
+ click { about.remove }
+ glossb "OK", :top => 500, :left => 0.45, :width => 70, :color => "dark" do
+ about.remove
+ end
+ click { about.remove }
+ end
+ end
+ addtab :Quit, :icon => "tab-quit.png", :position => :bottom do
+ close
+ end
+ addtab :Prefs, :hover => "Preferences", :icon => "tab-properties.png",
+ :position => :bottom
+ opentab :Home
+
+ @tour_notice =
+ stack :top => 46, :left => 22, :width => 250, :height => 54, :hidden => true do
+ fill black(0.6)
+ nostroke
+ shape 0, 20 do
+ line_to 23.6, 0
+ line_to 23.6, 10
+ line_to 0, 0
+ end
+ background black(0.6), :curve => 6, :left => 24, :width => 215
+ para "Check out the Hackety Hack Tour to get started!",
+ :stroke => "#FFF", :margin => 6, :size => 11, :margin_left => 22,
+ :align => "center"
+ end
+
+
+ # splash screen
+ stack :top => 0, :left => 0, :width => 1.0, :height => 1.0 do
+ splash
+ if HH::PREFS['first_run'].nil?
+ @tour_notice.toggle
+ @tour_notice.click { @tour_notice.hide }
+ HH::PREFS['first_run'] = true
+ HH::save_prefs
+ end
+ end
+
+
+end
diff --git a/app/app/ui/tabs/editor.rb b/app/app/ui/tabs/editor.rb
new file mode 100644
index 0000000..f7f35ff
--- /dev/null
+++ b/app/app/ui/tabs/editor.rb
@@ -0,0 +1,2 @@
+# just a redirect
+require 'app/ui/editor/editor'
diff --git a/app/app/ui/tabs/home.rb b/app/app/ui/tabs/home.rb
new file mode 100644
index 0000000..441e276
--- /dev/null
+++ b/app/app/ui/tabs/home.rb
@@ -0,0 +1,175 @@
+# the home tab content
+# partly unfinished: some features have just started being implemented
+
+class HH::SideTabs::Home < HH::SideTab
+# unfinished method that asks if the user wants to upgrade
+# def home_bulletin
+# stack do
+# background "#FF9".."#FFF"
+# subtitle "Upgrade to 0.7?", :font => "Phonetica", :align => "center", :margin => 8
+# para "A New Hackety Hack is Here!", :align => "center", :margin_bottom => 50
+# glossb "Upgrade", :top => 90, :left => 0.42, :width => 100, :color => "red" do
+# alert("No upgrades yet.")
+# end
+# end
+# stack do
+# background black(0.4)..black(0.0)
+# image 1, 10
+# end
+# end
+ def initialize *args, &blk
+ super *args, &blk
+ # never changes so is most efficient to load here
+ @samples = HH.samples
+ end
+
+ # auxiliary method to displays the arrows, for example in case
+ # more than 5 programs have to be listed
+ def home_arrows meth, start, total
+ stack :top => 0, :right => 10 do
+ nex = total > start + 5
+ if start > 0
+ glossb "<<", :top => 0, :right => 10 + (nex ? 100 : 0), :width => 50 do
+ @homepane.clear { send(meth, start - 5) }
+ end
+ end
+ if nex
+ glossb "Next 5 >>", :top => 0, :right => 10, :width => 100 do
+ @homepane.clear { send(meth, start + 5) }
+ end
+ end
+ end
+ end
+
+
+ def home_scripts start=0
+ display_scripts @scripts, start
+ end
+
+ def sample_scripts start=0
+ display_scripts @samples, start, true
+ end
+
+ # auxiliary function used to both display the user programs (scripts)
+ # and the samples
+ def display_scripts scripts, start, samples = false
+ if scripts.empty?
+ para "You have no programs.", :margin_left => 12, :font => "Lacuna Regular"
+ else
+ scripts[start,5].each do |script|
+ stack :margin_left => 8, :margin_top => 4 do
+ flow do
+ britelink "icon-file.png", script[:name], script[:mtime] do
+ load_file script
+ end
+ unless script[:sample]
+ # if it is not a sample file
+ para (link "x" do
+ if confirm("Do you really want to delete \"#{script[:name]}\"?")
+ delete script
+ end
+ end)
+ end
+ end
+ if script[:desc]
+ para script[:desc], :stroke => "#777", :size => 9,
+ :font => "Lacuna Regular", :margin => 0, :margin_left => 18,
+ :margin_right => 140
+ end
+ end
+ end
+ # FIXME: sometimes :sample_scripts
+ m = samples ? :sample_scripts : :home_scripts
+ home_arrows m, start, scripts.length
+ end
+ end
+
+ def delete script
+ File.delete "#{HH::USER}/#{script[:name]}.rb"
+ reset
+ end
+
+ # I think this was meant to show all tables currently in the database
+# def home_tables start = 0
+# if @tables.empty?
+# para "You have no tables.", :margin_left => 12, :font => "Lacuna Regular"
+# else
+# @tables[start,5].each do |name|
+# stack :margin_left => 8, :margin_top => 4 do
+# britelink "icon-table.png", name do
+# alert("No tables page yet.")
+# end
+# end
+# end
+# home_arrows :home_tables, start, @tables.length
+# end
+# end
+
+ def home_lessons
+ para "You have no lessons.", :margin_left => 12, :font => "Lacuna Regular"
+ end
+
+ # add a tab at the top of the homepane, for now there is only one tab:
+ # (Programs)
+ def hometab name, bg, starts = false, &blk
+ tab =
+ stack :margin_top => (starts ? 6 : 10), :margin_left => 14, :width => 120 do
+ off = background bg, :curve => 6, :hidden => starts
+ on = background rgb(233, 239, 224), :curve => (starts ? 6 : 0), :top => (starts ? 0 : 28)
+ title = link(name, :stroke => (starts ? black : white), :underline => "none") do
+ @tabs.each do |t|
+ next unless t.contents[0].hidden
+ t.margin_top = 10
+ t.contents[2].contents[0].stroke = white
+ t.contents[2].size = 11
+ t.contents[1].style(:curve => 0, :top => 28)
+ t.contents[0].show
+ end
+ tab.margin_top = 6
+ title.stroke = black
+ title.parent.size = 13
+ on.style(:curve => 6, :top => 0)
+ off.hide
+ blk[]
+ end
+ para title, :size => (starts ? 13 : 11), :align => "center",
+ :margin => 6, :margin_bottom => 12, :font => "Lacuna Regular"
+ end
+ @tabs << tab
+ end
+
+ def on_click
+ reset
+ end
+
+ # creates the content of the home tab
+ def content
+ image "#{HH::STATIC}/hhhello.png", :bottom => -120, :right => 0
+
+ @tabs, @tables = [], HH::DB.tables
+ @scripts = HH.scripts
+ stack :margin => 0, :margin_left => 0 do
+ stack do
+ background "#CDC", :height => 35
+ background black(0.05)..black(0.2), :height => 38
+ flow do
+ hometab "Programs", "#555", true do
+ @homepane.clear { home_scripts }
+ end
+ hometab "Samples", "#555", false do
+ @homepane.clear { sample_scripts }
+ end
+ end
+ end
+ stack do
+ @homepane = stack do
+ home_scripts
+ end
+ end
+ stack :margin_left => 12 do
+ background rgb(233, 239, 224, 0.85)..rgb(233, 239, 224, 0.0)
+ image 10, 70
+ end
+ end
+ end
+end
diff --git a/app/app/ui/tabs/lessons.rb b/app/app/ui/tabs/lessons.rb
new file mode 100644
index 0000000..9a14af3
--- /dev/null
+++ b/app/app/ui/tabs/lessons.rb
@@ -0,0 +1,32 @@
+module Kernel
+ # topmost instruction for the lessons DSL
+ # starts a lesson set
+ def lesson_set name, &blk
+ HH::SideTabs::Lessons.load_lesson name, blk
+ end
+end
+
+class HH::SideTabs::Lessons < HH::SideTab
+ # auxiliary function used by Kernel#lesson_set
+ # stores the code of the DSL used to write the lessons
+ def self.load_lesson name, blk
+ @@lessons << [name, blk]
+ end
+
+ # draws the lessons tab
+ def content
+ stack :margin => 10 do
+ title "Lessons"
+ @@lessons = []
+ Dir["#{HH::LESSONS}/*.rb"].each { |f| load f }
+ @@lessons.sort{|a, b| a[0] <=> b[0]}.each do |name, blk|
+ stack do
+ britelink "icon-file.png", name do
+ HH::APP.start_lessons name, blk
+ end
+ end
+ end
+ end
+ end
+end
+
diff --git a/app/app/ui/tabs/prefs.rb b/app/app/ui/tabs/prefs.rb
new file mode 100644
index 0000000..d01bcfb
--- /dev/null
+++ b/app/app/ui/tabs/prefs.rb
@@ -0,0 +1,68 @@
+# the messages tab content
+
+class HH::SideTabs::Prefs < HH::SideTab
+ def loading_stack *a
+ n = nil
+ s = stack(*a) do
+ background black(0.4), :curve => 12
+ stroke white(0.6)
+ fill white(0.6)
+ para "\nLoading.\n\n", :align => "center", :size => 17,
+ :font => "Lacuna Regular", :stroke => white
+ o1 = oval -400, 0, 16
+ o2 = oval -400, 0, 16
+ n = animate do |i|
+ v1 = Math.sin(i * 0.2) * 5
+ v2 = Math.cos(i * 0.2) * 5
+ o1.move(((s.width - 50) * 0.5) + v1, 85 + v2)
+ o2.move(((s.width - 50) * 0.5) - v1, 85 - v2)
+ end
+ end
+ s.instance_variable_set("@n", n)
+ def s.replace &blk
+ @n.stop; @n = nil
+ clear &blk
+ end
+ s
+ end
+
+ def clover_whoosh
+ background "#efefa0"
+ background "#efefa0".."#c1d5c0", :height => 150, :bottom => 150
+ end
+
+ def content
+ user = HH::PREFS['username']
+ clover_whoosh
+ stack :margin => [10, 20, 0, 20], :width => 1.0, :height => 1.0 do
+ subtitle "Your Preferences", :font => "Lacuna Regular", :margin => 0, :size => 22,
+ :stroke => "#377"
+ if user
+ para "Hello, #{user}! ",
+ else
+ para "Let's set up Hackety Hack to use on the Internet okay? ",
+ "Be sure you have an account from ",
+ link("hackety-hack.com", :click => "http://hackety-hack.com"), "."
+ end
+
+ @prefpane =
+ stack do
+ stack :margin => 20, :width => 400 do
+ para "Your username", :size => 10, :margin => 2, :stroke => "#352"
+ @user = edit_line user, :width => 1.0
+
+ para "Your password", :size => 10, :margin => 2, :stroke => "#352"
+ @pass = edit_line HH::PREFS['password'], :width => 1.0, :secret => true
+
+ button "Save", :margin_top => 10 do
+ HH::PREFS['username'] = @user.text
+ HH::PREFS['password'] = @pass.text
+ HH.save_prefs
+ alert("Saved, thanks!")
+ end
+ end
+ end
+
+ end
+ end
+end
diff --git a/app/app/ui/tabs/sidetabs.rb b/app/app/ui/tabs/sidetabs.rb
new file mode 100644
index 0000000..355b9e3
--- /dev/null
+++ b/app/app/ui/tabs/sidetabs.rb
@@ -0,0 +1,185 @@
+class HH::SideTabs
+ include HH::Observable
+ ICON_SIZE = 16
+ HOVER_WIDTH = 140
+ def initialize slot, dir
+ @slot, @directory = slot, dir
+ @n_tabs = {:top => 0, :bottom => 1}
+ # tabs whose file has been loaded
+ @loaded_tabs = {}
+ sidetabs = self
+ width = HOVER_WIDTH;
+ append_to @slot do
+ tip = nil
+ right = stack :margin_left => 38, :height => 1.0
+ left = stack :top => 0, :left => 0, :width => 38, :height => 1.0 do
+ tip = stack :top => 0, :left => 0, :width => width, :margin => 4,
+ :hidden => true do
+ background "#F7A", :curve => 6
+ para "HOME", :margin => 3, :margin_left => 40, :stroke => white
+ end
+ # colored background
+ background "#cdc", :width => 38
+ background "#dfa", :width => 36
+ background "#fda", :width => 30
+ background "#daf", :width => 24
+ background "#aaf", :width => 18
+ background "#7aa", :width => 12
+ background "#77a", :width => 6
+ end
+ sidetabs.instance_eval{@left, @right, @tip = left, right, tip}
+ end
+ end
+
+ # +opts+ is an hash
+ # if a block is given no file gets loaded
+ def addtab symbol, opts={}, &blk
+ # default options
+ if not symbol.is_a?(Symbol)
+ raise ArgumentError
+ end
+ tab = opts
+ tab[:symbol] = symbol
+ tab[:icon] ||= "icon-file.png"
+ tab[:position] ||= :top
+ tab[:hover] ||= symbol.to_s
+
+ pos = tab[:position]
+ pixelpos = @n_tabs[pos] * (ICON_SIZE + 10)
+ @n_tabs[pos] += 1
+ hover = tab[:hover]
+ icon_path = HH::STATIC + "/" + tab[:icon]
+ tip = @tip
+ onclick = proc do
+ opentab symbol
+ end
+ width = HOVER_WIDTH+22;
+ append_to @left do
+ stack pos => pixelpos, :left => 0, :width => 38, :margin => 4 do
+ bg = background "#DFA", :height => 26, :curve => 6, :hidden => true
+ image(icon_path, :margin => 4).
+ hover do
+ bg.show
+ tip.parent.width = width
+ tip.top = nil
+ tip.bottom = nil
+ tip.send("#{pos}=", pixelpos)
+ tip.contents[1].text = hover
+ tip.show
+ end.leave do
+ bg.hide
+ tip.hide
+ tip.parent.width = 40
+ end.click &onclick
+ end
+ end
+
+ if blk
+ @loaded_tabs[symbol] = HH::NoContentSideTab.new blk
+ end
+ end
+
+
+ def opentab symbol
+ tab = gettab symbol
+ if tab.has_content?
+ @current_tab.close if @current_tab
+ @current_tab = tab
+ end
+ tab.open
+ emit :tab_opened, symbol
+ end
+
+ def gettab symbol
+ if @loaded_tabs.include? symbol
+ return @loaded_tabs[symbol]
+ else
+ require "app/ui/tabs/#{symbol.downcase}.rb"
+ @loaded_tabs[symbol] = self.class.const_get(symbol).new(@right)
+ end
+ end
+
+private
+ def append_to slot, &blk
+ slot.app do
+ slot.append {self.instance_eval &blk}
+ end
+ end
+end
+
+module HH::HasSideTabs
+ def init_tabs slot, dir="app/ui/tabs"
+ @__side_tab_class = HH::SideTabs.new slot, dir
+ # effectively redirects event to HH::APP
+ @__side_tab_class.on_event :tab_opened, :any do |newtab|
+ emit :tab_opened, newtab
+ end
+ end
+
+ # returns the created tab
+ def addtab *args, &blk
+ @__side_tab_class.addtab *args, &blk
+ end
+
+ def opentab symbol
+ @__side_tab_class.opentab symbol
+ end
+
+ def gettab symbol
+ @__side_tab_class.gettab symbol
+ end
+end
+
+class HH::SideTab
+ def initialize slot
+ @slot = slot
+ slot.append do
+ @content = flow :hidden => true, :left => 0, :top => 0,
+ :width => 1.0, :height => 1.0 do content end
+ end
+ end
+
+ def open
+ on_click
+ if has_content?
+ @content.show
+ end
+ end
+
+ def close
+ if has_content?
+ @content.hide
+ end
+ end
+
+ def clear &blk
+ @content.clear &blk
+ end
+
+ def reset
+ clear {content}
+ end
+
+ def has_content?
+ self.class.method_defined?(:content)
+ end
+
+ def method_missing symbol, *args, &blk
+ #slot = @slot
+ @slot.app.send symbol, *args, &blk
+ end
+
+ def on_click
+ # by default does nothing
+ end
+end
+
+class HH::NoContentSideTab < HH::SideTab
+ def initialize blk
+ @blk = blk
+ end
+ def on_click
+ @blk.call
+ end
+end
+
diff --git a/app/app/ui/widgets.rb b/app/app/ui/widgets.rb
new file mode 100644
index 0000000..624a87e
--- /dev/null
+++ b/app/app/ui/widgets.rb
@@ -0,0 +1,296 @@
+# some extensions to shoes (subclasses of Shoes::Widget)
+# and auxiliary methods for the hh app (the HH::Widgets mixin)
+
+#def scroll_box(opts = {}, &blk)
+# opts = {:width => 1.0, :height => 300, :scroll => true}.merge(opts)
+# stack opts, &blk
+#end
+
+# a glossy button
+class Glossb < Shoes::Widget
+ def initialize(name, opts={}, &blk)
+ fg, bgfill = "#777", "#DDD"
+ case opts[:color]
+ when "dark"; fg, bgfill = "#CCC", "#000"
+ when "yellow"; fg, bgfill = "#FFF", "#7AA"
+ when "red"; fg, bgfill = "#FF5", "#F30"
+ end
+
+ txt = link(name, :underline => 'none', :stroke => fg) {}
+ stack :margin => 4 do
+ background bgfill, :curve => 5
+ @txt = para txt, :align => 'center', :margin => 4, :size => 11
+ hover { @over.show }
+ leave { @over.hide }
+ end
+
+ @over = stack :top => 0, :left => 0, :margin => 2, :hidden => true do
+ background bgfill, :curve => 5
+ @txt_over = para txt, :align => 'center', :margin => 4, :size => 14, :weight => "bold"
+ end
+ @fg = fg
+ click &blk
+ end
+
+ def text= txt
+ new_link = link(txt, :underline => 'none', :stroke => @fg) {}
+ @txt.replace(new_link)
+ @txt_over.replace(new_link)
+ end
+end
+
+class IconButton < Shoes::Widget
+# BSIZE = 16
+# MARGIN = 8
+# SIZE = BSIZE + MARGIN * 2
+ def initialize (type, tooltip, opts={}, &blk)
+ strokewidth 1
+ nofill
+
+ @tooltip_text = tooltip
+
+ stack do
+ stack :margin => 8, :width => 32, :height => 32 do
+ stroke white
+ send type
+ end
+
+ hover do
+ @over.show
+ if @tooltip_text
+ create_tooltip
+ end
+ end
+ leave do
+ @over.hide
+ if @tooltip
+ @tooltip.hide
+ @tooltip.remove
+ @tooltip = nil
+ end
+ end
+ end
+
+ style(:width => 32)
+
+ stack :margin => 8, :top => 0, :left => 0 do
+ @over = stack :width => 16, :height => 16, :hidden => true do
+ background gray(0.8)
+ stroke black
+ send type
+ end
+ end
+
+ click &blk
+ end
+
+ def create_tooltip
+ slot = parent
+ x, y = left, top
+ while not slot.respond_to? :tooltip
+ x += slot.left
+ y += slot.top
+ slot = slot.parent
+ end
+
+ @tooltip = slot.tooltip(@tooltip_text, x, y-20,
+ :fill => red, :stroke => white)
+ end
+
+ def arrow_right
+ line(1, 8, 14, 8)
+ line(14, 8, 10, 1+3)
+ line(14, 8, 10, 15-3)
+ end
+
+ def arrow_left
+ line(1, 8, 14, 8)
+ line(1, 8, 6, 1+3)
+ line(1, 8, 6, 15-3)
+ end
+
+ def x
+ line(2, 2, 13, 13)
+ line(2, 13, 13, 2)
+ end
+
+ def menu
+ rect 2, 2, 11, 11
+ line 4, 6, 11, 6
+ line 4, 8, 11, 8
+ line 4, 10, 11, 10
+ end
+end
+
+module HH::Tooltip
+ def tooltip str, x, y, opts={}
+ f = nil
+ #opts[:wrap] = "trim"
+ slot = self
+ app do
+ slot.append do
+ f = flow :left => x, :top => y do
+ para str, opts
+ end
+ end
+ end
+ f
+ end
+end
+
+#class Lightboard < Shoes::Widget
+# def initialize(width, height, actual = width * height)
+# @opts = []
+# nostroke
+# resize(width, height, actual)
+# end
+# def coords(x, y, opts)
+# at((y * @width) + x, opts)
+# end
+# def at(i, opts)
+# return if i > @actual
+# r, p = self.contents[i * 2], self.contents[(i * 2) + 1]
+# @opts[i] = (@opts[i] || {}).update(opts)
+# if opts[:text]
+# p.text = opts[:text]
+# end
+# if opts[:fill]
+# r.fill = opts[:fill]
+# end
+# end
+# def fits(length)
+# r = Math.sqrt(length)
+# resize(r.round, r.ceil, length)
+# end
+# def resize(width, height, actual = width * height)
+# @width, @height, @actual = width, height, actual
+# build
+# end
+# def build
+# height = parent.height / @height
+# width = parent.width / @width
+# clear do
+# @actual.times do |i|
+# y = i / @width
+# x = i % @width
+# top = y * height
+# left = x * width
+# opts = @opts[i] || {}
+# rect left + 4, top + 4, width - 8, height - 8,
+# :curve => 12, :fill => opts[:fill] || white
+# para opts[:text] || "", :top => top + (height * 0.5) - 32,
+# :left => left, :width => width, :align => "center",
+# :size => 31, :font => "Lacuna Regular"
+# end
+# end
+# end
+#end
+
+# splash screen and a link with a similar appearance as the gloss buttons
+# included by the hh app
+module HH::Widgets
+ def splash
+ nostroke
+ background black
+ color_names = Shoes::COLORS.keys
+
+ @c = :gray
+ @r = star :top => 410, :left => 310, :outer => 80, :inner => 100
+ @s =
+ stack :top => -400, :left => 100, :width => 370, :height => 370 do
+ @mask = mask do
+ star 210, 210, 130, 500, 90
+ end
+ image "#{HH::STATIC}/splash-hand.png", :top => 84, :left => 84
+ end
+ @t = title span(" Welcome to\n", :size => 15), strong("Hackety Hack"),
+ :stroke => black, :top => 180, :left => 80, :font => "Lacuna Regular"
+ @b = glossb "Ready", :top => 280, :left => 80, :width => 80, :hidden => true do
+ @an.stop
+ @ban = @s.parent.animate(30) do |i|
+ @s.parent.move(-(i*40), 0)
+ if i == 30
+ @s.parent.remove
+ end
+ end
+ end
+ # @v = video "music.wav",
+ # :autoplay => true, :width => 0, :height => 0, :top => -100, :left => -100
+ @an = animate(30) do |i|
+ @mask.clear do
+ rotate 1
+ star 210, 210, 130, 500, 90
+ end
+ if i == 60
+ @b.show
+ end
+ if @s.top < 198
+ dist = (208 - @s.top) / 6
+ dist = 10 if dist > 10
+ @s.top += dist
+ if @s.top > -40
+ @t.stroke = gray(@s.top + 55)
+ end
+ else
+ if @v
+ @v.remove
+ @v = nil
+ end
+ o = @r.style[:outer]
+ if o > 500
+ @r.style :inner => 100, :outer => 80, :points => (10..80).rand
+ @c = color_names[(0..color_names.length).rand]
+ elsif o > 200
+ @r.style :fill => send(@c, (500 - o).abs * 0.01), :outer => o + 10
+ else
+ @r.style :fill => send(@c, (0.1..0.6).rand), :outer => o + 1
+ end
+ end
+ end
+ end
+
+ # creates a link having a similar appearance as the gloss buttons
+ def britelink icon, name, time = nil, bg = "#8c9", &blk
+ bg = background bg, :curve => 6, :height => 29, :hidden => true
+ flow :margin => 4, :width => 300 do
+ image HH::STATIC + "/" + icon, :margin_right => 6, :margin => 3
+ p1 = link(name, :stroke => black, :underline => "none", &blk)
+ para p1, :size => 13, :font => "Lacuna Regular", :margin => 0,
+ :wrap => "trim", :width => 280
+ if time
+ p2 = para time.short, :stroke => "#396", :font => "Lacuna Regular",
+ :size => 9, :margin => 4, :margin_bottom => 0
+ end
+ ele = image 1, 1
+ p1.hover do
+ p1.parent.stroke = p1.stroke = white
+ p2.stroke = "#FF5" if time
+ bg.width = ele.left + 10
+ bg.show
+ end
+ p1.leave do
+ p1.parent.stroke = p1.stroke = black
+ p2.stroke = "#396" if time
+ bg.hide
+ end
+ end
+ end
+
+ # method to create a side tab (actually is just a stack with an image in it)
+ # +icon_path+:: the icon displayed in the tab
+ # +top+:: if > 0 indicates the distance from the top, else the distance from
+ # the bottom
+ # +name+:: text displayed on icon hover
+ # +blk+:: the block passed is executed on click
+ def sidetab(icon_path, top, name, &blk)
+ v = top < 0 ? :bottom : :top
+ stack v => top.abs, :left => 0, :width => 38, :margin => 4 do
+ bg = background "#DFA", :height => 26, :curve => 6, :hidden => true
+ image(icon_path, :margin => 4).
+ hover { bg.show; @tip.parent.width = 122; @tip.top = nil; @tip.bottom = nil
+ @tip.send("#{v}=", top.abs); @tip.contents[1].text = name; @tip.show }.
+ leave { bg.hide; @tip.hide; @tip.parent.width = 40 }.
+ click &blk
+ end
+ end
+end
diff --git a/app/fonts/Animals.ttf b/app/fonts/Animals.ttf
new file mode 100755
index 0000000..4f3781e
--- /dev/null
+++ b/app/fonts/Animals.ttf
Binary files differ
diff --git a/app/fonts/Arcade.ttf b/app/fonts/Arcade.ttf
new file mode 100755
index 0000000..2bde6a4
--- /dev/null
+++ b/app/fonts/Arcade.ttf
Binary files differ
diff --git a/app/fonts/Bruegheliana.ttf b/app/fonts/Bruegheliana.ttf
new file mode 100755
index 0000000..a2b673d
--- /dev/null
+++ b/app/fonts/Bruegheliana.ttf
Binary files differ
diff --git a/app/fonts/Carr Space.ttf b/app/fonts/Carr Space.ttf
new file mode 100755
index 0000000..f0c1165
--- /dev/null
+++ b/app/fonts/Carr Space.ttf
Binary files differ
diff --git a/app/fonts/Chess Utrecht.ttf b/app/fonts/Chess Utrecht.ttf
new file mode 100755
index 0000000..3023e2e
--- /dev/null
+++ b/app/fonts/Chess Utrecht.ttf
Binary files differ
diff --git a/app/fonts/DayRoman-X.ttf b/app/fonts/DayRoman-X.ttf
new file mode 100755
index 0000000..d12122c
--- /dev/null
+++ b/app/fonts/DayRoman-X.ttf
Binary files differ
diff --git a/app/fonts/DayRoman.ttf b/app/fonts/DayRoman.ttf
new file mode 100755
index 0000000..e2e813a
--- /dev/null
+++ b/app/fonts/DayRoman.ttf
Binary files differ
diff --git a/app/fonts/Delicious-Bold.otf b/app/fonts/Delicious-Bold.otf
new file mode 100755
index 0000000..e5b1e25
--- /dev/null
+++ b/app/fonts/Delicious-Bold.otf
Binary files differ
diff --git a/app/fonts/Delicious-BoldItalic.otf b/app/fonts/Delicious-BoldItalic.otf
new file mode 100755
index 0000000..81bf13b
--- /dev/null
+++ b/app/fonts/Delicious-BoldItalic.otf
Binary files differ
diff --git a/app/fonts/Delicious-Heavy.otf b/app/fonts/Delicious-Heavy.otf
new file mode 100755
index 0000000..c6faccd
--- /dev/null
+++ b/app/fonts/Delicious-Heavy.otf
Binary files differ
diff --git a/app/fonts/Delicious-Italic.otf b/app/fonts/Delicious-Italic.otf
new file mode 100755
index 0000000..d57df3b
--- /dev/null
+++ b/app/fonts/Delicious-Italic.otf
Binary files differ
diff --git a/app/fonts/Delicious-Roman.otf b/app/fonts/Delicious-Roman.otf
new file mode 100755
index 0000000..31ec11a
--- /dev/null
+++ b/app/fonts/Delicious-Roman.otf
Binary files differ
diff --git a/app/fonts/Delicious-SmallCaps.otf b/app/fonts/Delicious-SmallCaps.otf
new file mode 100755
index 0000000..05d1305
--- /dev/null
+++ b/app/fonts/Delicious-SmallCaps.otf
Binary files differ
diff --git a/app/fonts/Even More Dings JL.ttf b/app/fonts/Even More Dings JL.ttf
new file mode 100755
index 0000000..f13cafd
--- /dev/null
+++ b/app/fonts/Even More Dings JL.ttf
Binary files differ
diff --git a/app/fonts/Fontalicious.ttf b/app/fonts/Fontalicious.ttf
new file mode 100755
index 0000000..7e71830
--- /dev/null
+++ b/app/fonts/Fontalicious.ttf
Binary files differ
diff --git a/app/fonts/Fontin-Bold.otf b/app/fonts/Fontin-Bold.otf
new file mode 100755
index 0000000..678a7aa
--- /dev/null
+++ b/app/fonts/Fontin-Bold.otf
Binary files differ
diff --git a/app/fonts/Fontin-Italic.otf b/app/fonts/Fontin-Italic.otf
new file mode 100755
index 0000000..0d5d92e
--- /dev/null
+++ b/app/fonts/Fontin-Italic.otf
Binary files differ
diff --git a/app/fonts/Fontin-Regular.otf b/app/fonts/Fontin-Regular.otf
new file mode 100755
index 0000000..37b668e
--- /dev/null
+++ b/app/fonts/Fontin-Regular.otf
Binary files differ
diff --git a/app/fonts/Fontin-SmallCaps.otf b/app/fonts/Fontin-SmallCaps.otf
new file mode 100755
index 0000000..705654c
--- /dev/null
+++ b/app/fonts/Fontin-SmallCaps.otf
Binary files differ
diff --git a/app/fonts/Free.ttf b/app/fonts/Free.ttf
new file mode 100755
index 0000000..9aa527c
--- /dev/null
+++ b/app/fonts/Free.ttf
Binary files differ
diff --git a/app/fonts/Illustries.ttf b/app/fonts/Illustries.ttf
new file mode 100755
index 0000000..6c7465d
--- /dev/null
+++ b/app/fonts/Illustries.ttf
Binary files differ
diff --git a/app/fonts/JustOldFashion.ttf b/app/fonts/JustOldFashion.ttf
new file mode 100755
index 0000000..2c08ce5
--- /dev/null
+++ b/app/fonts/JustOldFashion.ttf
Binary files differ
diff --git a/app/fonts/Lacuna.ttf b/app/fonts/Lacuna.ttf
new file mode 100644
index 0000000..d4e5003
--- /dev/null
+++ b/app/fonts/Lacuna.ttf
Binary files differ
diff --git a/app/fonts/LiberationMono-Bold.ttf b/app/fonts/LiberationMono-Bold.ttf
new file mode 100755
index 0000000..95de753
--- /dev/null
+++ b/app/fonts/LiberationMono-Bold.ttf
Binary files differ
diff --git a/app/fonts/LiberationMono-Regular.ttf b/app/fonts/LiberationMono-Regular.ttf
new file mode 100644
index 0000000..e3024a0
--- /dev/null
+++ b/app/fonts/LiberationMono-Regular.ttf
Binary files differ
diff --git a/app/fonts/LiberationSans-Bold.ttf b/app/fonts/LiberationSans-Bold.ttf
new file mode 100755
index 0000000..53200d9
--- /dev/null
+++ b/app/fonts/LiberationSans-Bold.ttf
Binary files differ
diff --git a/app/fonts/LiberationSans-BoldItalic.ttf b/app/fonts/LiberationSans-BoldItalic.ttf
new file mode 100755
index 0000000..d06deca
--- /dev/null
+++ b/app/fonts/LiberationSans-BoldItalic.ttf
Binary files differ
diff --git a/app/fonts/LiberationSans-Italic.ttf b/app/fonts/LiberationSans-Italic.ttf
new file mode 100755
index 0000000..07275ad
--- /dev/null
+++ b/app/fonts/LiberationSans-Italic.ttf
Binary files differ
diff --git a/app/fonts/LiberationSans-Regular.ttf b/app/fonts/LiberationSans-Regular.ttf
new file mode 100755
index 0000000..09fac2f
--- /dev/null
+++ b/app/fonts/LiberationSans-Regular.ttf
Binary files differ
diff --git a/app/fonts/LiberationSerif-Bold.ttf b/app/fonts/LiberationSerif-Bold.ttf
new file mode 100755
index 0000000..3a4ab92
--- /dev/null
+++ b/app/fonts/LiberationSerif-Bold.ttf
Binary files differ
diff --git a/app/fonts/LiberationSerif-BoldItalic.ttf b/app/fonts/LiberationSerif-BoldItalic.ttf
new file mode 100755
index 0000000..dc75de8
--- /dev/null
+++ b/app/fonts/LiberationSerif-BoldItalic.ttf
Binary files differ
diff --git a/app/fonts/LiberationSerif-Italic.ttf b/app/fonts/LiberationSerif-Italic.ttf
new file mode 100755
index 0000000..d92b5e3
--- /dev/null
+++ b/app/fonts/LiberationSerif-Italic.ttf
Binary files differ
diff --git a/app/fonts/LiberationSerif-Regular.ttf b/app/fonts/LiberationSerif-Regular.ttf
new file mode 100755
index 0000000..d100691
--- /dev/null
+++ b/app/fonts/LiberationSerif-Regular.ttf
Binary files differ
diff --git a/app/fonts/Outer Space JL.ttf b/app/fonts/Outer Space JL.ttf
new file mode 100755
index 0000000..b3e48f6
--- /dev/null
+++ b/app/fonts/Outer Space JL.ttf
Binary files differ
diff --git a/app/fonts/Oxygene1.ttf b/app/fonts/Oxygene1.ttf
new file mode 100755
index 0000000..c98e9db
--- /dev/null
+++ b/app/fonts/Oxygene1.ttf
Binary files differ
diff --git a/app/fonts/Phonetica.ttf b/app/fonts/Phonetica.ttf
new file mode 100755
index 0000000..357826f
--- /dev/null
+++ b/app/fonts/Phonetica.ttf
Binary files differ
diff --git a/app/fonts/Pixelpoiiz.ttf b/app/fonts/Pixelpoiiz.ttf
new file mode 100644
index 0000000..b0c719d
--- /dev/null
+++ b/app/fonts/Pixelpoiiz.ttf
Binary files differ
diff --git a/app/fonts/Playing Cards.ttf b/app/fonts/Playing Cards.ttf
new file mode 100755
index 0000000..dcf631b
--- /dev/null
+++ b/app/fonts/Playing Cards.ttf
Binary files differ
diff --git a/app/fonts/Silhouette.ttf b/app/fonts/Silhouette.ttf
new file mode 100755
index 0000000..03cfa1d
--- /dev/null
+++ b/app/fonts/Silhouette.ttf
Binary files differ
diff --git a/app/fonts/TakaoGothic.otf b/app/fonts/TakaoGothic.otf
new file mode 100644
index 0000000..c2e6074
--- /dev/null
+++ b/app/fonts/TakaoGothic.otf
Binary files differ
diff --git a/app/fonts/YanoneKaffeesatz-Bold.otf b/app/fonts/YanoneKaffeesatz-Bold.otf
new file mode 100755
index 0000000..4ea5919
--- /dev/null
+++ b/app/fonts/YanoneKaffeesatz-Bold.otf
Binary files differ
diff --git a/app/fonts/YanoneKaffeesatz-Light.otf b/app/fonts/YanoneKaffeesatz-Light.otf
new file mode 100755
index 0000000..8a3e2bf
--- /dev/null
+++ b/app/fonts/YanoneKaffeesatz-Light.otf
Binary files differ
diff --git a/app/fonts/YanoneKaffeesatz-Regular.otf b/app/fonts/YanoneKaffeesatz-Regular.otf
new file mode 100755
index 0000000..bee3b05
--- /dev/null
+++ b/app/fonts/YanoneKaffeesatz-Regular.otf
Binary files differ
diff --git a/app/fonts/YanoneKaffeesatz-Thin.otf b/app/fonts/YanoneKaffeesatz-Thin.otf
new file mode 100755
index 0000000..764d902
--- /dev/null
+++ b/app/fonts/YanoneKaffeesatz-Thin.otf
Binary files differ
diff --git a/app/h-ety-h.rb b/app/h-ety-h.rb
new file mode 100644
index 0000000..210dfc2
--- /dev/null
+++ b/app/h-ety-h.rb
@@ -0,0 +1,5 @@
+#!/usr/bin/env shoes
+
+# the main application executable
+
+load 'app/ui/mainwindow.rb'
diff --git a/app/installer/HackFolder.ini b/app/installer/HackFolder.ini
new file mode 100755
index 0000000..2659b86
--- /dev/null
+++ b/app/installer/HackFolder.ini
@@ -0,0 +1,49 @@
+[Settings]
+NumFields=5
+
+[Field 1]
+Type=label
+Text=Where would you like your programs to be saved?
+Left=0
+Right=-1
+Top=0
+Bottom=10
+
+[Field 2]
+Type=RadioButton
+Text=A "My Hacks" folder in My Documents
+Left=8
+Right=-1
+Top=30
+Bottom=40
+State=1
+Flags=NOTIFY
+
+[Field 3]
+Type=RadioButton
+Text=A "Hackety Hack" folder on the desktop
+Left=8
+Right=-1
+Top=50
+Bottom=60
+State=0
+Flags=NOTIFY
+
+[Field 4]
+Type=RadioButton
+Text=Let me pick my own folder
+Left=8
+Right=-1
+Top=70
+Bottom=80
+State=0
+Flags=NOTIFY
+
+[Field 5]
+Type=DirRequest
+Text=Let me pick my own folder
+Left=18
+Right=-1
+Top=85
+Bottom=97
+Flags=DISABLED
diff --git a/app/installer/base.nsi b/app/installer/base.nsi
new file mode 100755
index 0000000..f4db8a9
--- /dev/null
+++ b/app/installer/base.nsi
@@ -0,0 +1,252 @@
+;--------------------------------
+;Definitions
+!define AppName "Hackety Hack"
+!define AppVersion "0.Y"
+!define AppMainEXE "hacketyhack.exe"
+!define ShortName "HacketyHack"
+!define InstallKey "Software\Hackety.org\${AppName}"
+
+;--------------------------------
+;Include Modern UI
+
+ !include "MUI.nsh"
+ !include LogicLib.nsh
+
+;--------------------------------
+;General
+
+ ;Name and file
+ Name "${AppName}"
+ OutFile "${ShortName}-${AppVersion}.exe"
+
+ ;Default installation folder
+ InstallDir "$PROGRAMFILES\${AppName}"
+
+ ;Get installation folder from registry if available
+ InstallDirRegKey HKCU "${InstallKey}" ""
+
+ ;Vista redirects $SMPROGRAMS to all users without this
+ RequestExecutionLevel admin
+
+;--------------------------------
+;Variables
+
+ Var MUI_TEMP
+ Var STARTMENU_FOLDER
+
+;--------------------------------
+;Interface Configuration
+
+ !define MUI_ABORTWARNING
+ !define MUI_ICON setup.ico
+ !define MUI_UNICON setup.ico
+ !define MUI_WELCOMEPAGE_TITLE_3LINES
+ !define MUI_WELCOMEFINISHPAGE_BITMAP installer-1.bmp
+ !define MUI_HEADERIMAGE
+ !define MUI_HEADERIMAGE_RIGHT
+ !define MUI_HEADERIMAGE_BITMAP installer-2.bmp
+ !define MUI_COMPONENTSPAGE_NODESC
+
+;--------------------------------
+;Pages
+
+ !insertmacro MUI_PAGE_WELCOME
+ !insertmacro MUI_PAGE_DIRECTORY
+ Page custom HackFolderPage HackFolderHook
+
+ ;Start Menu Folder Page Configuration
+ !define MUI_STARTMENUPAGE_REGISTRY_ROOT "HKCU"
+ !define MUI_STARTMENUPAGE_REGISTRY_KEY "${InstallKey}"
+ !define MUI_STARTMENUPAGE_REGISTRY_VALUENAME "Start Menu Folder"
+
+ !insertmacro MUI_PAGE_STARTMENU Application $STARTMENU_FOLDER
+ !insertmacro MUI_PAGE_INSTFILES
+
+ ; Finish Page
+ !define MUI_FINISHPAGE_NOREBOOTSUPPORT
+ !define MUI_FINISHPAGE_TITLE_3LINES
+ !define MUI_FINISHPAGE_RUN
+ !define MUI_FINISHPAGE_RUN_FUNCTION LaunchApp
+ !define MUI_FINISHPAGE_RUN_TEXT $(LAUNCH_TEXT)
+ !define MUI_PAGE_CUSTOMFUNCTION_PRE preFinish
+ !insertmacro MUI_PAGE_FINISH
+
+ !insertmacro MUI_UNPAGE_CONFIRM
+ !insertmacro MUI_UNPAGE_INSTFILES
+
+;--------------------------------
+;Languages
+
+ !insertmacro MUI_LANGUAGE "English"
+ LangString LAUNCH_TEXT ${LANG_ENGLISH} "Run Hackety Hack"
+ LangString TEXT_IO_TITLE ${LANG_ENGLISH} "Your Hackety Hack Settings"
+ LangString TEXT_IO_SUBTITLE ${LANG_ENGLISH} "Customize the way you use Hackety Hack"
+
+;--------------------------------
+;Reserve Files
+
+ ;If you are using solid compression, files that are required before
+ ;the actual installation should be stored first in the data block,
+ ;because this will make your installer start faster.
+
+ ReserveFile "HackFolder.ini"
+ !insertmacro MUI_RESERVEFILE_INSTALLOPTIONS
+
+;--------------------------------
+;Installer Section
+!macro MakeFileAssoc LangName LangExt
+ WriteRegStr HKCR ".hack-${LangExt}" "" "${ShortName}.${LangName}Program"
+ WriteRegStr HKCR "${ShortName}.${LangName}Program" "" "${AppName} ${LangName} Program"
+ WriteRegStr HKCR "${ShortName}.${LangName}Program\shell\open\command" "" '"$INSTDIR\${AppMainEXE}" "%1"'
+!macroend
+
+!macro UnmakeFileAssoc LangName LangExt
+ DeleteRegKey /ifempty HKCU ".hack-${LangExt}"
+ DeleteRegKey /ifempty HKCU "${ShortName}.${LangName}Program"
+!macroend
+
+Section "App Section" SecApp
+
+ SetOutPath "$INSTDIR"
+
+ File /r /x components\compreg.dat /x components\xpti.dat /x installer ..\*.*
+
+ ;Store installation folder
+ WriteRegStr HKCU "${InstallKey}" "" $INSTDIR
+
+ ;Store hacks folder
+ !insertmacro MUI_INSTALLOPTIONS_READ $0 "HackFolder.ini" "Field 3" "State"
+ ${If} $0 == "1"
+ WriteRegStr HKCU "${InstallKey}" "HackFolder" "%DESKTOP%/Hackety Hack"
+ ${Else}
+ !insertmacro MUI_INSTALLOPTIONS_READ $0 "HackFolder.ini" "Field 4" "State"
+ ${If} $0 == "1"
+ !insertmacro MUI_INSTALLOPTIONS_READ $0 "HackFolder.ini" "Field 5" "State"
+ WriteRegStr HKCU "${InstallKey}" "HackFolder" $0
+ ${Else}
+ WriteRegStr HKCU "${InstallKey}" "HackFolder" "%MYDOCUMENTS%/My Hacks"
+ ${EndIf}
+ ${EndIf}
+
+ ;Make associations
+ !insertmacro MakeFileAssoc "Ruby" "rb"
+
+ ;Create uninstaller
+ WriteUninstaller "$INSTDIR\Uninstall.exe"
+
+ !insertmacro MUI_STARTMENU_WRITE_BEGIN Application
+
+ ;Create shortcuts
+ CreateDirectory "$SMPROGRAMS\$STARTMENU_FOLDER"
+ CreateShortCut "$SMPROGRAMS\$STARTMENU_FOLDER\Hackety Hack.lnk" "$INSTDIR\${AppMainEXE}"
+ CreateShortCut "$SMPROGRAMS\$STARTMENU_FOLDER\Get Help.lnk" "$INSTDIR\${AppMainEXE}" "--manual"
+ CreateShortCut "$SMPROGRAMS\$STARTMENU_FOLDER\Uninstall.lnk" "$INSTDIR\Uninstall.exe"
+
+ !insertmacro MUI_STARTMENU_WRITE_END
+
+SectionEnd
+
+
+Function .onInit
+
+ ;Extract InstallOptions INI files
+ !insertmacro MUI_INSTALLOPTIONS_EXTRACT "HackFolder.ini"
+
+FunctionEnd
+
+Function HackFolderPage
+
+ !insertmacro MUI_HEADER_TEXT "$(TEXT_IO_TITLE)" "$(TEXT_IO_SUBTITLE)"
+ !insertmacro MUI_INSTALLOPTIONS_DISPLAY "HackFolder.ini"
+
+FunctionEnd
+
+Function HackFolderHook
+
+ !insertmacro MUI_INSTALLOPTIONS_READ $0 "HackFolder.ini" "Settings" "State"
+ StrCmp $0 0 validate
+ StrCmp $0 2 nofolder
+ StrCmp $0 3 nofolder
+ StrCmp $0 4 otherfolder ; Select your own folder
+ Abort
+
+nofolder:
+ !insertmacro MUI_INSTALLOPTIONS_READ $1 "HackFolder.ini" "Field 5" "HWND"
+ EnableWindow $1 0
+ !insertmacro MUI_INSTALLOPTIONS_READ $1 "HackFolder.ini" "Field 5" "HWND2"
+ EnableWindow $1 0
+ !insertmacro MUI_INSTALLOPTIONS_WRITE "HackFolder.ini" "Field 5" "Flags" "DISABLED"
+ Abort
+
+otherfolder:
+ !insertmacro MUI_INSTALLOPTIONS_READ $1 "HackFolder.ini" "Field 5" "HWND"
+ EnableWindow $1 1
+ !insertmacro MUI_INSTALLOPTIONS_READ $1 "HackFolder.ini" "Field 5" "HWND2"
+ EnableWindow $1 1
+ !insertmacro MUI_INSTALLOPTIONS_WRITE "HackFolder.ini" "Field 5" "Flags" ""
+ Abort
+
+validate:
+ !insertmacro MUI_INSTALLOPTIONS_READ $0 "HackFolder.ini" "Field 4" "State"
+ ${If} $0 == "1"
+ !insertmacro MUI_INSTALLOPTIONS_READ $0 "HackFolder.ini" "Field 5" "State"
+ ${If} $0 == ""
+ MessageBox MB_ICONEXCLAMATION|MB_OK "You must select a folder for your programs."
+ Abort
+ ${EndIf}
+ ${EndIf}
+FunctionEnd
+
+; When we add an optional action to the finish page the cancel button is
+; enabled. This disables it and leaves the finish button as the only choice.
+Function preFinish
+ !insertmacro MUI_INSTALLOPTIONS_WRITE "ioSpecial.ini" "settings" "cancelenabled" "0"
+FunctionEnd
+
+Function LaunchApp
+ ; ${CloseApp} "true" $(WARN_APP_RUNNING_INSTALL)
+ Exec "$INSTDIR\${AppMainEXE}"
+FunctionEnd
+
+;--------------------------------
+;Descriptions
+
+ ;Language strings
+ LangString DESC_SecApp ${LANG_ENGLISH} "A test section."
+
+ ;Assign language strings to sections
+ !insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN
+ !insertmacro MUI_DESCRIPTION_TEXT ${SecApp} $(DESC_SecApp)
+ !insertmacro MUI_FUNCTION_DESCRIPTION_END
+
+;--------------------------------
+;Uninstaller Section
+
+Section "Uninstall"
+
+ RMDir /r "$INSTDIR"
+
+ !insertmacro MUI_STARTMENU_GETFOLDER Application $MUI_TEMP
+
+ Delete "$SMPROGRAMS\$MUI_TEMP\Hackety Hack.lnk"
+ Delete "$SMPROGRAMS\$MUI_TEMP\Uninstall.lnk"
+
+ ;Delete empty start menu parent diretories
+ StrCpy $MUI_TEMP "$SMPROGRAMS\$MUI_TEMP"
+
+ startMenuDeleteLoop:
+ ClearErrors
+ RMDir $MUI_TEMP
+ GetFullPathName $MUI_TEMP "$MUI_TEMP\.."
+
+ IfErrors startMenuDeleteLoopDone
+
+ StrCmp $MUI_TEMP $SMPROGRAMS startMenuDeleteLoopDone startMenuDeleteLoop
+ startMenuDeleteLoopDone:
+
+ ;Unmake associations
+ !insertmacro UnmakeFileAssoc "Ruby" "rb"
+
+ DeleteRegKey /ifempty HKCU "${InstallKey}"
+
+SectionEnd
diff --git a/app/installer/installer-1.bmp b/app/installer/installer-1.bmp
new file mode 100755
index 0000000..ec13107
--- /dev/null
+++ b/app/installer/installer-1.bmp
Binary files differ
diff --git a/app/installer/installer-2.bmp b/app/installer/installer-2.bmp
new file mode 100755
index 0000000..9fcfed0
--- /dev/null
+++ b/app/installer/installer-2.bmp
Binary files differ
diff --git a/app/installer/setup.ico b/app/installer/setup.ico
new file mode 100755
index 0000000..1ec9242
--- /dev/null
+++ b/app/installer/setup.ico
Binary files differ
diff --git a/app/lessons/basic_programming.rb b/app/lessons/basic_programming.rb
new file mode 100644
index 0000000..abd0ca6
--- /dev/null
+++ b/app/lessons/basic_programming.rb
@@ -0,0 +1,302 @@
+lesson_set "2: Basic Programming" do
+
+ lesson "Hello there!"
+ page "Round One" do
+ para "So, you'd like to learn how to hack code with the best of 'em, eh? Well, ",
+ "you've come to the right place. This is the very first lesson I have to ",
+ "share with you. It all starts here."
+ para "I want to get you started off on the best possible foot with making ",
+ "programs, so we'll start off by talking a little bit about what ",
+ "programming is, and then we'll write some basic programs to draw fun ",
+ "things on the screen. Sound good? Off we go!"
+ flow do
+ para "(click the little "
+ icon_button :arrow_right, nil do
+ alert "Not this one! The one below!"
+ end
+ para " on the bottom of the screen to get started)"
+ end
+ end
+
+ page "Lesson Controls" do
+ para "Before we move on, Here's a refresher on the controls you can use ",
+ "to move around in the Lesson."
+ flow do
+ icon_button :arrow_left, nil
+ para strong("back"), ": goes back one page"
+ end
+ flow do
+ icon_button :arrow_right, nil
+ para strong("continue"), ": goes to the next page"
+ end
+ flow do
+ icon_button :menu, nil
+ para strong("menu"), ": makes it easy to jump around to any lesson"
+ end
+ flow do
+ icon_button :x, nil
+ para strong("close"), ": closes the tutor"
+ end
+ para "Don't forget! Press "
+ icon_button :arrow_right, nil
+ para "to move to the next part. Have at it!"
+ end
+
+ lesson "Let's talk about programming"
+ page "It's all about instructions" do
+ para "When you get down to it, programming is all about ", strong("algorithms"),
+ ". That's a big fancy word for 'a list of instructions.' Every program ",
+ "is simply a big to-do list of instructions for the computer to follow."
+ para "You can turn almost anything into a list of instructions if you really ",
+ "think about it. Most of the time, you've got so much practice at doing ",
+ "things that you don't even think about these individual steps. You just ",
+ "do them. It's very natural."
+ end
+
+ page "The computer is simple" do
+ para "Unfortunately, computers are actually quite simple. This may be contrary ",
+ "to everything you've ever heard, but it's the truth. Even though we ",
+ "compare computers to things like our brains, it's a really poor analogy. ",
+ "What computers are actually really good at is performing simple, boring ",
+ "things over and over again very accurately. They can't think for ",
+ "themselves!"
+ para "This is why computers appear to be complex. They blindly follow whatever ",
+ "orders they're given, without any thought about how those instructions ",
+ "are good or bad. It's hard to think in such simple terms!"
+ end
+
+ page "Explain yourself well" do
+ para "It's important to remember that you have to fully explain yourself to the ",
+ "computer when you're programming. It can't figure out what you're trying ",
+ "to say, so you have to say what you mean!"
+ para "This takes some practice, so we're going to start off with some exercises ",
+ "in explaining ourselves in very basic terms. It's almost like trying to ",
+ "explain math to a young child: you have to go slowly, triple check your ",
+ "work, and have some patience when it just doesn't quite get it."
+ end
+
+ lesson "Lists of Instructions"
+ page "A to-do list, not a shoping list" do
+ para "When I say that computers follow lists of instructions, I mean a to-do ",
+ "list, not a shopping list. What I'm trying to say is that these lists have ",
+ "an ", strong("order"), " to them that the computer follows. It does each ",
+ "step in turn as quickly as it possibly can."
+ para "A shopping list is a different kind of list entirely. You can go to ",
+ "whichever aisle you choose, and as long as you get everything before you ",
+ "leave, you're A-OK. This isn't what the computer does at all."
+ end
+
+ page "How would you tell a person to do it?" do
+ para "Let's try an example: if you had to tell someone in words how to draw a ",
+ "square on a piece of paper, how would you do it?"
+ para "You're not allowed to say \"like this\" or \"this way,\" that's cheating! ",
+ "You have to spell out every detail."
+ end
+
+ page "Once again: computers are simple" do
+ para "How'd you do? I can't see what you said, but here's an example of how ",
+ "simple computers are compared to people. Did you forget to mention how long ",
+ "each side of the square is? If you didn't good job!"
+ para "Here's how I'd do it, by the way. This isn't the only right answer, it's ",
+ "just an example:"
+ para "1. Put your pen down on the paper."
+ para "2. Draw right one inch."
+ para "3. Draw down one inch."
+ para "4. Draw left one inch."
+ para "5. Draw up one inch."
+ para "6. Take your pen off of the paper."
+ para "7. You're done!"
+ end
+
+ lesson "Turtles, all the way down."
+ page "Drawing... with turtles?" do
+ para "Okay! Enough of these thinking experiments. Let's actually make something. ",
+ "I bet you've been wondering when that was going to start, right? It's ",
+ "really important to get the basics down first."
+ para "We're going to tell the computer how to draw shapes... with turtles. Yes, ",
+ "you heard me right. You're going to have to give these instructions to a ",
+ "turtle."
+ para "This particular turtle is carrying a pen. You have a piece of paper. The ",
+ "turtle will follow your every word. But the turtle is a bit slow, and ",
+ "needs careful instruction. Are you up to it?"
+ end
+
+ page "The Turtle and its commands" do
+ para "We have to tell Hackety Hack that we want to tell the Turtle what to do. ",
+ "To do that, we have a ", code("Turtle"), " command. We can tell the ",
+ code("Turtle"), " two things: "
+ para code("draw"), ": the turtle will follow our instructions at lightning speed, "
+ "drawing our entire image in the blink of an eye."
+ para code("start"), ": an interactive window will appear, which lets you see ",
+ "the turtle move at each step in the program. You can move at your own ",
+ "pace. This is useful if the turtle isn't doing what you expect!"
+ para ""
+ flow do
+ para "Click on the editor tab ("
+ image "#{HH::STATIC}/tab-new.png", :margin => 6 do
+ alert("Not this one, silly! the one on the left!")
+ end
+ para ") to get started."
+ end
+
+ next_when :tab_opened, :Editor
+ end
+
+ page "Type it in!" do
+ para "Cool. Now type this: "
+ embed_code "Turtle.draw"
+ para "The period in between the ", code("Turtle"), " and the ", code("draw"),
+ " connects them together. Programming languages have rules, just like ",
+ "English has rules! You can think of ", code("Turtle"), " like a subject, ",
+ "and ", code("draw"), " as a verb. Together, they make a sentence: hey ",
+ "turtle, draw me something!"
+ para "Once you've typed that in, go ahead and click the 'Run' button. You'll see ",
+ "the turtle flash on the screen for just a brief moment, then dissapear."
+ end
+
+ page "Do... what I tell you to" do
+ para "Awesome! We've got the turtle to appear, at least. Now we need to tell ",
+ "it what we want to draw!"
+ para "Remember when we said that all programs are lists of instructions? In this ",
+ "case, our program only has one instruction: ", code("Turtle"), ", draw ",
+ "something! But we need to be able to give the ", code("Turtle"), " its ",
+ "own list of instructions."
+ para "To do this, we'll use two words: ", code("do"), " and ", code("end"), ". ",
+ "These two words together make up a ", em("sublist"), " of things, just for ",
+ "the ", code("Turtle"), "!"
+ end
+
+ page "Changing the background" do
+ para "Let's try this: we can tell the ", code("Turtle"), " that we want to use ",
+ "a different background color by using the ", code('background'), " command. ",
+ "Check it out:"
+ embed_code "Turtle.draw do
+ background maroon
+end"
+ para "Type this in and click 'Run'!"
+ end
+
+ page "The Turtle gets its orders" do
+ para "Cool stuff! The background is now maroon. You can find a full list of ",
+ "colors that are supported on the ", link("Shoes website", :click => 'http://shoesrb.com/manual/Colors.html'), "."
+ para "This is also how you make lists of instructions for the ", code("Turtle"),
+ " to follow. To make it a little easier to see, programmers will often ",
+ "put two spaces before sublists. Get in the habit now, you'll thank me later!"
+ end
+
+ page "The pen" do
+ para "Now that we've got a snazzy background color, how do we draw some lines? ",
+ "Well, the first thing you need to know about is the pen. The ",
+ code("Turtle"), " carries a pen along, and drags it along the ground behind ",
+ "itself. You can change the color of line the pen draws with the ",
+ code("pencolor"), " command."
+ end
+
+ lesson "Drawing lines"
+ page "Sally forth!" do
+ para "Okay, enough dilly-dallying. Let's tell the turtle to draw a line! Here's ",
+ "my line. Give this one a shot, then try your own colors and numbers!"
+ embed_code "Turtle.draw do
+ background lightslategray
+ pencolor honeydew
+ forward 50
+end"
+ para "50 is the number of pixels to move foward, by the way."
+ end
+
+ page "You spin me right round, baby" do
+ para "Great! So you've got a line. But what if you don't want the ",
+ code("Turtle"), " to move forward? Well, you can tell it to turn by using a ",
+ code("turnleft"), " or ", code("turnright"), " command, like this:"
+ embed_code "Turtle.draw do
+ background lightslategray
+ pencolor honeydew
+ forward 50
+ turnright 90
+ forward 50
+end"
+ para "Give that a shot, then play with it a bit!"
+ para "If you're wondering what 90 means, it's the number of degrees that it'll turn."
+ end
+
+ page "I like to move it, move it" do
+ para "Okay, now we're cooking! Let's break this down again:"
+ para code("Turtle.draw"), " tells the ", code("Turtle"), " we want it to draw ",
+ "some things. The period connects the two."
+ para code("do ... end"), " is a sublist of things. This is what we want the ",
+ code("Turtle"), " to be drawing. Not for the rest of our program."
+ para code("pencolor"), " sets the color of the pen the ", code("Turtle"), " is ",
+ "dragging along behind him, and ", code("background"), " sets the color of ",
+ "the background."
+ para code("turnright"), " (or its buddy ", code("turnleft"), ") tells the ",
+ code("Turtle"), " to turn to the right or left."
+ para code("forward"), " (or its friend ", code("backward"), ") tells the ",
+ code("Turtle"), " to move."
+ end
+
+ page "Let's try drawing that square" do
+ para "Go ahead. Give it a shot. Try to get the ", code("Turtle"), " to draw a ",
+ "square."
+ para "I'll wait. :)"
+ end
+
+ page "Here's my version" do
+ para "Here's how I did it:"
+ embed_code "Turtle.draw do
+ background lightslategray
+ pencolor honeydew
+ forward 50
+ turnright 90
+ forward 50
+ turnright 90
+ forward 50
+ turnright 90
+ forward 50
+end"
+ end
+
+ lesson "Repeating ourselves"
+ page "Pete and Repeat..." do
+ para "Man, that was a ton of reptition! My fingers almost fell off typing ",
+ code("forward"), " and ", code("turnright"), " there!"
+ para "I have good news, though: I mentioned something earlier about computers. ",
+ "It turns out that doing boring, repetitive things is something they're ",
+ "really good at! They'll do the same thing over and over again, forever even ",
+ "as long as you ask nicely."
+ end
+
+ page "Repeating repeating ourselves ourselves" do
+ para "Check it out: our ", code("Turtle"), " actually knows numbers. For ",
+ "exaple:"
+ embed_code "Turtle.draw do
+ background lightslategray
+ pencolor honeydew
+ 4.times do
+ forward 50
+ turnright 90
+ end
+end"
+ para "Try running this example. It also draws a square! Wow!"
+ end
+
+ page "4.times" do
+ para "It's pretty easy: ", code("4"), " can take instructions too, just like ",
+ "our ", code("Turtle"), ". This command repeats a list of instructions ",
+ "that many times. Fun! Four times. And the ", code("do"), " and ",
+ code("end"), " show which list of instructions go with the ", code("4"),
+ " rather than with the ", code("Turtle"), "."
+ end
+
+ page "Try it out!" do
+ para "Have a blast: make some fun shapes of your own!"
+ end
+
+ lesson "Summary"
+ page "Congradulations!" do
+ para "Wow, you're awesome. Pat yourself on the back. High five someone. You've ",
+ "got these basics down!"
+ para "Check out the ", em("Basic Ruby"), " lesson to pick up some totally ",
+ "different and exciting things!"
+ end
+
+end
diff --git a/app/lessons/basic_ruby.rb b/app/lessons/basic_ruby.rb
new file mode 100644
index 0000000..894e4bf
--- /dev/null
+++ b/app/lessons/basic_ruby.rb
@@ -0,0 +1,281 @@
+# encoding: UTF-8
+
+lesson_set "3: Basic Ruby" do
+
+ lesson "Hello there!"
+ page "Let's get started" do
+ para "Welcome to your first lesson in Ruby! You're going to have a blast."
+ para "Ruby is a great programming language that you can use to make all kinds of ",
+ "things with. Let's get going!"
+ flow do
+ para "(click the little "
+ icon_button :arrow_right, nil do
+ alert "Not this one! The one below!"
+ end
+ para " on the bottom of the screen to get started)"
+ end
+ end
+
+ page "Lesson Controls" do
+ para "Before we move on, Here's a refresher on the controls you can use ",
+ "to move around in the Lesson."
+ flow do
+ icon_button :arrow_left, nil
+ para strong("back"), ": goes back one page"
+ end
+ flow do
+ icon_button :arrow_right, nil
+ para strong("continue"), ": goes to the next page"
+ end
+ flow do
+ icon_button :menu, nil
+ para strong("menu"), ": makes it easy to jump around to any lesson"
+ end
+ flow do
+ icon_button :x, nil
+ para strong("close"), ": closes the tutor"
+ end
+ para "Don't forget! Press "
+ icon_button :arrow_right, nil
+ para "to move to the next part. Have at it!"
+ end
+
+ lesson "A bit more about Ruby"
+ page "Konnichiwa, Ruby!" do # can't do 日本語 without a bunch of work...
+ flow do # due to multiple fonts...
+ para em("Ruby"), " was created by "
+ para "まつもと ゆきひろ", :font => "TakaoGothic"
+ para " (you can just call him Matz) in 1995. If you couldn't guess, Matz is ",
+ "from Japan. Here he is:"
+ end
+ image "#{HH::STATIC}/matz.jpg"
+ end
+
+ page "Ruby is enjoyable" do
+ para "Matz has this to say about Ruby:\n"
+ para em("I hope to see Ruby help every programmer in the world to be productive, and to enjoy programming, and to be happy. That is the primary purpose of Ruby language.\n")
+ para "One more thing about Ruby: Rubyists (that's what people who like Ruby call ",
+ "themselves) have a saying: ", strong("MINSWAN"), ". This stands for ",
+ strong("M"), "atz ", strong("I"), "s ", strong("N"), "ice ",
+ strong("S"), "o ", strong("W"), "e ", strong("A"), "re ", strong("N"), "ice. ",
+ "Which is a pretty nice saying, itself. Be nice to everyone, and give them ",
+ "a hand when they need it!"
+
+ end
+
+ lesson "Displaying Things"
+ page "Let's do this!" do
+ para "Okay! The very first thing that you need to know is how to show something ",
+ "on the screen. Otherwise, you won't know what's going on!"
+ flow do
+ para "In order to start coding, we need to bring up the Editor. Click the ("
+ image "#{HH::STATIC}/tab-new.png", :margin => 6 do
+ alert("Not this one, silly! the one on the left!")
+ end
+ para ") to open it up."
+ end
+ next_when :tab_opened, :Editor
+ end
+
+ page "Hello, World!" do
+ para "There are two ways of doing this. Here's the first: alert"
+ embed_code 'alert "Hello, world!"'
+ para "Type this in and press the 'Run' button."
+ end
+
+ page "alert" do
+ para "Okay, let's break this down: There's two main parts to this little program: ",
+ "you have an ", code("alert"), ", and a ", code('"Hello, world!"'), ". These ",
+ "two parts work just like an English sentence: The ", code("alert"), " is a ",
+ 'verb and the stuff in the ""s is an object. In Ruby, we call verbs ',
+ strong("methods"), ". The ", code("alert"), " verb says 'Put an alert box ",
+ "on the screen, and the content of the box is whatever thing you give me.'"
+ para "We'll talk about the ", code('"Hello, world!"'), " in just a second. Here's ",
+ "the other way of making this happen: "
+ embed_code 'puts "Hello, world!"'
+ para "But if you try that here, it won't work! The ", code("puts"), " method ",
+ "doesn't display a dialog box, it puts text out to a command-line prompt. ",
+ "Since Hackety Hack is all graphical, this doesn't work here. So we'll ",
+ "be using ", code("alert"), "s throughout these tutorials, but if you look ",
+ "at other Ruby tutorials, you may see ", code("puts"), "."
+ end
+
+ lesson "Letters, words, and sentences"
+ page "Strings" do
+ para "Okay! Now that you've got that verb bit down, it's time to learn about ",
+ em("String"), "s. Strings are what we call a bunch of words between a pair ",
+ "of \" characters. The \"s are used to tell the computer what words you ",
+ "actually want to say. Let's think about our example:"
+ embed_code 'alert "Hello, world!"'
+ para "If you didn't have the \"s, the computer wouldn't know which words were ",
+ "methods and which ones were part of the string! And consider this:"
+ embed_code 'alert "I am on high alert!"', :run_button => true
+ para "Without making all of those words a string, how would Ruby know that the ",
+ "second alert was some text you wanted to say, rather than another alert box?"
+ end
+
+ page "Adding Strings" do
+ para "Now, if you want to put two bits of strings together, you can use the ",
+ code("+"), " character to do it. Try typing this:"
+ embed_code 'alert "Hello, " + "world!"'
+ para "Same thing! The ", code("+"), " sticks the two strings together. This ",
+ "will end up being super useful later!"
+ end
+
+ lesson "Numbers and Math"
+ page "Numbers" do
+ para "You can just use numbers, and Ruby understands them:"
+ embed_code "alert 2"
+ para "You can even use numbers that have a decimal point in them:"
+ embed_code "alert 1.5"
+ end
+
+ page "Basic Math" do
+ para "You can also do math with numbers, and it'll work out pretty well:"
+ embed_code "alert 1 + 2"
+ para ""
+ embed_code "alert 5 - 3"
+ para ""
+ embed_code "alert 2 * 3"
+ para ""
+ embed_code "alert 4 / 2"
+ para ""
+ para "But if you try this, nothing happens:"
+ embed_code 'alert "hey" + 2'
+ para "This is kind of fun and silly, though:"
+ embed_code 'alert "hey" * 2'
+ end
+
+ page "Errors" do
+ para "You know how nothing happened when you hit the Run button earlier? That ",
+ "was because there was an error. You can see any errors that run by hitting ",
+ "either Control-/ or Command-/, depending on what kind of computer you're using."
+ para "The error that results from ", code('alert "hey" + 2'), " is "
+ embed_code "can't convert Fixnum into String"
+ para "What is that?"
+ end
+
+ lesson "A few words about types"
+ page "Why's it do that?" do
+ para "Each part of a Ruby program is an ", code("Object"), ". Right now, all you ",
+ "need to know about ", code("Object"), "s is that it's sort of like saying ",
+ '"a thing." Your program is made up of a bunch of ', code("Object"), "s ",
+ "working together."
+ para "We'll learn more about ", code("Object"), "s in a future lesson, but there ",
+ "is one thing I'll tell you: ", code("Object"), "s have a 'type.' This lets ",
+ "Ruby know what kind of ", code("Object"), " it is."
+ end
+
+ page "Adding numbers to words" do
+ para "That's why"
+ embed_code 'puts "hey" + 2'
+ para 'doesn\'t really work: "hey" is a ', code("String"), " object, and 2 is a ",
+ code("Fixnum"), " object. And adding ", code("String"), "s and ",
+ code("Fixnum"), "s doesn't make any sense. We can make this code work, though!"
+ para "All we need to do is turn the ", code("Fixnum"), " into a ", code("String"),
+ ". We can do this by using the ", code("to_s"), " method."
+ embed_code 'puts "hey" + 2.to_s'
+ end
+
+ page "Let's look at that again" do
+ embed_code 'puts "hey" + 2.to_s'
+ para "Okay, this isn't bad. We have our ", code("puts"), " method. We're giving it ",
+ code('"hey" + 2.to_s'), ". The ", code("2.to_s"), " turns a ",
+ code("Fixnum"), " 2, which is like the mathematical idea of a 2, into the ",
+ code("String"), " 2, which is like when you write a 2 down on a piece of ",
+ "paper."
+ end
+
+ lesson "Variables"
+ page "They're like boxes" do
+ para "What happens if we want to keep something around? Most programs are not ",
+ "one line, I assure you. You can use a ", em("variable"), " to hold a ",
+ "value and use it later. It's like a box that you put things in."
+ para "Let's try one out:"
+ embed_code 'message = "Hello, world!"
+alert message'
+ para "Give that a run."
+ end
+ page "Assignment" do
+ para "Cool stuff! We used an ", code("="), " to ", em("assign"), " the ", code("String"), '"Hello, world!" into the variable ', code("message"), ". We then passed that ",
+ code("message"), " to the ", code("alert"), " method."
+ para "As you can see, we can use variables in place of another value. Try this:"
+ embed_code 'number = 5
+number = number * 2
+number = number - 8
+number = number + 1
+alert number'
+ para "Make a guess before you run this program."
+ end
+
+ lesson "User Input"
+ page "ask-ing for it." do
+ para "We can ask the user of our program for some input, and then put their answer ",
+ "into a variable. It's easy! Check this program out:"
+ embed_code 'name = ask "What is your name?"
+alert "Hello, " + name'
+ para "The ", code("ask"), " method brings up a box and lets our users type ",
+ "something in. Fun! We put their answer into the ", code("name"), " variable ",
+ "and then showed it with ", code("alert"), ". Sweet!"
+ end
+
+ lesson "Basic flow control"
+ page "if..." do
+ para "Remember back to that Beginning Programming lesson... we talked about how ",
+ "programs are one big list, that the computer follows in order."
+ para "Well, guess what? We can actually change this order by using certain bits ",
+ "of code. Compare these two programs:"
+ embed_code 'number = 2
+if number == 2
+ alert "Yes!"
+else
+ alert "No!"
+end'
+ embed_code 'number = 1
+if number == 2
+ alert "Yes!"
+else
+ alert "No!"
+end'
+ para "There are a few new things here."
+ end
+
+ page "==" do
+ para "Here it is again:"
+ embed_code 'number = 2
+if number == 2
+ alert "Yes!"
+else
+ alert "No!"
+end'
+ para "The == command is just a bit different than the = command. == tests the ",
+ code("Object"), " on its right against the ", code("Object"), " on its left. ",
+ "If the two are equal, then the code after the ", code("if"), " will run. ",
+ "If they're not equal, you get the code after the ", code("else"), ". The ",
+ code("end"), " lets us know we're done with our ", code("if"), "."
+ end
+
+ lesson "Example: a guessing game"
+ page "Guess!" do
+ para "Let's put this all together:"
+ embed_code 'secret_number = 42
+guess = ask "I have a secret number. Take a guess, see if you can figure it out!"
+if guess == secret_number
+ alert "Yes! You guessed right!"
+else
+ alert "Sorry, you\'ll have to try again."
+end'
+ end
+
+ lesson "Summary"
+ page "Good job!" do
+ para "Congrats! You've picked up all of the basics of Ruby. There's a lot more ",
+ "you still have to learn, though!"
+ para "Here's what you've learned so far:"
+ para "* ", code("alert"), " and ", code("ask")
+ para "* ", code("="), ", variables, and ", code("==")
+ para "* ", code("if"), " and ", code("else")
+ para "Awesome! You'll want to check out Basic Shoes next!"
+ end
+
+end
diff --git a/app/lessons/basic_shoes.rb b/app/lessons/basic_shoes.rb
new file mode 100644
index 0000000..4227b2e
--- /dev/null
+++ b/app/lessons/basic_shoes.rb
@@ -0,0 +1,183 @@
+# encoding: UTF-8
+
+lesson_set "4: Basic Shoes" do
+
+ lesson "Hello there!"
+ page "Let's get started" do
+ para "Welcome to your first lesson about Shoes! I'm going to introduce you to the ",
+ "basics that Shoes brings to everyone who programs."
+ para "If you didn't know, Shoes is a Ruby toolkit that lets you build GUI programs ",
+ "really easy and fun!"
+ flow do
+ para "(click the little "
+ icon_button :arrow_right, nil do
+ alert "Not this one! The one below!"
+ end
+ para " on the bottom of the screen to get started)"
+ end
+ end
+
+ page "Lesson Controls" do
+ para "Before we move on, Here's a refresher on the controls you can use ",
+ "to move around in the Lesson."
+ flow do
+ icon_button :arrow_left, nil
+ para strong("back"), ": goes back one page"
+ end
+ flow do
+ icon_button :arrow_right, nil
+ para strong("continue"), ": goes to the next page"
+ end
+ flow do
+ icon_button :menu, nil
+ para strong("menu"), ": makes it easy to jump around to any lesson"
+ end
+ flow do
+ icon_button :x, nil
+ para strong("close"), ": closes the tutor"
+ end
+ para "Don't forget! Press "
+ icon_button :arrow_right, nil
+ para "to move to the next part. Have at it!"
+ end
+
+ lesson "Apps"
+ page "Shoes.app" do
+ para "Okay! Shoes is tons of fun. It's really easy to get started. Here's the ",
+ "simplest Shoes app ever:"
+ embed_code "Shoes.app do
+end"
+ para "Give that a spin!"
+ end
+
+ page "It's just a block" do
+ para "You didn't say that you wanted anything in the app, so it just gives you ",
+ "a blank window. You can pass options in, too: "
+ embed_code "Shoes.app :height => 200, :width => 200 do
+end"
+ para "This'll give you whatever sized app you want! We'll be putting all of the ",
+ "fun stuff inside of the ", code("do...end"), "."
+ end
+
+ lesson "para"
+ page "The basics" do
+ para "Blank windows are pretty boring, so let's spice it up with some text!"
+ embed_code 'Shoes.app do
+ para "Hello, world"
+end'
+ para "You know what to do by now. ", code("para"), " is short for 'paragraph.' It ",
+ "lets you place text in your apps."
+ para code("para"), " and other Shoes widgets take bunches of options, too. Check ",
+ "it:"
+ embed_code 'Shoes.app do
+ para "Hello there, world", :font => "TakaoGothic"
+end'
+ end
+
+ lesson "stacks"
+ page "They're default!" do
+ para "If you're looking to lay out your Shoes widgets, there are two options. The ",
+ "first is a ", code("stack"), ". A Stack is the default layout a Shoes app ",
+ "has. So this won't look much differently than one without the stack:"
+ embed_code 'Shoes.app do
+ stack do
+ para "Hello!"
+ para "Hello!"
+ para "Hello!"
+ end
+end'
+ para "As you can see, the ", code("para"), "s are stacked on top of each other. ",
+ "By itself, kinda boring, since they already do this. But..."
+ end
+
+ lesson "flows"
+ page "The counterpart of stacks" do
+ para code("flow"), "s are kind of like stacks, but they go sideways rather than ",
+ "up and down. Try this as an example:"
+ embed_code 'Shoes.app do
+ flow do
+ para "Hello!"
+ para "Hello!"
+ para "Hello!"
+ end
+end'
+ para "Just a little bit different, eh?"
+ end
+
+ lesson "stacks + flows"
+ page "With their powers combined..." do
+ para "You can combine the ", code("stack"), " with the ", code("flow"), "s ",
+ "to make whatever kind of layout you want. For example: "
+ embed_code 'Shoes.app do
+ flow do
+ stack :width => "50" do
+ para "Hello!"
+ para "Hello!"
+ para "Hello!"
+ end
+ stack :width => "50" do
+ para "Goodbye!"
+ para "Goodbye!"
+ para "Goodbye!"
+ end
+ end
+end'
+ para "The ", code(":width"), " attribute sets how wide the stack is. Pretty simple."
+ end
+
+ lesson "button"
+ page "Push it real good" do
+ para "Buttons are also super simple in Shoes. Just give them a title and a ",
+ "bunch of code to run when they get pushed:"
+ embed_code 'Shoes.app do
+ button "Push me" do
+ alert "Good job."
+ end
+end'
+ para "I bet you're starting to see a pattern. Shoes loves to use blocks of code ",
+ "to make things super simple."
+ end
+
+ lesson "image"
+ page "Pics or it didn't happen" do
+ para "There are two ways that you can show an image in a Shoes app. Either you ",
+ "have the file on your computer:"
+ embed_code 'Shoes.app do
+ image "#{HH::STATIC}/matz.jpg"
+end'
+ para "(Can you figure out what this does? Don't feel bad if you can't.)"
+ para "You can also specify an image on the web:"
+ embed_code 'Shoes.app do
+ image "http://shoesrb.com/images/shoes-icon.png"
+end'
+ para "Either one is fine. Shoes cares not."
+ end
+
+ lesson "edit_line"
+ page "Getting some input" do
+ para "If you'd like to let someone type something in a box, well, ",
+ code("edit_line"), " is right up your alley!"
+ embed_code 'Shoes.app do
+ edit_line
+end'
+ para "This is sort of boring though... why not get the information from the box?"
+ embed_code 'Shoes.app do
+ line = edit_line
+ button "Push me!" do
+ alert line.text
+ end
+end'
+
+ end
+
+ lesson "Summary"
+ page "Great job!" do
+ para "There's a ton more things that you can do with Shoes, but you've got the ",
+ "basics down!"
+ para "If you'd like to learn more, you can visit the ",
+ link("Shoes website",
+ :click => "http://shoesrb.com/"),
+ " or press Control-M (or Command-M) to bring up the Shoes Manual."
+ end
+
+end
diff --git a/app/lessons/tour.rb b/app/lessons/tour.rb
new file mode 100644
index 0000000..abb4fcf
--- /dev/null
+++ b/app/lessons/tour.rb
@@ -0,0 +1,180 @@
+lesson_set "1: A Tour of Hackety Hack" do
+ lesson "Welcome!"
+ page "Why hello there!" do
+ para "Welcome to the Hackety Hack tour!"
+ flow do
+ para "This whole side of the screen is the ", em("Hackety Hack Tutor"),
+ ". You can move forward through the lessons by clicking the ",
+ em("Next"), " button("
+ icon_button :arrow_right, nil do
+ alert "You should click on the actual button, below! =)"
+ end
+ para "). Give it a shot!"
+ end end
+
+ page "Good Job!" do
+
+ para "See? Super easy. Let's explore the rest of Hackety Hack."
+ para "You can access the different functions of Hackety through the buttons ",
+ "on the left side of the screen. For example, you got here by clicking ",
+ "on 'Lessons.' There are 8 of those buttons, ",
+ "but since you're already on Lessons, let's talk about them first."
+ para "Before we move on, just take a minute to look at the controls in the bar below."
+ flow do
+ icon_button :arrow_left, nil
+ para strong("back"), ": goes back one page"
+ end
+ flow do
+ icon_button :arrow_right, nil
+ para strong("continue"), ": goes to the next page"
+ end
+ flow do
+ icon_button :menu, nil
+ para strong("menu"), ": makes it easy to jump around to any lesson"
+ end
+ flow do
+ icon_button :x, nil
+ para strong("close"), ": closes the tutor"
+ end
+ para "Don't forget! Press "
+ icon_button :arrow_right, nil
+ para "to move to the next part. Have at it!"
+ end
+
+ lesson "Lessons"
+
+ page "A Lesson lesson." do
+ para "When you click on the Lesson button, it'll bring you to a list of all of ",
+ "the lessons that come with Hackety. For now, there's two: This Tour, and ",
+ "a basic introduction to Ruby. More Lessons will be added, and eventually, ",
+ "you'll be able to write and share your own Lessons with other Hackety ",
+ "Hackers."
+ para "Lessons are just simple Ruby files. They're fun to make! You can even make ",
+ "lessons advance automatically based on certain events. For example, click ",
+ " on the Home button to move on."
+ para "The home button looks like this:"
+ image "#{HH::STATIC}/tab-home.png", :margin => 6 do
+ alert("Not this one, silly! the one on the left!")
+ end
+ next_when :tab_opened, :Home
+ end
+
+ lesson "Home"
+ page "Welcome Home" do
+ para "This is the home screen, which shows you two very important things: your ",
+ "own programs, and the sample programs. Everyone starts off with one simple ",
+ "program: Hello, world! I won't even ask you to open it, check it out:\n"
+ embed_code 'alert "Hello, world!"', :run_button => true
+ para "This is an actual Ruby program, click the button to try it out! You'll ",
+ "learn more about Ruby itself in the Beginning Ruby Lesson."
+ end
+
+ page "Samples" do flow do
+ para "If you click on the 'Samples' tab, you can see a bunch of sample programs ",
+ "that we've included for some inspiration. There's a few interesting ",
+ "animations, some games, and a few other things."
+ para "That's all there really is to say about the homepage. Try opening the ",
+ "Editor. Here's its icon:"
+ image "#{HH::STATIC}/tab-new.png", :margin => 6 do
+ alert("Not this one, silly! the one on the left!")
+ end
+ next_when :tab_opened, :Editor
+ end end
+
+ lesson "Editor"
+ page "Using the Editor" do
+ para "This is where the magic happens: all of your programs will be created in ",
+ "the editor. Give it a shot: try typing this program in.\n"
+ embed_code 'name = ask "What is your name?"
+alert "Hello, " + name + "."'
+ para "\nAfter doing so, you can try running the program by pressing the ",
+ "'Run' button in the lower right corner."
+ end
+
+ page "Saving and Uploading Programs" do
+ para "To save your program, simply click the 'Save' button. It'll prompt you for ",
+ "a title, and then the program will appear on your Home screen."
+ para "Once you've saved your program, two new buttons appear: 'Copy' and 'Upload.",
+ "' Copy will duplicate your program, and then ask you for a new name. This ",
+ " is really useful if you'd like to modify one of the example programs. ",
+ "Upload will send a copy of your program to the Hackety Hack website, ",
+ "where you can show it off to other Hackety Hackers. :) More about this ",
+ "when we talk about Preferences."
+ end
+
+ lesson "Help"
+ page "Getting Help" do flow do
+ para "The next tab is the Help tab. It looks like this: "
+ image "#{HH::STATIC}/tab-help.png", :margin => 6 do
+ alert("Not this one, silly! the one on the left!")
+ end
+ para " Click it, and it'll open up a new window. Browse around and come back, ",
+ "I'll be here."
+ next_when :tab_opened, :Help
+ end end
+
+ page "Okay, well... Shoes." do
+ para "That's a lot of help! Hackety Hack is built with Shoes, which is a ",
+ "toolkit for creating GUI programs in Ruby. All of the programs that ",
+ "you make in Hackety Hack are built with Shoes. That manual contains ",
+ "the entire Shoes reference, and there's a lot! Luckily, there's also ",
+ "a much shorter cheat sheet too. Go ahead click it:"
+ image "#{HH::STATIC}/tab-cheat.png", :margin => 6 do
+ alert("Not this one, silly! the one on the left!")
+ end
+ next_when :tab_opened, :Cheat
+ end
+
+ lesson "Cheat"
+ page "Short and sweet." do flow do
+ para "The Cheat Sheet is much simpler. It just contains some helpful bits ",
+ "that you should find useful. A quick reference of often used bits. ",
+ "And a short sheet deserves a short explanation. Check out the About ("
+ image "#{HH::STATIC}/tab-hand.png", :margin => 6 do
+ alert("Not this one, silly! the one on the left!")
+ end
+ para ") tab next."
+ next_when :tab_opened, :About
+ end end
+
+ lesson "About"
+ page "About Hackety" do
+ para "The classic About box. These have been around basically since the ",
+ "beginning of time. It's just a fun little image that tells you what ",
+ "version of Hackety Hack you're using. It'll change with every release."
+ para "Time for the last one: open up the Preferences tab."
+ next_when :tab_opened, :Prefs
+ end
+
+ lesson "Preferences"
+ page "I do prefer..." do
+ para "This lets you adjust your preferences for Hackety Hack. Right now, there's ",
+ "only one preference: linking Hackety with your account on ",
+ link("hackety-hack.com", :click => "http://hackety-hack.com"), ". You ",
+ strong("do"), " have one of those, right?"
+ para "If you link your account, you can upload your programs to the website ",
+ "and easily share them with others! More interesting features will be ",
+ "developed along these lines, so sign up, stick your info in, and prepare ",
+ "for all kinds of awesome."
+ para "I won't make you click the button to advance this time... instead, just ",
+ "click the arrow to advance."
+ end
+
+ lesson "Quit"
+ page "Self-explanatory" do
+ para "If you did click the quit button, well, you wouldn't be here anymore. ",
+ "And that'd be unfortunate. So, don't click it until you're good and ready. ",
+ "When it's your time to go, it'll be there waiting for you. Come back soon!"
+ end
+
+ lesson "... and beyond!"
+ page "What now?" do
+ para "This concludes the Hackety Hack tour. Good job! Now you know everything ",
+ "that Hackety Hack can do. It's pretty simple!"
+ para "This isn't the only lesson that we have for you, though. Give the ",
+ "'Beginning Programming' lesson a shot to actually start learning how to ",
+ "make programs of your own."
+ para "What are you waiting for? Get going!"
+ end
+
+end
diff --git a/app/lib/all.rb b/app/lib/all.rb
new file mode 100644
index 0000000..7b8f9bd
--- /dev/null
+++ b/app/lib/all.rb
@@ -0,0 +1,9 @@
+require 'lib/web/all'
+require 'lib/dev/init'
+
+require 'lib/art/turtle'
+require 'lib/enhancements'
+require 'lib/dev/errors'
+require 'lib/dev/events'
+require 'lib/dev/stdout'
+
diff --git a/app/lib/art/turtle.rb b/app/lib/art/turtle.rb
new file mode 100644
index 0000000..f4a7690
--- /dev/null
+++ b/app/lib/art/turtle.rb
@@ -0,0 +1,349 @@
+# a turtle graphics library
+
+require 'thread'
+
+class Shoes::TurtleCanvas < Shoes::Widget
+ # default values
+ WIDTH = 500
+ HEIGHT = 500
+ SPEED = 4 # power of two
+
+ include Math
+ DEG = PI / 180.0
+
+ # para with the next command written on it
+ attr_writer :next_command, :pen_info
+ attr_accessor :speed # powers of two
+ attr_reader :width, :height
+
+ def initialize
+ @width = WIDTH
+ @height = WIDTH
+ style width => @width, :height => @height
+ @queue = Queue.new
+ @image = image "#{HH::STATIC}/turtle.png"
+ @image.transform :center
+ @speed = SPEED
+ @paused = true
+ reset
+ move_turtle_to_top
+ end
+
+ def start_draw
+ @paused = false
+ @speed = nil
+ @image.hide
+ end
+
+
+ ### user commands ###
+
+ def reset
+ clear_orig
+ @pendown = true
+ @heading = 180*DEG # internal heading is rotated by 180 w.r.t user heading
+ @pendown = true
+ @turtle_angle = 180
+ @bg_color = white
+ @fg_color = black
+ @pen_size = 1
+ background_orig @bg_color
+ stroke @fg_color
+ strokewidth @pen_size
+ update_position(@width/2, @height/2)
+ update_turtle_heading
+ end
+
+ def forward len=100
+ is_step
+ x = len*sin(@heading) + @x
+ y = len*cos(@heading) + @y
+ if @pendown
+ l = [@x, @y, x, y]
+ line(*l)
+ end
+ update_position(x, y)
+ end
+ def backward len=100
+ forward(-len)
+ end
+ def turnleft angle=90
+ is_step
+ @heading += angle*DEG
+ @heading %= 2*PI
+ update_turtle_heading
+ end
+ def turnright angle=90
+ turnleft(-angle)
+ end
+ def setheading direction=180
+ is_step
+ direction += 180
+ direction %= 360
+ @heading = direction*DEG
+ update_turtle_heading
+ end
+ def penup
+ @pendown = false
+ end
+ def pendown
+ is_step
+ @pendown = true
+ end
+ def pendown?
+ return @pendown
+ end
+ def goto x, y
+ is_step
+ update_position(x, y)
+ end
+ def center
+ go(width/2, height/2)
+ end
+ def setx x
+ is_step
+ update_position(x, @y)
+ end
+ def sety y
+ is_step
+ update_position(@x, y)
+ end
+ def getx
+ @x
+ end
+ def gety
+ @y
+ end
+ def getposition
+ [@x, @y]
+ end
+ def getheading
+ degs = @heading/DEG
+ degs += 180
+ degs % 360
+ end
+
+ ### user commands already in shoes (the first two with another name ###
+
+ def pencolor args
+ is_step
+ stroke args
+ @fg_color = args
+ update_pen_info
+ end
+
+ def pensize args
+ is_step
+ strokewidth args
+ @pen_size = args
+ update_pen_info
+ end
+
+ alias clear_orig clear
+ alias background_orig background
+
+ def clear *args
+ in_step
+ clear_orig *args
+ end
+ def background args
+ is_step
+ background_orig args
+ move_turtle_to_top
+ @bg_color = args
+ update_pen_info
+ end
+
+
+ ## UI commands: should not be used by the user ##
+
+ def step
+ @queue.enq nil
+ end
+
+ def toggle_pause
+ @paused = !@paused
+ if !@paused
+ @speed = SPEED if @speed.nil?
+ step
+ end
+ @paused # return value
+ end
+
+ def save filename
+ _snapshot :filename => filename, :format => :pdf
+ end
+
+private
+ def update_position x, y
+ @x, @y = x, y
+ @image.move(x.round - 16, y.round - 16) unless drawing?
+ end
+
+
+ def update_turtle_heading
+ # update turtle image
+ angle_in_degrees = @heading/DEG
+ diff = (angle_in_degrees - @turtle_angle).round
+ @turtle_angle += diff
+ @image.rotate(diff) unless drawing?
+ end
+
+ def move_turtle_to_top
+ return if drawing?
+ s = @image.style
+ @image = image "#{HH::STATIC}/turtle.png"
+ @image.style s
+ end
+
+ def is_step
+ return if drawing?
+ display
+ if @paused
+ # wait for step
+ @queue.deq
+ else
+ sleep 1.0/@speed
+ if @paused # if it got paused in the meantime
+ @queue.deq
+ end
+ end
+ end
+
+ def display
+ method = nil
+ bt = caller
+ 1.upto 4 do |i|
+ m = bt[i][/`([^']*)'/, 1]
+ if m.nil? || m =~ /^block /
+ break
+ else
+ method = m
+ end
+ end
+ @next_command.replace(method)
+ end
+
+ # false if drawing directly the final result
+ def drawing?
+ @speed.nil? and not @paused
+ end
+
+ def update_pen_info
+ @pen_info.append do
+ background_orig @bg_color
+ line 5, 10, 35, 10, :stroke => @fg_color, :strokewidth => @pen_size
+ end if @pen_info
+ end
+end
+
+module Turtle
+ def self.draw opts={}, &blk
+ opts[:draw] = true
+ start opts, &blk
+ end
+
+ def self.start opts={}, &blk
+ w = opts[:width] || Shoes::TurtleCanvas::WIDTH
+ h = opts[:height] || Shoes::TurtleCanvas::HEIGHT
+ opts[:width] = w + 20
+ opts[:height] = h + ( opts[:draw]? 60 : 130)
+
+ Shoes.app opts do
+ extend Turtle # add methods back (after self changed)
+ @block = blk
+
+ unless opts[:draw]
+ para "pen: "
+ @pen_info = stack :top => 5, :width => 40, :height => 20 do
+ background white
+ line 5, 10, 35, 10
+ end
+ end
+
+ glossb "save...", :color => 'dark', :right => '-0px', :width => 100 do
+ filename = ask_save_file
+ unless filename.nil?
+ filename += '.pdf' unless filename =~ /\.pdf$/
+ @canvas.save filename
+ end
+ end
+
+ stack :height => h + 20 do
+ background gray
+ stack :top => 10, :left => 10, :width => w, :height => h do
+ shape do
+ background white
+ @canvas = turtle_canvas
+ end
+ end
+ end
+
+ if opts[:draw]
+ draw_all
+ else
+ draw_controls
+ @interactive_thread = Thread.new do
+ sleep 0.1 # HACK
+ @canvas.instance_eval &blk
+ @next_command.replace("(END)")
+ end
+ end
+ end
+ end
+
+private
+ def execute_canvas_code blk
+ @canvas.instance_eval do
+ shape do
+ self.instance_eval &blk
+ end
+ end
+ end
+
+ def draw_controls
+ flow do
+ stack do
+ flow do
+ para "next command: "
+ @next_command = para 'start', :font => 'Liberation Mono'
+ @canvas.next_command = @next_command
+ end
+ end
+ glossb "execute", :color => 'dark', :width => 100, :right => '-0px' do
+ @canvas.step
+ end
+ end
+
+ flow do
+ glossb "slower", :color => 'dark', :width => 100 do
+ @canvas.speed /= 2 if @canvas.speed > 2
+ end
+ @toggle_pause = glossb "play", :color => 'dark', :width => 100 do
+ paused = @canvas.toggle_pause
+ if paused
+ @toggle_pause.text = 'play'
+ else
+ @toggle_pause.text = 'pause'
+ end
+ end
+ glossb "faster", :color => 'dark', :width => 100 do
+ @canvas.speed *= 2
+ end
+ glossb "draw all", :color => 'dark', :right => '-0px', :width => 100 do
+ @interactive_thread.kill
+ @canvas.reset
+ @next_command.replace("(draw all)")
+ draw_all
+ end
+ end
+ @canvas.pen_info = @pen_info
+ end
+
+ def draw_all
+ timer 0.1 do
+ @canvas.start_draw
+ execute_canvas_code @block
+ end
+ end
+end
diff --git a/app/lib/dev/errors.rb b/app/lib/dev/errors.rb
new file mode 100644
index 0000000..bdae3e9
--- /dev/null
+++ b/app/lib/dev/errors.rb
@@ -0,0 +1,153 @@
+#
+# = Exceptions and Errors =
+#
+# attempting to bring friendlier
+# language to the whole thing.
+#
+class Exception
+ TRACE_RE = %r!(.+?):(.+?):in `(.+?)':\s*(.*)!
+ EXCLAMATIONS = ['Oops!', 'Holy cats!', 'By jove,', 'Pardon:',
+ 'Whoops!', 'Great coats!', 'Hot pickles!', 'Hot snacks!',
+ 'Actually:', 'Crikey,', 'Yipes,']
+ def file
+ message[TRACE_RE, 1]
+ end
+ def line
+ message[TRACE_RE, 2].to_i
+ end
+ def where
+ message[TRACE_RE, 3]
+ end
+ def says
+ message[TRACE_RE, 4] or message
+ end
+ def exclamation
+ EXCLAMATIONS[rand(EXCLAMATIONS.length)]
+ end
+ def token_name t
+ case t.downcase
+ when "'['"
+ "an opening bracket `[`"
+ when "']'"
+ "a closing bracket `]`"
+ when "'('"
+ "an opening parentheses `(`"
+ when "')'"
+ "a closing parentheses `)`"
+ when "'{'"
+ "an opening curly brace `{`"
+ when "'}'"
+ "a closing curly brace `}`"
+ when /'(.+?)'/
+ "a `#$1`"
+ when "tstar"
+ "an asterisk"
+ when /\$end/
+ "the end of the program"
+ when /t(.+)/
+ word = $1
+ if word =~ /^[aeiou]/
+ "an #{word}"
+ else
+ "a #{word}"
+ end
+ when /k(.+)/
+ word = $1
+ if word =~ /^[aeiou]/
+ "an `#{word}`"
+ else
+ "a `#{word}`"
+ end
+ else
+ t
+ end
+ end
+ def friendly
+ s = self.says
+ msg, xtra =
+ case self
+ when LocalJumpError
+ case s
+ when "no block given"
+ ["A block is missing."]
+ else [s]
+ end
+ when NoMethodError
+ case s
+ when /undefined method `([^']+)' for (.+?):(.+)/
+ ["No `#$1` method found.", "You tried to use the `#$1` method on a #$3 object. (The object was: #{$2})"]
+ when /private method `(.+?)' called for (.+?):(.+)/
+ ["The `#$1` method is private on #$3.", "Check the help page for #$3 objects."]
+ else [s]
+ end
+ when NameError
+ case s
+ when /uninitialized constant (.+)/
+ "There is nothing called `#$1`."
+ when /undefined local variable or method `(.+?)'/
+ "There is nothing called `#$1`."
+ else [s]
+ end
+ when ArgumentError
+ case s
+ when /wrong number of arguments \((\d+) for (\d+)\)/
+ given, needed = $1.to_i, $2.to_i
+ if given < needed
+ ["The `#{where}` method needs a bit more.", "You sent it #{given} arguments, while it needs #{needed}."]
+ else
+ ["The `#{where}` method was given too much.", "You gave it #{given} arguments, but it only needs #{needed}."]
+ end
+ else [s]
+ end
+ when SyntaxError
+ case s
+ when /unterminated string meets end of file/
+ ['You are missing an end quote.']
+ when /unexpected (.+?), expecting (.+)/
+ t1, t2 = $1, $2
+ ["#{token_name(t2).capitalize} went missing.", "#{token_name(t1).capitalize} was found where #{token_name(t2)} should be."]
+ when /unexpected (.+)/
+ if $1 == "\$end"
+ ["This program isn't finished. A block or method was left open."]
+ else
+ ["#{token_name($1).capitalize} doesn't match up."]
+ end
+ else [s]
+ end
+ when SocketError
+ case s
+ when /no address associated with hostname/
+ ['No such hostname (in `#{where}`.)', 'Is your internet connection okay? Check with a browser.']
+ else [s]
+ end
+ else
+ [self.class.name, self.says]
+ end
+ msg = "= #{exclamation} #{msg} ="
+ msg += "\n#{xtra}" if xtra
+ msg
+ rescue => e
+ "#{e.class}: #{e.says}"
+ end
+ EVAL_TRACE_RE = /^\(eval\):(\d+)/
+ METH_TRACE_RE = /in `([^']+)'/
+ def hint
+ line =
+ if l = message[TRACE_RE, 2]
+ l.to_i
+ elsif backtrace.first
+ if backtrace.first =~ /in `eval'|^\(eval\):/
+ if eval_line = backtrace.grep(EVAL_TRACE_RE).first
+ eval_line[EVAL_TRACE_RE, 1].to_i
+ end
+ end
+ end
+ if line
+ "Check line #{line} of your program."
+ elsif line = backtrace.grep(EVAL_TRACE_RE).first
+ line = line[EVAL_TRACE_RE, 1]
+ meth = backtrace.grep(METH_TRACE_RE).last[METH_TRACE_RE, 1]
+ "The problem appears to be inside the '''#{meth}''' method used on '''line #{line}''' of your program.\n\nThis problem could be Hackety Hack's fault. It'd be good to ask about this problem in the online [[http://talkety.hacketyhack.net forums]]."
+ end
+ end
+end
diff --git a/app/lib/dev/events.rb b/app/lib/dev/events.rb
new file mode 100644
index 0000000..c891ef5
--- /dev/null
+++ b/app/lib/dev/events.rb
@@ -0,0 +1,112 @@
+module HH; end
+
+class HH::EventConnection
+# module Array
+# def ===(other)
+# return false unless other.is_a? ::Array
+# (0...size).each do |i|
+# cond = self[i]
+# return false unless cond == :any || cond === other[i]
+# end
+# return false
+# end
+# end
+
+ attr_reader :event
+
+ def initialize event, args_cond, &blk
+ @event, @args_cond, @blk = event, args_cond, blk
+ end
+
+ # executes the connection if the arguments match
+ def try args
+ # the argument conditions matched
+ @blk.call *args if match? args
+ end
+
+private
+ # checks if the arguments +args+ match the conditions +@args_cond+
+ def match? args
+ # match size
+ return false if @args_cond.size != args.size
+
+ if @args_cond.size == 1 && @args_cond[0].is_a?(Hash)
+ return self.class.match_hash?(@args_cond[0], args[0])
+ end
+
+ # else match each element
+ (0...args.size).each do |i|
+ cond = @args_cond[i]
+ return false unless cond == :any || cond === args[i]
+ end
+ return true
+ end
+
+ def self.match_hash?(cond, hash)
+ cond.is_a?(Hash) or raise ArgumentError
+ return false unless hash.is_a?(Hash)
+ cond.each do |key, cond|
+ return false unless cond === hash[key]
+ end
+ return true
+ end
+
+# def observer
+# @blk.binding.eval("self")
+# end
+
+public
+ def to_s
+ "#<EventConnection :#{@event} #{@args_cond.inspect}] >"
+ end
+
+ alias inspect to_s
+end
+
+
+class HH::EventCondition
+ def initialize &blk
+ @blk = blk
+ end
+
+ def === args
+ if not args.is_a? Hash
+ raise ArgumentError, "for now EventCondition only works on hash events"
+ end
+ @blk.call args# && match_hash?(args, {})
+ end
+
+#private
+# def simple_condition_match? args
+# HH::EventConnection.match_hash?(@simple_condition, hash)
+# end
+end
+
+
+require 'set'
+
+module HH::Observable
+ def emit event, *args
+ return unless @event_connections
+ connections = @event_connections[event]
+ connections.each {|c| c.try(args)}
+ end
+
+ # :any is a condition that always matches
+ # returns the new connection (that can be useful later to delete it)
+ def on_event event, *args_cond, &blk
+ # in first call initialize @event_connections
+ @event_connections = Hash.new(Set.new) if @event_connections.nil?
+ new_conn = HH::EventConnection.new event, args_cond, &blk
+ @event_connections[event] += [new_conn]
+ #debug "#{new_conn} added"
+ #emit :new_event_connection, new_conn
+ new_conn
+ end
+
+ def delete_event_connection c
+ #debug "#{c} deleted!"
+ @event_connections[c.event].delete c
+ end
+end
+
diff --git a/app/lib/dev/init.rb b/app/lib/dev/init.rb
new file mode 100644
index 0000000..37d534b
--- /dev/null
+++ b/app/lib/dev/init.rb
@@ -0,0 +1,48 @@
+# sets constant in the HH module and environment variables
+# the current directory in set to HH::USER (~/.hacketyhack on unix systems)
+# (HH::APP is initialized in h-ety-h.rb instead)
+
+HH::NET = "hackety-hack.com"
+HH::REST = "http://hackety-hack.com"
+#for easy switching when developing
+#HH::NET = "localhost:3000"
+#HH::REST = "http://localhost:3000"
+HH::HOME = Dir.pwd
+HH::STATIC = HH::HOME + "/static"
+HH::FONTS = HH::HOME + "/fonts"
+HH::LESSONS = HH::HOME + "/lessons"
+$LOAD_PATH << HH::HOME
+
+# platform-specific directories
+case RUBY_PLATFORM when /win32/, /i386-mingw32/
+ require 'lib/dev/win32'
+ HOME = ENV['USERPROFILE'].gsub(/\\/, '/')
+ ENV['MYDOCUMENTS'] = HH.read_shell_folder('Personal')
+ ENV['APPDATA'] = HH.read_shell_folder('AppData')
+ ENV['DESKTOP'] = HH.read_shell_folder('Desktop')
+ HH::USER =
+ begin
+ HH.win_path(Win32::Registry::HKEY_CURRENT_USER.
+ open('Software\Hackety.org\Hackety Hack').
+ read_s('HackFolder'))
+ rescue
+ HH.win_path('%APPDATA%/Hackety Hack')
+ end
+else
+ ENV['DESKTOP'] = File.join(ENV['HOME'], "Desktop")
+ ENV['APPDATA'] = ENV['HOME']
+ ENV['MYDOCUMENTS'] = ENV['HOME']
+ HH::USER = File.join(ENV['HOME'], ".hacketyhack")
+end
+
+HH::DOWNLOADS = File.join(HH::USER, 'Downloads')
+FileUtils.makedirs(HH::DOWNLOADS)
+
+Dir.chdir(HH::USER)
+
+font "#{HH::FONTS}/Lacuna.ttf"
+font "#{HH::FONTS}/LiberationMono-Regular.ttf"
+font "#{HH::FONTS}/LiberationMono-Bold.ttf"
+font "#{HH::FONTS}/Pixelpoiiz.ttf"
+font "#{HH::FONTS}/Phonetica.ttf"
+font "#{HH::FONTS}/TakaoGothic.otf"
diff --git a/app/lib/dev/stdout.rb b/app/lib/dev/stdout.rb
new file mode 100644
index 0000000..e07e14c
--- /dev/null
+++ b/app/lib/dev/stdout.rb
@@ -0,0 +1,9 @@
+# allow to track standard output
+require 'lib/dev/events'
+
+STDOUT.extend HH::Observable
+
+def STDOUT.write str
+ emit :output, str if str
+ super # default behaviour
+end
diff --git a/app/lib/dev/win32.rb b/app/lib/dev/win32.rb
new file mode 100644
index 0000000..ef8687b
--- /dev/null
+++ b/app/lib/dev/win32.rb
@@ -0,0 +1,29 @@
+require 'Win32API'
+require 'win32/registry'
+
+module HH
+ SHGetFolderPath = Win32API.new "shell32.dll", "SHGetFolderPath", %w[P I P I P], "I"
+ class << self
+ def read_shell_folder(name)
+ x =
+ case name
+ when "Personal"; 0x05
+ when "AppData"; 0x1A
+ when "Desktop"; 0x00
+ end
+ path = " " * 256
+ SHGetFolderPath.call(0, x, 0, 0, path)
+ path.strip.gsub("\0", "").gsub(/\\/, '/')
+ end
+ def win_vars(str)
+ str.gsub(/%DESKTOP%/, ENV['DESKTOP']).
+ gsub(/%USERNAME%/) { HH::PREFS['hh_username'] }.
+ gsub(/%APPDATA%/, ENV['APPDATA']).
+ gsub(/%MYDOCUMENTS%/, ENV['MYDOCUMENTS']).
+ gsub(/%HACKETY_USER%/) { HH::USER }
+ end
+ def win_path(str)
+ win_vars(str.gsub(/\\/, '/'))
+ end
+ end
+end
diff --git a/app/lib/enhancements.rb b/app/lib/enhancements.rb
new file mode 100644
index 0000000..5ce6e1e
--- /dev/null
+++ b/app/lib/enhancements.rb
@@ -0,0 +1,158 @@
+# Extensions to existing classes
+
+module Kernel
+ def say arg
+ HH::APP.say arg
+ end
+end
+
+
+class Object
+ # rails-like blank? method
+ def blank?
+ if respond_to? :empty?
+ empty?
+ elsif respond_to? :zero?
+ zero?
+ else
+ !self
+ end
+ end
+
+ def try(method, *args)
+ respond_to?(method) ? send(method, *args) : self
+ end
+
+ # FIXME fixes the link inherited from FileUtils, I don't know why or where
+ # FileUtils is extended...
+ undef link if defined? link
+end
+
+class String
+ # checks if the string starts with the string +beginning+
+ def starts?( beginning )
+ self[0, beginning.length] == beginning
+ end
+ # checks if the string ends with the string +ending+
+ def ends?( ending )
+ self[-ending.length, ending.length] == ending
+ end
+ def remove( phrase )
+ r = dup
+ r[phrase] = ""
+ r
+ end
+ def to_a
+ self.split "\n"
+ end
+
+ # used to convert strings into slugs, just like the website uses.
+ def to_slug
+ self.gsub(/\s/, "_").gsub(/\W/, "").downcase
+ end
+
+ # rot 13 encoding
+ def rot13
+ tr("A-Za-z", "N-ZA-Mn-za-m")
+ end
+
+ # rot 13 encoding
+ def rot13!
+ tr!("A-Za-z", "N-ZA-Mn-za-m")
+ end
+end
+
+
+#
+# = Numbers
+#
+# Enhancements to the basic number classes.
+#
+class Fixnum
+ def ordinalize
+ case self
+ when 1; "1st"
+ when 2; "2nd"
+ when 3; "3rd"
+ else "#{self}th"
+ end
+ end
+ def weeks; self * 7*24*60*60; end
+end
+
+
+
+#
+# = Time =
+#
+# Enhancements to the clock.
+#
+class Time
+ def calendar
+ "#{strftime('%B')} #{day.ordinalize}, #{year}"
+ end
+ def calendar_with_time
+ "#{strftime('%B')} #{day.ordinalize}, #{year} at #{time_only}"
+ end
+ def time_only
+ h = hour % 12
+ h = 12 if h.zero?
+ "#{h}:#{strftime('%M %p')}"
+ end
+ def quick
+ "#{strftime('%b')} #{day}, #{year} at #{time_only.downcase.gsub(' ', '')}"
+ end
+ def short
+ "#{strftime('%b')} #{day}"
+ end
+ def full
+ strftime("%Y-%m-%d %H:%M:%S")
+ end
+ def since(new_time = Time.now, include_seconds = false)
+ time_span = new_time.to_i - self.to_i
+ distance_in_minutes = (((time_span).abs)/60.0).round
+ distance_in_seconds = ((time_span).abs).round
+
+ case distance_in_minutes
+ when 0..1
+ if not include_seconds
+ return (distance_in_seconds < 55) ? 'less than a minute' : '1 minute'
+ end
+ # else:
+ case distance_in_seconds
+ when 0..4 then 'less than 5 seconds'
+ when 5..9 then 'less than 10 seconds'
+ when 10..19 then 'less than 20 seconds'
+ when 20..39 then 'half a minute'
+ when 40..59 then 'less than a minute'
+ else '1 minute'
+ end
+ when 2..45 then "#{distance_in_minutes} minutes"
+ when 46..90 then 'about 1 hour'
+ when 91..1440 then "about #{(distance_in_minutes.to_f / 60.0).round} hours"
+ when 1441..2879 then '1 day'
+ else
+ days = (distance_in_minutes / 1440)
+ if (days / 365) > 0
+ "#{days / 365} years"
+ else
+ "#{days % 365} days"
+ end
+ end
+ end
+end
+
+require 'thread'
+class Thread
+ alias initialize_orig initialize
+ def initialize *args, &blk
+ initialize_orig *args do
+ begin
+ blk.call
+ rescue => ex
+ error ex
+ end
+ end
+ end
+end
+
diff --git a/app/lib/web/all.rb b/app/lib/web/all.rb
new file mode 100644
index 0000000..535e8b7
--- /dev/null
+++ b/app/lib/web/all.rb
@@ -0,0 +1,2 @@
+require 'lib/web/hacker'
+require 'lib/web/web'
diff --git a/app/lib/web/hacker.rb b/app/lib/web/hacker.rb
new file mode 100644
index 0000000..f74350f
--- /dev/null
+++ b/app/lib/web/hacker.rb
@@ -0,0 +1,37 @@
+# website integration
+
+require 'lib/web/yaml'
+
+def Hacker name
+ Hacker.new name
+end
+
+class Hacker
+ include HH::YAML
+
+ attr :name
+ attr :password
+
+ def initialize(who)
+ @name = who[:username]
+ @password = who[:password]
+ end
+
+ def inspect
+ "(Hacker #{@name})"
+ end
+
+ def channel(title)
+ Channel.new(@name, title)
+ end
+
+ def program_list &blk
+ http('GET', "/programs/#{@name}.json", :username => @name, :password => @password, &blk)
+ end
+
+ def save_program_to_the_cloud name, code
+ url = "/programs/#{@name}/#{name}.json"
+ http('PUT', url, {:creator_username => @name, :title => name, :code => code, :username => @name, :password => @password}) {|u| true }
+ end
+
+end
diff --git a/app/lib/web/web.rb b/app/lib/web/web.rb
new file mode 100644
index 0000000..38c148a
--- /dev/null
+++ b/app/lib/web/web.rb
@@ -0,0 +1,318 @@
+#
+# = Web =
+#
+# feeds and searches.
+#
+module Web
+ JSON_MIME_TYPES = ["application/x-javascript", "application/x-json", "application/json"]
+ XML_MIME_TYPES = ["application/rdf+xml", "application/rss+xml", "application/atom+xml", "application/xml", "text/xml"]
+ [JSON_MIME_TYPES, XML_MIME_TYPES].each do |ary|
+ ary.map! { |str| /^#{Regexp::quote(str)}/ }
+ end
+end
+
+module Hpricot
+ class Doc
+ def widget(slot)
+ ary = [:para, nil, []]
+ children.each { |c| c.build_list(ary) }
+ 0.step(ary.length - 1, 3) do |i|
+ case ary[i]
+ when :image
+ slot.send(ary[i], *ary[i+1])
+ else
+ unless ary[i+2].find_all { |x| !x.is_a?(String) or (x.gsub!(/^[\n\t]+|[\n\t]+$/, ''); x =~ /\S/) }.empty?
+ slot.send(ary[i], ary[i+2], ary[i+1])
+ end
+ end
+ end
+ end
+ def to_s
+ (self/"//*/text()").join(" ").gsub(/\n+^Z/, '')
+ end
+ def length
+ to_s.length
+ end
+ end
+ module Traverse
+ def build_list(ary, top = ary, inside = false) end
+ end
+ class Elem
+ def build_list(ary, top = ary, inside = false)
+ key, opts, text, block = nil, nil, nil, nil
+ case name
+ when "a"; key, opts = :link, {:click => self['href']}
+ when "b", "strong"; key = :strong
+ when "i", "em"; key = :em
+ when "sup"; key = :sup
+ when "sub"; key = :sub
+ when "br"; text = "\n"
+ when "img"; block, opts = :image, [self['src']]
+ when "p"; block = :para
+ when "blockquote"; block, opts = :para, {:margin => 20}
+ when "li"; block, opts = :para, {:margin => 10}
+ end
+
+ if key
+ ary2 = [key, opts, []]
+ children.each { |c| c.build_list(ary2, top, true) }
+ unless ary2.last.empty?
+ unless ary2.last.find_all { |x| !x.is_a?(String) or (x.gsub!(/^\n+|\n+$/, ''); x =~ /\S/) }.empty?
+ ele = HH::APP.send(ary2[0], ary2[2], ary2[1])
+ ary.last << ele
+ end
+ end
+ elsif text
+ ary.last.last << text if ary.last.last.is_a? String
+ elsif block == :image
+ if ary[0] == :link
+ opts << {:click => ary[1]}
+ end
+ top[-3,0] = [:image, opts, nil]
+ elsif block
+ if !inside
+ ary << block << opts << []
+ end
+ children.each { |c| c.build_list(ary, top, true) }
+ else
+ children.each { |c| c.build_list(ary, top, inside) }
+ end
+ ary
+ end
+ end
+ class Text
+ def build_list(ary, top = nil, inside = false)
+ ary = ary.last
+ txt = self.inner_text
+ txt.gsub!(/\r\n/, "\n")
+ txt.gsub!(/\n+/, "\n")
+ if ary.last.is_a? String
+ ary.last << txt
+ elsif txt =~ /\S/
+ ary << txt
+ end
+ ary
+ end
+ end
+end
+
+class Feed
+ attr_accessor :title, :link, :description, :items
+ def initialize(t, l, d, i)
+ @title, @link, @description, @items = t, l, d, i
+ end
+ def widget(slot)
+ slot.stack(:margin => 18).tap do |s|
+ s.inscription "Feed from #{self.link}"
+ s.title self.title
+ s.para self.description if self.description
+ items.each do |item|
+ item.widget(s)
+ end
+ end
+ end
+ def to_s
+ res = "Feed from #{link}\n"
+ res << "== #{title} ==\n"
+ res << "#{description}" if description
+ res << " (#{items.size} items)\n"
+ end
+ def each(&blk)
+ items.each(&blk)
+ end
+ def self.parse(data)
+ doc = Hpricot.XML(data)
+ if doc.at("/rss, /feed, rdf:rdf, rdf:RDF")
+ Feed.load(doc)
+ elsif link = doc.at("link[@type='application/atom+xml'], link[@type='application/rss+xml']")
+ URI(link['href'])
+ else
+ doc
+ end
+ end
+ def self.load(doc)
+ title = (doc/"feed/title, channel/title").inner_text
+ link = doc.at("feed/link, channel/link")
+ link = link['href'] || link.inner_text
+ description = (doc/"feed/tagline, channel/description").inner_text
+ items = []
+ (doc/"feed/entry, item").each do |item|
+ ilink = item.at("/link")
+ desc = item.at("content:encoded, content, description")
+ if desc.to_s =~ /<\w+( |>)/n
+ desc = Hpricot(desc.inner_text)
+ end
+ items << Feed::Item.new((item/"/title").inner_text,
+ ilink['href'] || ilink.inner_text,
+ desc)
+ end
+ self.new(title, link, description, items)
+ end
+end
+
+class Feed::Item
+ attr_accessor :title, :link, :description
+ def to_s; "(Feed::Item)" end
+ def initialize(t, l, d)
+ @title, @link, @description = t, l, d
+ end
+ def widget(slot)
+ slot.stack.tap do |s|
+ s.para s.link(self.title, :click => self.link, :size => 18, :stroke => "#777"),
+ " Feed::Item", :stroke => "#999"
+ if self.description.respond_to? :widget
+ self.description.widget(s)
+ else
+ s.para self.description
+ end
+ end
+ end
+ def to_s
+ res = "== #{title} ==\n"
+ res << "#{description}\n"
+ end
+end
+
+# downloads the file at URI to filename showing the progress in a window
+# if filename is a relative path, the file will be saved to the Downloads
+# directory of HH, by default the basename of the URI is used
+def Web.download uri, filename=nil, &blk
+ filename ||= File.basename(uri)
+ filename = File.expand_path(filename, "#{HH::USER}/Downloads/")
+ opts = {:save => filename }
+
+ Web.dowload_dialog uri, opts, &blk
+end
+
+def Web.dowload_dialog uri, opts = {}, &blk
+ window :width => 450, :height => 100, :margin => 10, :title => "Download" do
+ # method to close the window
+ def self.finished
+ timer 1 do
+ close
+ end
+ end
+
+ status = para "Downloading #{uri}"
+ p = progress :width => 1.0
+
+ opts[:start] = proc{|dl| status.text = 'Connecting'}
+ opts[:progress] = proc do |dl|
+ status.text = "Transferred #{dl.transferred} of #{dl.length} bytes (#{dl.percent}%)"
+ p.fraction = dl.percent * 0.01
+ end
+
+ opts[:finish] = proc do |dl|
+ status.text = 'Download finished'
+ finished
+ blk.call(dl) if blk
+ end
+ opts[:error] = proc{|dl, err| status.text = "Error: #{err}"; finished}
+ HH::APP.download uri, opts
+ end
+end
+
+def Web.fetch(uri, opts = {}, &blk)
+ Web.dowload_dialog uri, opts do |dl|
+ data = dl.response.body
+ unless opts[:as]
+ opts[:as] =
+ case dl.response.headers['Content-Type']
+ when *Web::JSON_MIME_TYPES; JSON
+ when *Web::XML_MIME_TYPES; Feed
+ end
+ end
+ if opts[:as]
+ if opts[:as].respond_to? :parse
+ obj = opts[:as].parse(data)
+ if obj.is_a? URI
+ Web.fetch(obj, opts, &blk)
+ else
+ blk[obj]
+ end
+ elsif opts[:as] == String
+ blk[data]
+ else
+ raise ArgumentError, "Web.fetch can't load into the #{opts[:as]} class"
+ end
+ else
+ blk[data]
+ end
+ end
+end
+
+def Web.delicious(search, opts = {})
+ search = search.try(:join, " ")
+ opts[:limit] ||= 10
+ url = "setcount=#{opts[:limit]}"
+ search = "\"#{search}\"" if opts[:exact]
+ if opts[:page].to_i > 1
+ url += "&page=#{opts[:page]}"
+ end
+
+ HH::APP.download("http://del.icio.us/search?p=#{URI.escape(search)}&#{url}") do |doc|
+ dls = Hpricot(doc.response.body)
+ list = (dls/"div.data").map do |ele|
+ link, meta = ele.at("h4 a"), ele.at(".delNavCount")
+ Feed::Item.new(link.inner_text, link['href'], "saved by #{meta.inner_text} people")
+ end
+ yield Feed.new("del.icio.us", "http://del.icio.us/",
+ "Delicious search for #{search}", list)
+ end
+end
+
+def Web.flickr(search, opts = {})
+ search = search.try(:join, " ").split(/\s+/).join(",")
+ HH::APP.download("http://api.flickr.com/services/feeds/photos_public.gne?tags=#{URI.escape(search)}") do |doc|
+ yield Feed.load(Hpricot(doc.response.body))
+ end
+end
+
+def Web.google(search, opts = {})
+ search = search.try(:join, " ")
+ opts[:limit] ||= 10
+ url = "num=#{opts[:limit]+3}"
+ search = "\"#{search}\"" if opts[:exact]
+ if opts[:page].to_i > 1
+ url += "&start=#{opts[:limit] * (opts[:page].to_i - 1)}"
+ end
+ if opts[:site]
+ if opts[:site].respond_to?(:join)
+ search += "( site:#{opts[:site].join(' | site:')} )"
+ else
+ search += " site:#{opts[:site]}"
+ end
+ end
+
+ HH::APP.download("http://www.google.com/search?q=#{URI.escape(search)}&#{url}") do |doc|
+ ggl = Hpricot(doc.response.body)
+ list = (ggl/".g")[0,opts[:limit]].map do |ele|
+ link = ele.at("a")
+ Feed::Item.new(link.inner_text, link['href'], (ele/(".s:first".."br")).inner_text)
+ end
+ yield Feed.new("Google", "http://google.com/",
+ "Google search for: #{search}", list)
+ end
+end
+
+def Web.yahoo(search, opts = {})
+ search = search.try(:join, " ")
+ opts[:limit] ||= 10
+ url = "n=#{opts[:limit]}"
+ search = "\"#{search}\"" if opts[:exact]
+ if opts[:page].to_i > 1
+ url += "&b=#{opts[:limit] * (opts[:page].to_i - 1)}"
+ end
+ url += "&vs=#{URI.escape([*opts[:site]].join(" | "))}" if opts[:site]
+
+ HH::APP.download("http://search.yahoo.com/search?p=#{URI.escape(search)}&#{url}") do |doc|
+ yahoo = Hpricot(doc.response.body)
+ list = (yahoo/"div#web li").map do |ele|
+ link = ele.at(".yschttl")
+ next unless link
+ Feed::Item.new(link.inner_text, link['href'], (ele/".abstr, .sm-abs").inner_text)
+ end.compact
+ yield Feed.new("Yahoo!", "http://yahoo.com/",
+ "Yahoo! search for: #{search}", list)
+ end
+end
diff --git a/app/lib/web/yaml.rb b/app/lib/web/yaml.rb
new file mode 100644
index 0000000..d389447
--- /dev/null
+++ b/app/lib/web/yaml.rb
@@ -0,0 +1,60 @@
+require 'yaml'
+
+class FetchError < StandardError; end
+class SharedAlreadyError < StandardError; end
+
+module HH::YAML
+ def http(meth, path, params = nil, &blk)
+ url = HH::REST + path.to_s
+ body, headers = nil, {'Accept' => 'text/yaml'}
+ case params
+ when String
+ body = params
+ when Hash
+ if params[:who]
+ headers['X-Who'] = params.delete(:who)
+ end
+
+ if params[:post]
+ body = params[:post]
+ else
+ x = qs(params)
+ if meth == 'GET'
+ url += "?" + x
+ else
+ body = x
+ headers['Content-Type'] = 'application/x-www-form-urlencoded'
+ end
+ end
+ end
+
+ # if HH::PREFS['username']
+ # req.basic_auth HH::PREFS['username'], HH::PREFS['pass']
+ # end
+ headers['Authorization'] = 'Basic ' + ["#{HH::PREFS['username']}:#{HH::PREFS['password']}"].pack("m").strip
+ HH::APP.download url, :method => meth, :body => body, :headers => headers do |dl|
+ blk[YAML.load(dl.response.body)] if blk
+ end
+ end
+
+ def escape(string)
+ string.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n) do
+ '%' + $1.unpack('H2' * $1.size).join('%').upcase
+ end.tr(' ', '+')
+ end
+
+ def qs(hsh, prefix = [])
+ hsh.map do |k, v|
+ ary = prefix + [k]
+ case v
+ when Hash
+ qs(v, ary)
+ else
+ ok = escape(ary.first) +
+ ary[1..-1].map { |x| "[#{escape(x)}]" }.join
+ "#{ok}=#{escape(v)}"
+ end
+ end.join("&")
+ end
+end
+
diff --git a/app/platform/mac/App.icns b/app/platform/mac/App.icns
new file mode 100644
index 0000000..83c98ec
--- /dev/null
+++ b/app/platform/mac/App.icns
Binary files differ
diff --git a/app/platform/mac/Cheat.icns b/app/platform/mac/Cheat.icns
new file mode 100644
index 0000000..0b3319b
--- /dev/null
+++ b/app/platform/mac/Cheat.icns
Binary files differ
diff --git a/app/platform/mac/Help.icns b/app/platform/mac/Help.icns
new file mode 100644
index 0000000..2c1e3dc
--- /dev/null
+++ b/app/platform/mac/Help.icns
Binary files differ
diff --git a/app/platform/mac/dmg_ds_store b/app/platform/mac/dmg_ds_store
new file mode 100644
index 0000000..36e1c56
--- /dev/null
+++ b/app/platform/mac/dmg_ds_store
Binary files differ
diff --git a/app/platform/msw/App.ico b/app/platform/msw/App.ico
new file mode 100755
index 0000000..a4d43f6
--- /dev/null
+++ b/app/platform/msw/App.ico
Binary files differ
diff --git a/app/platform/msw/Cheat.ico b/app/platform/msw/Cheat.ico
new file mode 100755
index 0000000..8375ac7
--- /dev/null
+++ b/app/platform/msw/Cheat.ico
Binary files differ
diff --git a/app/platform/msw/Help.ico b/app/platform/msw/Help.ico
new file mode 100755
index 0000000..009d2ef
--- /dev/null
+++ b/app/platform/msw/Help.ico
Binary files differ
diff --git a/app/platform/nix/app.png b/app/platform/nix/app.png
new file mode 100644
index 0000000..617aa3b
--- /dev/null
+++ b/app/platform/nix/app.png
Binary files differ
diff --git a/app/root/Home/.gitignore b/app/root/Home/.gitignore
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/app/root/Home/.gitignore
diff --git a/app/root/comics.txt b/app/root/comics.txt
new file mode 100755
index 0000000..36d31b8
--- /dev/null
+++ b/app/root/comics.txt
@@ -0,0 +1,4 @@
+Achewood: http://achewood.com/
+Dinosaur Comics: http://qwantz.com/
+Perry Bible Fellowship: http://cheston.com/pbf/archive.html
+Get Your War On: http://mnftiu.cc/
diff --git a/app/spec/all.rb b/app/spec/all.rb
new file mode 100755
index 0000000..f08b73b
--- /dev/null
+++ b/app/spec/all.rb
@@ -0,0 +1,5 @@
+#!/usr/bin/env ruby
+
+require 'spec/enhancements'
+require 'spec/events'
+require 'spec/stdout' \ No newline at end of file
diff --git a/app/spec/enhancements.rb b/app/spec/enhancements.rb
new file mode 100755
index 0000000..8d7b533
--- /dev/null
+++ b/app/spec/enhancements.rb
@@ -0,0 +1,452 @@
+#!/usr/bin/env ruby
+
+require 'lib/enhancements'
+
+require 'spec/autorun'
+
+describe Object, "blank?" do
+ it "should return true for empty strings" do
+ "".blank?.should == true
+ end
+
+ it "should return false for non empty strings" do
+ "a".blank?.should == false
+ ".".blank?.should == false
+ "dfasfa".blank?.should == false
+ "[]".blank?.should == false
+ "0".blank?.should == false
+ end
+
+ it "should return false for strings composed of just spaces" do
+ " ".blank?.should == false
+ " ".blank?.should == false
+ "\t".blank?.should == false
+ "\n".blank?.should == false
+ end
+
+ it "should return true for empty arrays" do
+ [].blank?.should == true
+ end
+
+ it "should return false for non empty arrays" do
+ [1, 2, 3].blank?.should == false
+ [""].blank?.should == false
+ [0].blank?.should == false
+ [nil].blank?.should == false
+ end
+
+ it "should return true for zero" do
+ 0.blank?.should == true
+ 0.0.blank?.should == true
+ Rational(0).blank?.should == true
+ end
+
+ it "should return false for non zero numbers" do
+ 1.blank?.should == false
+ 1.2.blank?.should == false
+ 0.0001.blank?.should == false
+ (Rational(1)/100).blank?.should == false
+ nan = 0.0/0.0
+ nan.blank?.should == false
+ inf = 1.0/0.0
+ inf.blank?.should == false
+ end
+
+ it "should return true for nil and false" do
+ nil.blank?.should == true
+ false.blank?.should == true
+ end
+
+ it "should return true for an object with empty? returning true" do
+ a = Object.new
+ a.blank?.should == false
+ def a.empty?; true end
+ a.blank?.should == true
+ end
+
+ it "should return true for an object with no empty? " +
+ "and zero? returning true" do
+ a = Object.new
+ a.blank?.should == false
+ def a.zero?; true end
+ a.blank?.should == true
+ end
+
+ # I didn't add the use case with empty? returning false and zero returning
+ # true, as I'm not sure the current implementation is the best way to go
+ # I will leave it undefined
+end
+
+
+
+
+describe Object, "#tap" do
+ it "should return self" do
+ obj = Object.new
+ obj.tap{}.should == obj
+ end
+
+ it "should yield self" do
+ obj = Object.new
+ obj.tap do |x|
+ x.should == obj
+ end
+ end
+end
+
+describe Object, "#try" do
+ it "should do nothing and return self on an inexistend method" do
+ obj = Object.new
+ obj.try(:inexistend, 1, 2).should == obj
+ end
+
+ context "with no arguments" do it "should call the method if it exists" do
+ obj = "123"
+ obj.try :reverse!
+ obj.should == "321"
+ end end
+
+ context "with more arguments" do it "should call the method if it exists" do
+ obj = "123"
+ obj.try :delete!, "2"
+ obj.should == "13"
+ end end
+
+ it "should return the result ot the method if it exists" do
+ obj = "123"
+ obj.freeze
+ obj.try(:delete, "2").should == "13"
+ obj.should == "123"
+ end
+end
+
+
+
+
+describe String, "#ends?" do
+ it "should return true if it starts with the given string" do
+ "hello".starts?("he").should == true
+ "hellohello".starts?("hello").should == true
+ end
+
+ it "should always return true if the given string is empty" do
+ "hello".starts?("").should == true
+ "".starts?("").should == true
+ end
+
+ it "should return false if it does not start with the given string" do
+ "hello".starts?("ello").should == false
+ " hello hello".starts?("hello").should == false
+ "hello".starts?("e").should == false
+ "hello".starts?("o").should == false
+ "".starts?("hello").should == false
+ "".starts?(" ").should == false
+ end
+end
+
+describe String, "#ends?" do
+ it "should return true if it starts with the given string" do
+ "hello".ends?("lo").should == true
+ "hellohello".ends?("hello").should == true
+ end
+
+ it "should always return true if the given string is empty" do
+ "hello".ends?("").should == true
+ "".ends?("").should == true
+ end
+
+ it "should return false if it does not start with the given string" do
+ "hello".ends?("hell").should == false
+ "hello hello ".ends?("hello").should == false
+ "hello".ends?("e").should == false
+ "hello".ends?("l").should == false
+ "".ends?("hello").should == false
+ "".ends?(" ").should == false
+ end
+end
+
+describe String, "#remove" do
+ it "should remove the substring equal to the argument" do
+ "hello".remove("el").should == "hlo"
+ "hellohello".remove("ell").should == "hohello"
+ "hello".remove("l").should == "helo"
+ end
+
+ it "should rase an exception if it doesn't contain the substring" do
+ lambda{"hello".remove("ello ")}.should raise_error
+ end
+
+ it "should not change the string" do
+ str = "hello"
+ str.remove "l"
+ str.should == "hello"
+ str.remove("he")
+ str.should == "hello"
+ end
+end
+
+describe String, "#to_slug" do
+ it "should contain no characters other than lowercase alphanumeric and _" do
+ # create a random string containing also a a lot of noise
+ random_string = ""
+ 1000.times do
+ random_string << rand(256).chr
+ end
+ random_string.to_slug.should =~ /^[a-z0-9_]*$/
+ end
+
+ it "should only return lowercase characters" do
+ "heLlO".to_slug.should == "hello"
+ end
+
+ it "should only return alphanumeric characters" do
+ "hello,world1!!!".to_slug.should == "helloworld1"
+ end
+
+ it "should transform whitespace to _" do
+ "a a a \n".to_slug.should == "a_a__a__"
+ end
+
+ it "should combine all the above correctly" do
+ "Hello, World1!!!".to_slug.should == "hello_world1"
+ end
+end
+
+
+describe String, "#rot13" do
+ it "should translitarate all alphabeti ascii characters correctly" do
+ str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ str.freeze
+ str.rot13.should == "nopqrstuvwxyzabcdefghijklmNOPQRSTUVWXYZABCDEFGHIJKLM"
+ end
+
+ it "should not change non alphabetic characters" do
+ str = "1 + 2i"
+ str.freeze
+ str.rot13.should == "1 + 2v"
+ end
+end
+
+describe String, "#rot13!" do
+ it "should translitarate all alphabeti ascii characters correctly" do
+ str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ str.rot13!
+ str.should == "nopqrstuvwxyzabcdefghijklmNOPQRSTUVWXYZABCDEFGHIJKLM"
+ end
+
+ it "should not change non alphabetic characters" do
+ str = "1 + 2i"
+ str.rot13!
+ str.should == "1 + 2v"
+ end
+
+ it "should return the transliterated string" do
+ str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ str.rot13!.should == "nopqrstuvwxyzabcdefghijklmNOPQRSTUVWXYZABCDEFGHIJKLM"
+ end
+end
+
+
+
+
+
+describe Fixnum, "#ordinalize" do
+ it "should return custom order for 1, 2, 3" do
+ 1.ordinalize.should == "1st"
+ 2.ordinalize.should == "2nd"
+ 3.ordinalize.should == "3rd"
+ 4.ordinalize.should == "4th"
+ 102.ordinalize.should == "102th"
+ end
+
+ it "should return the correct order for numbers > 3" do
+ 4.ordinalize.should == "4th"
+ 102.ordinalize.should == "102th"
+ end
+end
+
+describe Fixnum, "#weeks" do
+ it "should return the number of second in a self week" do
+ 1.weeks.should == 604800
+ 0.weeks.should == 0
+ 100.weeks.should == 60480000
+ end
+end
+
+
+require 'time'
+describe Time, "#calendar" do
+ it "should return an easy readable string of the date" do
+ Time.local(2010, 8, 27).calendar.should == "August 27th, 2010"
+ Time.local(2001, 1, 1).calendar.should == "January 1st, 2001"
+ end
+end
+
+describe Time, "#calendar_with_time" do
+ it "should return an easy readable string of the date and time" do
+ Time.local(2010, 8, 27, 15, 28).calendar_with_time.
+ should == "August 27th, 2010 at 3:28 PM"
+ Time.local(2001, 1, 1, 0, 1).calendar_with_time.
+ should == "January 1st, 2001 at 12:01 AM"
+ end
+end
+
+describe Time, "#time_only" do
+ it "should return an easy readable string of the time" do
+ Time.local(2010, 1, 1, 0, 0).time_only.should == "12:00 AM"
+ Time.local(2010, 1, 1, 23, 59).time_only.should == "11:59 PM"
+ Time.local(2010, 1, 1, 13, 1).time_only.should == "1:01 PM"
+ end
+end
+
+describe Time, "#quick" do
+ it "should return a readable short string of the date and time" do
+ Time.local(2010, 8, 27, 15, 28).quick.should == "Aug 27, 2010 at 3:28pm"
+ Time.local(2001, 1, 1, 0, 1).quick.should == "Jan 1, 2001 at 12:01am"
+ end
+end
+
+describe Time, "#short" do
+ it "should return a short string of the date and time" do
+ Time.local(2010, 8, 27, 15, 28).short.should == "Aug 27"
+ Time.local(2001, 1, 1, 0, 1).short.should == "Jan 1"
+ end
+end
+
+describe Time, "#full" do
+ it "should return a complete string of the date and time" do
+ Time.local(2010, 8, 27, 15, 28).full.should == "2010-08-27 15:28:00"
+ Time.local(2001, 1, 1, 0, 1, 1).full.should == "2001-01-01 00:01:01"
+ end
+end
+
+describe Time, "#since" do
+ def now
+ Time.local(2010, 8, 27, 15, 28, 18)
+ end
+
+ it "should return friendly formatting for times less than a minute ago" do
+ # zero seconds
+ Time.local(2010, 8, 27, 15, 28, 18).since(now).should == "less than a minute"
+ # one second
+ Time.local(2010, 8, 27, 15, 28, 17).since(now).should == "less than a minute"
+ # 40 seconds
+ Time.local(2010, 8, 27, 15, 27, 38).since(now).should == "less than a minute"
+ # 58 seconds
+ Time.local(2010, 8, 27, 15, 27, 20).since(now).should == "1 minute"
+ end
+
+ context "with include_seconds" do
+ it "should return friendly formatting for times less than a minute ago" do
+ # zero seconds
+ Time.local(2010, 8, 27, 15, 28, 18).since(now, true).should == "less than 5 seconds"
+ # 4 second
+ Time.local(2010, 8, 27, 15, 28, 14).since(now, true).should == "less than 5 seconds"
+ # 5 second
+ Time.local(2010, 8, 27, 15, 28, 13).since(now, true).should == "less than 10 seconds"
+ # 9 second
+ Time.local(2010, 8, 27, 15, 28, 9).since(now, true).should == "less than 10 seconds"
+ # 10 second
+ Time.local(2010, 8, 27, 15, 28, 8).since(now, true).should == "less than 20 seconds"
+ # 19 second
+ Time.local(2010, 8, 27, 15, 27, 59).since(now, true).should == "less than 20 seconds"
+ # 20 second
+ Time.local(2010, 8, 27, 15, 27, 58).since(now, true).should == "half a minute"
+ # 39 seconds
+ Time.local(2010, 8, 27, 15, 27, 39).since(now, true).should == "half a minute"
+ # 40 seconds
+ Time.local(2010, 8, 27, 15, 27, 38).since(now, true).should == "less than a minute"
+ # 58 seconds
+ Time.local(2010, 8, 27, 15, 27, 20).since(now, true).should == "less than a minute"
+ # one minute
+ Time.local(2010, 8, 27, 15, 27, 18).since(now, true).should == "1 minute"
+ end
+ end
+
+ it "should return friendly formatting for times less than an hour ago" do
+ # one minute
+ Time.local(2010, 8, 27, 15, 27, 18).since(now).should == "1 minute"
+ # one minute and one second
+ Time.local(2010, 8, 27, 15, 27, 17).since(now).should == "1 minute"
+ # about 3 minutes
+ Time.local(2010, 8, 27, 15, 25, 22).since(now).should == "3 minutes"
+ Time.local(2010, 8, 27, 15, 25, 16).since(now).should == "3 minutes"
+ # about 45 minutes
+ Time.local(2010, 8, 27, 14, 43, 22).since(now).should == "45 minutes"
+ Time.local(2010, 8, 27, 14, 43, 16).since(now).should == "45 minutes"
+ end
+
+ it "should return friendly formatting for times less than a day ago" do
+ # about 46 minutes
+ Time.local(2010, 8, 27, 14, 42, 22).since(now).should == "about 1 hour"
+ Time.local(2010, 8, 27, 14, 42, 16).since(now).should == "about 1 hour"
+ # about 90 minutes
+ Time.local(2010, 8, 27, 13, 58, 22).since(now).should == "about 1 hour"
+ Time.local(2010, 8, 27, 13, 58, 16).since(now).should == "about 1 hour"
+ # about 91 minutes
+ Time.local(2010, 8, 27, 13, 57, 22).since(now).should == "about 2 hours"
+ # 24 hours
+ Time.local(2010, 8, 26, 15, 28, 18).since(now).should == "about 24 hours"
+ end
+
+ it "should return friendly formatting for times less than a year ago" do
+ # about 24 hours and one minute
+ Time.local(2010, 8, 26, 15, 27, 18).since(now).should == "1 day"
+ # about 1 day and 12 hours
+ Time.local(2010, 8, 26, 3, 28, 18).since(now).should == "1 day"
+ # almost 2 days
+ Time.local(2010, 8, 25, 15, 29, 18).since(now).should == "1 day"
+ # 2 days
+ Time.local(2010, 8, 25, 15, 28, 18).since(now).should == "2 days"
+ # 26 days
+ Time.local(2010, 8, 1, 15, 28, 18).since(now).should == "26 days"
+ # 1 day less than a day
+ Time.local(2009, 8, 28, 15, 28, 18).since(now).should == "364 days"
+ end
+
+ it "should return friendly formatting for times more than a year" do
+ Time.local(2009, 8, 27, 15, 28, 18).since(now).should == "1 years"
+ # one day less then 2 years
+ Time.local(2008, 8, 28, 15, 28, 18).since(now).should == "1 years"
+ # two years
+ Time.local(2008, 8, 27, 15, 28, 18).since(now).should == "2 years"
+ end
+
+ it "should use Time.now by default" do
+ now_time = Time.now
+ Time.local(2010, 8, 25, 15, 29, 18).since(now_time).should ==
+ Time.local(2010, 8, 25, 15, 29, 18).since
+ end
+end
+
+
+describe Thread, "#new" do
+ it "should execute the block argument" do
+ block_called = false
+ t = Thread.new do
+ block_called = true
+ end
+ t.join
+ block_called.should == true
+ end
+
+ it "should start a new thread" do
+ another_thread = false
+ topmost = Thread.current
+ t = Thread.new do
+ another_thread = true if Thread.current != topmost
+ end
+ t.join
+ another_thread.should == true
+ end
+
+ it "should pass the arguments to the block" do
+ Thread.new :arg1, 123 do |arg1, arg2|
+ arg1.should == :arg1
+ arg2.should == 123
+ end
+ end
+
+ # TODO: needs shoes
+ #it "should call error on exception"
+end
diff --git a/app/spec/events.rb b/app/spec/events.rb
new file mode 100755
index 0000000..89420a1
--- /dev/null
+++ b/app/spec/events.rb
@@ -0,0 +1,217 @@
+#!/usr/bin/env ruby
+
+require 'lib/dev/events'
+
+require 'spec/autorun'
+
+
+describe HH::EventConnection, "#event" do
+ it "should return the correct value" do
+ ec = HH::EventConnection.new(:my_event, :any)
+ ec.event.should == :my_event
+ end
+end
+
+describe HH::EventConnection, "#try" do
+ # auxiliary methods calls #try with arguments +args+ on a connection
+ # with condition +conds+
+ # it returns :successful on :unsuccessful
+ def try(conds, args)
+ result = :unsuccessful
+ conn = HH::EventConnection.new(:my_event, conds) do
+ result = :successful
+ end
+ conn.try args
+ result
+ end
+
+ it "should not succeed with condition of wrong size" do
+ try([], [1]).should == :unsuccessful
+ try([1], []).should == :unsuccessful
+ try([1, 1], [1]).should == :unsuccessful
+ try([1], [1, 1]).should == :unsuccessful
+ end
+
+ it "should succeed with no conditions" do
+ try([], []).should == :successful
+ end
+
+ it "should succeed with one correct condition" do
+ try([String], ["str"]).should == :successful
+ try([nil], [nil]).should == :successful
+ try([/^\d$/], ["4"]).should == :successful
+ end
+
+ it "should succeed with the :any condition" do
+ try([:any], [[1,2,3]]).should == :successful
+ end
+
+ it "should not succeed with one wrong condition" do
+ try([String], [4]).should == :unsuccessful
+ try([nil], [false]).should == :unsuccessful
+ try([/^\d$/], ["44"]).should == :unsuccessful
+ try([[1,2,3]], [:any]).should == :unsuccessful
+ end
+
+ it "should succeed with multiple correct conditions" do
+ cond = [String, nil, /^\d$/, :any]
+ args = ["str", nil, "4", [1, 2, 3]]
+ try(cond, args).should == :successful
+ end
+
+ it "should not succeed if at least one condition is wrong" do
+ cond = [Numeric, nil, /^\d$/, :any]
+ args = ["str", nil, "4", [1, 2, 3]]
+ try(cond, args).should == :unsuccessful
+ cond = [String, nil, /^\d$/, :wrong]
+ try(cond, args).should == :unsuccessful
+ end
+
+ context "using hash arguments" do
+ def try cond, args
+ super [cond], [args]
+ end
+
+ it "should succeed with no conditions" do
+ try({}, {}).should == :successful
+ try({}, {:a => 1, :b => 2}).should == :successful
+ end
+
+ it "should not succeed if the argument isn't an hash" do
+ no_hash = []
+ cond = {:a => nil, :something => 1234}
+ try(cond, no_hash).should == :unsuccessful
+ try({}, no_hash).should == :unsuccessful
+ end
+
+ it "should succeed with one correct condition" do
+ try({:first => String}, {:first => "str"}).should == :successful
+ try({:a => nil}, {:a => nil}).should == :successful
+ try({:str => /^\d$/}, {:str => "4"}).should == :successful
+ end
+
+ it "should not succeed with one wrong condition" do
+ try({:first => String}, {:first => 5}).should == :unsuccessful
+ try({:a => nil}, {:a => ""}).should == :unsuccessful
+ try({:str => /^\d$/}, {:str => "44"}).should == :unsuccessful
+ end
+
+ it "should succeed with multiple correct conditions" do
+ cond = {:a => String, :b => nil, :c => /^\d$/}
+ args = {:a => "str", :b => nil, :c => "4", :d => [1, 2, 3]}
+ try(cond, args).should == :successful
+ end
+
+ it "should not succeed with at least one wrong condition" do
+ cond = {:a => Numeric, :b => nil, :c => /^\d$/}
+ args = {:a => "str", :b => nil, :c => "4", :d => [1, 2, 3]}
+ try(cond, args).should == :unsuccessful
+ cond = {:a => String, :b => nil, :c => /^\d$/, :wrong => Array}
+ try(cond, args).should == :unsuccessful
+ end
+ end
+end
+
+
+
+
+describe HH::Observable, "#emit and #on_event" do
+ it "should work when there are no connections for an event" do
+ obj = Object.new
+ obj.extend HH::Observable
+ obj.emit :my_event, "arg1", :arg2
+ obj.emit :another_event, {:arg1 => "str", :arg2 => :sym}
+ end
+
+ it "should call try on all and only the correct connections" do
+ obj = Object.new
+ obj.extend HH::Observable
+
+ conn1_called = conn2_called = conn3_called = 0
+ obj.on_event :event1 do
+ conn1_called += 1
+ end
+ obj.on_event :event1, String do
+ conn2_called += 1
+ end
+ obj.on_event :event2 do
+ conne3_called += 1
+ end
+ obj.emit :event1 # conn1
+ obj.emit :event1 # conn1
+ obj.emit :event1 # conn1
+ obj.emit :event1, 123 # no connection
+ obj.emit :event1, 123 # no connection
+ obj.emit :event1, "str" # conn 2
+ conn1_called.should == 3
+ conn2_called.should == 1
+ conn3_called.should == 0
+ end
+end
+
+describe HH::Observable, "#delete_event_connection" do
+ it "should delete the event connection" do
+ obj = Object.new
+ obj.extend HH::Observable
+
+ conn1_called = conn2_called = conn3_called = 0
+ obj.on_event :event1 do
+ conn1_called += 1
+ end
+ conn2 = obj.on_event :event1, String do
+ conn2_called += 1
+ end
+ obj.on_event :event2 do
+ conne3_called += 1
+ end
+ obj.delete_event_connection conn2
+ obj.emit :event1 # conn1
+ obj.emit :event1 # conn1
+ obj.emit :event1 # conn1
+ obj.emit :event1, 123 # no connection
+ obj.emit :event1, 123 # no connection
+ obj.emit :event1, "str" # conn 2
+ conn1_called.should == 3
+ conn2_called.should == 0 # never called because deleted
+ conn3_called.should == 0
+ end
+end
+
+###### the following tests test the implementation so may be changed #####
+#describe HH::EventConnection, "::match_hash?" do
+# def call cond, hash
+# HH::EventConnection.match_hash? cond, hash
+# end
+#
+# it "should return true with an empty condition" do
+# empty_cond = {}
+# call(empty_cond, {}).should == true
+# call(empty_cond, {:something => 123, [] => nil}).should == true
+# end
+#
+# it "should return true with one correct condition" do
+# call({:first => String}, {:first => "str"}).should == true
+# call({:a => nil}, {:a => nil}).should == true
+# call({:str => /^\d$/}, {:str => "4"}).should == true
+# end
+#
+# it "should return false with one wrong condition" do
+# call({:first => String}, {:first => 5}).should == false
+# call({:a => nil}, {:a => ""}).should == false
+# call({:str => /^\d$/}, {:str => "44"}).should == false
+# end
+#
+# it "should return false if the argument isn't a hash" do
+# no_hash = []
+# cond = {nil => nil, :something => 1234}
+# call(cond, no_hash).should == false
+# call({}, no_hash).should == false
+# end
+#
+# it "should raise an ArgumentError if the condition isn't a hash" do
+# cond = [] # no hash
+# lambda {call(cond, [])}.should raise_error(ArgumentError)
+# lambda {call(cond, :something)}.should raise_error(ArgumentError)
+# end
+#end
+
diff --git a/app/spec/rspec.rb b/app/spec/rspec.rb
new file mode 100755
index 0000000..0f4e9d0
--- /dev/null
+++ b/app/spec/rspec.rb
@@ -0,0 +1,30 @@
+# a test trying to get rspec to run with Shoes
+
+Shoes.setup do
+ gem 'rspec'
+end
+
+require 'spec/autorun'
+
+describe String, "#reverse" do
+ it "should reverse the string" do
+ "abcd".reverse.should == "dcba"
+ end
+end
+
+# test to look if rspec with shoes is working
+#describe Shoes::App, "#style" do
+# it "should have correct default values" do
+# Shoes.app do
+# style[:cap].should == nil
+# style[:strokewidth].should == 1.0
+# end
+# end
+#end
+
+# exit loop
+Shoes.app do
+ timer(0.01) do
+ close
+ end
+end
diff --git a/app/spec/stdout.rb b/app/spec/stdout.rb
new file mode 100755
index 0000000..bffa90b
--- /dev/null
+++ b/app/spec/stdout.rb
@@ -0,0 +1,40 @@
+#!/usr/bin/env ruby
+
+require 'lib/dev/stdout'
+
+require 'spec/autorun'
+
+# XXX: dots will be doubled because they are written also as part of the test
+# I chose to display dots so that the spec execution output looks good
+describe STDOUT, "#write" do
+ it "should emit the :output signal" do
+ event_called = false
+ conn = STDOUT.on_event :output, :any do
+ event_called = true
+ end
+ STDOUT.write "."
+ STDOUT.delete_event_connection conn
+ event_called.should == true
+ end
+
+ it "should emit a signal with the correct argument" do
+ event_called = false
+ conn = STDOUT.on_event :output, :any do |arg|
+ event_called = true
+ arg.should == "."
+ end
+ STDOUT.write "."
+ STDOUT.delete_event_connection conn
+ event_called.should == true
+ end
+
+ it "should be called when using print" do
+ event_called = false
+ conn = STDOUT.on_event :output, :any do
+ event_called = true
+ end
+ print "."
+ STDOUT.delete_event_connection conn
+ event_called.should == true
+ end
+end
diff --git a/app/static/hacketyhack-dmg.jpg b/app/static/hacketyhack-dmg.jpg
new file mode 100644
index 0000000..32458eb
--- /dev/null
+++ b/app/static/hacketyhack-dmg.jpg
Binary files differ
diff --git a/app/static/hhabout.png b/app/static/hhabout.png
new file mode 100644
index 0000000..2a199c6
--- /dev/null
+++ b/app/static/hhabout.png
Binary files differ
diff --git a/app/static/hhcheat.png b/app/static/hhcheat.png
new file mode 100755
index 0000000..d87dcfd
--- /dev/null
+++ b/app/static/hhcheat.png
Binary files differ
diff --git a/app/static/hhconsole.png b/app/static/hhconsole.png
new file mode 100755
index 0000000..ed02d47
--- /dev/null
+++ b/app/static/hhconsole.png
Binary files differ
diff --git a/app/static/hhhello.png b/app/static/hhhello.png
new file mode 100644
index 0000000..1c0dec7
--- /dev/null
+++ b/app/static/hhhello.png
Binary files differ
diff --git a/app/static/icon-art.png b/app/static/icon-art.png
new file mode 100644
index 0000000..0bfecd5
--- /dev/null
+++ b/app/static/icon-art.png
Binary files differ
diff --git a/app/static/icon-dingbat.png b/app/static/icon-dingbat.png
new file mode 100644
index 0000000..4ef151a
--- /dev/null
+++ b/app/static/icon-dingbat.png
Binary files differ
diff --git a/app/static/icon-email.png b/app/static/icon-email.png
new file mode 100644
index 0000000..f233bc7
--- /dev/null
+++ b/app/static/icon-email.png
Binary files differ
diff --git a/app/static/icon-file.png b/app/static/icon-file.png
new file mode 100755
index 0000000..8b8b1ca
--- /dev/null
+++ b/app/static/icon-file.png
Binary files differ
diff --git a/app/static/icon-sound.png b/app/static/icon-sound.png
new file mode 100644
index 0000000..0ca9074
--- /dev/null
+++ b/app/static/icon-sound.png
Binary files differ
diff --git a/app/static/icon-table.png b/app/static/icon-table.png
new file mode 100644
index 0000000..693709c
--- /dev/null
+++ b/app/static/icon-table.png
Binary files differ
diff --git a/app/static/matz.jpg b/app/static/matz.jpg
new file mode 100644
index 0000000..ec1539d
--- /dev/null
+++ b/app/static/matz.jpg
Binary files differ
diff --git a/app/static/splash-hand.png b/app/static/splash-hand.png
new file mode 100644
index 0000000..35f8c39
--- /dev/null
+++ b/app/static/splash-hand.png
Binary files differ
diff --git a/app/static/tab-cheat.png b/app/static/tab-cheat.png
new file mode 100644
index 0000000..a09c62d
--- /dev/null
+++ b/app/static/tab-cheat.png
Binary files differ
diff --git a/app/static/tab-email.png b/app/static/tab-email.png
new file mode 100644
index 0000000..7348aed
--- /dev/null
+++ b/app/static/tab-email.png
Binary files differ
diff --git a/app/static/tab-hand.png b/app/static/tab-hand.png
new file mode 100644
index 0000000..5a9ee7a
--- /dev/null
+++ b/app/static/tab-hand.png
Binary files differ
diff --git a/app/static/tab-help.png b/app/static/tab-help.png
new file mode 100644
index 0000000..b7cbbff
--- /dev/null
+++ b/app/static/tab-help.png
Binary files differ
diff --git a/app/static/tab-home.png b/app/static/tab-home.png
new file mode 100644
index 0000000..fed6221
--- /dev/null
+++ b/app/static/tab-home.png
Binary files differ
diff --git a/app/static/tab-new.png b/app/static/tab-new.png
new file mode 100644
index 0000000..813f712
--- /dev/null
+++ b/app/static/tab-new.png
Binary files differ
diff --git a/app/static/tab-properties.png b/app/static/tab-properties.png
new file mode 100644
index 0000000..ab0e8ea
--- /dev/null
+++ b/app/static/tab-properties.png
Binary files differ
diff --git a/app/static/tab-quit.png b/app/static/tab-quit.png
new file mode 100644
index 0000000..2541d2b
--- /dev/null
+++ b/app/static/tab-quit.png
Binary files differ
diff --git a/app/static/tab-tour.png b/app/static/tab-tour.png
new file mode 100644
index 0000000..d22fde8
--- /dev/null
+++ b/app/static/tab-tour.png
Binary files differ
diff --git a/app/static/tab-try.png b/app/static/tab-try.png
new file mode 100644
index 0000000..b3d8ce0
--- /dev/null
+++ b/app/static/tab-try.png
Binary files differ
diff --git a/app/static/turtle.png b/app/static/turtle.png
new file mode 100644
index 0000000..3ad4844
--- /dev/null
+++ b/app/static/turtle.png
Binary files differ