appear 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/.doc-coverage +1 -0
- data/.gitignore +51 -0
- data/.rspec +2 -0
- data/.travis.yml +19 -0
- data/Gemfile +4 -0
- data/LICENSE +21 -0
- data/README.md +101 -0
- data/Rakefile +53 -0
- data/appear.gemspec +44 -0
- data/bin/appear +7 -0
- data/bin/console +14 -0
- data/bin/pparents +23 -0
- data/bin/setup +7 -0
- data/lib/appear.rb +14 -0
- data/lib/appear/command.rb +57 -0
- data/lib/appear/config.rb +24 -0
- data/lib/appear/constants.rb +17 -0
- data/lib/appear/instance.rb +54 -0
- data/lib/appear/join.rb +100 -0
- data/lib/appear/lsof.rb +153 -0
- data/lib/appear/mac_os.rb +42 -0
- data/lib/appear/output.rb +30 -0
- data/lib/appear/processes.rb +88 -0
- data/lib/appear/revealers.rb +183 -0
- data/lib/appear/runner.rb +101 -0
- data/lib/appear/service.rb +65 -0
- data/lib/appear/tmux.rb +77 -0
- data/screenshot.gif +0 -0
- data/tools/macOS-helper.js +349 -0
- metadata +152 -0
@@ -0,0 +1,183 @@
|
|
1
|
+
require 'appear/service'
|
2
|
+
require 'appear/join'
|
3
|
+
|
4
|
+
module Appear
|
5
|
+
# stores all the ways we can appear something
|
6
|
+
REVEALERS = []
|
7
|
+
|
8
|
+
# The Revealers are the things that actually are in charge of revealing a PID
|
9
|
+
# in a terminal emulator. They consume the other services to do the real
|
10
|
+
# work.
|
11
|
+
module Revealers
|
12
|
+
# extend to implement more revealers
|
13
|
+
class BaseRevealer < Service
|
14
|
+
def call(tree)
|
15
|
+
target, *rest = tree
|
16
|
+
if supports_tree?(target, rest)
|
17
|
+
return reveal_tree(tree)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# TODO
|
22
|
+
def reveal_tree(tree)
|
23
|
+
raise "not implemented"
|
24
|
+
end
|
25
|
+
|
26
|
+
# appear the first process in this process tree.
|
27
|
+
# should return nil if no action was performed.
|
28
|
+
# otherwise, return true.
|
29
|
+
def supports_tree?(target, rest)
|
30
|
+
raise "not implemented"
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.register!
|
34
|
+
Appear::REVEALERS.push(self)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class MacRevealer < BaseRevealer
|
39
|
+
delegate :join_via_tty, :lsof
|
40
|
+
require_service :mac_os
|
41
|
+
|
42
|
+
def panes
|
43
|
+
raise "not implemented"
|
44
|
+
end
|
45
|
+
|
46
|
+
def reveal_hit(hit)
|
47
|
+
raise "not implemented"
|
48
|
+
end
|
49
|
+
|
50
|
+
def reveal_tree(tree)
|
51
|
+
hits = join_via_tty(tree, panes)
|
52
|
+
actual_hits = hits.uniq {|hit| hit.tty }.
|
53
|
+
reject {|hit| services.mac_os.has_gui?(hit.process) }.
|
54
|
+
each { |hit| reveal_hit(hit) }
|
55
|
+
|
56
|
+
return actual_hits.length > 0
|
57
|
+
end
|
58
|
+
|
59
|
+
# TODO: read the bundle identifier somehow, but this is close enough.
|
60
|
+
# or get the bundle identifier and enhance the process lists with it?
|
61
|
+
def has_gui_app_named?(tree, name)
|
62
|
+
tree.any? do |process|
|
63
|
+
process.name == name && services.mac_os.has_gui?(process)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
class Iterm2 < MacRevealer
|
69
|
+
require_service :processes
|
70
|
+
|
71
|
+
def supports_tree?(target, rest)
|
72
|
+
has_gui_app_named?(rest, 'iTerm2')
|
73
|
+
end
|
74
|
+
|
75
|
+
def panes
|
76
|
+
binding.pry
|
77
|
+
pids = services.processes.pgrep('iTerm2')
|
78
|
+
services.mac_os.call_method('iterm2_panes').map do |hash|
|
79
|
+
hash[:pids] = pids
|
80
|
+
OpenStruct.new(hash)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def reveal_hit(hit)
|
85
|
+
services.mac_os.call_method('iterm2_reveal_tty', hit.tty)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
class TerminalApp < MacRevealer
|
90
|
+
require_service :processes
|
91
|
+
|
92
|
+
def supports_tree?(target, rest)
|
93
|
+
has_gui_app_named?(rest, 'Terminal')
|
94
|
+
end
|
95
|
+
|
96
|
+
def panes
|
97
|
+
pids = services.processes.pgrep('Terminal.app')
|
98
|
+
services.mac_os.call_method('terminal_panes').map do |hash|
|
99
|
+
hash[:pids] = pids
|
100
|
+
OpenStruct.new(hash)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def reveal_hit(hit)
|
105
|
+
# iterm2 runs a non-gui server process. Because of implementation
|
106
|
+
# details of MacOs#has_gui?, we don't *techinically* have to worry
|
107
|
+
# about this, but we should in case I ever implement real mac
|
108
|
+
# gui-or-not lookup.
|
109
|
+
return if hit.process.name == 'iTerm2'
|
110
|
+
services.mac_os.call_method('terminal_reveal_tty', hit.tty)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
class Tmux < BaseRevealer
|
115
|
+
# TODO: cache services.tmux.panes, services.tmux.clients for this revealer?
|
116
|
+
require_service :tmux
|
117
|
+
require_service :lsof
|
118
|
+
require_service :revealer
|
119
|
+
require_service :processes
|
120
|
+
|
121
|
+
def supports_tree?(target, rest)
|
122
|
+
rest.any? { |p| p.name == 'tmux' }
|
123
|
+
end
|
124
|
+
|
125
|
+
def reveal_tree(tree)
|
126
|
+
relevent_panes = Join.join(:pid, tree, services.tmux.panes)
|
127
|
+
relevent_panes.each do |pane|
|
128
|
+
log("#{self.class.name}: revealing pane #{pane}")
|
129
|
+
services.tmux.reveal_pane(pane)
|
130
|
+
end
|
131
|
+
|
132
|
+
# we should also appear the tmux client for this tree in the gui
|
133
|
+
pid = tmux_client_for_tree(tree)
|
134
|
+
if pid
|
135
|
+
services.revealer.call(pid)
|
136
|
+
end
|
137
|
+
|
138
|
+
return relevent_panes.length > 0
|
139
|
+
end
|
140
|
+
|
141
|
+
# tmux does not tell us the PIDs of any of these clients. The only way
|
142
|
+
# to find the PID of a tmux client is to lsof() the TTY that the client
|
143
|
+
# is connected to, and then deduce the client PID, which will be a tmux
|
144
|
+
# process PID that is not the server PID.
|
145
|
+
def tmux_client_for_tree(tree)
|
146
|
+
tmux_server = tree.find {|p| p.name == 'tmux'}
|
147
|
+
|
148
|
+
# join processes on tmux panes by PID.
|
149
|
+
proc_and_panes = Join.join(:pid, services.tmux.panes, tree)
|
150
|
+
|
151
|
+
# Join the list of tmux clients with process_and_pid on :session.
|
152
|
+
# In tmux, every pane is addressed by session_name:window_index:pane_index.
|
153
|
+
# This gives us back a list of all the clients that have a pane that
|
154
|
+
# contains a process in our given process tree.
|
155
|
+
proc_and_clients = Join.join(:session, services.tmux.clients, proc_and_panes)
|
156
|
+
|
157
|
+
# there *should* be only one of these, unless there are two clients
|
158
|
+
# connected to the same tmux session. In that case we just choose one
|
159
|
+
# of the clients.
|
160
|
+
client = proc_and_clients.last
|
161
|
+
|
162
|
+
# at this point it's possible that none of our tree's processes are
|
163
|
+
# alive inside tmux.
|
164
|
+
return nil unless client
|
165
|
+
|
166
|
+
tty_of_client = client[:tty]
|
167
|
+
connections_to_tty = services.lsof.lsofs(
|
168
|
+
[tty_of_client],
|
169
|
+
:pids => services.processes.pgrep('tmux')
|
170
|
+
)[tty_of_client]
|
171
|
+
client_connection = connections_to_tty.find do |conn|
|
172
|
+
(conn.command_name =~ /^tmux/) && (conn.pid != tmux_server.pid)
|
173
|
+
end
|
174
|
+
|
175
|
+
client_connection.pid if client_connection
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
Iterm2.register!
|
180
|
+
TerminalApp.register!
|
181
|
+
Tmux.register!
|
182
|
+
end
|
183
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'open3'
|
2
|
+
require 'appear/constants'
|
3
|
+
require 'appear/service'
|
4
|
+
require 'shellwords'
|
5
|
+
require 'json'
|
6
|
+
|
7
|
+
module Appear
|
8
|
+
# raised when a command we want to run fails
|
9
|
+
class ExecutionFailure < Error
|
10
|
+
attr_reader :command, :output
|
11
|
+
def initialize(command, output)
|
12
|
+
@command = command
|
13
|
+
@output = output
|
14
|
+
super("Command #{command.inspect} failed with output #{output.inspect}")
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Service for executing commands. Better than a mixin everywhere.
|
19
|
+
class Runner < Service
|
20
|
+
# Run a command. Throws an exception if the command fails. Command can
|
21
|
+
# either be a string, or an array of command name and parameters.
|
22
|
+
# Returns the combinded STDERR and STDOUT of the command.
|
23
|
+
#
|
24
|
+
# @return String
|
25
|
+
def run(command)
|
26
|
+
start = Time.new
|
27
|
+
if command.is_a? Array
|
28
|
+
output, status = Open3.capture2e(*command)
|
29
|
+
else
|
30
|
+
output, status = Open3.capture2e(command)
|
31
|
+
end
|
32
|
+
finish = Time.new
|
33
|
+
log("Runner: ran #{command.inspect} in #{finish - start}s")
|
34
|
+
raise ExecutionFailure.new(command, output) unless status.success?
|
35
|
+
output
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Records every command run to a directory; intended to be useful for later integration tests.
|
40
|
+
class RunnerRecorder < Runner
|
41
|
+
OUTPUT_DIR = MODULE_DIR.join('spec/command_output')
|
42
|
+
INIT_AT = Time.new
|
43
|
+
|
44
|
+
def initialize(*args)
|
45
|
+
super(*args)
|
46
|
+
@command_runs = Hash.new { |h, k| h[k] = [] }
|
47
|
+
end
|
48
|
+
|
49
|
+
def run(command)
|
50
|
+
begin
|
51
|
+
result = super(command)
|
52
|
+
record_success(command, result)
|
53
|
+
return result
|
54
|
+
rescue ExecutionFailure => err
|
55
|
+
record_error(command, err)
|
56
|
+
raise err
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def command_name(command)
|
63
|
+
if command.is_a?(Array)
|
64
|
+
File.basename(command.first)
|
65
|
+
else
|
66
|
+
File.basename(command.split(/\s+/).first)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def record_success(command, result)
|
71
|
+
data = {
|
72
|
+
:command => command,
|
73
|
+
:output => result,
|
74
|
+
:status => :success,
|
75
|
+
}
|
76
|
+
record(command, data)
|
77
|
+
end
|
78
|
+
|
79
|
+
def record_error(command, err)
|
80
|
+
data = {
|
81
|
+
:command => command,
|
82
|
+
:output => err.output,
|
83
|
+
:status => :error,
|
84
|
+
}
|
85
|
+
record(command, data)
|
86
|
+
end
|
87
|
+
|
88
|
+
def record(command, data)
|
89
|
+
name = command_name(command)
|
90
|
+
run_index = @command_runs[name].length
|
91
|
+
|
92
|
+
data[:run_index] = run_index
|
93
|
+
data[:record_at] = Time.new
|
94
|
+
data[:init_at] = INIT_AT
|
95
|
+
|
96
|
+
@command_runs[name] << data
|
97
|
+
filename = "#{INIT_AT.to_i}-#{name}-run#{run_index}.json"
|
98
|
+
OUTPUT_DIR.join(filename).write(JSON.pretty_generate(data))
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
|
3
|
+
module Appear
|
4
|
+
# Dependency-injectable service class. Service will raise errors during
|
5
|
+
# initialization if its dependencies are not met.
|
6
|
+
class BaseService
|
7
|
+
def initialize(given_services = {})
|
8
|
+
req_service_instances = {}
|
9
|
+
self.class.required_services.each do |service|
|
10
|
+
unless given_services[service]
|
11
|
+
raise ArgumentError.new("required service #{service.inspect} not provided to instance of #{self.class.inspect}")
|
12
|
+
end
|
13
|
+
|
14
|
+
req_service_instances[service] = given_services[service]
|
15
|
+
end
|
16
|
+
@services = OpenStruct.new(req_service_instances)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Delegate a method to another service. Declares a dependency on that
|
20
|
+
# service.
|
21
|
+
def self.delegate(method, service)
|
22
|
+
require_service(service)
|
23
|
+
self.send(:define_method, method) do |*args, &block|
|
24
|
+
unless @services.send(service).respond_to?(method)
|
25
|
+
raise NoMethodError.new("Would call private method #{method.inspect} on #{service.inspect}")
|
26
|
+
end
|
27
|
+
@services.send(service).send(method, *args, &block)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# List all the services required by this service class.
|
32
|
+
def self.required_services
|
33
|
+
@required_services ||= []
|
34
|
+
|
35
|
+
if self.superclass.respond_to?(:required_services)
|
36
|
+
@required_services + self.superclass.required_services
|
37
|
+
else
|
38
|
+
@required_services
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Declare a dependency on another service.
|
43
|
+
def self.require_service(name)
|
44
|
+
@required_services ||= []
|
45
|
+
|
46
|
+
return if required_services.include?(name)
|
47
|
+
@required_services << name
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def services
|
54
|
+
@services
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# All regular services want to log and output stuff, so they inherit from
|
59
|
+
# here.
|
60
|
+
class Service < BaseService
|
61
|
+
delegate :log, :output
|
62
|
+
delegate :log_error, :output
|
63
|
+
delegate :output, :output
|
64
|
+
end
|
65
|
+
end
|
data/lib/appear/tmux.rb
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
require 'appear/service'
|
3
|
+
|
4
|
+
module Appear
|
5
|
+
# The Tmux service is in charge of interacting with `tmux` processes. It is
|
6
|
+
# used by the Tmux revealer, but could also be used as the building block for
|
7
|
+
# other tmux-related scripts.
|
8
|
+
#
|
9
|
+
# see the man page for tmux if you are curious about what clients, windows,
|
10
|
+
# panes, and sessions are in Tmux world.
|
11
|
+
class Tmux < Service
|
12
|
+
delegate :run, :runner
|
13
|
+
|
14
|
+
def clients
|
15
|
+
ipc([
|
16
|
+
'list-clients',
|
17
|
+
'-F',
|
18
|
+
format_string(
|
19
|
+
:tty => :client_tty,
|
20
|
+
:term => :client_termname,
|
21
|
+
:session => :client_session
|
22
|
+
),
|
23
|
+
])
|
24
|
+
end
|
25
|
+
|
26
|
+
def panes
|
27
|
+
panes = ipc([
|
28
|
+
'list-panes',
|
29
|
+
'-a',
|
30
|
+
'-F',
|
31
|
+
format_string(
|
32
|
+
:pid => :pane_pid,
|
33
|
+
:session => :session_name,
|
34
|
+
:window => :window_index,
|
35
|
+
:pane => :pane_index,
|
36
|
+
:command_name => :pane_current_command,
|
37
|
+
:active => :pane_active)
|
38
|
+
])
|
39
|
+
|
40
|
+
panes.each do |pane|
|
41
|
+
pane.window = pane.window.to_i
|
42
|
+
pane.pid = pane.pid.to_i
|
43
|
+
pane.active = pane.active.to_i != 0
|
44
|
+
end
|
45
|
+
|
46
|
+
panes
|
47
|
+
end
|
48
|
+
|
49
|
+
def reveal_pane(pane)
|
50
|
+
ipc(['select-pane', '-t', "#{pane.session}:#{pane.window}.#{pane.pane}"])
|
51
|
+
ipc(['select-window', '-t', "#{pane.session}:#{pane.window}"])
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def ipc(args)
|
57
|
+
res = run(['tmux'] + args)
|
58
|
+
res.lines.map do |line|
|
59
|
+
info = {}
|
60
|
+
line.strip.split(' ').each do |pair|
|
61
|
+
key, *value = pair.split(':')
|
62
|
+
info[key.to_sym] = value.join(':')
|
63
|
+
end
|
64
|
+
OpenStruct.new(info)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def format_string(spec)
|
69
|
+
result = ""
|
70
|
+
spec.each do |key, value|
|
71
|
+
part = ' ' + key.to_s + ':#{' + value.to_s + '}'
|
72
|
+
result += part
|
73
|
+
end
|
74
|
+
result
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
data/screenshot.gif
ADDED
Binary file
|
@@ -0,0 +1,349 @@
|
|
1
|
+
#!/usr/bin/env osascript -l JavaScript
|
2
|
+
// osascript is a OS X-native scripting tool that allows scripting the system.
|
3
|
+
// Usually scripts are written in AppleScript, but AppleScript really sucks, so
|
4
|
+
// we're going to write Javascript instead.
|
5
|
+
//
|
6
|
+
// osascript is interesting because it can inspect the state of the OS X gui,
|
7
|
+
// including enumerating windows and inspecting window contents, if
|
8
|
+
// accessibility is enabled in System Preferences.
|
9
|
+
//
|
10
|
+
// documentation:
|
11
|
+
// https://developer.apple.com/library/mac/releasenotes/InterapplicationCommunication/RN-JavaScriptForAutomation/Articles/OSX10-10.html#//apple_ref/doc/uid/TP40014508-CH109-SW1
|
12
|
+
|
13
|
+
// This script is called from Appear to interact with macOS GUI apps. Both
|
14
|
+
// Terminal.app and iTerm2 publish Applescript APIs, so they're super-easy to
|
15
|
+
// script without accessibility!
|
16
|
+
|
17
|
+
// libaries -----------------------------------------------
|
18
|
+
var SystemEvents = Application('System Events')
|
19
|
+
|
20
|
+
// allows using things like ThisApp.displayDialog
|
21
|
+
var ThisApp = Application.currentApplication()
|
22
|
+
ThisApp.includeStandardAdditions = true
|
23
|
+
|
24
|
+
// helpful
|
25
|
+
var ScriptContext = this
|
26
|
+
|
27
|
+
// -----------------------------------------------------------
|
28
|
+
var PROGRAM_NAME = 'appear-macOS-helper'
|
29
|
+
var Methods = {}
|
30
|
+
|
31
|
+
// entrypoint -------------------------------------------------
|
32
|
+
// this is the main method of this script when it is called from the command line.
|
33
|
+
// the remainer of the file is parsed and evaluated, and then the osascript
|
34
|
+
// environment calls this function with two arguments:
|
35
|
+
// 1: Array<String> argv
|
36
|
+
// 2: Object ???. Could be ScriptContext?
|
37
|
+
function run(argv, unknown) {
|
38
|
+
var method_name = argv[0]
|
39
|
+
var data = argv[1]
|
40
|
+
var message = "running method " + method_name
|
41
|
+
|
42
|
+
if (data) {
|
43
|
+
data = JSON.parse(data)
|
44
|
+
message = message + " with data"
|
45
|
+
}
|
46
|
+
|
47
|
+
try {
|
48
|
+
var method = Methods[method_name]
|
49
|
+
if (!method) throw new Error('unknown method ' + method_name)
|
50
|
+
// helpful for debugging sometimes! don't delete. just un-comment
|
51
|
+
//say(message)
|
52
|
+
var result = ok(method(data))
|
53
|
+
Subprocess.cleanup()
|
54
|
+
return JSON.stringify(result)
|
55
|
+
} catch (err) {
|
56
|
+
//say("failed because " + err.message)
|
57
|
+
Subprocess.cleanup()
|
58
|
+
return JSON.stringify(error(err))
|
59
|
+
}
|
60
|
+
}
|
61
|
+
|
62
|
+
function ok(result) {
|
63
|
+
return {status: 'ok', value: result}
|
64
|
+
}
|
65
|
+
|
66
|
+
function error(err) {
|
67
|
+
return {status: 'error', error: { message: err.message, stack: err.stack }}
|
68
|
+
}
|
69
|
+
|
70
|
+
// ------------------------------------------------------------
|
71
|
+
|
72
|
+
function TerminalEmulator() {}
|
73
|
+
TerminalEmulator.prototype.forEachPane = function(callback) {}
|
74
|
+
TerminalEmulator.prototype.panes = function panes() {
|
75
|
+
var panes = []
|
76
|
+
this.forEachPane(function(pane) {
|
77
|
+
panes.push(pane)
|
78
|
+
})
|
79
|
+
return panes;
|
80
|
+
}
|
81
|
+
TerminalEmulator.prototype.revealTty = function(tty) {}
|
82
|
+
|
83
|
+
// ------------------------------------------------------------
|
84
|
+
// Iterm2 library
|
85
|
+
|
86
|
+
function Iterm2() {
|
87
|
+
this.app = Application('com.googlecode.iterm2')
|
88
|
+
}
|
89
|
+
|
90
|
+
Iterm2.prototype = new TerminalEmulator();
|
91
|
+
|
92
|
+
Iterm2.prototype.forEachPane = function forEachPane(callback) {
|
93
|
+
this.app.windows().forEach(function(win) {
|
94
|
+
win.tabs().forEach(function(tab) {
|
95
|
+
tab.sessions().forEach(function(session) {
|
96
|
+
callback({
|
97
|
+
window: win,
|
98
|
+
tab: tab,
|
99
|
+
session: session,
|
100
|
+
tty: session.tty(),
|
101
|
+
})
|
102
|
+
})
|
103
|
+
})
|
104
|
+
})
|
105
|
+
}
|
106
|
+
|
107
|
+
Iterm2.prototype.revealTty = function revealTty(tty) {
|
108
|
+
var success = false;
|
109
|
+
|
110
|
+
this.forEachPane(function(pane) {
|
111
|
+
if (pane.tty !== tty) return
|
112
|
+
if (success) return
|
113
|
+
|
114
|
+
pane.tab.select()
|
115
|
+
pane.session.select()
|
116
|
+
pane.window.select()
|
117
|
+
success = true;
|
118
|
+
})
|
119
|
+
|
120
|
+
if (success) smartActivate(this.app)
|
121
|
+
return success;
|
122
|
+
}
|
123
|
+
|
124
|
+
Methods['iterm2_reveal_tty'] = function iterm2_reveal_tty(tty) {
|
125
|
+
var iterm2 = new Iterm2()
|
126
|
+
return iterm2.revealTty(tty)
|
127
|
+
}
|
128
|
+
|
129
|
+
Methods['iterm2_panes'] = function iterm2_panes() {
|
130
|
+
var iterm2 = new Iterm2()
|
131
|
+
return iterm2.panes()
|
132
|
+
}
|
133
|
+
|
134
|
+
// -------------------------------------------------------------
|
135
|
+
// Terminal.app library
|
136
|
+
|
137
|
+
function Terminal() {
|
138
|
+
this.app = Application('com.apple.Terminal')
|
139
|
+
}
|
140
|
+
|
141
|
+
Terminal.prototype = new TerminalEmulator();
|
142
|
+
|
143
|
+
Terminal.prototype.forEachPane = function iteratePanes(callback) {
|
144
|
+
this.app.windows().forEach(function(win) {
|
145
|
+
win.tabs().forEach(function(tab) {
|
146
|
+
callback({
|
147
|
+
window: win,
|
148
|
+
tab: tab,
|
149
|
+
tty: tab.tty(),
|
150
|
+
})
|
151
|
+
})
|
152
|
+
})
|
153
|
+
}
|
154
|
+
|
155
|
+
Terminal.prototype.revealTty = function revealTty(tty) {
|
156
|
+
var success = false;
|
157
|
+
|
158
|
+
this.forEachPane(function(pane) {
|
159
|
+
if (pane.tty !== tty) return;
|
160
|
+
if (success) return;
|
161
|
+
|
162
|
+
pane.tab.selected = true
|
163
|
+
pane.window.index = 0
|
164
|
+
success = true
|
165
|
+
})
|
166
|
+
|
167
|
+
if (success) smartActivate(this.app)
|
168
|
+
return success
|
169
|
+
}
|
170
|
+
|
171
|
+
Methods['terminal_reveal_tty'] = function terminal_reveal_tty(tty) {
|
172
|
+
var terminal = new Terminal()
|
173
|
+
return terminal.revealTty(tty)
|
174
|
+
}
|
175
|
+
|
176
|
+
Methods['terminal_panes'] = function terminal_panes() {
|
177
|
+
var terminal = new Terminal()
|
178
|
+
return terminal.panes()
|
179
|
+
}
|
180
|
+
|
181
|
+
// for tests ----------------------------------------------
|
182
|
+
|
183
|
+
Methods['test_ok'] = function test_ok(arg1) {
|
184
|
+
return arg1
|
185
|
+
}
|
186
|
+
|
187
|
+
Methods['test_err'] = function test_err(arg1) {
|
188
|
+
var error = new Error('testing error handling')
|
189
|
+
error.arg1 = arg1
|
190
|
+
throw error
|
191
|
+
}
|
192
|
+
|
193
|
+
|
194
|
+
// paths ---------------------------------------------------
|
195
|
+
Paths = (function(){
|
196
|
+
function splitPath(path) {
|
197
|
+
return path.split('/')
|
198
|
+
}
|
199
|
+
|
200
|
+
function joinPath(pathArray) {
|
201
|
+
var res = pathArray.join('/')
|
202
|
+
if (res[0] != '/') res = '/' + res
|
203
|
+
return res
|
204
|
+
}
|
205
|
+
|
206
|
+
function local(pathIn) {
|
207
|
+
var file = ThisApp.pathTo(ScriptContext).toString()
|
208
|
+
return join(dirname(file), pathIn)
|
209
|
+
}
|
210
|
+
|
211
|
+
function dirname(path) {
|
212
|
+
return joinPath(splitPath(path).slice(0, -1))
|
213
|
+
}
|
214
|
+
|
215
|
+
function basename(path) {
|
216
|
+
return splitPath(path).slice(-1)[0]
|
217
|
+
}
|
218
|
+
|
219
|
+
function join(root, extend) {
|
220
|
+
return joinPath(splitPath(root).concat(splitPath(extend)).filter(Boolean))
|
221
|
+
}
|
222
|
+
|
223
|
+
return {
|
224
|
+
local: local,
|
225
|
+
dirname: dirname,
|
226
|
+
basename: basename,
|
227
|
+
join: join,
|
228
|
+
}
|
229
|
+
})();
|
230
|
+
|
231
|
+
|
232
|
+
// Subprocess ---------------------------------------------------
|
233
|
+
|
234
|
+
var Subprocess = (function() {
|
235
|
+
var FILENAME_PREFIX = Paths.join('/tmp', PROGRAM_NAME + '-' + Date.now() + Math.random() + '-')
|
236
|
+
var _tmpfile = 0;
|
237
|
+
var _threads = [];
|
238
|
+
|
239
|
+
// if the script doesn't have a file to write to, it will block still.
|
240
|
+
function tmpfile() {
|
241
|
+
var filename = FILENAME_PREFIX + _tmpfile++ + '.log'
|
242
|
+
return filename
|
243
|
+
}
|
244
|
+
|
245
|
+
// fork a command, and return the PID.
|
246
|
+
function fork(command, detatch) {
|
247
|
+
var output = tmpfile()
|
248
|
+
var script = command + ' &> ' + output + ' & echo $!'
|
249
|
+
var thread = {
|
250
|
+
command: command,
|
251
|
+
pid: ThisApp.doShellScript(script),
|
252
|
+
output: output,
|
253
|
+
detatch: detatch,
|
254
|
+
}
|
255
|
+
console.log('forked process', JSON.stringify(thread, null, 2))
|
256
|
+
_threads.push(thread)
|
257
|
+
return thread
|
258
|
+
}
|
259
|
+
|
260
|
+
function kill(pid) {
|
261
|
+
try {
|
262
|
+
// this will raise an error if the kill command cant find that process.
|
263
|
+
ThisApp.doShellScript('kill ' + pid)
|
264
|
+
return true
|
265
|
+
} catch (err) {
|
266
|
+
return false
|
267
|
+
}
|
268
|
+
}
|
269
|
+
|
270
|
+
function del(filename) {
|
271
|
+
var path = Path(filename)
|
272
|
+
if (SystemEvents.exists(path)) {
|
273
|
+
SystemEvents.delete(path)
|
274
|
+
return true
|
275
|
+
}
|
276
|
+
return false
|
277
|
+
}
|
278
|
+
|
279
|
+
function cleanup() {
|
280
|
+
_threads.forEach(function(thread) {
|
281
|
+
if (!thread.detatch) kill(thread.pid)
|
282
|
+
del(thread.output)
|
283
|
+
})
|
284
|
+
}
|
285
|
+
|
286
|
+
return {
|
287
|
+
fork: fork,
|
288
|
+
cleanup: cleanup,
|
289
|
+
}
|
290
|
+
})();
|
291
|
+
|
292
|
+
// various utils ----------------------------------------------
|
293
|
+
|
294
|
+
function smartActivate(app) {
|
295
|
+
if (!app.frontmost()) {
|
296
|
+
app.activate()
|
297
|
+
}
|
298
|
+
}
|
299
|
+
|
300
|
+
function quotedForm(s) {
|
301
|
+
return "'" + s.replace(/'/g, "'\\''") + "'"
|
302
|
+
}
|
303
|
+
|
304
|
+
// non-blocking say text
|
305
|
+
function say(text) {
|
306
|
+
Subprocess.fork('say ' + quotedForm(text), true)
|
307
|
+
}
|
308
|
+
|
309
|
+
// debugging -----------------------------------------------
|
310
|
+
// these are left in here because they're useful if you ever want to develop this file again.
|
311
|
+
|
312
|
+
function log(obj, fieldName) {
|
313
|
+
var fn = fieldName || '>'
|
314
|
+
console.log(fn, Automation.getDisplayString(obj))
|
315
|
+
}
|
316
|
+
|
317
|
+
function typeName(obj) {
|
318
|
+
return Object.prototype.toString.call(obj)
|
319
|
+
}
|
320
|
+
|
321
|
+
function inspect(obj) {
|
322
|
+
console.log("--------v")
|
323
|
+
log(obj)
|
324
|
+
if (obj !== undefined) inspectDetail(obj)
|
325
|
+
console.log('--------^')
|
326
|
+
}
|
327
|
+
|
328
|
+
function inspectDetail(obj) {
|
329
|
+
var proto = obj.__proto__;
|
330
|
+
var constructor = obj.constructor;
|
331
|
+
var name = typeName(obj)
|
332
|
+
|
333
|
+
console.log('')
|
334
|
+
|
335
|
+
log(name, 'type name')
|
336
|
+
log(proto, 'prototype')
|
337
|
+
|
338
|
+
console.log('')
|
339
|
+
|
340
|
+
log(Object.keys(obj), 'keys')
|
341
|
+
for (var thing in obj) {
|
342
|
+
log(obj[thing], 'prop ' + Automation.getDisplayString(thing) + ':')
|
343
|
+
}
|
344
|
+
|
345
|
+
console.log('')
|
346
|
+
|
347
|
+
log(constructor, 'constructor')
|
348
|
+
}
|
349
|
+
// ---------------------------------------------------------
|