public
Created

QuickSilver plugin info.plist based templating

  • Download Gist
README.md
Markdown

QuickSilver plugin plist template tool

Synopsis

Searches a list of folders containing *.qsplugin/Contents/Info.plist, applies matching overrides, run a template with the data for each entry.

Base use case is generating wiki pages for documenting QuickSilver plugins.

Examples

./plisttemplate.rb -v -t basic -o ./overrides/ 'Core Support' OnePassword QSOpera 'Finder Module' Creates the following files:

  • com.blacktree.Quicksilver.QSCorePlugIn.txt
  • com.blacktree.Quicksilver.QSFinderPlugIn.txt
  • com.blacktree.Quicksilver.QSOpera.txt
  • com.robertson.Quicksilver.OnePassword.txt

Templates

Template engine is handled by Tilt, so you can use Markdown, haml and what not.

Partials are relative to the template path provided with the -t basic flag, so the partial for item is in basic/partials/_item.erb.

Upon loading a template, it's init.rb file is required. That's all that happens, so make it count. Templates can access the App instance through App.shared.

The template has all the bundle information available as methods of self (though this was a bad idea in retrospect). Brackets are very important or Ruby thinks we're talking about a constant (many keys have capital first letters).

Override mechanism

Use -o path to add a path of override files.

For every bundle loaded, the tool looks in any override paths provided for a file named bundle.id.plist (or bundle.id.yaml, eg: overrides/com.blacktree.Quicksilver.QSCorePlugIn.yaml). Each one is loaded, and the resulting hash table is 'merged into' the bundle's info plist. The data is then available to the templates.

Additionally, a key QSModifiedDate with the date of last modification of the .qsbundle's folder in the strftime format %Y-%m-%d %H:%M:%S %z is added to the root.

Localisations

When one or more languages are specified (with --language en,fr,de,it), for each override folder specified, a file is loaded from language_code/bundle.id.{plist,yaml}.

Note: No, thats not a good solution for the template text...

Requirements

sudo gem install plist tilt OptionParser mediawiki-gateway

basic/init.rb
Ruby
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
app = App.shared
plugins = {}
def output(name, template, data, locals = {})
context = RenderContext.new(data)
context.set_template_path(File.dirname(__FILE__))
App.shared.write_to_output(name, context.render(template, context, locals))
end
app.bundles.each do |bundle|
id = bundle['CFBundleIdentifier']
name = bundle['CFBundleName']
is_plugin = bundle['QSPlugIn'] && name
plugins[id] = bundle if is_plugin
output(id + ".txt", "#{is_plugin ? "plugin_home" : "external_bundle" }.erb", bundle)
output(MediaWiki::wiki_to_uri(name.gsub(/[\/()]/, "_")) + ".txt", "redirect.erb", bundle, :@destination => bundle) if is_plugin
end
output("ListOfPlugins.txt", "list.erb", plugins)
basic/list.erb
HTML+ERB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
<%
categories = {}
entries_by_name = values.sort { |a, b| a['CFBundleName'].downcase <=> b['CFBundleName'].downcase }
each_pair do |id, plug|
((plug['QSPlugIn'] || {})['categories'] || ["None"]).each {|c| (categories[c] ||= []) << plug}
end
entries_by_name.each do |plug| %>
*<%=bundle_link(plug)%><%end%>
 
===Categories===
 
<%
keys = categories.keys.sort { |a, b| a.downcase <=> b.downcase }
keys.each do |c|
list = categories[c].sort { |a, b| a['CFBundleName'].downcase <=> b['CFBundleName'].downcase } %>
* <%= c %><% list.each do |plug| %>
** <%=bundle_link(plug)%><%end%>
<%end%>
basic/partials/_command_items_table.erb
HTML+ERB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
<%
@current_table = { :col => nil, :stack => []}
def render_row args = {}
%w[pane1 pane2 pane3 reqs notes emphasis].each { |c| args[c.to_sym] ||= "" }
partial('command_items_table_row', self, args).strip() + "\n"
end
def flush_rows
stack = @current_table[:stack]
result = ""
unless stack.empty?
@current_table[:stack] = []
if stack.count > 1
col = @current_table[:col]
stack[0][col] = " rowspan=\"#{stack.count}\" | #{stack[0][col]}"
(stack.count - 1).times do |i|
item = stack[i + 1].dup
item[col] = :skip
stack[i + 1] = item
end
end
#puts stack.to_yaml
stack.each { |r| result << render_row(r) }
end
result
end
def push_row args = {}
col = args[:continue]
stack = @current_table[:stack]
must_flush = stack.count > 0
must_flush = (col != @current_table[:col]) || stack.last[col] != args[col] if must_flush
result = ""
result << flush_rows if must_flush
#puts "Broke #{@current_table[:stack].last[col].inspect} != #{args[col].inspect}" if @current_table[:stack].last[col] != args[col]
if col
@current_table[:col] = col
@current_table[:stack] << args
else
@current_table[:col] = nil
result << flush_rows
result << render_row(args)
end
result
end
#requiresBundle: org.mozilla.mozilla
def action_to_row id, action, action_index
direct_types = action['directTypes'] || ["*"]
direct_file_types = action['directFileTypes']
direct_list = list(direct_types.map { |t| qs_object_type_link(t, direct_file_types) }.sort)
indirect_types = action['indirectTypes'] || []
indirect_list = list(indirect_types.map { |t| qs_object_type_link(t) })
requirements = []
requirements << application_link(action['requiresBundle']) if action['requiresBundle']
requirements << "Feature level &gt; #{action['feature']}" if action['feature']
requirements << "Must be enabled first" if action['enabled'] === false
requirements = wrap(requirements.join("\n* "), "\n* ", "")
notes = []
notes << action['description'] if action['description']
notes << "Re-opens Quicksilver with the results" if action['displaysResult']
notes << "Hidden by default" if action['hidden']
alternate_action = action['alternateAction']
alternate_action = action_index[alternate_action] if alternate_action
notes << "Hold ⌘ to run ''#{alternate_action['name'] || alternate_action['id']}'' instead" if alternate_action
notes << "Implemented in AppleScript" if action['actionClass'] == "QSAppleScriptActions"
notes = wrap(notes.join("\n* "), "\n* ", "")
{
:emphasis => 2,
:continue => :pane1,
:pane1 => direct_list,
:pane2 => nowiki(action['name'] || id),
:pane3 => indirect_list,
:reqs => requirements,
:notes => notes,
}
end
def get_rows_of_actions
result = []
action_index = {}
each_action.each_pair do |id, action|
action_index[id] = action
end
action_index.each_pair do |id, action|
result << action_to_row(id, action, action_index)
end
result = result.sort do |a, b|
r = (a[:pane1].downcase <=> b[:pane1].downcase)
r == 0 ? a[:pane2].downcase <=> b[:pane2].downcase : r
end
result
end
def row_of_object_source object_source, requirements = nil, notes = nil, action = nil
{
:emphasis => 1,
:pane1 => object_source,
:pane2 => action,
:reqs => requirements,
:notes => notes,
}
end
rows = get_rows_of_actions()
#todo: add catalogs, with "<br />[→ into #{id} (Catalog) ]" and actions
each_command { |id, entry| rows << row_of_object_source(entry['name'] || id, nil, entry['description']) }
each_proxy { |id, entry| rows << row_of_object_source(qs_object_proxy_link(id, entry), nil, entry['description']) }
#todo: add possible types to action from key entry['types']
each_internal_object { |id, entry| rows << row_of_object_source(qs_object_internal_object_link(id, entry), nil, entry['description']) }
each_browsable_external_bundle { |id, entry| rows << row_of_object_source(application_link(id) + "<br />[→ into #{id} ]") }
%><% if rows.count > 0 %>
==Commands==
To execute [[Commands]]: select the Objects and Actions for each pane in Quicksilver, and press enter. Items exclusive to the plugin are in bold. Items in brackets are additional instructions for when typing in the panes.
{| class="wikitable"
|-
! Pane 1
! Pane 2
! Pane 3
! Extra Requirements
! Notes
<% rows.each do |e| %><%= push_row(e) %><% end %><%= flush_rows %>
|-
|}
<br />
<% end %>
basic/partials/_command_items_table_row.erb
HTML+ERB
1 2 3 4 5 6
|-
<%= pane1 == :skip ? "" : (emphasis == 1 ? "! align=\"left\" | " : "|") + pane1 + "\n" %>
<%= pane2 == :skip ? "" : (emphasis == 2 ? "! align=\"left\" | " : "|") + pane2 + "\n" %>
<%= pane3 == :skip ? "" : (emphasis == 3 ? "! align=\"left\" | " : "|") + pane3 + "\n" %>
|<%= reqs %>
|<%= notes %>
basic/partials/_preference_items_table.erb
HTML+ERB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
==Preference Items==
Activate Quicksilver and press ⌘comma to open [[Preferences]]. Each column represents a page within the main Preferences window. Best practise is to activate the items in the columns from left to right. Bullet points indicate items within the containing item. The items to check are in bold.
 
{| class="wikitable"
| Catalog
| Preferences
* Actions
|-
|
<% if each_catalog_entry.count > 0 %>
* Modules
<% each_catalog_entry do |entry, depth| %>
<%= "*" * (depth + 2) %> <%= qs_object_source_link(entry) %><%= entry['enabled'] === false ? " (''disabled'')" : "" %> <%= entry['description'] %>
 
<% end %><% end %>
<% if each_preference_pane.count > 0 %>
* Preference Panes
<% each_preference_pane do |entry_id, entry| %>
** <%= qs_pref_pane_link(entry_id, entry) %> <%= entry['description'] %>
 
<% end %><% end %>
<% if each_ui_controller.count > 0 %>
* Appearance
** Command Interface
<% each_ui_controller do |entry_id| %>
*** <%= qs_ui_controller_link(self, entry_id) %>
<% end %><% end %>
<br />
<% if each_proxy.count > 0 || each_internal_object.count > 0 || each_browsable_external_bundle.count > 0 %>
* Quicksilver
<% if each_proxy.count > 0 %>
** <b>Proxy Objects</b>
<% each_proxy do |id, entry| %>
*** <%= qs_object_proxy_link(id, entry) %> <%= entry['description'] %> <%= wrap(enum_with_sep(entry['types'], ' or '){ |e| qs_object_type_link(e) }, '(', ')')%>
<% end %><% end %>
<% if each_internal_object.count > 0 %>
** <b>Internal Objects</b>
<% each_internal_object do |id, entry| %>
*** <%= qs_object_internal_object_link(id, entry) %> <%= entry['description'] %> <%= wrap(enum_with_sep(entry['types'], ' or '){ |e| qs_object_type_link(e) }, '(', ')')%>
<% end %><% end %>
<% if each_browsable_external_bundle.count > 0 %>
** <b>Browsable bundles</b>
<% each_browsable_external_bundle do |id, value| %>
*** <%= application_link(id) %>
<% end %><% end %>
<% end %>
| align="up" |
<% each_action_sorted do |id, action| %>
* <%= wrap_tag(action['name'], 'b')%>
<% end %>
|-
|}
basic/plugin_home.erb
HTML+ERB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
==Overview==
<%= CFBundleName() %>
 
<% if QSPlugIn()['hidden'] %>
 
'''Plugin is hidden'''
 
<% end %>
----
 
<%= QSPlugIn()['description'] %>
 
<%= image(ScreenShot() && ScreenShot()['main']) %>
 
<%= partial('preference_items_table', self) %>
 
<%= partial('command_items_table', self) %>
<% if Tutorials() && Tutorials().count > 0 %>
 
==Tutorials==
 
<% Tutorials().each do |tut| %>
*<%= tut['title']%>
:<%=external_link(tut['link'])%><%=youtube(tut['youtube'])%><%=tut['description']%>
<%end%><%end%>
<% if QSPlugIn()['extendedDescription'] %>
 
==Description==
 
<%= QSPlugIn()['extendedDescription'] %>
 
<%end%>
 
<% if QSRequirements() && QSRequirements().count > 0 %>
 
==Requirements==
 
<%QSRequirements().each do |req|%>
<pre>
<%= req.to_yaml %>
</pre>
<% end %><% end %>
<% if QSPlugIn()['author'] || QSPlugIn()['homepage'] %>
 
==Credits==
 
<%= QSPlugIn()['author'] %>
 
<%= external_link(QSPlugIn()['homepage']) %>
 
<%= NSHumanReadableCopyright() %>
<% end %>
 
<% if true || App.shared.options[:debug] %>
==Debug==
<pre><%= self.to_yaml %></pre>
<%end%>
basic/redirect.erb
HTML+ERB
1
<%=redirect(bundle_link(@destination))%>
helpers/helpers.rb
Ruby
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
module Helpers
def when_not_empty value, &block
yield(value) if (value || '').to_s.length > 0
end
def wrap text = nil, prefix = "<nowiki>", suffix = "</nowiki>"
result = ""
text = (yield(text)) || text if block_given?
when_not_empty(text) { result = "#{prefix}#{text}#{suffix}" }
result
end
def wrap_tag text, tag = "nowiki", &block
wrap(text, "<#{tag}>", "</#{tag}>", &block)
end
def enum_with_sep enumerable, last_sep = ' and ', sep = ', '
return nil unless enumerable
result = []
have_item = false
prev_item = nil
enumerable.each do |*arguments|
if !have_item
have_item = true
else
result << sep if !result.empty?
result << prev_item
end
prev_item = block_given? ? yield(*arguments) : arguments.first
end
if have_item
result << last_sep if !result.empty?
result << prev_item
end
result.join("")
end
end
helpers/media_wiki_helpers.rb
Ruby
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
module MediaWikiHelpers
def nowiki text, &block
wrap_tag(text, "nowiki", &block)
end
def image path
wrap(path, "[[File:", "]]")
end
def internal_link url, title = nil
url = MediaWiki::wiki_to_uri(url.gsub(/[\/()]/, "_")) if url
url = (App.shared.options[:wiki_prefix] || "") + url if url
wrap("#{url}#{title ? "|#{title}" : ""}", "[[", "]]")
end
def external_link url, title = nil
wrap("#{url}#{title ? " #{title}" : ""}", "[", "]")
end
def hover label, text
wrap(text, "<span title=\"", "\" style=\"border-bottom:1px dotted\">#{label}</span>")
#wrap(text, "{{H:title|#{label}|", "}}")
end
def youtube id
wrap(id, "{{#ev:youtube|", "}}")
end
def application_link bundle_id
internal_link "Application_#{bundle_id}"
end
def redirect target_link
"#REDIRECT #{target}"
end
def list(enum, depth = 1)
prefix = "\n#{"*" * depth} "
wrap(enum ? enum.join(prefix) : nil, prefix, "")
end
end
helpers/qs_helpers.rb
Ruby
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
module QSLinkHelpers
def catalog_link id
return "qs://catalog##{id}"
end
def bundle_link bundle
internal_link(bundle['CFBundleIdentifier'], bundle['CFBundleName'])
end
def qs_object_source_link preset_entry
#external_link(catalog_link(preset_entry['ID']), preset_entry['name'])
internal_link("QSObjectSource_" + preset_entry['source'], preset_entry['name'] || preset_entry['ID'])
end
def qs_format_file_type_list list
enum_with_sep(list, ' or ')
end
def qs_object_type_link type, file_types = nil
name = qs_object_kind_get_name(type)
file_types = nil if !file_types || type != "NSFilenamesPboardType" || file_types.empty?
type = "_STAR_" if type == "*"
internal_link("ObjectType_" + type, name + (hover("(specific types)", wrap(qs_format_file_type_list(file_types), ' (', ')')) || ""))
end
def qs_pref_pane_link id, entry
name = entry['name'] || id
internal_link("PrefPane_" + id, entry['description'] ? hover(name, entry['description']) : name)
end
def qs_ui_controller_link bundle, entry
internal_link(bundle['CFBundleIdentifier'] + "_UI_" + entry, entry)
end
def qs_object_proxy_link id, proxy
name = proxy['name'] || id
internal_link("ObjectProxy_" + id, name)
end
alias_method :qs_object_internal_object_link, :qs_object_proxy_link
end
module QSHelpers
def qs_object_kind_get_name kind
quicksilver_bundle['QSKindDescriptions'][kind]
end
def each_command bundle = self, &blk # :yields: id, entry
return self.enum_for(:each_command, bundle) unless block_given?
items = (bundle['QSRegistration'] || {})['QSCommands']
(items || {}).each_pair(&blk)
end
def each_preference_pane bundle = self, &blk # :yields: id, internal_object
return self.enum_for(:each_preference_pane, bundle) unless block_given?
items = (bundle['QSRegistration'] || {})['QSPreferencePanes']
(items || {}).each_pair(&blk)
end
def each_ui_controller bundle = self, &blk
return self.enum_for(:each_ui_controller, bundle) unless block_given?
items = (bundle['QSRegistration'] || {})['QSCommandInterfaceControllers']
(items || {}).each_key(&blk)
end
def each_catalog_entry bundle = self, depth = 0, &blk # :yields: catalog_preset, number_of_parent_groups
return self.enum_for(:each_catalog_entry, bundle, depth) unless block_given?
root = bundle['QSPresetAdditions'] || bundle['children']
(root || []).each do |entry|
yield entry, depth
each_catalog_entry(entry, depth + 1, &blk) if entry['source'] == "QSGroupObjectSource"
end
end
def each_proxy bundle = self, &blk # :yields: id, proxy
return self.enum_for(:each_proxy, bundle) unless block_given?
proxies = (bundle['QSRegistration'] || {})['QSProxies']
(proxies || {}).each_pair(&blk)
end
def each_internal_object bundle = self, &blk # :yields: id, internal_object
return self.enum_for(:each_internal_object, bundle) unless block_given?
items = (bundle['QSRegistration'] || {})['QSInternalObjects']
(items || {}).each_pair(&blk)
end
def each_browsable_external_bundle bundle = self, &blk # :yields: bundle_id, handler_klass
return self.enum_for(:each_browsable_external_bundle, bundle) unless block_given?
bundle = (bundle['QSRegistration'] || {})
items = {}
(bundle['QSBundleChildHandlers'] || {}).each_pair { |k, v| items[k] = v}
(bundle['QSBundleChildPresets'] || {}).each_pair { |k, v| items[k] = v}
items.each(&blk)
end
def each_action bundle = self
(bundle['QSActions'] || {})
end
def each_action_sorted bundle = self, &blk # :yields: id, action
return self.enum_for(:each_action_sorted, bundle) unless block_given?
actions = each_action(bundle)
actions.each_pair { |id, action| action['name'] ||= (action['id'] = id) }
actions = actions.values.sort { |a, b| a['name'].downcase <=> a['name'].downcase }
actions.each { |e| yield(e['id'], e) }
end
end
overrides/com.blacktree.Quicksilver.QSOpera.yaml
YAML
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
QSPresetAdditions:
- ID: QSPresetOperaGroup
children:
- ID: QSPresetOperaBrowserControlScripts
description: Various scripts for copying or moving tabs between browsers
QSActions:
QSOperaAction:
description: Opens the URL with Opera regardless of the URL handler
QSPlugIn:
homepage: 'http://s-softs.com/Projects/QSOpera/index.html'
ScreenShot:
main: Open with Opera Action 1 1.jpg
Tutorials:
- title: Module setup and use.
youtube: IlWinySuoX4
overrides/com.blacktree.Quicksilver.yaml
YAML
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
QSKindDescriptions:
'*': any type
NSStringPboardType: plain text
NSColorPboardType: colors
NSFileContentsPboardType: file contents
NSFilesPromisePboardType: file promises
NSFontPboardType: fonts
NSHTMLPboardType: HTML content
NSPDFPboardType: PDF content
NSPICTPboardType: PICT content
NSPostScriptPboardType: PostScript content
NSRTFDPboardType: RTFD content
NSRTFPboardType: RTF content
NSRulerPboardType: Ruler content
NSTIFFPboardType: TIFF content
NSTabularTextPboardType: Tabular Text content
NSURLPboardType: URLs
NSVCardPboardType: vCards
QS1PasswordForm: QS1PasswordForm
QS1PasswordIdentity: QS1PasswordIdentity
QS1PasswordOnlineService: QS1PasswordOnlineService
QS1PasswordSecureNote: QS1PasswordSecureNote
QS1PasswordSoftwareLicense: QS1PasswordSoftwareLicense
QS1PasswordWalletItem: QS1PasswordWalletItem
QSFormulaType: QSFormulaType
QSKeychainAppleSharePasswordType: QSKeychainAppleSharePasswordType
QSKeychainGenericPasswordType: QSKeychainGenericPasswordType
QSKeychainInternetPasswordType: QSKeychainInternetPasswordType
QSRemoteHostsType: QSRemoteHostsType
WindowsType: WindowsType
YTDeMinWinType: YTDeMinWinType
com.apple.itunes.playlist: iTunes playlists
com.apple.itunes.qsbrowsercriteria: com.apple.itunes.qsbrowsercriteria
com.apple.itunes.track: iTunes track
qs.action: qs.action
qs.apple.iPhoto.album: qs.apple.iPhoto.album
qs.apple.iPhoto.photo: qs.apple.iPhoto.photo
qs.catalogentry: qs.catalogentry
qs.command: qs.command
qs.im.account: qs.im.account
qs.networklocation: qs.networklocation
qs.process: qs.process
qs.shelf: qs.shelf
qs.shellcommand: qs.shellcommand
qs.tag.file: qs.tag.file
qs.ui.element: qs.ui.element
overrides/com.robertson.Quicksilver.OnePassword.yaml
YAML
1 2 3 4 5 6 7 8 9 10
QSActions:
viewInOnePwd:
description: Opens the selected 1Password object in the 1Password application
QSPresetAdditions:
- ID: QSPresetOnePassword
children:
- ID: QSPreset1PasswordForm
description: List of Logins in 1Password
- ID: QSPreset1PasswordSecureNote
description: The SecureNotes stored in 1Password
overrides/fr/com.robertson.Quicksilver.OnePassword.yaml
YAML
1 2 3 4 5 6 7 8 9 10 11 12
QSPlugIn:
description: 'Permet à QuickSilver d''utiliser 1Password'
QSActions:
viewInOnePwd:
description: Ouvre l'objet séléctionné dans 1Password
QSPresetAdditions:
- ID: QSPresetOnePassword
children:
- ID: QSPreset1PasswordForm
description: Liste des identifiants stockés dans 1Password
- ID: QSPreset1PasswordSecureNote
description: Liste des notes sécurisées stockées dans 1Password
plist/init.rb
Ruby
1 2 3 4 5 6 7 8 9 10 11 12 13
app = App.shared
INCLUDE_KEY_IN_PLIST = %w[CFBundleIdentifier QSModifiedDate CFBundleName CFBundleVersion QSPlugIn QSRequirements CFBundleShortVersionString]
data = app.bundles.map do |e|
result = {}
e.each_pair do |k,v|
result[k] = v if INCLUDE_KEY_IN_PLIST.index(k)
end
result
end
# maybe add a filter for compatible QS version at this point ?
data = { :plugins => data }
data[:fullIndex] = true # Remove for incremental updates
app.write_to_output(app.options[:plist], data.to_plist)
plisttemplate.rb
Ruby
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409
#!/usr/bin/env ruby
begin
%w[plist erb tilt pathname pp optparse shellwords yaml media_wiki].
each { |e| require e }
rescue LoadError => e
retry if require 'rubygems'
raise
end
 
$:.unshift(File.expand_path(File.join(File.dirname(__FILE__), '.')))
require "helpers/helpers"
require "helpers/media_wiki_helpers"
require "helpers/qs_helpers"
 
QS_BUNDLE_ID = "com.blacktree.Quicksilver"
class PluginInfoLoader
def initialize options
@opt = options
@bundles = {}
@registry = nil
@@merge_policy = lambda do |key, old_val, new_val|
return new_val if old_val.nil?
return old_val if new_val.nil?
if old_val.is_a?(Array)
if new_val.is_a?(Array)
if old_val.all? { |e| e['ID'] }
old_items_by_id = {}
new_items_by_id = {}
old_val.each { |entry| old_items_by_id[entry['ID']] = entry }
new_val.each { |entry| new_items_by_id[entry['ID']] = entry }
old_items_by_id.merge!(new_items_by_id, &@@merge_policy)
old_val = old_items_by_id.values
else
old_val += new_val
end
else
raise "Cannot merge #{key}, incompatible types: #{old_val.inspect} and #{new_val.inspect}"
end
old_val
elsif old_val.is_a?(Hash)
if new_val.is_a?(Hash)
old_val.merge!(new_val, &@@merge_policy)
else
raise "Cannot merge #{key}, incompatible types: #{old_val.inspect} and #{new_val.inspect}"
end
old_val
else
new_val
end
end
end
def load_overrides_for_id id
result = {}
@opt[:override_paths].each do |path|
{
"plist" => lambda { |file_path| Plist::parse_xml(file_path) },
"yaml" => lambda { |file_path| File.open(file_path) { |file| YAML.load(file) } }
}.each_pair do |extension, loader|
item = Pathname.new(path) + "#{id}.#{extension}"
item = item.realpath if item.exist?
hash = item.exist? ? loader.call(item.to_s) : nil
if hash && hash.is_a?(Hash)
$stderr.puts "Merging with overrides from #{item.to_s}" if @opt[:verbose]
result.merge!(hash, &@@merge_policy)
end
end
end
result
end
def reading_error path
return "" unless @opt[:verbose]
File.exists?(path) ?
"\n\t (I only understand xml plists, and hate encoding errors, so if you dont mind looking into this: " +
"\n\t - check with: iconv -t utf-8 '#{path}'" +
"\n\t - edit with: ${EDITOR:-mate} '#{path}'" +
"\n\t - convert with: plutil -convert xml1 '#{path}'" +
"\n\t )" : ""
end
def load_override_localisations id
result = {}
@opt[:languages].reverse.each do |lang|
overrides = load_overrides_for_id("#{lang}/#{id}")
result.merge!(overrides, &@@merge_policy) if overrides
end
result
end
def load_localisations id, path
lang_rx = @opt[:languages].map { |e| /.*?\/#{e}\.lproj\/.*/i }
result = {}
path = Pathname.new(path + "Contents/Resources/")
{ 'QSAction' => ['name', 'commandFormat', 'description'],
'QSObjectSource' => ['name'],
'QSCatalogPreset' => ['name']
}.each_pair do |root_key, properties|
properties.each do |property_name|
picked_file = nil
files = Dir[path + "*.lproj/#{root_key}.#{property_name}.strings"]
if !files.empty?
lang_rx.each do |lang|
picked_file = files.select { |e| lang =~ e.to_s }.first
break if picked_file
end
picked_file = files.first unless picked_file
end
picked_file = path + "#{root_key}.#{property_name}.strings" unless picked_file
if picked_file && File.exists?(picked_file.to_s)
$stderr.puts "Using localisations from #{picked_file.inspect}" if @opt[:verbose]
begin
values = Plist::parse_xml(picked_file.to_s)
raise "No data read" unless values && values.is_a?(Hash)
root_key_name = root_key
case root_key
when "QSAction"
root_key_name = "QSActions" # =(
end
values.each_pair { |k, v| ((result[root_key_name] ||= {})[k] ||= {})[property_name] = v }
rescue Exception => e
$stderr.puts "Error: Reading localisation in '#{picked_file}': #{e}.#{reading_error(picked_file.to_s)}"
end
end
end
end
result.merge!(load_override_localisations(id), &@@merge_policy)
result
end
def find_with_mdutil id
# You're not supposed to use Shellwords.shellescape in quotes... hope that's ok... Bundlenames shouldn't pose a problem, but a malicious plist...
raise "Must make find_with_mdutil safe before using it elsewhere" unless id =~ /^[a-z0-9\.-_]+$/
`mdfind 'kMDItemContentType == "com.apple.application-bundle" && kMDItemCFBundleIdentifier == "#{Shellwords.shellescape(id)}"'`.split("\n").first
end
def load_quicksilver_plists path
result = {}
%w[ResourceLocations QSKindDescriptions].each do |plist|
res_path = path + "Contents/Resources/#{plist}.plist"
if res_path.file?
$stderr.puts "Using Quicksilver ressource #{res_path.to_s.inspect}" if @opt[:verbose]
begin
values = Plist::parse_xml(res_path.to_s)
(result[plist] ||= {}).merge!(values, &@@merge_policy)
rescue Exception => e
$stderr.puts "Error: Reading Quicksilver ressource in '#{res_path}': #{e}.#{reading_error(res_path.to_s)}"
end
end
end
result
end
def load_global_data id = QS_BUNDLE_ID
quicksilver_path = @opt[:qs_app_path] || find_with_mdutil(id)
quicksilver_path = Pathname.new(quicksilver_path).expand_path if quicksilver_path
result = load_quicksilver_plists(quicksilver_path)
$stderr.puts "Warning: Nothing loaded from Quicksilver.app. Check path: #{quicksilver_path.inspect}" if result.empty?
overrides = load_overrides_for_id(id)
result.merge!(overrides, &@@merge_policy) if overrides
result.merge!(load_override_localisations(id), &@@merge_policy)
@bundles[id] = result
result
end
def load_qsplugin path
path = Pathname.new(path) unless path.is_a?(Pathname)
path = path.realpath if path.exist?
begin
if !path.directory?
raise "Couldnt find #{path}!" if @opt[:verbose]
return nil
end
$stderr.puts "Loading #{path}" if @opt[:verbose]
result = Plist::parse_xml(path + 'Contents/info.plist')
id = result["CFBundleIdentifier"]
if result && id
result["QSModifiedDate"] = path.stat.mtime.strftime("%Y-%m-%d %H:%M:%S %z")
result.merge!(self.load_overrides_for_id(id), &@@merge_policy)
result.merge!(self.load_localisations(id, path), &@@merge_policy)
$stderr.puts "More that one plugin with same ID, second at #{path}" if @opt[:verbose] && @bundles[id]
@bundles[id] = result
return result
end
rescue Exception => e
$stderr.puts "Warning: #{e}#{@opt[:verbose] ? reading_error(path) : ""}"
raise if @opt[:debug]
end
return nil
end
attr_reader :registry
def build_registry
@registry = {}
resources = @registry['QSResourceAdditions'] = {}
@bundles.each_pair do |id, bundle|
bundle = YAML.load(bundle.to_yaml) #yuk
reg = bundle['QSRegistration']
if reg
reg.each_pair do |name, val|
val.values.each { |e| e[:provided_by] = id } if val.values.all? { |e| e.is_a?(Hash) }
end
@registry.merge!({'QSRegistration' => reg}, &@@merge_policy)
end
res = bundle['QSResourceAdditions']
if res
res.each_pair do |k, v|
$stderr.puts "Warning: Resource is defined twice: #{resources[k].inspect} and #{v.inspect}" if resources[k]
v = v.dup
v[:provided_by] = id if v.is_a?(Hash)
resources[k] = v
end
@registry.merge!({'QSResourceAdditions' => res}, &@@merge_policy)
end
end
end
def load name
@opt[:plugin_paths].map do |path|
item = Pathname.new(path) + name
item = item.realpath if item.exist?
item = Pathname.new(path) + "#{name}.qsplugin" unless item.directory?
item = item.realpath if item.exist?
result = self.load_qsplugin(item)
return result if result
end
return nil
end
def load_all
result = []
@opt[:plugin_paths].map do |path|
item = Pathname.new(path)
item = item.realpath if item.exist?
plugins = Dir[item + "*.qsplugin"]
result += plugins.map { |e| load_qsplugin(e) }.select {|e| e}.to_a
end
build_registry
result
end
end
 
module RenderingHelpers
def app
App.shared
end
def quicksilver_bundle
app.quicksilver_bundle
end
@@template_path = nil
def load_template name
result = (@@templates ||= {})[name.to_s]
return result if result
name = name.to_s
file_name = (@@template_path || ".") + "/" + name
if !File.exist?(file_name)
matches = Dir[file_name + ".*"]
raise "Error loading template #{name} from #{file_name} (#{matches.count} matches)" if matches.count != 1
file_name = matches[0]
end
@@template_path ||= File.dirname(file_name.to_s)
@@templates[name.to_s] = Tilt.new(file_name.to_s, :trim => "<>")
end
def set_template_path path, clear = true
@@template_path = path
@@templates = {} if clear
end
def render_depth substract = 0
result = 0
cursor = self
while cursor.parent
cursor = cursor.parent
result += 1
end
result - substract
end
def render name, item, locals = {}
template = load_template(name)
item = RenderContext.new(item, self) if item.is_a?(Hash)
template.render(item, locals)
end
def partial name, item, locals = {}
render("partials/_#{name}", item, locals)
end
end
class RenderContext
include Helpers
include RenderingHelpers
include MediaWikiHelpers
include QSLinkHelpers
include QSHelpers
def initialize hash, parent = nil
@vals = {}
@parent = parent
hash.each_pair { |name, val| @vals[name] = val } if hash
end
attr_reader :parent
def [](index)
result = @vals[index.to_s]
return RenderContext.new(result, self) if result && result.is_a?(Hash)
result
end
def method_missing(meth, *args, &blk)
result = self[meth.to_s]
if !result && @vals.respond_to?(meth)
@vals.send(meth, *args, &blk)
else
result
end
end
end
class App
@@default = nil
def self.shared
@@default
end
def initialize(options = {})
@options = options
raise "Cannot have more than one App" if @@default
@@default = self
end
attr_reader :options, :bundles, :quicksilver_bundle
def parse_options
args = OptionParser.new do |opts|
opts.banner = "Usage: plisttemplate.rb [options] [plugin base name]"
 
opts.on("--out PATH", "Prefix all output files to specified path") do |v|
@options[:output] = v
end
 
opts.on("-o", "--override-path PATH", "Add path containing override plists") do |v|
@options[:override_paths] << v
end
 
opts.on("--plist NAME", "Write a plist stream of the plugins to the specified filename") do |v|
@options[:templates] << "plist"
@options[:plist] = v
end
 
opts.on("-p", "--plugin-search-path PATH", "Add path containing .qsplugin folders") do |v|
@options[:plugin_paths] << v
end
 
opts.on("-l", "--languages en,fr,de,it", "Set languages to try and use when multiple are available") do |v|
@options[:languages] = v.split(/[^a-z-]+/i)
end
 
opts.on("-t", "--templates FILEPATH", "Add template to run. Must contain an init.rb file.") do |v|
@options[:templates] << v
end
 
opts.on("--wiki-prefix PREFIX", "Add prefix to all local wiki links when creating Page name") do |v|
@options[:wiki_prefix] = v
end
 
opts.on("--qs-path PATH", "Set the path to Quicksilver.app from which load resources") do |v|
@options[:qs_app_path] = v
end
 
opts.on("-v", "--[no-]verbose", "Run verbosely") do |v|
@options[:verbose] = v
end
opts.on("-d", "--[no-]debug", "Output debug information") do |v|
@options[:debug] = v
@options[:verbose] = v if v
end
end
begin
args.parse!
rescue Exception => e
$stderr.puts("Error in the arguments: #{e}\n\n")
$stderr.puts(args)
exit 1
end
 
if @options[:plugin_paths].empty?
@options[:plugin_paths] += ['~/Library/Application Support/Quicksilver/PlugIns',
'/Applications/Quicksilver.app/Contents/PlugIns/'].map { |e| Pathname.new(e).expand_path }
end
end
def run_template(name)
template_init = (Pathname.new(name) + 'init.rb').expand_path
if template_init.file?
require template_init.to_s.gsub(/.rb$/i, "")
end
end
def run
parse_options
@loader = PluginInfoLoader.new @options
@quicksilver_bundle = @loader.load_global_data
@bundles = []
raise "You need to specify which plugins to generate on the command line. If you really want to generate all, specify '*'" if ARGV.count == 0
raise "Must specify at least one template, eg: -t basic or --plist plugins.xml" if @options[:templates].empty?
@bundles = ARGV == ["*"] ? @loader.load_all : ARGV.map { |e| @loader.load(e) }
if !File.directory?(@options[:output])
FileUtils::makedirs(@options[:output])
raise "Directory '#{@options[:output]}' doesn't exist" if !File.directory?(@options[:output])
end
@bundles.each_with_index do |input, index|
raise "Couldn't find plugin (#{ARGV[index] || "<unidentifiable>"})#{@options[:verbose] ? "" : " (run with verbose: -v to see search paths)"}" if input.nil?
end
@options[:templates].each { |t| run_template(t) }
end
def registry
@loader.registry
end
def write_to_output(filename, contents)
output_path = @options[:output] + "/" + filename
puts "Writing output to #{output_path}" if options[:verbose]
File.open(output_path, 'w') {|f| f.write(contents) }
end
end
App.new({
:plugin_paths => [],
:override_paths => [],
:languages => ['en'],
:output => './',
:plist => "plugins.plist",
:templates => [],
}).run
wikirobot.rb
Ruby
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104
#!/usr/bin/env ruby
 
begin
%w[media_wiki pathname shellwords pp].each { |e| require e }
rescue LoadError => e
retry if require 'rubygems'
raise
end
class App
def parse_options
@options = {
:wiki_url => "https://192.168.0.125/w/api.php",
:wiki_creds => ['guest', 'guest'],
:wiki_prefix => "Auto/",
:wiki_comment => 'Robot commit',
}
arguments = OptionParser.new do |opts|
opts.banner = "Upload pages and files to a MediaWiki server\nUsage: wiki.rb [options] files_to_upload.\n.txt get their extension removed."
 
opts.on("--wiki URL", "Path to wiki api.php") do |v|
@options[:wiki_url] = v
end
opts.on("-c", "--credentials USER", "Username to access the wiki") do |v|
res = v.split(":", 2)
raise "Credentials must be in the format user:pass" unless res.count == 2
raise "Credential username cannot be empty" unless !res[0].empty?
@options[:wiki_creds] = res
end
opts.on("-k", "--use-keychain ITEM_NAME", "Load credentials from Mac OS X Keychain") do |v|
keychain = {}
`security find-internet-password -gl #{Shellwords.shellescape(v)} 2>&1`.
scan(/^\s*(?:([^:]+): |"([a-z0-9 _]{4})"<[^>]*>=)"(.*)"$/i).each { |entry| keychain[entry[0] || entry[1]] = entry[2] if entry[2]}
raise "Keychain item #{v.inspect} not found (or no account)" if !keychain["acct"] || keychain["acct"].empty?
raise "Keychain item #{v.inspect} not found (or no password)" if !keychain["password"]
@options[:wiki_creds] = [keychain["acct"], keychain["password"]]
$stderr.puts "Using account #{keychain["acct"].inspect} from keychain"
end
opts.on("-m", "--comment MSG", "Comment to use on wiki page updates") do |v|
@options[:wiki_comment] = v.split(":")
end
opts.on("-p", "--prefix PREFIX", "Always add prefix to any Page name") do |v|
@options[:wiki_prefix] = v
end
opts.on("-l", "--list [PREFIX]", "List pages matching PREFIX and exit") do |v|
@options[:ls] = v || ""
end
opts.on("--delete-all PREFIX", "Delete all pages matching PREFIX and exit") do |v|
@options[:delete_all] = v
end
end
begin
arguments.parse!
rescue Exception => e
puts "Error: #{e}\n\n"
puts arguments
exit 1
end
end
def get_page_name file
file = file[0..-5] if file =~ /\.txt$/i
@options[:wiki_prefix] + File.basename(file)
end
def login
return @wm if @wm
mw = MediaWiki::Gateway.new(@options[:wiki_url])
mw.login(*@options[:wiki_creds])
$stderr.puts "Logged on to #{@options[:wiki_url]} as #{@options[:wiki_creds][0]}" +
(@options[:wiki_prefix] ? " (using prefix #{@options[:wiki_prefix].inspect})" : "")
@wm = mw
end
def upload(file, name)
cx = login
$stderr.puts "Uploading #{name} from #{file}"
result = nil
File.open(file, "r") { |f| result = cx.edit(name, f.read(), :summary => @options[:wiki_comment]) }
puts result[0].to_s
end
def run
parse_options
if @options[:ls]
prefix = @options[:wiki_prefix] + @options[:ls]
$stderr.puts "List matching #{prefix.inspect}"
p login.list(prefix)
exit 0 unless @options[:delete_all]
end
if @options[:delete_all]
prefix = @options[:wiki_prefix] + @options[:delete_all]
list = login.list(prefix)
$stderr.puts "Deleting all pages matching #{prefix.inspect} (#{list.count} pages)"
list.each do |p|
login.delete p
$stderr.puts " => #{p.inspect} deleted"
end
exit 0
end
ARGV.each do |page|
path = Pathname.new(page).expand_path
if path.exist?
upload(path, get_page_name(path.to_s))
end
end
end
end
App.new.run

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.