Compare commits

...

5 Commits

Author SHA1 Message Date
John Bintz be054685ac ensure log doesn't die if no file there 2012-08-09 13:58:05 -04:00
John Bintz ce5128b244 finalize some stuff, make it ready for use 2012-06-05 20:21:13 -04:00
John Bintz 53ab8e23bc set time between checks 2012-05-08 20:11:35 -04:00
John Bintz 6e291cc583 fixxxxes 2012-05-08 06:46:10 -04:00
John Bintz 1c379bdbe7 wow it works even better now 2012-05-07 18:21:03 -04:00
22 changed files with 686 additions and 257 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,5 +0,0 @@
desc 'Build app'
task :build_app do
cp_r 'skel/UnisonWatch.app', '.'
end

BIN
assets/unison.icns Normal file

Binary file not shown.

BIN
bin/setfileicon Executable file

Binary file not shown.

View File

@ -1,259 +1,23 @@
#!/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 'unison'
require 'unison/watcher'
require 'unison/cli'
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'
class UnisonWatcher < Qt::Application
TRANSFER_LOG = '~/unison.log'
SYNC_CHECK_COUNT = 600
SYNC_CHECK_TIME = 0.1
def initialize(profiles, *args) Unison::CLI.start
super(*args)
@profiles = profiles
@queue = Atomic.new([])
@sync_now = false
@exiting = false
@active = true
end
def <<(dirs)
@queue.update { |q| q += [ dirs ].flatten ; q }
end
def processed_profiles
@processed_profiles ||= Unison::Profile.process(@profiles)
end
def watch
require 'rbconfig'
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
@watch.run
rescue => e
puts e.message
puts e.backtrace.join("\n")
exit
end
end
watcher[:paths] = processed_profiles.collect(&:paths_with_local_root).flatten
watcher[:app] = self
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
def menu
@menu = Qt::Menu.new
@current_text = 'Unison watch idle.'
@status = Qt::Action.new(@status_text, @menu)
@status.enabled = false
sync_now = Qt::Action.new("Sync now", @menu)
sync_now.connect(SIGNAL :triggered) { @sync_now = true }
@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
end
def update_ui
if @current_icon != @prior_icon
if !@icons[@current_icon]
@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
end
def ui
@icon = Qt::SystemTrayIcon.new
@icon.contextMenu = menu
toggle_status true
@prior_icon = nil
@prior_text = nil
@icons = {}
@remote_sync_check = SYNC_CHECK_COUNT
while !@exiting
check
update_ui
sleep SYNC_CHECK_TIME
end
end
def start
self.objectName = "cats"
watch
ui
end
def check
begin
if @active && (@queue.value.length > 0 || @remote_sync_check == 0 || @sync_now)
dir = nil
@current_text = "Syncing..."
@done = false
runner = Thread.new do
@profiles.each do |profile|
system %{bash -c 'unison -log -logfile #{TRANSFER_LOG} -batch #{profile}'}
end
@done = true
end
index = 0
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
@sync_now = false
@queue.update { [] }
File.open(File.expand_path(TRANSFER_LOG), 'r') { |fh|
fh.seek(@current_log_size)
@fileview.plainText = fh.read
}
end
rescue => e
puts e.message
puts e.backtrace.join("\n")
exit
end
@remote_sync_check -= 1
if @active
@current_text = "Next check in #{sprintf("%.0d", SYNC_CHECK_TIME * @remote_sync_check)} secs."
else
@current_text = "Syncing paused."
@remote_sync_check = SYNC_CHECK_COUNT
end
end
def toggle_status(set = nil)
@active = set || !@active
@active_status.text = @active ? "Pause syncing" : "Resume syncing"
@current_icon = @active ? 'idle' : 'paused'
end
end
class UnisonCLI < Thor
desc 'start profile <profile> ...', 'Run Unison Watch using the provided profiles'
def start(*profiles)
UnisonWatcher.new(profiles, ARGV).start
end
default_task :run
def method_missing(*args)
start(*args)
end
end
UnisonCLI.start

6
lib/unison.rb Normal file
View File

@ -0,0 +1,6 @@
module Unison
class << self
def root ; File.expand_path('../..', __FILE__) ; end
end
end

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

@ -0,0 +1,32 @@
module Unison
class Bridge
def self.run(*args, &block)
new(*args).run(&block)
end
def initialize(config, log)
@config, @log = config, log
end
def run(&block)
Thread.new do
begin
@config.profiles.each do |profile|
system %{bash -c '#{@config.unison_binary} -ui text -log -logfile #{@log} -batch #{profile}'}
if $?.exitstatus != 0
system %{bash -c '#{@config.unison_binary} -ui graphic #{profile}'}
end
end
block.call
rescue => e
puts e.message
puts e.backtrace.join("\n")
exit 1
end
end
end
end
end

43
lib/unison/cli.rb Normal file
View File

@ -0,0 +1,43 @@
require 'thor'
module Unison
class CLI < Thor
include Thor::Actions
desc 'start profile <profile> ...', 'Run Unison Watch using the provided profiles'
def start(*profiles)
Watcher.new(profiles, ARGV).start
end
default_task :run
def method_missing(*args)
start(*args)
end
def self.source_root
File.join(Unison.root, 'skel')
end
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")
system %{bin/setfileicon assets/unison.icns UnisonWatch.app}
end
no_tasks do
def gem_directory
Unison.root
end
end
end
end

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

@ -0,0 +1,99 @@
require 'yaml'
require 'atomic'
module Unison
class Config
DEFAULT_TIME_BETWEEN_CHECKS = 60
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' => [],
'time_between_checks' => DEFAULT_TIME_BETWEEN_CHECKS,
'unison_binary' => '/usr/bin/unison'
}
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
save
end
def on_update(&block)
@on_update ||= []
@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 time_between_checks
data['time_between_checks']
end
def time_between_checks=(time)
time = DEFAULT_TIME_BETWEEN_CHECKS if time <= 10
data['time_between_checks'] = time
save
end
def unison_binary
data['unison_binary']
end
def unison_binary=(binary)
data['unison_binary'] = binary
save
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
if @on_update
@on_update.each(&:call)
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

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

@ -0,0 +1,36 @@
module Unison
module UI
class FileView < Qt::TextEdit
INITIAL = "Watching for changes..."
def initialize(file, *args)
super(*args)
@file = file
@current_log_size = File.exist?(@file) ? File.size(@file) : 0
self.plainText = INITIAL
self.readOnly = true
self.resize 800, 600
self.connect(SIGNAL(:textChanged), &method(:on_text_change))
end
def show
super
self.raise
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,60 @@
module Unison
module UI
class Preferences < Qt::Widget
def initialize(config, *args)
super(*args)
@config = config
generate
end
def show
super
self.raise
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
performance_group = Qt::GroupBox.new("Performance")
performance_group_layout = Qt::VBoxLayout.new
performance_group.setLayout(performance_group_layout)
fields = Qt::Widget.new
fields_layout = Qt::GridLayout.new
fields.setLayout(fields_layout)
count_label = Qt::Label.new("Seconds between checks (min 10s):")
count_field = Qt::LineEdit.new(@config.time_between_checks.to_s)
count_field.connect(SIGNAL "textChanged(QString)") { |string| @config.time_between_checks = string.to_i }
binary_label = Qt::Label.new("Unison binary:")
binary_field = Qt::LineEdit.new(@config.unison_binary)
binary_field.connect(SIGNAL "textChanged(QString)") { |string| @config.unison_binary = string }
fields_layout.addWidget(count_label, 0, 0)
fields_layout.addWidget(count_field, 0, 1)
fields_layout.addWidget(binary_label, 1, 0)
fields_layout.addWidget(binary_field, 1, 1)
performance_group_layout.addWidget(fields)
layout.addWidget(profile_group, 0, 0)
layout.addWidget(performance_group, 0, 1)
setLayout(layout)
end
end
end
end

167
lib/unison/watcher.rb Normal file
View File

@ -0,0 +1,167 @@
require 'forwardable'
module Unison
class Watcher < Qt::Application
TRANSFER_LOG = '~/unison.log'
SYNC_CHECK_TIME = 0.05
extend Forwardable
def initialize(profiles, *args)
super(*args)
@config = Unison::Config.ensure(File.expand_path('~/.unison/watch.yml'))
@queue = Atomic.new([])
@sync_now = false
@exiting = false
@active = true
@config.on_update { @remote_sync_check = time_between_checks }
end
def time_between_checks
@config.time_between_checks * 20
end
def <<(dirs)
@queue.update { |q| q += [ dirs ].flatten ; q }
end
def processed_profiles
@processed_profiles ||= Unison::Profile.process(@config.profiles)
end
def watch
Unison::FilesystemWatcher.new(paths_to_watch, self)
end
def paths_to_watch
processed_profiles.collect(&:paths_with_local_root).flatten
end
def fileview
@fileview ||= Unison::UI::FileView.new(File.expand_path(TRANSFER_LOG))
end
def menu
return @menu if @menu
@menu = Unison::UI::Menu.new
@menu.on(:sync_now) { @sync_now = true }
@menu.on(:toggle_status) { toggle_status }
@menu.on(:view_log) { fileview.show }
@menu.on(:quit) { @exiting = true }
@menu.on(:preferences) { preferences.show }
@menu.generate
@menu
end
def update_ui
@icon.current_icon = @current_icon
@menu.status_text = @current_text
processEvents
end
def ui
@icon = Unison::UI::Icon.new(menu, self, @config.profiles, File.join(Unison.root, 'assets'))
@config.on_update { @icon.profiles = @config.profiles }
@current_icon = 'idle'
toggle_status true
@prior_text = nil
@icons = {}
@remote_sync_check = time_between_checks
while !@exiting
check
update_ui
sleep SYNC_CHECK_TIME
end
end
def start
if !@config.active_profiles?
preferences.show
end
watch
ui
end
def preferences
@preferences ||= Unison::UI::Preferences.new(@config)
end
def show_working
index = 0
while true do
@current_icon = "working-#{index + 1}"
fileview.read!
update_ui
break if @done or @exiting
sleep 0.25
index = (index + 1) % 2
end
@current_icon = current_icon
fileview.read!
end
def check
begin
if @active && (@queue.value.length > 0 || @remote_sync_check == 0 || @sync_now)
dir = nil
@current_text = "Syncing..."
@done = false
Unison::Bridge.run(@config, TRANSFER_LOG) { @done = true }
show_working
@remote_sync_check = time_between_checks
@sync_now = false
@queue.update { [] }
end
rescue => e
puts e.message
puts e.backtrace.join("\n")
exit 1
end
@remote_sync_check -= 1
if @active
@current_text = "Next check in #{sprintf("%.0d", SYNC_CHECK_TIME * @remote_sync_check)} secs."
else
@current_text = "Syncing paused."
@remote_sync_check = time_between_checks
end
end
def toggle_status(set = nil)
@active = set || !@active
@menu.active_status_text = @active ? "Pause syncing" : "Resume syncing"
@current_icon = current_icon
end
def current_icon
@active ? 'idle' : 'paused'
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>unisonwatch</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

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