diff --git a/.gitignore b/.gitignore index d8e4748..bb250ce 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *~ root/Home/* -test* -.* +*.swp +tags +.DS_Store diff --git a/.rvmrc b/.rvmrc new file mode 100644 index 0000000..1ae6ed0 --- /dev/null +++ b/.rvmrc @@ -0,0 +1 @@ +rvm --create 1.9.3-p429@hacketyhack diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..741fe56 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,6 @@ +language: ruby +rvm: + - 1.9.3 +notifications: + irc: "irc.freenode.org#hacketyhack" + diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..9b2242d --- /dev/null +++ b/Gemfile @@ -0,0 +1,7 @@ +source 'https://rubygems.org' + +gem "cucumber" +gem "hpricot" +gem "rspec" +gem "rake" +gem "sqlite3" diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..9e561da --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,34 @@ +GEM + remote: https://rubygems.org/ + specs: + builder (3.2.2) + cucumber (1.3.2) + builder (>= 2.1.2) + diff-lcs (>= 1.1.3) + gherkin (~> 2.12.0) + multi_json (~> 1.3) + diff-lcs (1.2.4) + gherkin (2.12.0) + multi_json (~> 1.3) + hpricot (0.8.6) + multi_json (1.7.6) + rake (12.3.3) + rspec (2.13.0) + rspec-core (~> 2.13.0) + rspec-expectations (~> 2.13.0) + rspec-mocks (~> 2.13.0) + rspec-core (2.13.1) + rspec-expectations (2.13.0) + diff-lcs (>= 1.1.3, < 2.0) + rspec-mocks (2.13.1) + sqlite3 (1.3.7) + +PLATFORMS + ruby + +DEPENDENCIES + cucumber + hpricot + rake + rspec + sqlite3 diff --git a/README.textile b/README.textile index 61bf8dc..383f03b 100644 --- a/README.textile +++ b/README.textile @@ -1,9 +1,17 @@ h1. Hackety Hack (for Mac OS X, Windows, and Linux) +!https://travis-ci.org/hacketyhack/hacketyhack.png?branch=master!:https://travis-ci.org/hacketyhack/hacketyhack + 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. Attention upstream issues! + +Hackty Hack highly depends on its upstream project shoes (which is what hacketyhack is written in). Right now shoes is undergoing a massiv rewrite with "shoes4":https://github.com/shoes/shoes4. One of the reasons for this rewrite are problems with the old shoes that sadly keep us from releasing a new version of Hackety Hack :'( + +So for this reason most people concentrate their effors on shoes4. If you want to help out (which is awesome, thank you!) that might also be a good option. Shoes4 is already quite far and many elements already work but Hackty Hack doesn't (yet). In the issues section here there also some tickets tagged with "shoes4 compatibility" you might also tackle those for a faster transition :-) + 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. diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..90df2ac --- /dev/null +++ b/Rakefile @@ -0,0 +1,12 @@ +require 'cucumber' +require 'cucumber/rake/task' +require 'rspec/core/rake_task' + +Cucumber::Rake::Task.new(:features) do |t| + t.cucumber_opts = "--format pretty" +end + +RSpec::Core::RakeTask.new(:spec) + +task :default => [:spec, :features] + diff --git a/app/boot.rb b/app/boot.rb index 40b98d4..ad4012c 100644 --- a/app/boot.rb +++ b/app/boot.rb @@ -15,8 +15,6 @@ def HH.anonymous_binding require 'lib/all' require 'app/syntax/markup' -require 'app/db/sequel' - require 'app/ui/lessons' require 'app/ui/widgets' require 'app/ui/completion' @@ -26,7 +24,7 @@ def HH.anonymous_binding if HH::PREFS['first_run'].nil? File.open(File.join(HH::USER, "Hello World.rb"), "w") do |f| f << 'alert "Hello, world!"' - end + end #the first_run pref will get set by the tour notice in app/ui/mainwindow end diff --git a/app/db/connection_pool.rb b/app/db/connection_pool.rb deleted file mode 100644 index 72e47e1..0000000 --- a/app/db/connection_pool.rb +++ /dev/null @@ -1,65 +0,0 @@ -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/db/core_ext.rb b/app/db/core_ext.rb deleted file mode 100644 index 81046a0..0000000 --- a/app/db/core_ext.rb +++ /dev/null @@ -1,9 +0,0 @@ -# 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/db/database.rb b/app/db/database.rb deleted file mode 100644 index ce5679d..0000000 --- a/app/db/database.rb +++ /dev/null @@ -1,119 +0,0 @@ -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/db/dataset.rb b/app/db/dataset.rb deleted file mode 100644 index ca653ab..0000000 --- a/app/db/dataset.rb +++ /dev/null @@ -1,354 +0,0 @@ -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/db/http.rb b/app/db/http.rb deleted file mode 100644 index 96bdf97..0000000 --- a/app/db/http.rb +++ /dev/null @@ -1,78 +0,0 @@ -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/db/model.rb b/app/db/model.rb deleted file mode 100644 index db1b2c9..0000000 --- a/app/db/model.rb +++ /dev/null @@ -1,235 +0,0 @@ - -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/db/schema.rb b/app/db/schema.rb deleted file mode 100644 index 3315385..0000000 --- a/app/db/schema.rb +++ /dev/null @@ -1,161 +0,0 @@ - -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/db/sequel.rb b/app/db/sequel.rb deleted file mode 100644 index ec23298..0000000 --- a/app/db/sequel.rb +++ /dev/null @@ -1,21 +0,0 @@ -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/db/sqlite.rb b/app/db/sqlite.rb deleted file mode 100644 index 2267c2f..0000000 --- a/app/db/sqlite.rb +++ /dev/null @@ -1,112 +0,0 @@ -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/db/table.rb b/app/db/table.rb deleted file mode 100644 index 25563a3..0000000 --- a/app/db/table.rb +++ /dev/null @@ -1,231 +0,0 @@ -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/ui/editor/editor.rb b/app/ui/editor/editor.rb index 88fd0f6..2f86592 100644 --- a/app/ui/editor/editor.rb +++ b/app/ui/editor/editor.rb @@ -1,140 +1,75 @@ -# the code editor tab contents +#TODO: If I get this far, check whether it works with green shoes +#TODO: Really figure out hit_sloppy +# the code editor tab contents +# the logic behind it is handled in the CodeEditor class +# this /should/ be pure presentation 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 + include HH::Markup - def self.on_insert_text &block - @@insert_text = block - end - def self.on_delete_text &block - @@delete_text = block - end + UNNAMED_PROGRAM = "An unnamed program" - # 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 + def content + draw_content 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 + def load script + unless saved? + name = @code_editor.name || UNNAMED_PROGRAM + unless 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 - 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 + def saved? + @save_button.hidden 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 empty? string + string.nil? or string.empty? + 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 + def choose_program_name + msg = "" + name = "" + while empty?(name) or HH.script_exists?(name) + name = ask(msg + "Give your program a name.") + msg = "Come on give your program a name :-)\n" if empty?(name) + msg = "You already have a program named '" + name + "'.\n" if HH.script_exists?(name) end + name end -end # module HH::Editor - -class HH::SideTabs::Editor - include HH::Markup - include UndoRedo - def content - draw_content + def save_program name + @sname.text = name + @code_editor.save name + @stale.text = "Last saved #{@code_editor.last_saved.since} ago." 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 + # saves the file, asks for a new name if a nil argument is passed + def save name + name = choose_program_name unless name + + if name + save_program name + true + else + false end - clear {draw_content script} - true end - # asks confirmation and then saves (or not if save is) + # asks confirmation and then saves (or not if cancel is pressed) def save_if_confirmed - if not @save_button.hidden - name = @script[:name] || "unnamed program" + unless saved? + name = @code_editor.name || UNNAMED_PROGRAM question = "I'm going to save modifications to \"#{name}\". Is that okay?\n" + "Press OK if it is, and cancel if it's not." if confirm(question) - save @script[:name] + save name true else false @@ -142,311 +77,371 @@ def save_if_confirmed 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, + def setup_editor(script) + @code_editor = HH::Editor::CodeEditor.new(script) + + # TODO: is there a way to work around this? + HH::Editor::InsertionDeletionCommand.on_insert_text {|pos, str| insert_text(pos, str)} + HH::Editor::InsertionDeletionCommand.on_delete_text {|pos, len| delete_text(pos, len)} + end + + def top_bar + stack :margin_left => 10, :margin_top => 10, :width => 1.0, :height => 92 do + progam_name = @code_editor.name || UNNAMED_PROGRAM + @sname = subtitle progam_name, :font => "Lacuna Regular", :size => 22, :margin => 0, :wrap => "trim" - @stale = para(script[:mtime] ? "Last saved #{script[:mtime].since} ago." : + @stale = para(@code_editor.last_saved ? "Last saved #{@code_editor.last_saved.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 + + def define_hit_sloppy + # some kind of cursor movement + def @code_para.hit_sloppy(x, y) + x -= 6 + c = hit(x, y) + if c + c + 1 + elsif x <= 48 + hit(48, y) end 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) do |response| - if response.status == 200 - alert("Uploaded!") - else - alert("There was a problem, sorry!") - end - end - end - glossb "Run", :width => 52, :top => 2, :left => 130 do - eval(@str, HH.anonymous_binding) - end + def line_numbering + stack :width => 37, :margin_right => 6 do + @line_numbers = para "1", :font => "Liberation Mono", :size => 10, + :stroke => "#777", :align => "right" end + end - every 20 do - if script[:mtime] - @stale.text = "Last saved #{script[:mtime].since} ago." - end + def editor_text + stack :width => -37, :margin_left => 6, :margin_bottom => 60 do + @code_para = para "", :font => "Liberation Mono", :size => 10, + :stroke => "#662", :wrap => "trim", + :margin_right => 28 + @code_para.cursor = 0 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 + define_hit_sloppy + 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] + # TODO long if jungle - but how to make it short/work around it + def mouse_motion + motion do |x, y| + c = @code_para.hit_sloppy(x, y) + if c + if self.cursor == :arrow + self.cursor = :text 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) + if self.mouse[0] == 1 and @clicked + if @code_para.marker.nil? + @code_para.marker = c else - horz = 1 + @code_para.cursor = c 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 + elsif self.cursor == :text + self.cursor = :arrow end + end + end + def mouse_release + release do + @clicked = false + end + end + + def mouse_click + click do |_, x, y| + c = @code_para.hit_sloppy(x, y) + if c + @clicked = true + @code_para.marker = nil + @code_para.cursor = c + end update_text end + end + + def mouse_leave + leave { self.cursor = :arrow } + end - spaces = [?\t, ?\s, ?\n] + def mouse_actions + mouse_motion + mouse_release + mouse_click + mouse_leave + end - 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 + def main_editor_window + 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 + line_numbering + editor_text + mouse_actions end + end + end + def copy_button + @copy_button = glossb "Copy", :width => 60, :top => 2, :left => 70 do + save(nil) end + end - # for samples do not allow to upload to cloud when just opened - @save_to_cloud_button.hide if script[:sample] - update_text + def save_button + @save_button = + glossb "Save", :width => 60, :top => 2, :left => 70, :hidden => true do + if save(@code_editor.name) + timer 0.1 do + @save_button.hide + @copy_button.show + @upload_button.show + end + end + end 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" + def upload_program + hacker = Hacker.new :username => HH::PREFS['username'], :password => HH::PREFS['password'] + hacker.save_program_to_the_cloud(@code_editor.name, @code_editor.script) do |response| + if response.code == "200" || response.code == "302" + alert("Uploaded!") + else + alert("There was a problem, sorry!") 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 + + def upload_button + @upload_button = + glossb "Upload", :width => 70, :top => 2, :left => 0 do + if HH::PREFS['username'] + upload_program + else + alert("To upload, first connect your account on hackety-hack.com by \ + clicking Preferences near the bottom left of the window.") + end + end + end + + def run_button + glossb "Run", :width => 52, :top => 2, :left => 130 do + @code_editor.run + end + end + + def bottom_menu + stack :height => 40, :width => 182, :bottom => -3, :right => 0 do + copy_button + save_button + upload_button + run_button + end + end + + def update_time + every 20 do + @stale.text = "Last saved #{@code_editor.last_saved.since} ago." if @code_editor.last_saved end end - + + # do this somewhere else? + def hide_upload_for_samples(script) + @upload_button.hide if script[:sample] + end + + def draw_content(script = {}) + setup_editor(script) + top_bar + main_editor_window + bottom_menu + update_time + on_keypress + hide_upload_for_samples(script) + update_text + end + def update_text - @t.replace *highlight(@str, @t.cursor) - @ln.replace [*1..(@str.count("\n")+1)].join("\n") + @code_para.replace *highlight(@code_editor.script, @code_para.cursor) + @line_numbers.replace [*1..(@code_editor.script.count("\n")+1)].join("\n") end def text_changed - if @save_button.hidden + if saved? @copy_button.hide @save_button.show - @save_to_cloud_button.hide + @upload_button.hide end end + # TODO: handle/inserts are awkward but somehow necessary for undo/redo to + # work, better figure out another way however + + # called when the user wants to insert text + def handle_text_insertion text + pos, len = @code_para.highlight + handle_text_deletion(pos, len) if len > 0 + + @code_editor.handle_text_insertion pos, text + insert_text pos, text + end + + # called when the user wants to delete text + def handle_text_deletion pos, len + @code_editor.handle_text_deletion pos, len + delete_text pos, len + end + + def insert_text pos, text + @code_editor.insert_text pos, text + @code_para.cursor = pos + text.size + @code_para.cursor = :marker # XXX ??? + end + + def delete_text pos, len + @code_editor.delete_text pos, len + @code_para.cursor = pos + @code_para.cursor = :marker + 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) + pos = @code_editor.script.rindex("\n", @code_para.cursor-1) return 0 if pos.nil? pos += 1 - ind_size = 0 - while @str[pos, 1] == ' ' + while @code_editor.script[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) + def on_keypress + keypress do |k| + onkey(k) + if @code_para.cursor_top < @scroll.scroll_top + @scroll.scroll_top = @code_para.cursor_top + elsif @code_para.cursor_top + 92 > @scroll.scroll_top + @scroll.height + @scroll.scroll_top = (@code_para.cursor_top + 92) - @scroll.height + end + end 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 + # TODO this delegates to @code_editor + # wouldn't it probably be better for the editor to have his own case? hm + def onkey(key) + case key + when :shift_home, :shift_end, :shift_up, :shift_left, :shift_down, :shift_right + @code_para.marker = @code_para.cursor unless @code_para.marker + when :home, :end, :up, :left, :down, :right + @code_para.marker = nil + end - def insert_text pos, text - @str.insert(pos, text) - @t.cursor = pos + text.size - @t.cursor = :marker # XXX ??? - #update_text - end + # keypress wilderness + case key + when String + if key == "\n" + # handle indentation + ind = indentation_size + handle_text_insertion(key) + handle_text_insertion(" " * ind) if ind > 0 + else + # usual case + handle_text_insertion(key) + end + when :backspace, :shift_backspace, :control_backspace + if @code_para.cursor > 0 and @code_para.marker.nil? + @code_para.marker = @code_para.cursor - 1 # make highlight length at least 1 + end + sel = @code_para.highlight + if sel[0] > 0 or sel[1] > 0 + handle_text_deletion(*sel) + end + when :delete + sel = @code_para.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 + @code_para.marker = 0 + @code_para.cursor = @code_editor.script.length + when :control_x, :alt_x + if @code_para.marker + sel = @code_para.highlight + self.clipboard = @code_editor.script[*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 @code_para.marker + self.clipboard = @code_editor.script[*@code_para.highlight] + end + when :control_v, :alt_v, :shift_insert + handle_text_insertion(self.clipboard) if self.clipboard + when :control_z + @code_editor.undo_command + when :control_y, :alt_Z, :shift_alt_z + @code_editor.redo_command + when :shift_home, :home + nl = @code_editor.script.rindex("\n", @code_para.cursor - 1) || -1 + @code_para.cursor = nl + 1 + when :shift_end, :end + nl = @code_editor.script.index("\n", @code_para.cursor) || @code_editor.script.length + @code_para.cursor = nl + when :shift_up, :up + if @code_para.cursor > 0 + nl = @code_editor.script.rindex("\n", @code_para.cursor - 1) + if nl + horz = @code_para.cursor - nl + upnl = @code_editor.script.rindex("\n", nl - 1) || -1 + @code_para.cursor = upnl + horz + @code_para.cursor = nl if @code_para.cursor > nl + end + end + when :shift_down, :down + nl = @code_editor.script.index("\n", @code_para.cursor) + if nl + if @code_para.cursor > 0 + horz = @code_para.cursor - (@code_editor.script.rindex("\n", @code_para.cursor - 1) || -1) + else + horz = 1 + end + dnl = @code_editor.script.index("\n", nl + 1) || @code_editor.script.length + @code_para.cursor = nl + horz + @code_para.cursor = dnl if @code_para.cursor > dnl + end + when :shift_right, :right + @code_para.cursor += 1 if @code_para.cursor < @code_editor.script.length + when :shift_left, :left + @code_para.cursor -= 1 if @code_para.cursor > 0 + end - def delete_text pos, len - @str[pos, len] = "" # TODO use slice? - @t.cursor = pos - @t.cursor = :marker - #update_text + if key + text_changed + end + update_text end + end + diff --git a/app/ui/mainwindow.rb b/app/ui/mainwindow.rb index b2dde5b..f91651a 100755 --- a/app/ui/mainwindow.rb +++ b/app/ui/mainwindow.rb @@ -41,7 +41,7 @@ def say arg def finalization # this method gets called on close - HH::LessonSet.close_lesson + HH::LessonSet.close_open_lesson gettab(:Editor).save_if_confirmed HH::PREFS['width'] = width @@ -69,7 +69,7 @@ def finalization 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" @@ -118,7 +118,7 @@ def finalization # splash screen stack :top => 0, :left => 0, :width => 1.0, :height => 1.0 do - splash + splash unless HH::PREFS['skip_intro'] if HH::PREFS['first_run'].nil? @tour_notice.toggle @tour_notice.click { @tour_notice.hide } @@ -127,5 +127,8 @@ def finalization end end - + on_event :tab_opened, :Lessons do + @tour_notice.hide + end + end diff --git a/app/ui/tabs/home.rb b/app/ui/tabs/home.rb index a9b6908..9e8572b 100644 --- a/app/ui/tabs/home.rb +++ b/app/ui/tabs/home.rb @@ -15,9 +15,9 @@ def initialize *args, &blk super *args, &blk # never changes so is most efficient to load here @samples = HH.samples - Upgrade::check_latest_version do |version| - if version['current_version'] != HH::VERSION - home_bulletin(version['current_version']) + if Web.internet_connection? + Upgrade::check_latest_version do |version| + home_bulletin(version['version']) if version['version'] != HH::VERSION end end end @@ -88,22 +88,6 @@ def delete script 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 @@ -143,7 +127,7 @@ def on_click def content image "#{HH::STATIC}/hhhello.png", :bottom => -120, :right => 0 - @tabs, @tables = [], HH::DB.tables + @tabs= [] @scripts = HH.scripts stack :margin => 0, :margin_left => 0 do stack do diff --git a/app/ui/tabs/lessons.rb b/app/ui/tabs/lessons.rb index 9a14af3..c878121 100644 --- a/app/ui/tabs/lessons.rb +++ b/app/ui/tabs/lessons.rb @@ -19,10 +19,16 @@ def content title "Lessons" @@lessons = [] Dir["#{HH::LESSONS}/*.rb"].each { |f| load f } - @@lessons.sort{|a, b| a[0] <=> b[0]}.each do |name, blk| + + %w[beginner intermediate advanced expert].each do |d| + @@difficulty = d.capitalize + Dir["#{HH::LESSONS}/#{d}/*.rb"].each { |f| load f } + end + + @@lessons.sort_by{|lesson| lesson[0]}.each do |lesson| stack do - britelink "icon-file.png", name do - HH::APP.start_lessons name, blk + britelink "icon-file.png", lesson[0] do + HH::APP.start_lessons lesson[0], lesson[1] end end end diff --git a/app/ui/tabs/prefs.rb b/app/ui/tabs/prefs.rb index 9c509b6..40a3f1e 100644 --- a/app/ui/tabs/prefs.rb +++ b/app/ui/tabs/prefs.rb @@ -36,23 +36,38 @@ def content 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" + + @logout_stack = stack do + para "Hello, #{user}! " + button "Log out", :width => 100 do + HH::PREFS['username'] = nil + HH::PREFS['password'] = nil + HH::save_prefs + + @question_stack.show + @logout_stack.hide + end + end + + @question_stack = stack do + para "You can connect with your account on ", + link("hackety-hack.com", :click => "http://hackety-hack.com"), + " to do all kinds of fun stuff. Do you have one?" + button "Yes" do + @question_stack.toggle + @prefpane.toggle + end + + button "No" do + @question_stack.toggle + @signup_stack.toggle + end + end + if user - para "Hello, #{user}! ", + @question_stack.hide else - @question_stack = stack do - para "You can connect with your account on ", - link("hackety-hack.com", :click => "http://hackety-hack.com"), - " to do all kinds of fun stuff. Do you have one?" - button "Yes" do - @question_stack.toggle - @prefpane.toggle - end - - button "No" do - @question_stack.toggle - @signup_stack.toggle - end - end + @logout_stack.hide end @prefpane = stack :margin => 20, :width => 400 do @@ -64,14 +79,16 @@ def content @pass = edit_line HH::PREFS['password'], :width => 1.0, :secret => true button "Save", :margin_top => 10 do - hacker = Hacker.new :username => @user.text, :password => @pass.text + hacker = Hacker.new :username => @user.text, :password => @pass.text hacker.auth_check do |response| - if response.status == 200 + if response.code == "200" || response.code == "302" HH::PREFS['username'] = @user.text HH::PREFS['password'] = @pass.text HH.save_prefs alert("Saved, thanks!") + @prefpane.hide + @logout_stack.show else alert("Sorry, I couldn't authenticate you. Did you sign up for an account at http://hackety-hack.com/ ? Please double check what you've typed.") end @@ -84,22 +101,22 @@ def content para "Website Account Signup", :size => :large para "Let's get you set up with one! All fields are required." para "Username:", :size => 10, :margin => 2, :stroke => "#352" - @user = edit_line "", :width => 1.0 + @user_signup = edit_line "", :width => 1.0 para "Email:", :size => 10, :margin => 2, :stroke => "#352" - @email = edit_line "", :width => 1.0 + @email_signup = edit_line "", :width => 1.0 para "Password", :size => 10, :margin => 2, :stroke => "#352" - @pass = edit_line "", :width => 1.0, :secret => true + @pass_signup = edit_line "", :width => 1.0, :secret => true button "Sign up", :margin_top => 10 do - hacker = Hacker.new :username => @user.text, :email => @email.text, :password => @pass.text + hacker = Hacker.new :username => @user_signup.text, :email => @email_signup.text, :password => @pass_signup.text hacker.sign_up! do |response| if response.status == 200 alert("Great! We've got you signed up.") - HH::PREFS['username'] = @user.text - HH::PREFS['password'] = @pass.text - HH::PREFS['email'] = @email.text + HH::PREFS['username'] = @user_signup.text + HH::PREFS['password'] = @pass_signup.text + HH::PREFS['email'] = @email_signup.text HH.save_prefs @signup_stack.toggle @prefpane.toggle @@ -112,6 +129,14 @@ def content end @signup_stack.toggle + stack do + flow do + @skip_intro = check {|me| HH::PREFS['skip_intro'] = me.checked?; HH::save_prefs } + @skip_intro.checked = HH::PREFS['skip_intro'] + para "Skip intro" + end + end end end end + diff --git a/app/ui/widgets.rb b/app/ui/widgets.rb index 624a87e..062662d 100644 --- a/app/ui/widgets.rb +++ b/app/ui/widgets.rb @@ -93,8 +93,7 @@ def create_tooltip slot = slot.parent end - @tooltip = slot.tooltip(@tooltip_text, x, y-20, - :fill => red, :stroke => white) + @tooltip = slot.tooltip(@tooltip_text, x, y-20, "#f00", :stroke => white) end def arrow_right @@ -123,18 +122,28 @@ def menu end module HH::Tooltip - def tooltip str, x, y, opts={} - f = nil + def tooltip str, x, y, bg="#f00", opts={} + s = nil #opts[:wrap] = "trim" slot = self app do - slot.append do - f = flow :left => x, :top => y do - para str, opts - end - end + slot.append do + s = stack :left => x, :top => y do + bg = background bg, :curve => 6, :height => 26, :width => 40 + p1 = nil + flow :width => 300 do + p1 = para str, opts + end + timer 0 do + bg.width = p1.width + if slot.width < s.left + bg.width + s.left -= s.left + bg.width - slot.width + end + end + end + end end - f + s end end diff --git a/features/hello_world.feature b/features/hello_world.feature new file mode 100644 index 0000000..c1110c6 --- /dev/null +++ b/features/hello_world.feature @@ -0,0 +1,9 @@ +Feature: Hello World + In order to make tests for Hackety + I need to get the harness bootstrapped + + Scenario: Test something silly + Given that things are installed + When I assert that true is true + Then I should get no errors + diff --git a/features/step_definitions/hello_steps.rb b/features/step_definitions/hello_steps.rb new file mode 100644 index 0000000..1268797 --- /dev/null +++ b/features/step_definitions/hello_steps.rb @@ -0,0 +1,12 @@ +Given /^that things are installed$/ do + +end + +When /^I assert that true is true$/ do + @result = true == true +end + +Then /^I should get no errors$/ do + @result.should == true +end + diff --git a/lessons/Better Guessing Game.rb b/lessons/Better Guessing Game.rb new file mode 100644 index 0000000..687cf5f --- /dev/null +++ b/lessons/Better Guessing Game.rb @@ -0,0 +1,157 @@ +# encoding: UTF-8 + +lesson_set "5: Better Guessing Game" do + + lesson "Hello There!" + page "Let's get started" do + para "Hey, did you like making that guessing game at the end of Beginner Ruby? Great! Let's make it BETTER. In this lesson we'll add ways to:" + + para "* Keep the game running until the player guesses correctly" + para "* Give the player some hints" + para "* Count the number of guesses" + para "* End the game after 10 tries" + para "* Create a random number, instead of just using 42" + + para "Let's do it!" + 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 + + page "How did it go again?" do + para "Here's the code from the original guessing game:" + + embed_code 'secret_number = 42.to_s +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' + + para "If you don't have this saved, type it in again and save it. Before we really get rolling, let's add something at the very top:" + embed_code 'guess = nil' + para "This just makes an guess equal nothing at all. It'll help out later, honest. Before we get going, see if you can guess how to add the features we want!" + end + + lesson "Yeah! New code!" + page "A little bit of logic" do + para "So we want to keep this game going. For how long? Well, as long as the player keeps guessing wrong, right? How do we say that the player is guessing wrong?" + embed_code 'guess != secret_number' + para "The ", code("!="), " means 'not equal to.' You can try it out in a new editor:" + embed_code 'alert 5 != 10 +alert 5 != 5' + para "This is the opposite of equals. Things like this are called comparisons. Here's a few more ways of comparing numbers:" + embed_code 'alert 5 > 10 +alert 5 < 10' + para "The first one means '5 is greater than 10' and the second means '5 is less than 10.' Which one comes back as true?" + end + + page "The While Loop: Ruby on Repeat!" do + para "The first thing we'll add to this game is a while loop. We call it a loop because it goes around and around. Check it out, it's easy!" + embed_code 'guess = nil +secret_number = 42.to_s +while guess != secret_number + 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' + para "Here's how you read the new code: 'While guess is not equal to secret_number... do everything until the end.' Run it and see! As long as you guess wrong it will keep asking you to guess. And that's what a while loop does!" + para "You might have noticed that ", code("guess = nil"), " is coming in handy. Without it, we're asking the computer a question about guess before we even mention it. Without that, the code breaks." + end + + page "Get a clue" do + para "Since the player can keep guessing, how about we help them out? Instead of just 'Sorry, try again', how about this?" + embed_code 'guess = nil +secret_number = 42.to_s +while guess != secret_number + guess = ask "I have a secret number. Can you guess it?" + if guess == secret_number + alert "Yes! You guessed right!" + elsif guess > secret_number + alert "Sorry, you guessed too high." + else + alert "Sorry, you guessed too low." + end +end' + para "Hey, look at that greater than sign coming back! Remember what that means?" + para "There's something else there too: ", code("elsif"), ". That's just a short way of saying ", code("else"), " and ", code("if"), " at the same time." + para "Run that code! Try numbers above and below 42. Now this is starting to look like a game, huh?" + end + + page "Counters and Breaks" do + para "This is fun and all, but most games have some kind of time limit right? Or maybe you only get a few lives. So how about we put the player under some pressure and end the game after 10 tries? The first step is counting the tries, which we can do like so:" + embed_code 'counter = 0 +guess = nil +secret_number = 42.to_s +while guess != secret_number + guess = ask "I have a secret number. Can you guess it?" + counter += 1 + if guess == secret_number + alert "Yes! You guessed right!" + elsif guess > secret_number + alert "Sorry, you guessed too high." + else + alert "Sorry, you guessed too low." + end +end' + para "You already know what this does! The counter starts at 0. Every time the player guesses, it goes up by 1. That's what ", code("+="), " does. What else can we do? Add this just before the last ", code("end"),":" + embed_code 'if counter == 10 + alert "You used 10 guesses. Please play again." + break +end' + para "Break is super cool! It let's you 'break out' from a loop!" + end + + page "It's the Final Countdown!" do + para "It would really add to the sense of urgency if the player knew how many tries were left. Let's put the counter in the alert!" + embed_code 'alert "Sorry, you guessed too high. Guesses used: " + counter.to_s + "/10"' + para "Do you remember what ", code("to_s"), " does? It makes the counter number a string. The plus signs let us stick a the counter variable in the middle of the string. When you do that, it's called concatenation." + para "You know what? You're doing so well, you can probably put the counter in the other alerts all on your own!" + end + + lesson "This is totally random" + page "The easiest refactor ever" do + para "This game is pretty lame once you know the number, right? We're going to 'refactor' it, which is a programming word for 'redo it and make it work better.'" + para "Try replacing ", code("42"), " with ", code("rand(100)") + para "This means 'pick a random number from 0-99.' The count starts at 0 because it's just a crazy thing that programmers tend to do. You can use any number in random, so that your little cousin can play 0-10, and you, being an obvious super-genius, can play 0-1000." + para "That's pretty much it! You can check out this game in the samples, and see if it matches yours!" + end + + lesson "Summary" + page "Awesome job!" do + para "Hey, that was kinda fun! And we learned some stuff, too!" + para "* ", code("While"), " loops" + para "* ", code("!="), " , ", code(">"), " and ", code("<"), " comparisons" + para "* counting with ", code("+=") + para "* ", code("rand()") + para "* ", code("break") + para "* Concatenation with ", code("+") + para "Way to go! Check out 'Data Structures' next, where you can start doing some of totally hardcore hacking!" + end + +end \ No newline at end of file diff --git a/lessons/array_methods.rb b/lessons/array_methods.rb new file mode 100644 index 0000000..3066348 --- /dev/null +++ b/lessons/array_methods.rb @@ -0,0 +1,77 @@ +lesson_set "Fun with Arrays" do + + lesson "Hello There!" + page "Let's get started" do + para "This lesson will give you a better handle on all the things you can" + "do with arrays. " + 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 + + page "What's an array again?" do + + para "An array is like a dresser where each drawer has a number.", + "The number is called a key, and the thing inside the drawer is called," + "a value." + + para "You can save an array in a variable, like this:" + + embed_code 'my_array = [1,99,"shoes",doggy, ["apple"","pear"]]' + + para "Arrays can hold all kinds of data! You get the items out by calling an object's key." + + embed_code 'alert my_array[0] returns 1, which is a number' + embed_code 'alert my_array[1] returns 2, which is also a number' + embed_code 'alert my_array[2] returns "shoes", which is a string' + embed_code 'alert my_array[3] returns doggy, which is a variable' + embed_code 'alert my_array[4] returns ["apple"","pear"], which is another array!' + + para "You can try typing in your own array and returning values until you get the hang of it." + end + + page "Adding on to arrays" do + + para "One of the cool things about Ruby as a programming language is that it makes it", + "really easy to work with arrays. Try this out:" + + embed_code 'zoo = ["lion","tiger"]' + embed_code 'alert zoo' + + para "We just made an array! Now lets say we want to put 'elephant' in the array.", + "We've already learned we can put it in the third position by doing this:" + + embed_code 'zoo[2] = "elephant"' + + para "But then we always have to know how many items are in the array. There's an", + "easier way: we can 'push' the new item into the array. Try it out:" + + embed_code 'zoo.push("lemur")' + embed_code 'alert zoo' + + para "There's even a shorter way to write this:" + + embed_code 'zoo << "gorilla"' + end +end diff --git a/lessons/beginner/datastrucs.rb b/lessons/beginner/datastrucs.rb new file mode 100644 index 0000000..c63ecfc --- /dev/null +++ b/lessons/beginner/datastrucs.rb @@ -0,0 +1,117 @@ +lesson_set "Beginner Data Structures" do + + lesson "Hello there!" + page "Let's get started" do + para "Welcome to your first lesson about Data Structures! Now that you ", + "know how to obtain data with Ruby you're also going to be able to store it in collections. ", + "Arrays and Hashes are two key classes in Ruby that are able to help with ", + " storing your data! 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 + + page "What are Arrays?" do + para "An array can be thought of as a dresser where each drawer is a numbered spot. This ", + " dresser is a collection of slots where one thing can be in each place. Arrays are ", + " used to store data like numbers, strings, or even other Arrays! Ruby makes it really easy ", + " to create and store things inside of them. Let's get going!" + end + + page "What can we do with them?" do + embed_code 'my_array = [1,2,"shoes"]' + para "Try typing this and press the Run button." + para "You created an array with the numbers 1, 2, and the string 'shoes' in it. Things in the array can be ", + "gotten by typing the array's variable name(my_array) and square brackets( [] ) with a number ", + "inside of the brackets. This number can be thought of as the address to that spot in the array. ", + "It's good to remember that arrays start at 0 in Ruby." + embed_code 'alert a[0]' + para "What thing in the array are you going to get back? Type this in and press the 'Run' button." + end + + page "Arrays in action!" do + para "When using Arrays we need to know a few things first. Arrays in Ruby", + " can expand to the size that you need them. So if you wanted to put the string", + " 'cat' at spot 99(which would be the 100th item in the array) we could put: " + embed_code 'my_array[99] = "cat"' + para "If there is nothing in a spot you will have 'nil' filling it." + para "If we wanted to print out everything in an array we could do something like this: " + embed_code 'my_new_array = ["cat","dog","mouse"]' + para "Then we would put: " + embed_code 'my_new_array.each {|animal| alert animal}' + para "Type this all in and press the 'Run' button." + end + + page "Arrays in even more action!" do + para "We've seen what we can do with arrays, but what other things can they do to help us?", + " What if we had an array of numbers and we wanted to sort it? Try typing this this and running it: " + embed_code 'num_array = [4,3,22,19,100,45]' + embed_code 'alert num_array.sort' + para "That was really easy to sort it from lowest to highest! What if we want it from highest to lowest though?", + " Type this in next and press the 'Run' button." + embed_code 'alert num_array.reverse' + para "The array class has so many methods that you can call upon it. ", + "Take a look on the Ruby API: http://www.ruby-doc.org/core/classes/Array.html" + end + +lesson "The Hash" + page "What are Hashes?" do + para "Now that we've gotten an introduction to Arrays we can also learn about Hashes!", + " Hashes in other languages are sometimes called Dictionaries. Well, what do they do?", + " Like in a dictionary you are able to look up a word or 'key' which corresponds to a 'value'.", + " You separate the key and value with a hashrocket (=>). Just like Arrays you can access a certain key ", + " by typing the hashes variable name and the key in square brackets ([]). ", + " Let's try working with a hash!" + embed_code 'my_hash = { "dog" => "puppy", "cat" => "kitten" }' + embed_code 'alert my_hash["dog"]' + para "Try typing this all in and press the 'Run' button!" + end + + page "Working with Hashes" do + para "So what else are we able to do with hashes? ", + "Let's try something will help us see if something is in the hash as a key or value", + " The methods 'has_key?' and 'has_value?' are exactly what we're looking for!" + embed_code 'new_hash = { "1" => "one", "2" => "two"}' + embed_code 'alert new_hash.has_key?("1")' + embed_code 'alert new_hash.has_value?("one")' + para "Try typing these in and press the 'Run' button for each of the methods!" + end + + page "Let's tie these Hashes and Arrays together!" do + para "We've looked at two different data structures that are able to hold data for us and let us access ", + "certain parts of the collections. Both Arrays and Hashes are commonly used by programmers and are great to ", + " have knowledge about! Both of these data structures have so many methods that can be read about in the ", + " Ruby Documentation: http://www.ruby-doc.org/core/. Now with Arrays and Hashes you should be able to ", + "keep your data organized and usable!" + end + +end diff --git a/lessons/intermediate/data_types.rb b/lessons/intermediate/data_types.rb new file mode 100644 index 0000000..dea55c3 --- /dev/null +++ b/lessons/intermediate/data_types.rb @@ -0,0 +1,139 @@ +# encoding: UTF-8 + +lesson_set "Data Types" do + + lesson "This is where it starts getting fun!" + page "Let's get started" do + para "Awesome! Glad to see you here! You're going to learn some great ", + "things in this lesson. Ruby has a few different ways to manipulate ", + "data, and you're going to play with them!" + 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 "Strings" + page "You've got this down!" do + para em("String"), "s are something you already know about! Let's ", + "refresh your memory." + para "Basically, Strings let you manipulate a bunch of characters. It's ", + "sort of like writing something down: and often, Strings are used ", + "for handling input and output. Check it out:" + embed_code 'name = "Steve"' + "\n" + 'alert name' + para "This should be familliar. If it isn't you may want to go review ", + "the Basic Ruby lesson before moving on. Gotta learn the basics ", + "before you can understand the hard stuff!" + end + + page "Concatawhat?" do + para "Here's a big word for you: ", strong("concatenation"), ". It's a ", + "mouthful, but luckily for you, it means something really simple: " + "addition. Check this code out:" + embed_code 'first_name = "Steve"' + "\n" + + 'last_name = "Klabnik"' + "\n" + + 'alert first_name + last_name' + para "See what I mean by addition? The ", em("+"), " lets us ", + strong("concatenate"), " the two Strings together. The first name ", + "goes up front, and the last name goes in the back. Nice and easy." + end + + page "Interpawho?" do + para "Okay, since you did so well with that word, I'm going to throw ", + "another one at you, while you're still trying to recover: ", + strong("interpolation"), ". It kinda means 'put into.' See if this ", + "makes sense:" + embed_code 'first_name = "Steve"' + "\n" + + 'alert "The first name is #{first_name}".' + para "Whoah! What's up with that? Try running it, and see what it does." + end + + page "They're like pincers" do + para "Terrible analogy alert: See that { and its partner in crime, }? ", + "These two ", em("curly braces"), " are like the pincers of some ", + "strange species of crab. You can put whatever you want between ", + "them, and they hold your info in place in the middle of a string." + para "" + para "Oh, and the # (a ", em("hash"), "), is a funky hat the crab wears. ", + "Or something. I dunno. Point is, you need all three parts, \#{} ", + "and something in the middle. And that's ", strong("interpolation"), + "." + end + + lesson "Arrays" + page "The 411" do + para em("Ruby"), " was created by " + end + + page "Concatination" do + + end + + page "A short shout-out to Modules" do + + end + + page "Not a treasure map..." do + + end + + page "Gotta collect 'em all!" do + + end + + lesson "Hashes" + page "A slightly different Array" do + para em("Ruby"), " was created by " + end + + lesson "Putting them together" + page "Arrays of Arrays" do + + end + + page "Arrays of hashes" do + + end + + page "Hashes of hashes" do + + end + + page "Hashes of Arrays" do + + end + + lesson "Summary" + page "Good job!" do + para "Awesome! You should be prepared to play around with all kinds of ", + "data now. Keep up all the good work!" + end + +end diff --git a/lib/all.rb b/lib/all.rb index 7b8f9bd..76252b5 100644 --- a/lib/all.rb +++ b/lib/all.rb @@ -1,9 +1,18 @@ -require 'lib/web/all' -require 'lib/dev/init' +require 'lib/database' +require 'lib/preferences' -require 'lib/art/turtle' -require 'lib/enhancements' +require 'lib/dev/init' require 'lib/dev/errors' require 'lib/dev/events' require 'lib/dev/stdout' +module HH + PREFS = Preferences.new +end + +require 'lib/web/all' +require 'lib/editor/all' + +require 'lib/art/turtle' +require 'lib/enhancements' + diff --git a/lib/art/turtle.rb b/lib/art/turtle.rb index f4a7690..ddaaeaf 100644 --- a/lib/art/turtle.rb +++ b/lib/art/turtle.rb @@ -7,7 +7,7 @@ class Shoes::TurtleCanvas < Shoes::Widget WIDTH = 500 HEIGHT = 500 SPEED = 4 # power of two - + include Math DEG = PI / 180.0 @@ -169,7 +169,7 @@ def toggle_pause end @paused # return value end - + def save filename _snapshot :filename => filename, :format => :pdf end @@ -278,7 +278,7 @@ def self.start opts={}, &blk end end end - + if opts[:draw] draw_all else diff --git a/lib/database.rb b/lib/database.rb new file mode 100644 index 0000000..024f33a --- /dev/null +++ b/lib/database.rb @@ -0,0 +1,36 @@ +require 'sqlite3' + +module HH + class Database + def initialize(database=nil) + @database = database || SQLite3::Database.new(File.join(HH::USER, "db.sqlite3")) + end + + def save(table, data) + sql = %Q{CREATE TABLE IF NOT EXISTS #{table} (key text UNIQUE, value text)} + @database.execute(sql) + + data.each do |key, value| + sql = if value.nil? || value == false + %Q{DELETE FROM #{table} WHERE key = "#{key}"} + else + %Q{INSERT OR REPLACE INTO #{table} (key,value) VALUES ("#{key}", "#{value}")} + end + @database.execute(sql) + end + end + + def load(table) + sql = %Q{CREATE TABLE IF NOT EXISTS #{table} (key text UNIQUE, value text)} + @database.execute(sql) + + rows = @database.execute("SELECT * FROM #{table}") + + {}.tap do |result| + rows.each do |row| + result[row[0].to_sym] = row[1] + end + end + end + end +end diff --git a/lib/dev/init.rb b/lib/dev/init.rb index 296c32f..8406f31 100644 --- a/lib/dev/init.rb +++ b/lib/dev/init.rb @@ -4,9 +4,13 @@ HH::NET = "hackety-hack.com" HH::REST = "http://hackety-hack.com" +HH::API_ROOT = "http://api.hackety-hack.com" + #for easy switching when developing -#HH::NET = "localhost:9292" -#HH::REST = "http://localhost:9292" +#HH::NET = "api.hackety-hack.com.dev" +#HH::REST = "http://api.hackety-hack.com.dev" +#HH::API_ROOT = "http://api.hackety-hack.com.dev" + HH::HOME = Dir.pwd HH::STATIC = HH::HOME + "/static" HH::FONTS = HH::HOME + "/fonts" @@ -21,7 +25,7 @@ ENV['MYDOCUMENTS'] = HH.read_shell_folder('Personal') ENV['APPDATA'] = HH.read_shell_folder('AppData') ENV['DESKTOP'] = HH.read_shell_folder('Desktop') - HH::USER = + HH::USER = begin HH.win_path(Win32::Registry::HKEY_CURRENT_USER. open('Software\Hackety.org\Hackety Hack'). diff --git a/lib/editor/all.rb b/lib/editor/all.rb new file mode 100644 index 0000000..afb84cd --- /dev/null +++ b/lib/editor/all.rb @@ -0,0 +1,3 @@ +require 'lib/editor/commands' +require 'lib/editor/undo_redo' +require 'lib/editor/code_editor' diff --git a/lib/editor/code_editor.rb b/lib/editor/code_editor.rb new file mode 100644 index 0000000..64118d6 --- /dev/null +++ b/lib/editor/code_editor.rb @@ -0,0 +1,48 @@ +module HH::Editor + + class CodeEditor + include UndoRedo + + attr_reader :script, :name, :last_saved, :description + + def initialize script + @script = script[:script] || "" + @name = script[:name] + @last_saved = script[:mtime] + @description = script[:desc] + reset_undo_redo + end + + # TODO: handle/inserts are awkward but somehow necessary for undo/redo to + # work, better figure out another way however + def handle_text_insertion pos, text + add_command InsertionCommand.new(pos, text) + end + + def insert_text pos, text + @script.insert(pos, text) + end + + def handle_text_deletion pos, len + text = @script[pos, len] + add_command DeletionCommand.new(pos, text) unless text.empty? + end + + def delete_text pos, len + @script[pos, len] = "" # TODO use slice? + end + + def save name + @name = name + HH.save_script(@name, @script) + @last_saved = Time.now + end + + def run + eval(script, HH.anonymous_binding) + end + + end + +end + diff --git a/lib/editor/commands.rb b/lib/editor/commands.rb new file mode 100644 index 0000000..9c387c1 --- /dev/null +++ b/lib/editor/commands.rb @@ -0,0 +1,81 @@ +# the different commands for insertion/deletion + +module HH::Editor + # common code between InsertionAction and DeletionAction + # on_insert_text and on_delete_text should be called before any subclass + # can be used + # TODO: weirdly generic... isn't there a better solution? + # (it needs actions that are not defined in this class itself) + 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, text + @position, @string = pos, text + 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 + +end + diff --git a/lib/editor/undo_redo.rb b/lib/editor/undo_redo.rb new file mode 100644 index 0000000..ef3bc2e --- /dev/null +++ b/lib/editor/undo_redo.rb @@ -0,0 +1,37 @@ +module HH::Editor + + module UndoRedo + + def reset_undo_redo + @command_stack = [] # array of actions + @stack_position = 0; + @last_position = nil + end + + def undo_command + return if @stack_position == 0 + @stack_position -= 1; + @command_stack[@stack_position].unexecute; + end + + 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 + diff --git a/lib/enhancements.rb b/lib/enhancements.rb index 5ce6e1e..dcd293e 100644 --- a/lib/enhancements.rb +++ b/lib/enhancements.rb @@ -64,7 +64,7 @@ def rot13! # -# = Numbers +# = Numbers # # Enhancements to the basic number classes. # @@ -141,18 +141,3 @@ def since(new_time = Time.now, include_seconds = false) 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/lib/preferences.rb b/lib/preferences.rb new file mode 100644 index 0000000..cfcf646 --- /dev/null +++ b/lib/preferences.rb @@ -0,0 +1,27 @@ +require_relative 'database' + +module HH + class Preferences + def initialize + begin + @target = Database.new.load("preferences") + rescue + @target = {} + end + end + + def method_missing(meth, *args, &blk) + if meth == :[] || meth == :[]= + #convert hash key to symbol + args[0] = args[0].to_sym + end + + @target.send(meth, *args, &blk) + end + + def save + Database.new.save("preferences", @target) + end + end +end + diff --git a/lib/web/all.rb b/lib/web/all.rb index 9960591..a374e1f 100644 --- a/lib/web/all.rb +++ b/lib/web/all.rb @@ -1,3 +1,4 @@ require 'lib/web/hacker' require 'lib/web/web' require 'lib/web/version' +require 'lib/web/api' diff --git a/lib/web/api.rb b/lib/web/api.rb new file mode 100644 index 0000000..02264c8 --- /dev/null +++ b/lib/web/api.rb @@ -0,0 +1,31 @@ +module HH::API + @root = nil + @refresh_time = nil + + class << self + def root + if !@refresh_time || (Time.now.to_i - @refresh_time.to_i) > 3600 + @refresh_time = Time.now + @root = get('/') { |f| Hpricot(f.body) } + else + @root + end + end + + def get(path) + url = URI.parse(HH::API_ROOT + path) + response = Net::HTTP.get_response(url) + + yield response + end + + def post(path, params) + url = URI.parse(HH::API_ROOT + path) + response = Net::HTTP.post_form(url, params) + + yield response + end + + end + +end diff --git a/lib/web/hacker.rb b/lib/web/hacker.rb index 05c7e8f..a0097c8 100644 --- a/lib/web/hacker.rb +++ b/lib/web/hacker.rb @@ -1,13 +1,13 @@ # website integration -require 'lib/web/yaml' +require 'lib/web/api' def Hacker name Hacker.new name end class Hacker - include HH::YAML + include HH::API attr :name attr :password @@ -28,26 +28,91 @@ def channel(title) end def program_list &blk - http('GET', "/programs/#{@name}.json", :username => @name, :password => @password, &blk) + programs_rel = HH::API.root.at("//a[@rel='/rels/program-index']") + HH::API.get(program_rel.attributes['href'], &blk) end def auth_check &blk - http('POST', "/check_credentials", {:username => @name, :password => @password}) do |result| - blk[result.response] + # Hacking around CSRF, This will go away when we switch to http digest auth + sign_in = HH::API.get('/users/sign_in') { |f| Hpricot(f.body)} + csrf = sign_in.at("//input[@name='authenticity_token']").attributes['value'] + + HH::API.post("/users/sign_in", {"authenticity_token" => csrf, "user[username]" => @name, "user[password]" => @password, "user[remember_me]" => 1}) do |response| + blk[response] end end def sign_up! &blk - http('POST', "/signup_via_api", {:username => @name, :email => @email, :password => @password}) do |result| - blk[result.response] + # Hacking around CSRF, This will go away when we switch to http digest auth + sign_in = HH::API.get('/users/sign_up') { |f| Hpricot(f.body)} + csrf = sign_in.at("//input[@name='authenticity_token']").attributes['value'] + + HH::API.post("/users", {"authenticity_token" => csrf, "user[username]" => @name, "user[email]" => @email, "user[password]" => @password, "user[password_conformation]" => @password}) do |response| + blk[response] end end def save_program_to_the_cloud name, code, &blk - url = "/programs/#{@name}/#{name}.json" - http('PUT', url, {:creator_username => @name, :title => name, :code => code, :username => @name, :password => @password}) do |result| - blk[result.response] + # Hacking around CSRF, This will go away when we switch to http digest auth + program_rel = HH::API.root.at("//a[@rel='/rels/program-new']") + new_program = HH::API.get(program_rel.attributes['href']){ |f| Hpricot(f.body) } + csrf = new_program.at("//meta[@name='csrf-token']") + form = new_program.at("//form") + HH::API.post(form.attributes['action'], {"authenticity_token" => csrf, "program[author_username]" => @name, "program[title]" => name, "program[source_code]" => code}) do |response| + blk[response] end end +end + +# I feel like these belong in Hacker. Trying to not do too much at once +module HH + class << self + def scripts + Dir["#{HH::USER}/*.rb"].map { |path| get_script(path) }. + sort_by { |script| Time.now - script[:mtime] } + 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 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 save_prefs + HH::PREFS.save + 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 + + end end + diff --git a/lib/web/version.rb b/lib/web/version.rb index 40133dd..5db621e 100644 --- a/lib/web/version.rb +++ b/lib/web/version.rb @@ -1,13 +1,18 @@ -require 'lib/web/yaml' +require 'lib/web/api' +require 'net/http' module Upgrade class << self - include HH::YAML + include HH::API def check_latest_version &blk - http('GET', "/version.json") do |result| - response = JSON.parse(result.response.body) - blk[response] + root = HH::API.root + return unless root + + version_rel = root.at("//a[@rel='/rels/current-application-version']") + HH::API.get(version_rel.attributes['href']) do |response| + body = JSON.parse(response.body) + blk[body] end end end diff --git a/lib/web/web.rb b/lib/web/web.rb index 38c148a..7ddadde 100644 --- a/lib/web/web.rb +++ b/lib/web/web.rb @@ -9,6 +9,22 @@ module Web [JSON_MIME_TYPES, XML_MIME_TYPES].each do |ary| ary.map! { |str| /^#{Regexp::quote(str)}/ } end + + # checking for an internet connection to deactivate functionality, requiring + # an internet connection when there is no connection or the API is down + def self.check_internet_connection + begin + HH::API.get "" do |response| return response.kind_of? Net::HTTPOK end + rescue + return false + end + end + + # caching the result so we don't have to do a new request for each check + def self.internet_connection? + @connection ||= check_internet_connection + end + end module Hpricot diff --git a/lib/web/yaml.rb b/lib/web/yaml.rb deleted file mode 100644 index c879460..0000000 --- a/lib/web/yaml.rb +++ /dev/null @@ -1,60 +0,0 @@ -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[dl] 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/samples/Guessing Game.rb b/samples/Guessing Game.rb new file mode 100644 index 0000000..d160be5 --- /dev/null +++ b/samples/Guessing Game.rb @@ -0,0 +1,18 @@ +counter = 0 +guess = nil +secret_number = rand(100).to_s +while guess != secret_number + guess = ask "I have a secret number. Can you guess it?" + counter += 1 + if guess == secret_number + alert "Yes! You guessed right in just "+counter.to_s+" guesses." + elsif guess > secret_number + alert "Sorry, you guessed too high. Guesses used: "+counter.to_s+"/10" + else + alert "Sorry, you guessed too low. Guesses used: "+counter.to_s+"/10" + end + if counter == 10 + alert "You used 10 guesses. Please play again." + break + end +end \ No newline at end of file diff --git a/spec/all.rb b/spec/all.rb deleted file mode 100755 index f08b73b..0000000 --- a/spec/all.rb +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env ruby - -require 'spec/enhancements' -require 'spec/events' -require 'spec/stdout' \ No newline at end of file diff --git a/spec/enhancements.rb b/spec/enhancements.rb deleted file mode 100755 index 8d7b533..0000000 --- a/spec/enhancements.rb +++ /dev/null @@ -1,452 +0,0 @@ -#!/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/spec/events.rb b/spec/events.rb deleted file mode 100755 index 89420a1..0000000 --- a/spec/events.rb +++ /dev/null @@ -1,217 +0,0 @@ -#!/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/spec/lib/database_spec.rb b/spec/lib/database_spec.rb new file mode 100644 index 0000000..6b0bb18 --- /dev/null +++ b/spec/lib/database_spec.rb @@ -0,0 +1,19 @@ +require_relative '../../lib/database' + +describe HH::Database do + context ".save" do + it "inserts on each new key" do + data = {:key => :value} + db = HH::Database.new(double(:execute => [])) + db.save("names", data) + end + end + + context ".load" do + it "returns a proper hash" do + db = HH::Database.new(double(:execute => [ ["key1", "value1"], ["key2", "value2"] ])) + + db.load("names").should eql({:key1 => "value1", :key2 => "value2"}) + end + end +end diff --git a/spec/lib/preferences_spec.rb b/spec/lib/preferences_spec.rb new file mode 100644 index 0000000..1f39160 --- /dev/null +++ b/spec/lib/preferences_spec.rb @@ -0,0 +1,13 @@ +require_relative '../../lib/preferences' + +describe HH::Preferences do + it "behaves like a hash" do + subject[:key] = :value + subject[:key].should eql(:value) + end + + it "is able to be saved" do + HH::Database.should_receive(:new).twice.and_return(stub(:load => [], :save => true)) + subject.save + end +end diff --git a/spec/rspec.rb b/spec/rspec.rb deleted file mode 100755 index 0f4e9d0..0000000 --- a/spec/rspec.rb +++ /dev/null @@ -1,30 +0,0 @@ -# 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/spec/stdout.rb b/spec/stdout.rb deleted file mode 100755 index bffa90b..0000000 --- a/spec/stdout.rb +++ /dev/null @@ -1,40 +0,0 @@ -#!/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 pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy