Skip to content

Instantly share code, notes, and snippets.

@ttscoff
Created January 25, 2012 15:05
Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save ttscoff/1676667 to your computer and use it in GitHub Desktop.
Save ttscoff/1676667 to your computer and use it in GitHub Desktop.
Watch a Scrivener project for changes and update Marked.app
#!/usr/bin/ruby
# scrivwatch.rb by Brett Terpstra, 2011
# Modifications to merge into one file for Markdown viewing in Marked by BooneJS, 2012
# Modified to use REXML and add titles by Brett Terpstra, 2012 <https://gist.github.com/1676667>
#
# Watch a Scrivener project for changes and update a preview file for Marked
# Usage: scrivwatch.rb /path/to/document.scriv
# <http://markedapp.com>
require 'fileutils'
require 'rexml/document'
require 'optparse'
$version = "1.5"
$out = STDOUT
$progressbar = STDERR
$options = {}
optparse = OptionParser.new do|opts|
opts.banner = "ScrivWatcher version #{$version}\nUsage: scrivwatcher.rb [-dqt] [--cache-dir PATH] /path/to/document.scriv"
$options[:debug] = false
opts.on( '-d','--debug', 'Debug mode' ) do
$options[:debug] = true
end
$options[:progress] = false
opts.on('-p','--progress','Send progress bar commands for Platypus') do
$options[:progress] = true
end
$options[:quiet] = false
opts.on( '-q','--quiet', 'Run quietly (no progress bar/messages)' ) do
$options[:quiet] = true unless $options[:progress]
end
$options[:titles] = false
opts.on( '-t', '--titles', 'Generate Markdown headers from Scrivener page titles' ) do
$options[:titles] = true
end
$options[:cachedir] = File.expand_path("~/ScrivWatcher")
opts.on( '-c','--cache-dir PATH', 'Define the directory for cache files (default "~/ScrivWatcher"') do|cachedir|
$options[:cachedir] = File.expand_path(cachedir).gsub(/\/$/,'')
end
$options[:openscriv] = false
opts.on( '--open-scrivener', 'When loading a file in Marked, open it in Scrivener automatically') do
$options[:openscriv] = true
end
opts.on( '-v', 'Display version number and exit') do
puts "ScrivWatcher v#{$version}"
exit
end
opts.on( '-h', '--help', 'Display this screen' ) do
puts opts
exit
end
end
optparse.parse!
def check_running()
newpid = ''
newpid = %x{ps Ao pid,comm|grep "Marked.app"|grep -v grep|awk '{print $1}'}
return newpid.empty? ? false : true
end
def get_children(ele,path,files,depth)
ele.elements.each('*/BinderItem') do |child|
# Ignore docs set to not include in compile
includetag = REXML::XPath.first( child, "MetaData/IncludeInCompile" )
if !includetag.nil? && includetag.text == "Yes"
id = child.attributes["ID"]
# passing type, would eventually use to control header/title output
type = child.attributes["Type"]
title = child.elements.to_a[0].text
file = "#{path}/Files/Docs/#{id}.rtf"
filepath = File.exists?(file) ? file : false
files << { 'path' => filepath, 'title' => title, 'depth' => depth, 'type' => type }
end
get_children(child,path,files,depth+1)
end
end
# Take the path to the scrivener file and open the internal XML file.
def get_rtf_file_array(scrivpath)
scrivpath = File.expand_path(scrivpath)
scrivx = File.basename("#{scrivpath}", ".scriv") + ".scrivx"
scrivxpath = "#{scrivpath}/#{scrivx}"
# handle cases where the package has been renamed after creation
unless File.exists?(scrivxpath)
scrivxpath = %x{ls -1 #{scrivpath}/*.scrivx|head -n 1}.strip
end
files = []
doc = REXML::Document.new File.new(scrivxpath)
doc.elements.each('ScrivenerProject/Binder/BinderItem') do |ele|
if ele.attributes['Type'] == "DraftFolder"
get_children(ele,scrivpath,files,1)
end
end
return files
end
def usage
puts "Usage: scrivwatcher.rb /path/to/document.scriv"
puts "Use `scrivwatcher -h` for help."
exit
end
trap("SIGINT") { exit }
unless $options[:quiet] || $options[:debug]
if (ARGV[0] !~ /\.scriv\/?$/)
usage
end
end
if $options[:debug] && ARGV.length == 0
path = File.expand_path("~/Dropbox/ScrivTest.scriv")
else
if ARGV[0] =~ /\.scriv\/?$/
path = File.expand_path(ARGV[0].gsub(/\/$/,''))
else
usage
end
end
sw_name = File.basename(path,".scriv")
$out.printf("Watching %s:\n",sw_name) unless $options[:quiet]
sw_target = File.expand_path($options[:cachedir])
Dir.mkdir(sw_target) unless File.exists?(sw_target)
sw_cache_dir = File.expand_path("#{$options[:cachedir]}/cache")
Dir.mkdir(sw_cache_dir) unless File.exists?(sw_cache_dir)
sw_cache = File.expand_path("#{sw_cache_dir}/#{sw_name}")
# Clear cache
if File.exists?(sw_cache)
File.delete(*Dir["#{sw_cache}/*"]) if Dir.glob("#{sw_cache}/*").length > 0
Dir.rmdir(sw_cache)
end
Dir.mkdir(sw_cache)
sw_note = "#{sw_target}/ScrivWatcher - #{sw_name}.md"
File.delete(sw_note) if File.exists?(sw_note)
FileUtils.touch(sw_note)
%x{open -a Scrivener "#{path}"} if $options[:openscriv]
%x{open -a /Applications/Marked.app "#{sw_note}"}
first = true
files = []
while true do # repeat infinitely
unless check_running
puts "Marked quit, exiting"
exit
end
notetext = ""
# tracking the xml file for ordering changes as well
new_xml_time = File.stat(path).mtime.to_i
xml_time ||= new_xml_time
diff_xml_time = new_xml_time - xml_time
unless diff_xml_time == 0 && first == false
files = get_rtf_file_array(path)
end
# track any file changes in folder
new_hash = files.collect {|f| [ f['path'], File.stat(f['path']).mtime.to_i ] if f['path'] } # create a hash of timestamps
hash ||= new_hash
diff_hash = new_hash - hash # check for changes
arr = [0,10,20,30,40,50,60,70,80,90,100]
unless first == false && diff_hash.empty? && diff_xml_time == 0 # if changes were found in the timestamps
$out.print("change detected\n") unless first || $options[:quiet]
hash = new_hash
xml_time = new_xml_time
cachefiles = []
total = files.length
current = 0
files.each{ |f|
current += 1
if f['path']
cachefile = sw_cache + "/" + File.basename(f['path'],'.rtf') + ".md"
if !File.exists?(cachefile) || (File.stat(f['path']).mtime.to_i > File.stat(cachefile).mtime.to_i)
# $stdout.puts "Caching #{f['path']}" if $options[:debug]
note = f['path'] ? %x{/usr/bin/textutil -convert txt -stdout "#{f['path']}"} : ''
leader = ""
if $options[:titles] && !f['depth'].nil?
f['depth'].times { leader += "#" }
leader = "#{leader} #{f['title']}\n\n"
end
notetext = leader + note + "\n\n"
File.open(cachefile,"w"){|cf| cf.puts notetext }
end
cachefiles.push(cachefile)
unless $options[:quiet]
#progress bar
percent = (current * 100 / total).ceil
progress = arr.select{|item| item <= percent }.max
cache_message = first ? "Building cache:" : "Updating cache:"
if $options[:progress]
$progressbar.printf("%s\n",cache_message)
$progressbar.printf("PROGRESS:%d\n",progress)
else
$out.printf("%s [",cache_message)
$out.print("=="*(progress/10))
$out.print(" "*(10-(progress/10))) unless progress == 100
$out.print("]")
$out.printf(" %d%%\r",percent)
$out.flush
end
end
end
}
$out.print("\n") unless $options[:quiet]
first = false
# write the result to the preview file
concat_note = "Concatenating #{cachefiles.length} sections to #{sw_note}..."
if $options[:progress]
$progressbar.printf("%s\n",concat_note)
else
$out.print(concat_note) unless $options[:quiet]
end
File.open(sw_note,"w"){|f| f.puts cachefiles.map{|s| IO.read(s)} }
if $options[:progress]
$progressbar.printf("PROGRESS:0\n")
$progressbar.printf("Watching %s\n",sw_name)
else
$out.puts("Done.\nWatching...") unless $options[:quiet]
end
end
sleep 1
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment