Skip to content

Instantly share code, notes, and snippets.

@ttscoff
Created September 29, 2012 17:43
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ttscoff/3804682 to your computer and use it in GitHub Desktop.
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.
#!/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