commit 4d3744ff43430bdae9dc4b55264912320872fa9a Author: Thibaud Guillaume-Gentil Date: Sun Oct 3 23:00:33 2010 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..73a47c6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +pkg/* +*.gem +.bundle + +## MAC OS +.DS_Store +.Trashes +.com.apple.timemachine.supported +.fseventsd +Desktop DB +Desktop DF \ No newline at end of file diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..c80ee36 --- /dev/null +++ b/Gemfile @@ -0,0 +1,3 @@ +source "http://rubygems.org" + +gemspec diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..7b5c9d4 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,45 @@ +PATH + remote: . + specs: + guard (0.1.0.beta.1) + growl (~> 1.0.3) + libnotify (~> 0.1.3) + rb-inotify + sys-uname (~> 0.8.4) + thor (~> 0.14.2) + +GEM + remote: http://rubygems.org/ + specs: + diff-lcs (1.1.2) + ffi (0.6.3) + rake (>= 0.8.7) + growl (1.0.3) + libnotify (0.1.4) + ffi (>= 0.6.2) + rake (0.8.7) + rb-inotify (0.8.1) + ffi (>= 0.5.0) + rspec (2.0.0.beta.19) + rspec-core (= 2.0.0.beta.19) + rspec-expectations (= 2.0.0.beta.19) + rspec-mocks (= 2.0.0.beta.19) + rspec-core (2.0.0.beta.19) + rspec-expectations (2.0.0.beta.19) + diff-lcs (>= 1.1.2) + rspec-mocks (2.0.0.beta.19) + sys-uname (0.8.4) + thor (0.14.2) + +PLATFORMS + ruby + +DEPENDENCIES + bundler (~> 1.0.1) + growl (~> 1.0.3) + guard! + libnotify (~> 0.1.3) + rb-inotify + rspec (~> 2.0.0.beta.22) + sys-uname (~> 0.8.4) + thor (~> 0.14.2) diff --git a/Guardfile b/Guardfile new file mode 100644 index 0000000..6757c85 --- /dev/null +++ b/Guardfile @@ -0,0 +1,25 @@ +# guard 'spork', :rspec_port => 9010 do +# watch('^config/initializers/.*') +# end + +def super + `say yo` +end + +guard 'rspec', :version => 2 do + watch('^spec/(.*)_spec.rb') + watch('^lib/(.*).rb') { |m| "spec/#{m[1]}_spec.rb" } + watch('^spec/spec_helper.rb') { "spec" } + # watch('^spec/spec_helper.rb') { `say hello` } + # watch('^spec/(.*)_spec\.rb') + # watch('^app/(.*)\.rb') { |m| "spec/#{m[1]}_spec.rb" } + # watch('^app/(.*)\.html.erb') { |m| "spec/#{m[1]}_spec.rb" } + # watch('^lib/(.*)\.rb') { |m| "spec/lib/#{m[1]}_spec.rb" } + # watch('^spec/spec_helper\.rb') { |m| "spec" } + # watch('^config/routes\.rb') { |m| "spec/routing" } + # watch('^spec/factories\.rb') { |m| "spec/model" } + # watch('^app/controllers/application_controller\.rb') { |m| "spec/controllers" } +end + +# guard 'livereload' do +# end \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f38f46d --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2010 Thibaud Guillaume-Gentil + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.rdoc b/README.rdoc new file mode 100644 index 0000000..56b29c9 --- /dev/null +++ b/README.rdoc @@ -0,0 +1,3 @@ += Guard + +Documentation is coming. \ No newline at end of file diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..4b3388f --- /dev/null +++ b/Rakefile @@ -0,0 +1,6 @@ +require 'bundler' +Bundler::GemHelper.install_tasks + +require 'rspec/core/rake_task' +RSpec::Core::RakeTask.new(:spec) +task :default => :spec \ No newline at end of file diff --git a/bin/guard b/bin/guard new file mode 100755 index 0000000..d30b664 --- /dev/null +++ b/bin/guard @@ -0,0 +1,6 @@ +#!/usr/bin/env ruby + +require 'guard' +require 'guard/cli' + +Guard::CLI.start \ No newline at end of file diff --git a/bin/inotify_watch b/bin/inotify_watch new file mode 100755 index 0000000..65b6eb7 --- /dev/null +++ b/bin/inotify_watch @@ -0,0 +1,25 @@ +#!/usr/bin/env ruby +require 'rubygems' +require 'rb-inotify' + +folders = Array.new +notifier = INotify::Notifier.new + +notifier.watch(ARGV.first || '.', :modify, :recursive) do |event| + dir = File.expand_path(File.dirname(event.absolute_name)) + '/' + if !folders.include?(dir) + folders << dir + end +end + +while true do + notifier.process + + if !folders.empty? + $stdout.puts folders.join(' ') + $stdout.flush + folders.clear + end + + sleep(0.1) +end \ No newline at end of file diff --git a/ext/extconf.rb b/ext/extconf.rb new file mode 100644 index 0000000..cdadaa7 --- /dev/null +++ b/ext/extconf.rb @@ -0,0 +1,18 @@ +# Workaround to make Rubygems believe it builds a native gem +require 'mkmf' +create_makefile('none') + +if `uname -s`.chomp == 'Darwin' + gem_root = File.expand_path(File.join('..')) + darwin_verion = `uname -r`.to_i + sdk_verion = { 9 => '10.5', 10 => '10.6', 11 => '10.7' }[darwin_verion] + + raise "Darwin #{darwin_verion} is not supported" unless sdk_verion + + # Compile the actual fsevent_watch binary + system("CFLAGS='-isysroot /Developer/SDKs/MacOSX#{sdk_verion}.sdk -mmacosx-version-min=#{sdk_verion}' /usr/bin/gcc -framework CoreServices -o '#{gem_root}/bin/fsevent_watch' fsevent/fsevent_watch.c") + + unless File.executable?("#{gem_root}/bin/fsevent_watch") + raise "Compilation of fsevent_watch failed (see README)" + end +end diff --git a/ext/fsevent/fsevent_watch.c b/ext/fsevent/fsevent_watch.c new file mode 100644 index 0000000..c77c2ef --- /dev/null +++ b/ext/fsevent/fsevent_watch.c @@ -0,0 +1,44 @@ +#include + +void callback(ConstFSEventStreamRef streamRef, + void *clientCallBackInfo, + size_t numEvents, + void *eventPaths, + const FSEventStreamEventFlags eventFlags[], + const FSEventStreamEventId eventIds[] +) { + // Print modified dirs + int i; + char **paths = eventPaths; + for (i = 0; i < numEvents; i++) { + printf("%s", paths[i]); + printf(" "); + } + printf("\n"); + fflush(stdout); +} + +int main (int argc, const char * argv[]) { + // Create event stream + CFStringRef pathToWatch = CFStringCreateWithCString(kCFAllocatorDefault, argv[1], kCFStringEncodingUTF8); + CFArrayRef pathsToWatch = CFArrayCreate(NULL, (const void **)&pathToWatch, 1, NULL); + void *callbackInfo = NULL; + FSEventStreamRef stream; + CFAbsoluteTime latency = 0.1; + stream = FSEventStreamCreate( + kCFAllocatorDefault, + callback, + callbackInfo, + pathsToWatch, + kFSEventStreamEventIdSinceNow, + latency, + kFSEventStreamCreateFlagNone + ); + + // Add stream to run loop + FSEventStreamScheduleWithRunLoop(stream, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); + FSEventStreamStart(stream); + CFRunLoopRun(); + + return 2; +} \ No newline at end of file diff --git a/guard.gemspec b/guard.gemspec new file mode 100644 index 0000000..d818850 --- /dev/null +++ b/guard.gemspec @@ -0,0 +1,32 @@ +# -*- encoding: utf-8 -*- +$:.push File.expand_path('../lib', __FILE__) +require 'guard/version' + +Gem::Specification.new do |s| + s.name = 'guard' + s.version = Guard::VERSION + s.platform = Gem::Platform::RUBY + s.authors = ['Thibaud Guillaume-Gentil'] + s.email = ['thibaud@thibaud.me'] + s.homepage = 'http://rubygems.org/gems/guard' + s.summary = 'Guard keep an eye on your files event' + s.description = 'Guard is a command line tool to easly manage script launch when your files change' + + s.rubyforge_project = 'guard' + + s.add_development_dependency 'bundler', '~> 1.0.1' + s.add_development_dependency 'rspec', '~> 2.0.0.beta.22' + + s.add_dependency 'thor', '~> 0.14.2' + s.add_dependency 'sys-uname', '~> 0.8.4' + # Mac OS X + s.add_dependency 'growl', '~> 1.0.3' + # Linux + s.add_dependency 'rb-inotify' + s.add_dependency 'libnotify', '~> 0.1.3' + + s.files = Dir.glob('{bin,images,lib,ext}/**/*') + %w[LICENSE README.rdoc] + s.extensions = ['ext/extconf.rb'] + s.executable = 'guard' + s.require_path = 'lib' +end \ No newline at end of file diff --git a/images/failed.png b/images/failed.png new file mode 100644 index 0000000..8235d0d Binary files /dev/null and b/images/failed.png differ diff --git a/images/pending.png b/images/pending.png new file mode 100755 index 0000000..8cff392 Binary files /dev/null and b/images/pending.png differ diff --git a/images/success.png b/images/success.png new file mode 100644 index 0000000..dd37dd4 Binary files /dev/null and b/images/success.png differ diff --git a/lib/guard.rb b/lib/guard.rb new file mode 100644 index 0000000..cbb2d23 --- /dev/null +++ b/lib/guard.rb @@ -0,0 +1,49 @@ +module Guard + + autoload :UI, 'guard/ui' + autoload :Dsl, 'guard/dsl' + autoload :Interactor, 'guard/interactor' + autoload :Listener, 'guard/listener' + autoload :Watcher, 'guard/watcher' + autoload :Notifier, 'guard/notifier' + + class << self + attr_accessor :options, :guards, :listener + + def start(options = {}) + @options = options + @listener = Listener.new + @guards = [] + + Dsl.evaluate_guardfile + Interactor.init_signal_traps + + listener.on_change do |files| + run do + guards.each do |guard| + paths = Watcher.match_files(guard, files) + guard.run_on_change(paths) unless paths.empty? + end + end + end + + UI.info "Guard is now watching at '#{Dir.pwd}'" + guards.each(&:start) + listener.start + end + + def add_guard(name, watchers = [], options = {}) + require "guard/#{name.downcase}" + guard_class = ObjectSpace.each_object(Class).detect { |c| c.to_s.downcase.match "^guard::#{name.downcase}" } + @guards << guard_class.new(watchers, options) + end + + def run + listener.stop + yield + listener.start + end + + end + +end \ No newline at end of file diff --git a/lib/guard/cli.rb b/lib/guard/cli.rb new file mode 100644 index 0000000..63e7627 --- /dev/null +++ b/lib/guard/cli.rb @@ -0,0 +1,20 @@ +require 'thor' +require 'guard/version' + +module Guard + class CLI < Thor + default_task :start + + desc "start", "Starts guard" + method_option :clear, :type => :boolean, :default => false, :aliases => '-c', :banner => "Auto clear shell after each change" + def start + Guard.start(options) + end + + desc "version", "Prints the guard's version information" + def version + Guard::UI.info "Guard version #{Guard::VERSION}" + end + map %w(-v --version) => :version + end +end \ No newline at end of file diff --git a/lib/guard/dsl.rb b/lib/guard/dsl.rb new file mode 100644 index 0000000..eb01889 --- /dev/null +++ b/lib/guard/dsl.rb @@ -0,0 +1,24 @@ +module Guard + class Dsl + + def self.evaluate_guardfile + guardfile = "#{Dir.pwd}/Guardfile" + dsl = new + dsl.instance_eval(File.read(guardfile.to_s), guardfile.to_s, 1) + rescue + UI.error "Guardfile not found or invalid" + exit 1 + end + + def guard(name, options = {}, &definition) + @watchers = [] + definition.call + Guard.add_guard(name, @watchers, options) + end + + def watch(pattern, &action) + @watchers << Guard::Watcher.new(pattern, action) + end + + end +end diff --git a/lib/guard/guard.rb b/lib/guard/guard.rb new file mode 100644 index 0000000..8da5cbe --- /dev/null +++ b/lib/guard/guard.rb @@ -0,0 +1,34 @@ +module Guard + class Guard + attr_accessor :watchers, :options + + def initialize(watchers = [], options = {}) + @watchers, @options = watchers, options + end + + # ================ + # = Guard method = + # ================ + + def start + true + end + + def stop + true + end + + def reload + true + end + + def run_all + true + end + + def run_on_change(paths) + true + end + + end +end \ No newline at end of file diff --git a/lib/guard/interactor.rb b/lib/guard/interactor.rb new file mode 100644 index 0000000..ef5fc39 --- /dev/null +++ b/lib/guard/interactor.rb @@ -0,0 +1,32 @@ +module Guard + module Interactor + + def self.init_signal_traps + # Run all (Ctrl-\) + Signal.trap('QUIT') do + ::Guard.run do + ::Guard.guards.each(&:run_all) + end + end + + # Stop (Ctrl-C) + Signal.trap('INT') do + ::Guard.listener.stop + if ::Guard.guards.all?(&:stop) + UI.info "Bye bye...", :reset => true, :clear => false + abort("\n") + else + ::Guard.listener.start + end + end + + # Reload (Ctrl-Z) + Signal.trap('TSTP') do + ::Guard.run do + ::Guard.guards.each(&:reload) + end + end + end + + end +end \ No newline at end of file diff --git a/lib/guard/listener.rb b/lib/guard/listener.rb new file mode 100644 index 0000000..3d28607 --- /dev/null +++ b/lib/guard/listener.rb @@ -0,0 +1,64 @@ +require 'sys/uname' + +module Guard + class Listener + attr_reader :last_event, :callback, :pipe + + def initialize + update_last_event + end + + def on_change(&block) + @callback = block + end + + def start + @pipe = case Sys::Uname.sysname + when 'Darwin' + IO.popen("#{bin_path}/fsevent_watch .") + when 'Linux' + IO.popen("#{bin_path}/inotify_watch .") + end + watch_change + end + + def stop + Process.kill("HUP", pipe.pid) if pipe + end + + private + + def watch_change + while !pipe.eof? + if line = pipe.readline + modified_dirs = line.split(" ") + files = modified_files(modified_dirs) + update_last_event + callback.call(files) + end + end + end + + def modified_files(dirs) + files = potentially_modified_files(dirs).select { |file| recent_file?(file) } + files.map! { |file| file.gsub("#{Dir.pwd}/", '') } + end + + def potentially_modified_files(dirs) + Dir.glob(dirs.map { |dir| "#{dir}*" }) + end + + def recent_file?(file) + File.mtime(file) >= last_event + end + + def update_last_event + @last_event = Time.now + end + + def bin_path + File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'bin')) + end + + end +end \ No newline at end of file diff --git a/lib/guard/notifier.rb b/lib/guard/notifier.rb new file mode 100644 index 0000000..cdc483e --- /dev/null +++ b/lib/guard/notifier.rb @@ -0,0 +1,45 @@ +require 'sys/uname' +require 'pathname' + +case Sys::Uname.sysname +when 'Darwin' + require 'growl' +when 'Linux' + require 'libnotify' +end + +module Guard + module Notifier + + def self.notify(message, options = {}) + unless ENV["GUARD_ENV"] == "test" + image = options[:image] || :success + title = options[:title] || "Guard" + case Sys::Uname.sysname + when 'Darwin' + Growl.notify message, :title => title, :icon => image_path(image), :name => "Guard" + when 'Linux' + Libnotify.show :body => message, :summary => title, :icon_path => image_path(image) + end + end + end + + private + + def self.image_path(image) + images_path = Pathname.new(File.dirname(__FILE__)).join('../../images') + case image + when :failed + images_path.join("failed.png").to_s + when :pending + images_path.join("pending.png").to_s + when :success + images_path.join("success.png").to_s + else + # path given + image + end + end + + end +end \ No newline at end of file diff --git a/lib/guard/ui.rb b/lib/guard/ui.rb new file mode 100644 index 0000000..80ba2ef --- /dev/null +++ b/lib/guard/ui.rb @@ -0,0 +1,37 @@ +module Guard + module UI + class << self + + def info(message, options = {}) + unless ENV["GUARD_ENV"] == "test" + reset_line if options[:reset] + clear if options.key?(:clear) ? options[:clear] : ::Guard.options[:clear] + puts reset_color(message) if message != '' + end + end + + def error(message) + puts "ERROR: #{message}" + end + + def reset_line + print "\r\e " + end + + private + + def clear + system("clear;") + end + + def reset_color(text) + color(text, "\e[0m") + end + + def color(text, color_code) + "#{color_code}#{text}\e[0m" + end + + end + end +end diff --git a/lib/guard/version.rb b/lib/guard/version.rb new file mode 100644 index 0000000..3c73419 --- /dev/null +++ b/lib/guard/version.rb @@ -0,0 +1,3 @@ +module Guard + VERSION = "0.1.0.beta.1" +end \ No newline at end of file diff --git a/lib/guard/watcher.rb b/lib/guard/watcher.rb new file mode 100644 index 0000000..b96f9e2 --- /dev/null +++ b/lib/guard/watcher.rb @@ -0,0 +1,35 @@ +module Guard + class Watcher + attr_accessor :pattern, :action + + def initialize(pattern, action = nil) + @pattern, @action = pattern, action + end + + def self.match_files(guard, files) + guard.watchers.inject([]) do |paths, watcher| + files.each do |file| + if matches = file.match(watcher.pattern) + if watcher.action + begin + case watcher.action.arity + when -1 + result = watcher.action.call + when 1 + result = watcher.action.call(matches) + end + rescue + UI.info "Problem with watch action" + end + paths << result if result.is_a?(String) && result != '' + else + paths << matches[0] + end + end + end + paths + end + end + + end +end \ No newline at end of file diff --git a/spec/guard/listener_spec.rb b/spec/guard/listener_spec.rb new file mode 100644 index 0000000..1dc4386 --- /dev/null +++ b/spec/guard/listener_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +describe Guard::Listener do + subject { described_class.new } + + its(:last_event) { should < Time.now } + + describe "start" do + let(:pipe_mock) { mock("pipe", :eof? => true) } + + it "should use fsevent_watch on Mac OS X" do + Sys::Uname.stub(:sysname).and_return('Darwin') + IO.should_receive(:popen).with(/.*\/fsevent_watch\s\./).and_return(pipe_mock) + subject.start + end + + it "should use inotify_watch on Linux" do + Sys::Uname.stub(:sysname).and_return('Linux') + IO.should_receive(:popen).with(/.*\/inotify_watch\s\./).and_return(pipe_mock) + subject.start + end + + end + +end \ No newline at end of file diff --git a/spec/guard/notifier_spec.rb b/spec/guard/notifier_spec.rb new file mode 100644 index 0000000..8f670ac --- /dev/null +++ b/spec/guard/notifier_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe Guard::Notifier do + subject { Guard::Notifier } + + describe "notify" do + before(:each) { ENV["GUARD_ENV"] = 'special_test' } + + it "should use Growl on Mac OS X" do + Sys::Uname.stub(:sysname).and_return('Darwin') + Growl.should_receive(:notify).with("great", + :title => "Guard", + :icon => Pathname.new(File.dirname(__FILE__)).join('../../images/success.png').to_s, + :name => "Guard" + ) + subject.notify 'great', :title => 'Guard' + end + + # it "should use Libnotify on Linux" do + # Sys::Uname.stub(:sysname).and_return('Linux') + # Libnotify.should_receive(:show).with( + # :body => "great", + # :summary => 'Guard', + # :icon_path => 'image/path' + # ) + # subject.notify 'great', 'Guard', 'image/path' + # end + + after(:each) { ENV["GUARD_ENV"] = 'test' } + end + +end \ No newline at end of file diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..5fe8b17 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,17 @@ +require 'rubygems' +require 'guard' +require 'rspec' + +fixture_path = Pathname.new(File.expand_path('../fixtures/', __FILE__)) + +RSpec.configure do |config| + config.color_enabled = true + + config.before(:each) do + ENV["GUARD_ENV"] = 'test' + @fixture_path = fixture_path + end + + config.after(:all) do + end +end \ No newline at end of file