Initial commit

This commit is contained in:
Thibaud Guillaume-Gentil 2010-10-03 23:00:33 +02:00
commit 4d3744ff43
28 changed files with 655 additions and 0 deletions

11
.gitignore vendored Normal file
View File

@ -0,0 +1,11 @@
pkg/*
*.gem
.bundle
## MAC OS
.DS_Store
.Trashes
.com.apple.timemachine.supported
.fseventsd
Desktop DB
Desktop DF

3
Gemfile Normal file
View File

@ -0,0 +1,3 @@
source "http://rubygems.org"
gemspec

45
Gemfile.lock Normal file
View File

@ -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)

25
Guardfile Normal file
View File

@ -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

20
LICENSE Normal file
View File

@ -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.

3
README.rdoc Normal file
View File

@ -0,0 +1,3 @@
= Guard
Documentation is coming.

6
Rakefile Normal file
View File

@ -0,0 +1,6 @@
require 'bundler'
Bundler::GemHelper.install_tasks
require 'rspec/core/rake_task'
RSpec::Core::RakeTask.new(:spec)
task :default => :spec

6
bin/guard Executable file
View File

@ -0,0 +1,6 @@
#!/usr/bin/env ruby
require 'guard'
require 'guard/cli'
Guard::CLI.start

25
bin/inotify_watch Executable file
View File

@ -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

18
ext/extconf.rb Normal file
View File

@ -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

View File

@ -0,0 +1,44 @@
#include <CoreServices/CoreServices.h>
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;
}

32
guard.gemspec Normal file
View File

@ -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

BIN
images/failed.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

BIN
images/pending.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
images/success.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

49
lib/guard.rb Normal file
View File

@ -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

20
lib/guard/cli.rb Normal file
View File

@ -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

24
lib/guard/dsl.rb Normal file
View File

@ -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

34
lib/guard/guard.rb Normal file
View File

@ -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

32
lib/guard/interactor.rb Normal file
View File

@ -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

64
lib/guard/listener.rb Normal file
View File

@ -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

45
lib/guard/notifier.rb Normal file
View File

@ -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

37
lib/guard/ui.rb Normal file
View File

@ -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

3
lib/guard/version.rb Normal file
View File

@ -0,0 +1,3 @@
module Guard
VERSION = "0.1.0.beta.1"
end

35
lib/guard/watcher.rb Normal file
View File

@ -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

View File

@ -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

View File

@ -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

17
spec/spec_helper.rb Normal file
View File

@ -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