dockedit 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rspec +4 -0
- data/CHANGELOG.md +8 -0
- data/Gemfile +12 -0
- data/Gemfile.lock +37 -0
- data/README.md +262 -0
- data/Rakefile +97 -0
- data/bin/dockedit +10 -0
- data/build_standalone.rb +88 -0
- data/dockedit +1461 -0
- data/lib/dockedit/app_finder.rb +46 -0
- data/lib/dockedit/cli.rb +166 -0
- data/lib/dockedit/commands.rb +372 -0
- data/lib/dockedit/constants.rb +44 -0
- data/lib/dockedit/dock.rb +98 -0
- data/lib/dockedit/folder_updater.rb +58 -0
- data/lib/dockedit/matcher.rb +136 -0
- data/lib/dockedit/parsers.rb +62 -0
- data/lib/dockedit/path_utils.rb +44 -0
- data/lib/dockedit/plist_reader.rb +158 -0
- data/lib/dockedit/plist_writer.rb +52 -0
- data/lib/dockedit/tile_factory.rb +189 -0
- data/lib/dockedit/version.rb +7 -0
- data/lib/dockedit.rb +25 -0
- metadata +82 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DockEdit
|
|
4
|
+
# Handles finding applications on disk using Spotlight (mdfind).
|
|
5
|
+
#
|
|
6
|
+
# This class is used by commands that accept an application name (e.g. "Safari")
|
|
7
|
+
# and need to resolve it to a full `.app` bundle path.
|
|
8
|
+
class AppFinder
|
|
9
|
+
# Find an application bundle on disk using Spotlight.
|
|
10
|
+
#
|
|
11
|
+
# The search is limited to `/Applications` and `/System/Applications`.
|
|
12
|
+
# Results are scored using {DockEdit::Matcher.match_score} and the best
|
|
13
|
+
# matching `.app` path is returned.
|
|
14
|
+
#
|
|
15
|
+
# @param query [String] Human-friendly app name to search for.
|
|
16
|
+
# @return [String, nil] Absolute path to the best matching `.app`, or +nil+
|
|
17
|
+
# if no match is found.
|
|
18
|
+
def self.find_app_on_disk(query)
|
|
19
|
+
# Search in /Applications and /System/Applications
|
|
20
|
+
result = `mdfind -onlyin /Applications -onlyin /System/Applications 'kMDItemKind == "Application" && kMDItemDisplayName == "*#{query}*"cd' 2>/dev/null`.strip
|
|
21
|
+
|
|
22
|
+
if result.empty?
|
|
23
|
+
# Fallback to filename search
|
|
24
|
+
result = `mdfind -onlyin /Applications -onlyin /System/Applications 'kMDItemFSName == "*#{query}*.app"cd' 2>/dev/null`.strip
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
return nil if result.empty?
|
|
28
|
+
|
|
29
|
+
apps = result.split("\n").select { |p| p.end_with?('.app') }
|
|
30
|
+
return nil if apps.empty?
|
|
31
|
+
|
|
32
|
+
# Score and sort by best match (shortest name wins on equal score)
|
|
33
|
+
scored = apps.map do |path|
|
|
34
|
+
name = File.basename(path, '.app')
|
|
35
|
+
score = Matcher.match_score(query, name)
|
|
36
|
+
[path, name, score || 999, name.length]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Sort by score, then by name length
|
|
40
|
+
scored.sort_by! { |_, _, score, len| [score, len] }
|
|
41
|
+
|
|
42
|
+
scored.first&.first
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
data/lib/dockedit/cli.rb
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DockEdit
|
|
4
|
+
# Command-line interface and option parsing for the `dockedit` executable.
|
|
5
|
+
#
|
|
6
|
+
# This class is responsible for parsing global arguments and subcommands and
|
|
7
|
+
# delegating to the appropriate handlers in {DockEdit::Commands}.
|
|
8
|
+
class CLI
|
|
9
|
+
# Build the option parser for the `add` subcommand.
|
|
10
|
+
#
|
|
11
|
+
# @param options [Hash] Mutable options hash to be populated by OptionParser.
|
|
12
|
+
# @return [OptionParser] Configured parser instance.
|
|
13
|
+
def self.add_parser(options)
|
|
14
|
+
OptionParser.new do |opts|
|
|
15
|
+
opts.banner = "Usage: dockedit add [options] <app_or_folder> [...]"
|
|
16
|
+
opts.separator ""
|
|
17
|
+
opts.separator "Examples:"
|
|
18
|
+
opts.separator " dockedit add Safari Terminal"
|
|
19
|
+
opts.separator " dockedit add ~/Downloads --show grid --display stack"
|
|
20
|
+
opts.separator " dockedit add --after Safari Notes"
|
|
21
|
+
opts.separator " dockedit add ~/Sites --display folder --show grid"
|
|
22
|
+
opts.on('-a', '--after ITEM', 'Insert after specified app/folder (fuzzy match)') { |app| options[:after] = app }
|
|
23
|
+
opts.on('--show TYPE', '--view TYPE', 'Folder view: fan/f, grid/g, list/l, auto/a (default: auto)') { |t| options[:show_as] = Parsers.parse_show_as(t) }
|
|
24
|
+
opts.on('--display TYPE', 'Folder style: folder/f, stack/s (default: folder)') { |t| options[:display_as] = Parsers.parse_display_as(t) }
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Build the option parser for the `space` subcommand.
|
|
29
|
+
#
|
|
30
|
+
# @param options [Hash] Mutable options hash to be populated by OptionParser.
|
|
31
|
+
# @return [OptionParser] Configured parser instance.
|
|
32
|
+
def self.space_parser(options)
|
|
33
|
+
OptionParser.new do |opts|
|
|
34
|
+
opts.banner = "Usage: dockedit space [options]"
|
|
35
|
+
opts.separator ""
|
|
36
|
+
opts.separator "Examples:"
|
|
37
|
+
opts.separator " dockedit space"
|
|
38
|
+
opts.separator " dockedit space --small"
|
|
39
|
+
opts.separator " dockedit space --after Safari"
|
|
40
|
+
opts.separator " dockedit space --small --after Terminal --after Safari"
|
|
41
|
+
opts.on('-s', '--small', '--half', 'Insert a small/half-size space') { options[:small] = true }
|
|
42
|
+
opts.on('-a', '--after APP', 'Insert after specified app (fuzzy match, repeatable)') { |app| options[:after] << app }
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Build the option parser for the `move` subcommand.
|
|
47
|
+
#
|
|
48
|
+
# @param options [Hash] Mutable options hash to be populated by OptionParser.
|
|
49
|
+
# @return [OptionParser] Configured parser instance.
|
|
50
|
+
def self.move_parser(options)
|
|
51
|
+
OptionParser.new do |opts|
|
|
52
|
+
opts.banner = "Usage: dockedit move --after <target> <item_to_move> OR dockedit move <item_to_move> --after <target>"
|
|
53
|
+
opts.separator ""
|
|
54
|
+
opts.separator "Examples:"
|
|
55
|
+
opts.separator " dockedit move --after Terminal Safari"
|
|
56
|
+
opts.separator " dockedit move Safari --after Terminal"
|
|
57
|
+
opts.on('-a', '--after ITEM', 'Move after specified app/folder (required, fuzzy match)') { |app| options[:after] = app }
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Build the option parser for the `remove` subcommand.
|
|
62
|
+
#
|
|
63
|
+
# @param _options [Hash] Present for API symmetry; currently unused.
|
|
64
|
+
# @return [OptionParser] Configured parser instance.
|
|
65
|
+
def self.remove_parser(_options = {})
|
|
66
|
+
OptionParser.new do |opts|
|
|
67
|
+
opts.banner = "Usage: dockedit remove <app_or_folder> [...]"
|
|
68
|
+
opts.separator ""
|
|
69
|
+
opts.separator "Examples:"
|
|
70
|
+
opts.separator " dockedit remove Safari Terminal"
|
|
71
|
+
opts.separator " dockedit remove ~/Downloads"
|
|
72
|
+
opts.separator " dockedit remove --help"
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Build the top-level option parser for the `dockedit` command.
|
|
77
|
+
#
|
|
78
|
+
# This parser prints a summary of all subcommands and global options.
|
|
79
|
+
#
|
|
80
|
+
# @return [OptionParser]
|
|
81
|
+
def self.main_parser
|
|
82
|
+
OptionParser.new do |opts|
|
|
83
|
+
opts.banner = "Usage: dockedit <subcommand> [options] [args]"
|
|
84
|
+
opts.separator ""
|
|
85
|
+
opts.separator "Subcommands:"
|
|
86
|
+
opts.separator " add [-a|--after <item>] [--show-as TYPE] <item>... Add app(s)/folder(s)"
|
|
87
|
+
opts.separator " move -a|--after <item> <item> Move an item after another"
|
|
88
|
+
opts.separator " remove <item>... Remove app(s)/folder(s)"
|
|
89
|
+
opts.separator " space [-s|--small] [-a|--after <app>] Insert space(s)"
|
|
90
|
+
opts.separator " help [subcommand] Show help for a subcommand"
|
|
91
|
+
opts.separator ""
|
|
92
|
+
opts.separator "Folder shortcuts: desktop, downloads, home, library, documents, applications, sites"
|
|
93
|
+
opts.separator ""
|
|
94
|
+
opts.separator "Examples:"
|
|
95
|
+
opts.separator " dockedit add Safari Terminal"
|
|
96
|
+
opts.separator " dockedit add ~/Downloads --show grid --display stack"
|
|
97
|
+
opts.separator " dockedit add Notes --after Safari"
|
|
98
|
+
opts.separator " dockedit space --small --after Safari"
|
|
99
|
+
opts.separator " dockedit move --after Terminal Safari"
|
|
100
|
+
opts.separator " dockedit move Safari --after Terminal"
|
|
101
|
+
opts.separator " dockedit help add"
|
|
102
|
+
opts.separator ""
|
|
103
|
+
opts.separator "Options:"
|
|
104
|
+
opts.on('-h', '--help', 'Show this help') do
|
|
105
|
+
puts opts
|
|
106
|
+
exit 0
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Main entry point for the `dockedit` CLI.
|
|
112
|
+
#
|
|
113
|
+
# Reads +ARGV+, dispatches to subcommands, and exits with an appropriate
|
|
114
|
+
# status code.
|
|
115
|
+
#
|
|
116
|
+
# @return [void]
|
|
117
|
+
def self.run
|
|
118
|
+
global_parser = main_parser
|
|
119
|
+
|
|
120
|
+
if ARGV.empty?
|
|
121
|
+
puts global_parser
|
|
122
|
+
exit 0
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
subcommand = ARGV.shift
|
|
126
|
+
|
|
127
|
+
case subcommand
|
|
128
|
+
when '-v', '--version'
|
|
129
|
+
puts DockEdit::VERSION
|
|
130
|
+
exit 0
|
|
131
|
+
when 'add'
|
|
132
|
+
Commands.add(ARGV)
|
|
133
|
+
when 'move'
|
|
134
|
+
Commands.move(ARGV)
|
|
135
|
+
when 'remove'
|
|
136
|
+
Commands.remove(ARGV)
|
|
137
|
+
when 'space'
|
|
138
|
+
Commands.space(ARGV)
|
|
139
|
+
when 'help', '-h', '--help'
|
|
140
|
+
help_target = ARGV.shift
|
|
141
|
+
case help_target
|
|
142
|
+
when nil
|
|
143
|
+
puts main_parser
|
|
144
|
+
when 'add'
|
|
145
|
+
puts add_parser({})
|
|
146
|
+
when 'move'
|
|
147
|
+
puts move_parser({})
|
|
148
|
+
when 'remove'
|
|
149
|
+
puts remove_parser({})
|
|
150
|
+
when 'space'
|
|
151
|
+
puts space_parser({})
|
|
152
|
+
else
|
|
153
|
+
$stderr.puts "Unknown subcommand for help: #{help_target}"
|
|
154
|
+
$stderr.puts "Valid subcommands: add, move, remove, space"
|
|
155
|
+
exit 1
|
|
156
|
+
end
|
|
157
|
+
exit 0
|
|
158
|
+
else
|
|
159
|
+
$stderr.puts "Unknown subcommand: #{subcommand}"
|
|
160
|
+
$stderr.puts "Run 'dockedit --help' for usage"
|
|
161
|
+
exit 1
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DockEdit
|
|
4
|
+
# Command handlers for `dockedit` subcommands.
|
|
5
|
+
#
|
|
6
|
+
# Each method in this module implements the behavior for a single
|
|
7
|
+
# subcommand, operating on the Dock plist via {DockEdit::Dock}.
|
|
8
|
+
module Commands
|
|
9
|
+
# Implementation of the `space` subcommand.
|
|
10
|
+
#
|
|
11
|
+
# Inserts one or more spacer tiles in the apps section of the Dock.
|
|
12
|
+
#
|
|
13
|
+
# @param args [Array<String>] Command-line arguments for the subcommand.
|
|
14
|
+
# @return [void]
|
|
15
|
+
def self.space(args)
|
|
16
|
+
options = { small: false, after: [] }
|
|
17
|
+
|
|
18
|
+
parser = CLI.space_parser(options)
|
|
19
|
+
parser.order!(args)
|
|
20
|
+
|
|
21
|
+
small = options[:small]
|
|
22
|
+
after_apps = options[:after]
|
|
23
|
+
spacer_type = small ? 'Small space' : 'Space'
|
|
24
|
+
|
|
25
|
+
dock = Dock.new
|
|
26
|
+
messages = []
|
|
27
|
+
|
|
28
|
+
if after_apps.empty?
|
|
29
|
+
# Add single space at end
|
|
30
|
+
spacer = TileFactory.create_spacer_tile(small: small)
|
|
31
|
+
dock.apps_array.add(spacer)
|
|
32
|
+
messages << "#{spacer_type} added to end of Dock."
|
|
33
|
+
else
|
|
34
|
+
# Process each --after app
|
|
35
|
+
after_apps.each do |after_app|
|
|
36
|
+
# Re-fetch app_dicts each time since array changes after insert
|
|
37
|
+
app_dicts = dock.get_app_dicts
|
|
38
|
+
|
|
39
|
+
index, name = dock.find_app(after_app)
|
|
40
|
+
unless index
|
|
41
|
+
$stderr.puts "Error: App '#{after_app}' not found in Dock"
|
|
42
|
+
exit 1
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
spacer = TileFactory.create_spacer_tile(small: small)
|
|
46
|
+
dock.apps_array.insert_after(app_dicts[index], spacer)
|
|
47
|
+
messages << "#{spacer_type} inserted after '#{name}'."
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
dock.save(messages)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Implementation of the `add` subcommand.
|
|
55
|
+
#
|
|
56
|
+
# Adds applications and/or folders to the Dock, optionally positioning
|
|
57
|
+
# them relative to an existing item and updating folder view/style.
|
|
58
|
+
#
|
|
59
|
+
# @param args [Array<String>] Command-line arguments for the subcommand.
|
|
60
|
+
# @return [void]
|
|
61
|
+
def self.add(args)
|
|
62
|
+
options = { after: nil, show_as: nil, display_as: nil }
|
|
63
|
+
|
|
64
|
+
parser = CLI.add_parser(options)
|
|
65
|
+
parser.permute!(args)
|
|
66
|
+
|
|
67
|
+
after_target = options[:after]
|
|
68
|
+
show_as = options[:show_as]
|
|
69
|
+
display_as = options[:display_as]
|
|
70
|
+
|
|
71
|
+
if args.empty?
|
|
72
|
+
$stderr.puts parser
|
|
73
|
+
exit 1
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
dock = Dock.new
|
|
77
|
+
messages = []
|
|
78
|
+
last_inserted_app_element = nil
|
|
79
|
+
last_inserted_folder_element = nil
|
|
80
|
+
|
|
81
|
+
# Find initial insertion point if --after specified
|
|
82
|
+
if after_target
|
|
83
|
+
after_array, after_element, _after_name = dock.find_item(after_target, check_url: true)
|
|
84
|
+
|
|
85
|
+
if after_element
|
|
86
|
+
if after_array == dock.apps_array
|
|
87
|
+
last_inserted_app_element = after_element
|
|
88
|
+
else
|
|
89
|
+
last_inserted_folder_element = after_element
|
|
90
|
+
end
|
|
91
|
+
else
|
|
92
|
+
$stderr.puts "Error: '#{after_target}' not found in Dock"
|
|
93
|
+
exit 1
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Process each argument
|
|
98
|
+
args.each do |item_query|
|
|
99
|
+
# Determine if this is a folder or app
|
|
100
|
+
if PathUtils.folder_path?(item_query)
|
|
101
|
+
# It's a folder
|
|
102
|
+
folder_path = PathUtils.expand_path_shortcut(item_query)
|
|
103
|
+
|
|
104
|
+
unless File.directory?(folder_path)
|
|
105
|
+
$stderr.puts "Error: Folder '#{folder_path}' not found"
|
|
106
|
+
exit 1
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
folder_name = File.basename(folder_path)
|
|
110
|
+
|
|
111
|
+
# Check if folder is already in dock
|
|
112
|
+
existing_index, existing_name = dock.find_folder(folder_path, check_url: true)
|
|
113
|
+
|
|
114
|
+
if existing_index
|
|
115
|
+
# If show_as or display_as was specified, update the existing folder
|
|
116
|
+
updates = []
|
|
117
|
+
folder_dicts = dock.get_folder_dicts
|
|
118
|
+
if show_as
|
|
119
|
+
FolderUpdater.update_folder_showas(folder_dicts[existing_index], show_as)
|
|
120
|
+
updates << "view=#{Parsers.show_as_name(show_as)}"
|
|
121
|
+
end
|
|
122
|
+
if display_as
|
|
123
|
+
FolderUpdater.update_folder_displayas(folder_dicts[existing_index], display_as)
|
|
124
|
+
updates << "style=#{Parsers.display_as_name(display_as)}"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
if updates.any?
|
|
128
|
+
messages << "'#{existing_name}' updated (#{updates.join(', ')})."
|
|
129
|
+
else
|
|
130
|
+
$stderr.puts "Warning: '#{existing_name}' is already in the Dock, skipping"
|
|
131
|
+
end
|
|
132
|
+
next
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Create the folder tile (use defaults if not specified)
|
|
136
|
+
folder_tile = TileFactory.create_folder_tile(folder_path, show_as: show_as || 4, display_as: display_as || 1)
|
|
137
|
+
|
|
138
|
+
if last_inserted_folder_element
|
|
139
|
+
dock.others_array.insert_after(last_inserted_folder_element, folder_tile)
|
|
140
|
+
else
|
|
141
|
+
dock.others_array.add(folder_tile)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
last_inserted_folder_element = folder_tile
|
|
145
|
+
messages << "'#{folder_name}' folder added to Dock."
|
|
146
|
+
|
|
147
|
+
elsif PathUtils.explicit_app_path?(item_query)
|
|
148
|
+
# Explicit app path given
|
|
149
|
+
app_path = File.expand_path(item_query)
|
|
150
|
+
|
|
151
|
+
unless File.exist?(File.join(app_path, 'Contents', 'Info.plist'))
|
|
152
|
+
$stderr.puts "Error: Application path '#{app_path}' not found"
|
|
153
|
+
exit 1
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
app_info = PlistReader.read_app_info(app_path)
|
|
157
|
+
unless app_info && app_info['CFBundleIdentifier']
|
|
158
|
+
$stderr.puts "Error: Could not read app info from '#{app_path}'"
|
|
159
|
+
exit 1
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
app_name = app_info['CFBundleName'] || app_info['CFBundleDisplayName'] || File.basename(app_path, '.app')
|
|
163
|
+
|
|
164
|
+
# Check if app is already in dock
|
|
165
|
+
existing_index, existing_name = dock.find_app(app_info['CFBundleIdentifier'])
|
|
166
|
+
|
|
167
|
+
if existing_index
|
|
168
|
+
$stderr.puts "Warning: '#{existing_name}' is already in the Dock, skipping"
|
|
169
|
+
next
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Create the app tile
|
|
173
|
+
app_tile = TileFactory.create_app_tile(app_path, app_info)
|
|
174
|
+
|
|
175
|
+
if last_inserted_app_element
|
|
176
|
+
dock.apps_array.insert_after(last_inserted_app_element, app_tile)
|
|
177
|
+
last_inserted_app_element = app_tile
|
|
178
|
+
else
|
|
179
|
+
dock.apps_array.add(app_tile)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
messages << "'#{app_name}' added to Dock."
|
|
183
|
+
|
|
184
|
+
else
|
|
185
|
+
# Search for app by name
|
|
186
|
+
app_path = AppFinder.find_app_on_disk(item_query)
|
|
187
|
+
unless app_path
|
|
188
|
+
$stderr.puts "Error: App '#{item_query}' not found"
|
|
189
|
+
exit 1
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
app_info = PlistReader.read_app_info(app_path)
|
|
193
|
+
unless app_info && app_info['CFBundleIdentifier']
|
|
194
|
+
$stderr.puts "Error: Could not read app info from '#{app_path}'"
|
|
195
|
+
exit 1
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
app_name = app_info['CFBundleName'] || app_info['CFBundleDisplayName'] || File.basename(app_path, '.app')
|
|
199
|
+
|
|
200
|
+
# Check if app is already in dock
|
|
201
|
+
existing_index, existing_name = dock.find_app(app_info['CFBundleIdentifier'])
|
|
202
|
+
|
|
203
|
+
if existing_index
|
|
204
|
+
$stderr.puts "Warning: '#{existing_name}' is already in the Dock, skipping"
|
|
205
|
+
next
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Create the app tile
|
|
209
|
+
app_tile = TileFactory.create_app_tile(app_path, app_info)
|
|
210
|
+
|
|
211
|
+
if last_inserted_app_element
|
|
212
|
+
dock.apps_array.insert_after(last_inserted_app_element, app_tile)
|
|
213
|
+
last_inserted_app_element = app_tile
|
|
214
|
+
else
|
|
215
|
+
dock.apps_array.add(app_tile)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
messages << "'#{app_name}' added to Dock."
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
if messages.empty?
|
|
223
|
+
$stderr.puts "No items were added to the Dock"
|
|
224
|
+
exit 1
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
dock.save(messages)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Implementation of the `remove` subcommand.
|
|
231
|
+
#
|
|
232
|
+
# Removes apps and/or folders from the Dock by name, bundle identifier,
|
|
233
|
+
# or path.
|
|
234
|
+
#
|
|
235
|
+
# @param args [Array<String>] Command-line arguments for the subcommand.
|
|
236
|
+
# @return [void]
|
|
237
|
+
def self.remove(args)
|
|
238
|
+
parser = CLI.remove_parser({})
|
|
239
|
+
parser.order!(args)
|
|
240
|
+
|
|
241
|
+
if args.empty?
|
|
242
|
+
$stderr.puts parser
|
|
243
|
+
exit 1
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
dock = Dock.new
|
|
247
|
+
messages = []
|
|
248
|
+
|
|
249
|
+
# Process each argument
|
|
250
|
+
args.each do |item_query|
|
|
251
|
+
found = false
|
|
252
|
+
|
|
253
|
+
# Determine if this looks like a path
|
|
254
|
+
is_path = item_query.include?('/')
|
|
255
|
+
|
|
256
|
+
# First check persistent-apps
|
|
257
|
+
index, name = dock.find_app(item_query)
|
|
258
|
+
|
|
259
|
+
if index
|
|
260
|
+
app_dicts = dock.get_app_dicts
|
|
261
|
+
dock.apps_array.delete(app_dicts[index])
|
|
262
|
+
messages << "'#{name}' removed from Dock."
|
|
263
|
+
found = true
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# If not found in apps, check persistent-others (folders)
|
|
267
|
+
unless found
|
|
268
|
+
index, name = dock.find_folder(item_query, check_url: is_path)
|
|
269
|
+
|
|
270
|
+
if index
|
|
271
|
+
folder_dicts = dock.get_folder_dicts
|
|
272
|
+
dock.others_array.delete(folder_dicts[index])
|
|
273
|
+
messages << "'#{name}' removed from Dock."
|
|
274
|
+
found = true
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
unless found
|
|
279
|
+
$stderr.puts "Warning: '#{item_query}' not found in Dock, skipping"
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
if messages.empty?
|
|
284
|
+
$stderr.puts "No items were removed from the Dock"
|
|
285
|
+
exit 1
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
dock.save(messages)
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# Implementation of the `move` subcommand.
|
|
292
|
+
#
|
|
293
|
+
# Moves an existing Dock item after another item in the same section.
|
|
294
|
+
#
|
|
295
|
+
# @param args [Array<String>] Command-line arguments for the subcommand.
|
|
296
|
+
# @return [void]
|
|
297
|
+
def self.move(args)
|
|
298
|
+
# Accept either order: move --after TARGET ITEM or move ITEM --after TARGET
|
|
299
|
+
options = { after: nil }
|
|
300
|
+
parser = CLI.move_parser(options)
|
|
301
|
+
parser.permute!(args)
|
|
302
|
+
|
|
303
|
+
# Now, args should contain the non-option arguments (either [item] or [target, item] or [item, target])
|
|
304
|
+
after_target = options[:after]
|
|
305
|
+
|
|
306
|
+
# Accept either order: --after TARGET ITEM or ITEM --after TARGET
|
|
307
|
+
item_query = nil
|
|
308
|
+
if after_target && args.length == 1
|
|
309
|
+
item_query = args.first
|
|
310
|
+
elsif after_target && args.length == 2
|
|
311
|
+
# Try to infer which is the item and which is the target
|
|
312
|
+
if args[0].downcase == after_target.downcase
|
|
313
|
+
item_query = args[1]
|
|
314
|
+
elsif args[1].downcase == after_target.downcase
|
|
315
|
+
item_query = args[0]
|
|
316
|
+
else
|
|
317
|
+
# Default: treat first as item
|
|
318
|
+
item_query = args[0]
|
|
319
|
+
end
|
|
320
|
+
else
|
|
321
|
+
$stderr.puts parser
|
|
322
|
+
$stderr.puts "\nError: You must specify an item to move and a target with --after."
|
|
323
|
+
exit 1
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
if !after_target || !item_query
|
|
327
|
+
$stderr.puts parser
|
|
328
|
+
$stderr.puts "\nError: You must specify an item to move and a target with --after."
|
|
329
|
+
exit 1
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
dock = Dock.new
|
|
333
|
+
|
|
334
|
+
# Find the item to move (check apps first, then folders)
|
|
335
|
+
move_array, move_element, move_name = dock.find_item(item_query, check_url: true)
|
|
336
|
+
|
|
337
|
+
unless move_element
|
|
338
|
+
$stderr.puts "Error: '#{item_query}' not found in Dock"
|
|
339
|
+
exit 1
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# Find the target (check apps first, then folders)
|
|
343
|
+
after_array, after_element, after_name = dock.find_item(after_target, check_url: true)
|
|
344
|
+
|
|
345
|
+
unless after_element
|
|
346
|
+
$stderr.puts "Error: '#{after_target}' not found in Dock"
|
|
347
|
+
exit 1
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# Check if they're the same item
|
|
351
|
+
if move_element == after_element
|
|
352
|
+
$stderr.puts "Error: Cannot move an item after itself"
|
|
353
|
+
exit 1
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# Check if moving between arrays (apps <-> folders) - not allowed
|
|
357
|
+
if move_array != after_array
|
|
358
|
+
$stderr.puts "Error: Cannot move items between apps and folders sections"
|
|
359
|
+
exit 1
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# Remove from current position
|
|
363
|
+
move_array.delete(move_element)
|
|
364
|
+
|
|
365
|
+
# Insert after target
|
|
366
|
+
move_array.insert_after(after_element, move_element)
|
|
367
|
+
|
|
368
|
+
dock.save("'#{move_name}' moved after '#{after_name}'.")
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DockEdit
|
|
4
|
+
# Constants used across the dockedit implementation.
|
|
5
|
+
module Constants
|
|
6
|
+
# Absolute path to the Dock preferences plist.
|
|
7
|
+
#
|
|
8
|
+
# In tests this constant is overridden to point at a temporary copy.
|
|
9
|
+
DOCK_PLIST = File.expand_path('~/Library/Preferences/com.apple.dock.plist')
|
|
10
|
+
|
|
11
|
+
# Mapping of short folder names to full filesystem paths.
|
|
12
|
+
#
|
|
13
|
+
# Keys are regular expressions matched against user input; values are
|
|
14
|
+
# expanded paths.
|
|
15
|
+
PATH_SHORTCUTS = {
|
|
16
|
+
/^desktop$/i => File.expand_path('~/Desktop'),
|
|
17
|
+
/^downloads$/i => File.expand_path('~/Downloads'),
|
|
18
|
+
/^(home|~)$/i => File.expand_path('~'),
|
|
19
|
+
/^library$/i => File.expand_path('~/Library'),
|
|
20
|
+
/^documents$/i => File.expand_path('~/Documents'),
|
|
21
|
+
/^(applications|apps)$/i => '/Applications',
|
|
22
|
+
/^sites$/i => File.expand_path('~/Sites')
|
|
23
|
+
}.freeze
|
|
24
|
+
|
|
25
|
+
# Mapping of show-as string aliases to integer values used in the plist.
|
|
26
|
+
#
|
|
27
|
+
# 1 = fan, 2 = grid, 3 = list, 4 = auto.
|
|
28
|
+
SHOW_AS_VALUES = {
|
|
29
|
+
'f' => 1, 'fan' => 1,
|
|
30
|
+
'g' => 2, 'grid' => 2,
|
|
31
|
+
'l' => 3, 'list' => 3,
|
|
32
|
+
'a' => 4, 'auto' => 4
|
|
33
|
+
}.freeze
|
|
34
|
+
|
|
35
|
+
# Mapping of display-as string aliases to integer values used in the plist.
|
|
36
|
+
#
|
|
37
|
+
# 0 = stack, 1 = folder.
|
|
38
|
+
DISPLAY_AS_VALUES = {
|
|
39
|
+
's' => 0, 'stack' => 0,
|
|
40
|
+
'f' => 1, 'folder' => 1
|
|
41
|
+
}.freeze
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|