Created
September 29, 2012 17:43
-
-
Save ttscoff/3804682 to your computer and use it in GitHub Desktop.
If you've ever wondered whether I have psychological issues, see what happened to this script before I even finished it.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
# encoding: utf-8 | |
# nvAir: a First Class way to use nvALT/Notational Velocity with the worst documentation ever! | |
# Built by: Brett Terpstra, <http://brettterpstra.com> | |
# Destination: WikiLinks and [[wiki links]] to nvALT notes from anywhere on your system. | |
# This script is at your Service. (You can make it into a System Service with Automator, I mean). | |
# | |
# Security line is short today! Please have a photo id in hand. | | |
# I just need to see your ticket and the folder where you store your note files. o_________(_)________o | |
$notes_path = '~/Dropbox/nvALT2.2/' # o^o X \_/ X o^o | |
# --- Backscatter scanners are down. I'm going to have to frisk you. Sorry. | |
# The background music in the food court is brought to you courtesy of | |
# Jamis Buck and the FuzzyFileFinders: ______ | |
# |--------------------------------------------^ | | # \ | | |
# | https://github.com/jamis/fuzzy_file_finder | ____ \_________|----^"|"""""|"\___________ | | |
# |-------------------------------------------^ \___\ >> `"""""""" ===== "|D | |
# ^^-------____--""""""""""+""--_ __--"| | |
# `""|"-->####)+---|`"" | | |
# | |
# Boarding begins at line 25, please be at the Terminal by then. | |
# => I could probably come up with some correlation that makes | |
# this whole airplane theme make sense, but I won't. Or can't. Not even sure at this point. | |
#===================# # nvAIR WELCOMES YOU ON BOARD. | |
# __|__ # # Welcome to nvAIR. I'll be your steward today. | |
require 'cgi' # ---o-(_)-o--- # # If you have any special requirements, please notify one of us. | |
class String #-------------------# # On your left you'll see a small utility to turn | |
def break_camel # WikiLink into "Wiki link." | |
return downcase if match(/\A[A-Z]+\z/) # Excuse me, sir... your notes must be completely inside your notes folder | |
gsub(/([A-Z]+)([A-Z][a-z])/, '\1 \2'). # before takeoff. There is no space in the overhead compartment. | |
gsub(/([a-z])([A-Z])/, '\1 \2'). # We are unfortunately unable to accomodate child folders. | |
downcase # In case of emergency, a ctrl-c will drop from the | |
end # compartment above your terminal. | |
end | |
class WikiLinker # [^1] | |
def run(input, args) # We've been cleared for takeoff. | |
unless input.nil? # There's some STDIN input coming... | |
input.strip! # So there may be some minor turbulence. Please keep your seatbelt fastened. | |
if args.length > 0 # If the control tower has any arguments... | |
firstmatch = args # their decision on the note title is final. | |
else # Otherwise... | |
firstmatch = input.match(/\[\[(.*?)\]\]/) # Does anyone have a [[link]]? | |
if firstmatch.nil? # If there are no [[link]] volunteers... | |
# do we have any WikiLinks on board today? | |
firstmatch = input.match(/\b([A-Z][a-z]+)([A-Z][a-z]*)+\b/) | |
if firstmatch.nil? # Seriously folks, if there are no links at all... | |
# Why are you wasting my time? I'm outta here. | |
# *Grabs sodas from fridge and inflates emergency slide* | |
$stderr.puts "No wiki links found in text"; exit 1 | |
else # Ooh, sorry, Mr. WikiLink, I didn't see you in the back. | |
firstmatch = firstmatch[0] # You're eligible to be the note title. | |
end | |
else # Hold up, Mrs. [[link]]... | |
firstmatch = firstmatch[1] # is a First Class customer | |
# She gets priority seating and free note titles. | |
end # You can use the interpreter at the back of the script. | |
end | |
else | |
exit 0 | |
end | |
finder = FuzzyFileFinder.new($notes_path) # We've just made a new flight class available. It chooses passengers | |
# for upgrades based on their name. | |
# I swear it's not a race thing. Just profiling your name. | |
res = finder.find(firstmatch.break_camel) # This class is available only to passengers in the notes folder | |
res = res.sort {|a,b| | |
a[:score] <=> b[:score] | |
}.reverse[0][:path] if res.length > 1 | |
if res.length == 0 # If no one claims the free upgrade, | |
# We'll be producing our own in-flight movie. | |
# Your input will be used in the script, though. | |
txt = CGI.escapeHTML(input.sub(/\[\[(.*?)\]\]/,"\\1")) | |
# Paging Ms. [[link]]. No? Is Mr. WikiLink still on board? | |
# You're eligible for the only parachute on this craft... | |
title = CGI.escapeHTML(firstmatch.break_camel.capitalize) | |
# because this flight is going to be ending early. | |
txt = "New note" if txt == '' | |
note_url = "nvalt://make/?title=#{title}&txt=#{txt}" | |
puts note_url | |
%x{open "#{note_url}"} | |
%x{osascript -e 'tell app "nvALT" to activate'} | |
# Announcemnt from the captain: | |
# swap the line above with the line below to do a search for the WikiLink instead of creating a new note with it | |
# %x{open "nvalt://find/#{firstmatch.break_camel.capitalize.gsub(/ /,'%20')}"} | |
else # Oh, nevermind. We found a few extra turbines in the luggage compartment. | |
topmatch = res[0][:path] # we've attached our best guess to the wing. Pray for a safe landing. | |
# Here we go. Please remain seated with your seatbelts fastened. | |
%x{osascript -e 'tell app "nvALT" to open POSIX file "#{topmatch}"' -e 'tell app "nvALT" to activate' &} | |
# Success! *Crowd cheers in relief* | |
# | | |
end # o_________(_)________o | |
exit 0 # o^o X \_/ X o^o | |
# We know you have a choice when it comes to wiki linking. | |
# Thanks for choosing nvAIR, we hope to see you again soon. | |
end | |
end | |
### And now, THE FUZZY FILE FINDERS! | |
class FuzzyFileFinder | |
module Version | |
MAJOR = 1 | |
MINOR = 0 | |
TINY = 4 | |
STRING = [MAJOR, MINOR, TINY].join(".") | |
end | |
class TooManyEntries < RuntimeError; end | |
class CharacterRun < Struct.new(:string, :inside) #:nodoc: | |
def to_s | |
if inside | |
"(#{string})" | |
else | |
string | |
end | |
end | |
end | |
class FileSystemEntry #:nodoc: | |
attr_reader :parent | |
attr_reader :name | |
def initialize(parent, name) | |
@parent = parent | |
@name = name | |
end | |
def path | |
File.join(parent.name, name) | |
end | |
end | |
class Directory #:nodoc: | |
attr_reader :name | |
def initialize(name, is_root=false) | |
@name = name | |
@is_root = is_root | |
end | |
def root? | |
is_root | |
end | |
end | |
attr_reader :roots | |
attr_reader :files | |
attr_reader :ceiling | |
attr_reader :shared_prefix | |
attr_reader :ignores | |
def initialize(directories=['.'], ceiling=10_000, ignores=nil) | |
directories = Array(directories) | |
directories << "." if directories.empty? | |
root_dirnames = directories.map { |d| File.expand_path(d) }.select { |d| File.directory?(d) }.uniq | |
@roots = root_dirnames.map { |d| Directory.new(d, true) } | |
@shared_prefix = determine_shared_prefix | |
@shared_prefix_re = Regexp.new("^#{Regexp.escape(shared_prefix)}" + (shared_prefix.empty? ? "" : "/")) | |
@files = [] | |
@ceiling = ceiling | |
@ignores = Array(ignores) | |
rescan! | |
end | |
def rescan! | |
@files.clear | |
roots.each { |root| follow_tree(root) } | |
end | |
def search(pattern, &block) | |
pattern.gsub!(" ", "") | |
path_parts = pattern.split("/") | |
path_parts.push "" if pattern[-1,1] == "/" | |
file_name_part = path_parts.pop || "" | |
if path_parts.any? | |
path_regex_raw = "^(.*?)" + path_parts.map { |part| make_pattern(part) }.join("(.*?/.*?)") + "(.*?)$" | |
path_regex = Regexp.new(path_regex_raw, Regexp::IGNORECASE) | |
end | |
file_regex_raw = "^(.*?)" << make_pattern(file_name_part) << "(.*)$" | |
file_regex = Regexp.new(file_regex_raw, Regexp::IGNORECASE) | |
path_matches = {} | |
files.each do |file| | |
path_match = match_path(file.parent, path_matches, path_regex, path_parts.length) | |
next if path_match[:missed] | |
match_file(file, file_regex, path_match, &block) | |
end | |
end | |
def find(pattern, max=nil) | |
results = [] | |
search(pattern) do |match| | |
results << match | |
break if max && results.length >= max | |
end | |
return results | |
end | |
def inspect #:nodoc: | |
"#<%s:0x%x roots=%s, files=%d>" % [self.class.name, object_id, roots.map { |r| r.name.inspect }.join(", "), files.length] | |
end | |
private | |
def follow_tree(directory) | |
Dir.entries(directory.name).each do |entry| | |
next if entry[0,1] == "." | |
next if ignore?(directory.name) # Ignore whole directory hierarchies | |
raise TooManyEntries if files.length > ceiling | |
full = File.join(directory.name, entry) | |
if File.directory?(full) | |
follow_tree(Directory.new(full)) | |
elsif !ignore?(full.sub(@shared_prefix_re, "")) | |
files.push(FileSystemEntry.new(directory, entry)) | |
end | |
end | |
end | |
def ignore?(name) | |
ignores.any? { |pattern| File.fnmatch(pattern, name) } | |
end | |
def make_pattern(pattern) | |
pattern = pattern.split(//) | |
pattern << "" if pattern.empty? | |
pattern.inject("") do |regex, character| | |
regex << "([^/]*?)" if regex.length > 0 | |
regex << "(" << Regexp.escape(character) << ")" | |
end | |
end | |
def build_match_result(match, inside_segments) | |
runs = [] | |
inside_chars = total_chars = 0 | |
match.captures.each_with_index do |capture, index| | |
if capture.length > 0 | |
inside = index % 2 != 0 | |
total_chars += capture.gsub(%r(/), "").length # ignore '/' delimiters | |
inside_chars += capture.length if inside | |
if runs.last && runs.last.inside == inside | |
runs.last.string << capture | |
else | |
runs << CharacterRun.new(capture, inside) | |
end | |
end | |
end | |
inside_runs = runs.select { |r| r.inside } | |
run_ratio = inside_runs.length.zero? ? 1 : inside_segments / inside_runs.length.to_f | |
char_ratio = total_chars.zero? ? 1 : inside_chars.to_f / total_chars | |
score = run_ratio * char_ratio | |
return { :score => score, :result => runs.join } | |
end | |
def match_path(path, path_matches, path_regex, path_segments) | |
return path_matches[path] if path_matches.key?(path) | |
name_with_slash = path.name + "/" # add a trailing slash for matching the prefix | |
matchable_name = name_with_slash.sub(@shared_prefix_re, "") | |
matchable_name.chop! # kill the trailing slash | |
if path_regex | |
match = matchable_name.match(path_regex) | |
path_matches[path] = | |
match && build_match_result(match, path_segments) || | |
{ :score => 1, :result => matchable_name, :missed => true } | |
else | |
path_matches[path] = { :score => 1, :result => matchable_name } | |
end | |
end | |
def match_file(file, file_regex, path_match, &block) | |
if file_match = file.name.match(file_regex) | |
match_result = build_match_result(file_match, 1) | |
full_match_result = path_match[:result].empty? ? match_result[:result] : File.join(path_match[:result], match_result[:result]) | |
shortened_path = path_match[:result].gsub(/[^\/]+/) { |m| m.index("(") ? m : m[0,1] } | |
abbr = shortened_path.empty? ? match_result[:result] : File.join(shortened_path, match_result[:result]) | |
result = { :path => file.path, | |
:abbr => abbr, | |
:directory => file.parent.name, | |
:name => file.name, | |
:highlighted_directory => path_match[:result], | |
:highlighted_name => match_result[:result], | |
:highlighted_path => full_match_result, | |
:score => path_match[:score] * match_result[:score] } | |
yield result | |
end | |
end | |
def determine_shared_prefix | |
return roots.first.name if roots.length == 1 | |
split_roots = roots.map { |root| root.name.split(%r{/}) } | |
segments = split_roots.map { |root| root.length }.max | |
master = split_roots.pop | |
segments.times do |segment| | |
if !split_roots.all? { |root| root[segment] == master[segment] } | |
return master[0,segment].join("/") | |
end | |
end | |
return roots.first.name | |
end | |
end | |
input = '' | |
input = STDIN.read if STDIN.stat.size > 0 # There's a reason nobody does documentation like this. Now you know. | |
WikiLinker.new.run(input,ARGV.join(' ')) # .____ __ _ | |
# __o__ _______ _ _ _ / / | |
# \ ~\ / / | |
# \ '\ ..../ .' | |
# . ' ' . ~\ ' / / | |
# . _ . ~ \ .+~\~ ~ ' ' " " ' ' ~ - - - - - -''_ / | |
# . <# . - - -/' . ' \ __ '~ - \ | |
# .. - ~-.._ / |__| ( ) ( ) ( ) 0 o _ _ ~ . | |
# .-' .- ~ '-. -. | |
# < . ~ ' ' . . - ~ ~ -.__~_. _ _ | |
# ~- . N121PP . . . . . ,- ~ | |
# ' ~ - - - - =. <#> . \.._ | |
# . ~ ____ _ .. .. .- . | |
# . ' ~ -. ~ -. | |
# ' . . ' ~ - . ~-. | |
# ~ - . ~ . | |
# ~ -...0..~. ____ | |
# I don't know what the hell is wrong with me. I can't stop. Please. Help. | |
# [^1]: Sadly, the only reason this is a class is so I could embed FuzzyFileFinder at the botttom and not mess up my stolen ASCII art. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment