wow it works even better now

This commit is contained in:
John Bintz 2012-05-07 18:21:03 -04:00
parent 88400268c5
commit 1c379bdbe7
16 changed files with 493 additions and 144 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
Gemfile.lock Gemfile.lock
.DS_Store

View File

@ -1,6 +1,6 @@
source :rubygems source :rubygems
gemspec
gem 'qtbindings'
require 'rbconfig' require 'rbconfig'
case RbConfig::CONFIG['host_os'] case RbConfig::CONFIG['host_os']
@ -10,5 +10,3 @@ when /linux/
gem 'rb-inotify' gem 'rb-inotify'
end end
gem 'atomic'
gem 'thor'

View File

@ -1,21 +1,36 @@
#!/usr/bin/env ruby #!/usr/bin/env ruby
$: << File.expand_path('../../lib', __FILE__)
require 'Qt4' require 'Qt4'
require 'thread' require 'thread'
require 'atomic' require 'atomic'
require 'thor' require 'thor'
require 'unison/profile' require 'unison/profile'
require 'unison/config'
require 'unison/bridge'
require 'unison/filesystem_watcher'
require 'unison/ui/icon'
require 'unison/ui/menu'
require 'unison/ui/fileview'
require 'unison/ui/preferences'
require 'forwardable'
class UnisonWatcher < Qt::Application class UnisonWatcher < Qt::Application
TRANSFER_LOG = '~/unison.log' TRANSFER_LOG = '~/unison.log'
SYNC_CHECK_COUNT = 600 SYNC_CHECK_COUNT = 600
SYNC_CHECK_TIME = 0.1 SYNC_CHECK_TIME = 0.1
extend Forwardable
def_delegators :@config, :profiles
def initialize(profiles, *args) def initialize(profiles, *args)
super(*args) super(*args)
@profiles = profiles @config = Unison::Config.ensure(File.expand_path('~/.unison/watch.yml'))
@queue = Atomic.new([]) @queue = Atomic.new([])
@sync_now = false @sync_now = false
@ -28,130 +43,50 @@ class UnisonWatcher < Qt::Application
end end
def processed_profiles def processed_profiles
@processed_profiles ||= Unison::Profile.process(@profiles) @processed_profiles ||= Unison::Profile.process(profiles)
end end
def watch def watch
require 'rbconfig' Unison::FilesystemWatcher.new(paths_to_watch, self)
watcher = Thread.new do
while !Thread.current[:app]; sleep 0.1; end
begin
@watch = nil
case RbConfig::CONFIG['host_os']
when /darwin/
require 'rb-fsevent'
@watch = FSEvent.new
@watch.watch Thread.current[:paths], :latency => 0.1 do |directories|
Thread.current[:app] << directories
end
when /linux/
require 'rb-inotify'
@watch = INotify::Notifier.new
Thread.current[:paths].each do |path|
FileUtils.mkdir_p path
@watch.watch path, :recursive, :modify, :create, :delete do |event|
Thread.current[:app] << event.absolute_name
end
end
end end
@watch.run def paths_to_watch
rescue => e processed_profiles.collect(&:paths_with_local_root).flatten
puts e.message
puts e.backtrace.join("\n")
exit
end
end end
watcher[:paths] = processed_profiles.collect(&:paths_with_local_root).flatten def fileview
watcher[:app] = self @fileview ||= Unison::UI::FileView.new(File.expand_path(TRANSFER_LOG))
end
def update_status(status)
new_status = Qt::Action.new(status, @menu)
new_status.enabled = false
@menu.insertAction(@active_status, new_status)
@menu.removeAction(@status)
@status = new_status
end end
def menu def menu
@menu = Qt::Menu.new return @menu if @menu
@current_text = 'Unison watch idle.' @menu = Unison::UI::Menu.new
@status = Qt::Action.new(@status_text, @menu) @menu.on(:sync_now) { @sync_now = true }
@status.enabled = false @menu.on(:toggle_status) { toggle_status }
@menu.on(:view_log) { fileview.show }
sync_now = Qt::Action.new("Sync now", @menu) @menu.on(:quit) { @exiting = true }
sync_now.connect(SIGNAL :triggered) { @sync_now = true } @menu.on(:preferences) { preferences.show }
@menu.generate
@active_status = Qt::Action.new("Pause syncing", @menu)
@active_status.connect(SIGNAL :triggered) { toggle_status }
@current_log_size = File.size(File.expand_path(TRANSFER_LOG))
@fileview = Qt::TextEdit.new
@fileview.plainText = "Watching for changes..."
@fileview.readOnly = true
@fileview.resize 800, 600
@fileview.connect(SIGNAL :textChanged) { @fileview.moveCursor Qt::TextCursor::End }
@log = Qt::Action.new("View transfer log", @menu)
@log.connect(SIGNAL :triggered) { @fileview.show }
quit = Qt::Action.new("Quit", @menu)
quit.connect(SIGNAL :triggered) { @exiting = true }
@menu.addAction @active_status
@menu.addAction sync_now
@menu.addSeparator
@menu.addAction @log
@menu.addAction quit
@current_text = 'Unison watch idle.'
update_status @current_text
@menu @menu
end end
def update_ui def update_ui
if @current_icon != @prior_icon @icon.current_icon = @current_icon
if !@icons[@current_icon] @menu.status_text = @current_text
@icons[@current_icon] = Qt::Icon.new(File.expand_path("../../assets/#{@current_icon}.png", __FILE__))
@icons["large-#{@current_icon}"] = Qt::Icon.new(File.expand_path("../../assets/large-#{@current_icon}.png", __FILE__))
end
@icon.icon = @icons[@current_icon]
self.windowIcon = @icons["large-#{@current_icon}"]
@icon.toolTip = "Unison Agent\nUsing #{@profiles.join(', ')} profile"
if !@prior_icon
@icon.show
end
@prior_icon = @current_icon
end
if @current_text!= @prior_text
update_status @current_text
@prior_text = @current_text
end
processEvents processEvents
end end
def ui def ui
@icon = Qt::SystemTrayIcon.new @icon = Unison::UI::Icon.new(menu, self, profiles, File.expand_path('../../assets', __FILE__))
@icon.contextMenu = menu @config.on_update { @icon.profiles = @config.profiles }
@current_icon = 'idle'
toggle_status true toggle_status true
@prior_icon = nil
@prior_text = nil @prior_text = nil
@icons = {} @icons = {}
@ -168,12 +103,36 @@ class UnisonWatcher < Qt::Application
end end
def start def start
self.objectName = "cats" if !@config.active_profiles?
preferences.show
end
watch watch
ui ui
end end
def preferences
@preferences ||= Unison::UI::Preferences.new(@config)
end
def show_working
index = 0
while !@done
@current_icon = "working-#{index + 1}"
fileview.read!
update_ui
break if @done
sleep 0.25
index = (index + 1) % 2
end
@current_icon = 'idle'
fileview.read!
end
def check def check
begin begin
if @active && (@queue.value.length > 0 || @remote_sync_check == 0 || @sync_now) if @active && (@queue.value.length > 0 || @remote_sync_check == 0 || @sync_now)
@ -183,39 +142,13 @@ class UnisonWatcher < Qt::Application
@done = false @done = false
runner = Thread.new do Unison::Bridge.run(profiles, TRANSFER_LOG) { @done = true }
@profiles.each do |profile|
system %{bash -c 'unison -log -logfile #{TRANSFER_LOG} -batch #{profile}'}
end
@done = true
end
index = 0 show_working
while !@done
@current_icon = "working-#{index + 1}"
File.open(File.expand_path(TRANSFER_LOG), 'r') { |fh|
fh.seek(@current_log_size)
@fileview.plainText = fh.read
}
update_ui
break if @done
sleep 0.25
index = (index + 1) % 2
end
@current_icon = 'idle'
@remote_sync_check = SYNC_CHECK_COUNT @remote_sync_check = SYNC_CHECK_COUNT
@sync_now = false @sync_now = false
@queue.update { [] } @queue.update { [] }
File.open(File.expand_path(TRANSFER_LOG), 'r') { |fh|
fh.seek(@current_log_size)
@fileview.plainText = fh.read
}
end end
rescue => e rescue => e
puts e.message puts e.message
@ -237,12 +170,15 @@ class UnisonWatcher < Qt::Application
def toggle_status(set = nil) def toggle_status(set = nil)
@active = set || !@active @active = set || !@active
@active_status.text = @active ? "Pause syncing" : "Resume syncing" @menu.active_status_text = @active ? "Pause syncing" : "Resume syncing"
@current_icon = @active ? 'idle' : 'paused' @current_icon = @active ? 'idle' : 'paused'
end end
end end
class UnisonCLI < Thor module Unison
class CLI < Thor
include Thor::Actions
desc 'start profile <profile> ...', 'Run Unison Watch using the provided profiles' desc 'start profile <profile> ...', 'Run Unison Watch using the provided profiles'
def start(*profiles) def start(*profiles)
UnisonWatcher.new(profiles, ARGV).start UnisonWatcher.new(profiles, ARGV).start
@ -253,7 +189,32 @@ class UnisonCLI < Thor
def method_missing(*args) def method_missing(*args)
start(*args) start(*args)
end end
def self.source_root
File.expand_path('../../skel', __FILE__)
end end
UnisonCLI.start desc 'app-bundle', 'Make an app bundle in the current directory'
def app_bundle
destination_path = Dir.pwd
directory 'UnisonWatch.app', 'UnisonWatch.app'
Dir['UnisonWatch.app/**/*'].each do |file|
if File.directory?(file)
File.chmod(0755, file)
end
end
File.chmod(0755, "UnisonWatch.app/Contents/MacOS/UnisonWatch")
end
no_tasks do
def gem_directory
File.expand_path('../..', __FILE__)
end
end
end
end
Unison::CLI.start

28
lib/unison/bridge.rb Normal file
View File

@ -0,0 +1,28 @@
module Unison
class Bridge
def self.run(*args, &block)
new(*args).run(&block)
end
def initialize(profiles, log)
@profiles, @log = profiles, log
end
def run(&block)
Thread.new do
begin
@profiles.each do |profile|
system %{bash -c 'unison -log -logfile #{@log} -batch #{profile} 2>>#{@log.stderr} >>#{@log.stdout}'}
end
block.call
rescue => e
puts e.message
puts e.backtrace.join("\n")
raise e
end
end
end
end
end

70
lib/unison/config.rb Normal file
View File

@ -0,0 +1,70 @@
require 'yaml'
require 'atomic'
module Unison
class Config
def self.ensure(file)
if !File.file?(file)
File.open(file, 'wb') { |fh| fh.print YAML.dump(skel_data) }
end
new(file)
end
def self.skel_data
{ 'profiles' => [] }
end
def set_profile(profile, is_set)
@data.update do |d|
if is_set
d['profiles'] << profile
else
d['profiles'].delete(profile)
end
d['profiles'].uniq!
d
end
@on_update.call if @on_update
save
end
def on_update(&block)
@on_update = block
end
def initialize(file)
@file = file
@data = Atomic.new(nil)
end
def active?(profile)
profiles.include?(profile)
end
def profiles
data['profiles']
end
def data
@data.update { |d| d || YAML.load_file(@file) }
@data.value
end
def active_profiles?
!profiles.empty?
end
def save
@data.update do |d|
File.open(@file, 'wb') { |fh| fh.print YAML.dump(d) }
d
end
end
end
end

View File

@ -0,0 +1,58 @@
module Unison
class FilesystemWatcher
class NoWatcherAvailable < StandardError ; end
def initialize(paths, owner)
@paths, @owner = paths, owner
end
def run
require 'rbconfig'
@watcher = Thread.new do
while !Thread.current[:app]; sleep 0.1; end
begin
case RbConfig::CONFIG['host_os']
when /(darwin|linux)/
@watch = send("watcher_for_#{$1}")
else
raise NoWatcherAvailable.new
end
@watch.run
rescue => e
puts e.message
puts e.backtrace.join("\n")
exit
end
end
@watcher[:paths] = @paths
@watcher[:app] = @owner
end
def watcher_for_darwin
require 'rb-fsevent'
watch = FSEvent.new
watch.watch Thread.current[:paths], :latency => 1.0 do |directories|
Thread.current[:app] << directories
end
watch
end
def watcher_for_linux
require 'rb-inotify'
watch = INotify::Notifier.new
Thread.current[:paths].each do |path|
FileUtils.mkdir_p path
watch.watch path, :recursive, :modify, :create, :delete do |event|
Thread.current[:app] << event.absolute_name
end
end
watch
end
end
end

View File

@ -1,9 +1,15 @@
module Unison module Unison
class Profile class Profile
PROFILE_DIR = File.expand_path('~/.unison')
def self.process(profiles) def self.process(profiles)
profiles.collect { |profile| new(profile) } profiles.collect { |profile| new(profile) }
end end
def self.available
Dir[File.join(PROFILE_DIR, '*.prf')].collect { |file| File.basename(file).gsub('.prf', '') }.sort
end
def initialize(which) def initialize(which)
@which = which @which = which
end end

31
lib/unison/ui/fileview.rb Normal file
View File

@ -0,0 +1,31 @@
module Unison
module UI
class FileView < Qt::TextEdit
INITIAL = "Watching for changes..."
def initialize(file, *args)
super(*args)
@file = file
@current_log_size = File.size(@file)
self.plainText = INITIAL
self.readOnly = true
self.resize 800, 600
self.connect(SIGNAL(:textChanged), &method(:on_text_change))
end
def on_text_change
self.moveCursor Qt::TextCursor::End
end
def read!
File.open(@file, 'r') { |fh|
fh.seek(@current_log_size)
self.plainText = fh.read
}
end
end
end
end

45
lib/unison/ui/icon.rb Normal file
View File

@ -0,0 +1,45 @@
module Unison
module UI
class Icon < Qt::SystemTrayIcon
def initialize(menu, window, profiles, icon_source, *args)
super(*args)
self.contextMenu = menu
@window, @icon_source, @profiles = window, icon_source, profiles
@current_icon = nil
@icons = {}
end
def qt_icon_for(name)
@icons[name] ||= Qt::Icon.new(File.join(@icon_source, "#{name}.png"))
end
def large_qt_icon_for(name)
qt_icon_for("large-#{name}")
end
def profiles=(profiles)
@profiles = profiles
set_tooltip
end
def current_icon=(icon)
if icon != @current_icon
self.icon = qt_icon_for(icon)
@window.windowIcon = large_qt_icon_for(icon)
set_tooltip
show if !@current_icon
@current_icon = icon
end
end
def set_tooltip
self.toolTip = "Unison Agent\nUsing #{@profiles.join(', ')} profile"
show
end
end
end
end

70
lib/unison/ui/menu.rb Normal file
View File

@ -0,0 +1,70 @@
module Unison
module UI
class Menu < Qt::Menu
IDLE = 'Unison watch idle.'
SYNC_NOW = 'Sync now'
PAUSE_SYNCING = 'Pause syncing'
VIEW_TRANSFER_LOG = "View transfer log"
QUIT = 'Quit'
def initialize(*args)
super(*args)
@status_text = IDLE
end
def on(event, &block)
@on ||= {}
@on[event] = block
end
def generate
generate_status
sync_now = Qt::Action.new(SYNC_NOW, self)
sync_now.connect(SIGNAL(:triggered), &@on[:sync_now])
@active_status = Qt::Action.new(PAUSE_SYNCING, self)
@active_status.connect(SIGNAL(:triggered), &@on[:toggle_status])
@log = Qt::Action.new(VIEW_TRANSFER_LOG, @menu)
@log.connect(SIGNAL(:triggered), &@on[:view_log])
@preferences = Qt::Action.new('Preferences...', @menu)
@preferences.connect(SIGNAL(:triggered), &@on[:preferences])
quit = Qt::Action.new(QUIT, @menu)
quit.connect(SIGNAL(:triggered), &@on[:quit])
addAction @active_status
addAction sync_now
addSeparator
addAction @log
addAction @preferences
addAction quit
end
def status_text=(text)
if @status_text != text
@status_text = text
generate_status
end
end
def active_status_text=(text)
@active_status.text = text
end
def generate_status
new_status = Qt::Action.new(@status_text, self)
new_status.enabled = false
insertAction(@active_status, new_status)
removeAction(@status)
@status = new_status
end
end
end
end

View File

@ -0,0 +1,32 @@
module Unison
module UI
class Preferences < Qt::Widget
def initialize(config, *args)
super(*args)
@config = config
generate
end
def generate
layout = Qt::GridLayout.new
profile_group = Qt::GroupBox.new("Profiles")
profile_group_layout = Qt::VBoxLayout.new
profile_group.setLayout(profile_group_layout)
Unison::Profile.available.each do |profile|
radio = Qt::CheckBox.new(profile)
radio.checked = @config.active?(profile)
radio.connect(SIGNAL "toggled(bool)") { |checked| @config.set_profile(profile, checked) }
profile_group_layout.addWidget(radio)
end
layout.addWidget(profile_group)
setLayout(layout)
end
end
end
end

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>UnisonWatch</string>
<key>CFBundleGetInfoString</key>
<string>Unison Watch</string>
<key>CFBundleIdentifier</key>
<string>webapps.webrunner</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>UnisonWatch</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.3.0</string>
<key>CFBundleSignature</key>
<string>WRUN</string>
<key>CFBundleVersion</key>
<string>0.0.1.20070616</string>
</dict>
</plist>

View File

@ -0,0 +1,5 @@
#!/bin/bash
cd <%= gem_directory %>
GEM_HOME=<%= ENV['GEM_HOME'] %> GEM_PATH=<%= ENV['GEM_PATH'] %> PATH=<%= `which ruby`.gsub(%r{/[^/]+$}, '') %>:$PATH bin/unison-watch

View File

@ -0,0 +1 @@
APPLWRUN

View File

19
unison-watch.gemspec Normal file
View File

@ -0,0 +1,19 @@
Gem::Specification.new do |gem|
gem.authors = ["John Bintz"]
gem.email = ["john@coswellproductions.com"]
gem.description = %q{No-nonsense JavaScript testing solution.}
gem.summary = %q{No-nonsense JavaScript testing solution.}
gem.homepage = ""
gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
gem.files = `git ls-files`.split("\n")
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
gem.name = "unison-watch"
gem.require_paths = ["lib"]
gem.version = '0.0.1'
gem.add_dependency 'qtbindings'
gem.add_dependency 'thor'
gem.add_dependency 'atomic'
end