appear 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
// ---------------------------------------------------------
|