osxsub 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ coverage
6
+ InstalledFiles
7
+ lib/bundler/man
8
+ pkg
9
+ rdoc
10
+ spec/reports
11
+ test/tmp
12
+ test/version_tmp
13
+ tmp
14
+
15
+ # YARD artifacts
16
+ .yardoc
17
+ _yardoc
18
+ doc/
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ gem "rake"
data/Gemfile.lock ADDED
@@ -0,0 +1,16 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ osxsub (0.1.0)
5
+
6
+ GEM
7
+ remote: http://rubygems.org/
8
+ specs:
9
+ rake (10.0.3)
10
+
11
+ PLATFORMS
12
+ ruby
13
+
14
+ DEPENDENCIES
15
+ osxsub!
16
+ rake
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright © 2013 Mark Wunsch
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject
9
+ to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
17
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
18
+ ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
19
+ CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,59 @@
1
+ `osxsub`
2
+
3
+ A command line utility for using the OS X Text Substitution System Preferences in a unix pipeline. Like `sed` but for Mac users who are taking advantage of a mostly hidden system preference.
4
+
5
+ Basic usage is like this:
6
+
7
+ $ echo "Copyright (c) 2013 Mark Wunsch" | osxsub
8
+ Copyright © 2013 Mark Wunsch
9
+
10
+ Text Substitutions are available for a handful of Mac applications, and can be set up by going to `System Preferences -> Language & Text` and selecting the `Text` tab.
11
+
12
+ Read more about OS X Text Substitution:
13
+
14
+ + [_How to use text substitution in Snow Leopard_ -- Macworld](http://www.macworld.com/article/1142708/slsubstitutions.html)
15
+ + [_Mac 101: Making Text Replacement Work_ -- TUAW](http://www.tuaw.com/2009/12/31/mac-101-making-text-replacement-work/)
16
+ + [_Do Yourself a Favor: Set Up Mountain Lion’s Built-in Text Expansion with These Shortcuts_ -- Lifehacker](http://lifehacker.com/5931337/do-yourself-a-favor-set-up-mountain-lions-built+in-text-expansion-with-these-shortcuts)
17
+
18
+ The goals for this program are:
19
+
20
+ + Backup your text substitution preferences, along with the ability to
21
+ + Load new preferences, so that you can
22
+ + Share substitution preferences across machines, and
23
+ + Have access to them in a greater set of applications, by
24
+ + Allowing Text Substitutions to be a part of your command line toolkit.
25
+
26
+ Some more options can be given to the tool to make managing your Preferences easy:
27
+
28
+ $ osxsub --print
29
+ ## Print a plist of your substition preferences
30
+
31
+ $ osxsub --merge PATH_TO_PLIST
32
+ ## Merge another plist into your preferences.
33
+
34
+ $ osxsub --add REPLACE,WITH
35
+ ## Add a new pair of substitutions (they're turned on by default).
36
+
37
+ $ osxsub --clear
38
+ ## Clear all your preferences.
39
+
40
+ $ osxsub --repl
41
+ ## Start an interactive session to test out substitutions.
42
+
43
+ ## Installation
44
+
45
+ A Homebrew Formula is available, but has yet to be moved to the master Homebrew repo: https://gist.github.com/4657560
46
+
47
+ Alternatively, `osxsub` is available as a Rubygem.
48
+
49
+ $ gem install osxsub
50
+
51
+ Or clone the repository and put the `bin` directory somewhere on your `$PATH`.
52
+
53
+ ## TODO
54
+
55
+ + Manpages
56
+ + Documentation
57
+ + Tests
58
+
59
+ Copyright © 2013 Mark Wunsch.
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
data/bin/osxsub ADDED
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env ruby
2
+ require 'optparse'
3
+
4
+ libdir = File.expand_path File.join(File.dirname(__FILE__), "/../lib")
5
+ $:.unshift libdir unless $:.include? libdir
6
+ require 'osxsub'
7
+
8
+ options = ARGV.options do |opts|
9
+ opts.banner = <<-BANNER
10
+ osxsub: OS X Text Substitution Preferences on the command line
11
+
12
+ Usage: Run STDIN through Text Substitions set in System Preferences -> Language & Text
13
+
14
+ echo "Copyright (c) 2013 Mark Wunsch" | osxsub
15
+ #=> Copyright © 2013 Mark Wunsch
16
+
17
+ BANNER
18
+ opts.version = OsxSub::VERSION
19
+
20
+ opts.on('-p', '--print', "Print an XML Plist of the Text Substitution Preferences") do
21
+ puts OsxSub::NSUserReplacementItems.from_plist_buddy.to_plist
22
+ exit
23
+ end
24
+
25
+ opts.on('-m', '--merge [PATH]', "Merge an XML Plist (from either PATH or STDIN) into the Preferences") do |path|
26
+ xml = if path && !path.empty?
27
+ full_path = File.expand_path(path)
28
+ opts.abort("Plist file does not exist!") unless File.exists? full_path
29
+ IO.read(full_path)
30
+ elsif !STDIN.tty?
31
+ STDIN.read
32
+ else
33
+ opts.abort("Must give either a PATH or a Plist in STDIN")
34
+ end
35
+ subs = OsxSub::NSUserReplacementItems.from_xml(xml)
36
+ subs.merge!
37
+ puts subs.to_plist
38
+ exit
39
+ end
40
+
41
+ opts.on('-a', '--add REPLACE,WITH', Array, "Add a new pair of substitions to the list") do |list|
42
+ opts.abort "REPLACE,WITH must have a pair and they can't be the same" if list.first == list.last
43
+ sub = OsxSub::Substitution.new(list.first, list.last)
44
+ replacement_item = OsxSub::NSUserReplacementItems.new([sub])
45
+ replacement_item.merge!
46
+ puts replacement_item.to_plist
47
+ exit
48
+ end
49
+
50
+ opts.on('-r', '--repl', "Start a REPL (read-eval-print loop)") do
51
+ opts.abort("Can't start REPL unless STDIN is a tty") unless STDIN.tty?
52
+ opts.warn("Send an EOT signal (Ctrl-D) to end")
53
+ print ">> "
54
+ while STDIN.gets
55
+ puts " => " + OsxSub::NSUserReplacementItems.from_plist_buddy.sub($_)
56
+ print ">> "
57
+ end
58
+ puts ""
59
+ exit
60
+ end
61
+
62
+ opts.on('-c', '--clear', "Clear out the set of substitution preferences") do
63
+ plist_buddy = OsxSub::PlistBuddy.instance
64
+ begin
65
+ plist_buddy.delete
66
+ plist_buddy.add
67
+ rescue OsxSub::PlistBuddyError => e
68
+ plist_buddy.add unless e.exists?
69
+ end
70
+ puts plist_buddy.print
71
+ exit
72
+ end
73
+
74
+ opts.separator ""
75
+
76
+ opts.on_tail('-h', '--help') { puts opts ; exit }
77
+ opts.on_tail('--version', "Print version information and exit") { puts opts.version ; exit }
78
+ begin
79
+ opts.parse!
80
+ rescue OptionParser::ParseError
81
+ opts.warn $!
82
+ end
83
+ opts
84
+ end
85
+
86
+ if !STDIN.tty?
87
+ STDIN.each_line do |line|
88
+ STDOUT.write OsxSub::NSUserReplacementItems.from_plist_buddy.sub(line)
89
+ end
90
+ exit
91
+ end
92
+
93
+ abort options.help
94
+
@@ -0,0 +1,107 @@
1
+ require 'rexml/document'
2
+
3
+ module OsxSub
4
+ class NSUserReplacementItems
5
+ PLIST_TEMPLATE = <<-PLIST
6
+ <?xml version="1.0" encoding="UTF-8"?>
7
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
8
+ <plist version="1.0">
9
+ <array>
10
+ <% substitutions.each do |substitution| %>
11
+ <dict>
12
+ <% if substitution.on? %>
13
+ <key>on</key>
14
+ <integer>1</integer>
15
+ <% end %>
16
+ <key>replace</key>
17
+ <string><%= substitution.replace %></string>
18
+ <key>with</key>
19
+ <string><%= substitution.with %></string>
20
+ </dict>
21
+ <% end %>
22
+ </array>
23
+ </plist>
24
+ PLIST
25
+
26
+ def self.from_plist_buddy
27
+ plist_buddy = PlistBuddy.instance
28
+ self.from_xml plist_buddy.print
29
+ rescue PlistBuddyError => e
30
+ unless e.exists?
31
+ warn("#{e}Adding and Merging default preferences")
32
+ plist_buddy.add
33
+ self.defaults.merge!
34
+ retry
35
+ end
36
+ end
37
+
38
+ def self.from_xml(xml)
39
+ doc = REXML::Document.new(xml, :ignore_whitespace_nodes => :all)
40
+ plist = doc.elements['plist/array'].map do |dict|
41
+ tuple = dict.each.select {|node| node.has_name?("key") }.map do |key|
42
+ [key.text, key.next_element.text]
43
+ end
44
+ Hash[tuple]
45
+ end
46
+ self.new plist.map {|dict| Substitution.new(dict['replace'], dict['with'], dict['on']) }
47
+ end
48
+
49
+ def self.defaults
50
+ default_substitutions = [
51
+ Substitution.new("(c)", "\xC2\xA9"),
52
+ Substitution.new("(r)", "\xC2\xAE"),
53
+ Substitution.new("(p)", "\xE2\x84\x97"),
54
+ Substitution.new("TM", "\xE2\x84\xA2"),
55
+ Substitution.new("c/o", "\xE2\x84\x85"),
56
+ Substitution.new("...", "\xE2\x80\xA6"),
57
+ Substitution.new("1/2", "\xC2\xBD", false),
58
+ Substitution.new("1/3", "\xE2\x85\x93", false),
59
+ Substitution.new("2/3", "\xE2\x85\x94", false),
60
+ Substitution.new("1/4", "\xC2\xBC", false),
61
+ Substitution.new("3/4", "\xC2\xBE", false),
62
+ Substitution.new("1/8", "\xE2\x85\x9B", false),
63
+ Substitution.new("3/8", "\xE2\x85\x9C", false),
64
+ Substitution.new("5/8", "\xE2\x85\x9D", false),
65
+ Substitution.new("7/8", "\xE2\x85\x9E", false)
66
+ ]
67
+ self.new(default_substitutions)
68
+ end
69
+
70
+ attr_reader :substitutions
71
+
72
+ def initialize(substitutions)
73
+ @substitutions = substitutions
74
+ end
75
+
76
+ def sub(text)
77
+ substitutions.select(&:on?).inject(text) {|memo, obj| obj.sub(memo) }
78
+ end
79
+
80
+ def to_plist
81
+ require 'erb'
82
+ template = ERB.new(PLIST_TEMPLATE)
83
+ template.result(binding)
84
+ end
85
+
86
+ def merge!
87
+ tmp_file = temp_plist_file
88
+ PlistBuddy.instance.merge tmp_file.path
89
+ ensure
90
+ if tmp_file
91
+ tmp_file.close
92
+ tmp_file.unlink
93
+ end
94
+ end
95
+
96
+ private
97
+
98
+ def temp_plist_file
99
+ require 'tempfile'
100
+ tmp_file = Tempfile.new("nsuser_replacement_items_plist")
101
+ tmp_file.write to_plist
102
+ tmp_file.rewind
103
+ tmp_file
104
+ end
105
+ end
106
+ end
107
+
@@ -0,0 +1,50 @@
1
+ module OsxSub
2
+ class PlistBuddyError < StandardError
3
+ def exists?
4
+ !message.match(/Does Not Exist$/)
5
+ end
6
+ end
7
+
8
+ class PlistBuddy
9
+ require 'singleton'
10
+
11
+ include Singleton
12
+
13
+ PLIST_BUDDY = '/usr/libexec/PlistBuddy'
14
+ GLOBAL_PREFERENCES_PLIST = File.expand_path('~/Library/Preferences/.GlobalPreferences.plist')
15
+ NSUSER_REPLACEMENT_ITEMS = 'NSUserReplacementItems'
16
+
17
+ def print
18
+ execute plist_buddy(:print)
19
+ end
20
+
21
+ def delete
22
+ execute plist_buddy(:delete)
23
+ end
24
+
25
+ def add
26
+ execute plist_buddy(:add, "array")
27
+ end
28
+
29
+ def merge(path)
30
+ execute plist_buddy("Merge #{path}")
31
+ end
32
+
33
+ def plist_buddy(command, *extras)
34
+ %Q[#{PLIST_BUDDY} -x -c "#{command.to_s.capitalize} #{NSUSER_REPLACEMENT_ITEMS} #{extras.join(' ')}" #{GLOBAL_PREFERENCES_PLIST}]
35
+ end
36
+
37
+ def execute(command)
38
+ require 'stringio'
39
+ require 'open3'
40
+
41
+ out, err = ::StringIO.new, ::StringIO.new
42
+ ::Open3.popen3(command) do |stdin, stdout, stderr|
43
+ out.write stdout.read
44
+ err.write stderr.read
45
+ end
46
+ raise PlistBuddyError, err.string unless err.string.empty?
47
+ out.string
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,20 @@
1
+ module OsxSub
2
+ class Substitution
3
+ attr_reader :replace, :with
4
+
5
+ def initialize(replace, with, on = true)
6
+ @replace = replace
7
+ @with = with
8
+ @on = on
9
+ end
10
+
11
+ def on?
12
+ return false if @on.is_a? String and @on.to_i <= 0
13
+ true & @on
14
+ end
15
+
16
+ def sub(string)
17
+ string.gsub(replace, with)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,3 @@
1
+ module OsxSub
2
+ VERSION = "0.1.0"
3
+ end
data/lib/osxsub.rb ADDED
@@ -0,0 +1,6 @@
1
+ module OsxSub
2
+ require 'osxsub/version'
3
+ require 'osxsub/substitution'
4
+ require 'osxsub/plist_buddy'
5
+ require 'osxsub/ns_user_replacement_items'
6
+ end
data/osxsub.gemspec ADDED
@@ -0,0 +1,17 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/osxsub/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Mark Wunsch"]
6
+ gem.email = ["mark@markwunsch.com"]
7
+ gem.description = %q{Command line utility for OS X Text Substition Preferences.}
8
+ gem.summary = %q{Command line utility for OS X Text Substition Preferences.}
9
+ gem.homepage = %q{http://github.com/mwunsch/osxsub}
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "osxsub"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = OsxSub::VERSION
17
+ end
metadata ADDED
@@ -0,0 +1,80 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: osxsub
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 0
10
+ version: 0.1.0
11
+ platform: ruby
12
+ authors:
13
+ - Mark Wunsch
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2013-01-28 00:00:00 -05:00
19
+ default_executable:
20
+ dependencies: []
21
+
22
+ description: Command line utility for OS X Text Substition Preferences.
23
+ email:
24
+ - mark@markwunsch.com
25
+ executables:
26
+ - osxsub
27
+ extensions: []
28
+
29
+ extra_rdoc_files: []
30
+
31
+ files:
32
+ - .gitignore
33
+ - Gemfile
34
+ - Gemfile.lock
35
+ - LICENSE
36
+ - README.md
37
+ - Rakefile
38
+ - bin/osxsub
39
+ - lib/osxsub.rb
40
+ - lib/osxsub/ns_user_replacement_items.rb
41
+ - lib/osxsub/plist_buddy.rb
42
+ - lib/osxsub/substitution.rb
43
+ - lib/osxsub/version.rb
44
+ - osxsub.gemspec
45
+ has_rdoc: true
46
+ homepage: http://github.com/mwunsch/osxsub
47
+ licenses: []
48
+
49
+ post_install_message:
50
+ rdoc_options: []
51
+
52
+ require_paths:
53
+ - lib
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ none: false
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ hash: 3
60
+ segments:
61
+ - 0
62
+ version: "0"
63
+ required_rubygems_version: !ruby/object:Gem::Requirement
64
+ none: false
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ hash: 3
69
+ segments:
70
+ - 0
71
+ version: "0"
72
+ requirements: []
73
+
74
+ rubyforge_project:
75
+ rubygems_version: 1.3.7
76
+ signing_key:
77
+ specification_version: 3
78
+ summary: Command line utility for OS X Text Substition Preferences.
79
+ test_files: []
80
+