osxsub 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +18 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +16 -0
- data/LICENSE +20 -0
- data/README.md +59 -0
- data/Rakefile +2 -0
- data/bin/osxsub +94 -0
- data/lib/osxsub/ns_user_replacement_items.rb +107 -0
- data/lib/osxsub/plist_buddy.rb +50 -0
- data/lib/osxsub/substitution.rb +20 -0
- data/lib/osxsub/version.rb +3 -0
- data/lib/osxsub.rb +6 -0
- data/osxsub.gemspec +17 -0
- metadata +80 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
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
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
|
data/lib/osxsub.rb
ADDED
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
|
+
|