termdump 0.2.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/README.md +80 -0
- data/Rakefile +20 -0
- data/bin/termdump +6 -0
- data/etc/Rakefile +19 -0
- data/etc/_termdump +25 -0
- data/etc/man.sh +3 -0
- data/etc/termdump +47 -0
- data/etc/termdump.1 +79 -0
- data/etc/termdump.1.md +61 -0
- data/lib/termdump/command.rb +81 -0
- data/lib/termdump/main.rb +474 -0
- data/lib/termdump/process.rb +19 -0
- data/lib/termdump/session.rb +84 -0
- data/lib/termdump/terminal/base/base.rb +50 -0
- data/lib/termdump/terminal/base/default.rb +6 -0
- data/lib/termdump/terminal/base/mock.rb +50 -0
- data/lib/termdump/terminal/base/terminal_helper.rb +35 -0
- data/lib/termdump/terminal/gnome_terminal.rb +44 -0
- data/lib/termdump/terminal/guake.rb +48 -0
- data/lib/termdump/terminal/terminator.rb +94 -0
- data/lib/termdump/terminal/xterm.rb +22 -0
- data/lib/termdump/version.rb +4 -0
- data/lib/termdump.rb +1 -0
- data/test/run_cmd.sh +8 -0
- data/test/test_basic_terminal.rb +32 -0
- data/test/test_command.rb +66 -0
- data/test/test_gnome_terminal.rb +13 -0
- data/test/test_main.rb +241 -0
- data/test/test_session.rb +112 -0
- data/test/test_terminator.rb +27 -0
- metadata +88 -0
@@ -0,0 +1,474 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'pathname'
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
require 'termdump/process'
|
6
|
+
require 'termdump/session'
|
7
|
+
|
8
|
+
module TermDump
|
9
|
+
class SessionSyntaxError < StandardError; end
|
10
|
+
|
11
|
+
class Main
|
12
|
+
BASE_DIR = "#{Dir.home}/.config/termdump/" # only for posix file system
|
13
|
+
@@session_dir = BASE_DIR + "session"
|
14
|
+
@@config_file = BASE_DIR + "config.yml"
|
15
|
+
|
16
|
+
def initialize
|
17
|
+
@config = read_configure
|
18
|
+
end
|
19
|
+
|
20
|
+
# Read configure from @@config_file if this file exists and return a Hash as result.
|
21
|
+
# The configure format is yaml.
|
22
|
+
def read_configure
|
23
|
+
config = {}
|
24
|
+
config = YAML.load(IO.read(@@config_file)) if File.exist? @@config_file
|
25
|
+
config
|
26
|
+
end
|
27
|
+
|
28
|
+
# initialize configure and session directory interactively
|
29
|
+
def init
|
30
|
+
if Dir.exist?(BASE_DIR)
|
31
|
+
return puts "The configure has been initialized yet"
|
32
|
+
end
|
33
|
+
dir = File.dirname(File.realpath(__FILE__)) # for ruby > 2.0.0, use __dir__
|
34
|
+
files = File.join(dir, 'terminal', '*.rb')
|
35
|
+
support_term = Dir.glob(files).map {|fn| File.basename(fn, '.rb')}
|
36
|
+
|
37
|
+
puts "Currently support terminals:"
|
38
|
+
support_term.each_with_index {|term, i| puts "[#{i}]\t#{term}"}
|
39
|
+
print "Select your terminal: "
|
40
|
+
choice = $stdin.gets.chomp
|
41
|
+
if choice.to_i != 0 || choice == '0'
|
42
|
+
choice = choice.to_i
|
43
|
+
if 0 <= choice && choice < support_term.size
|
44
|
+
print "Will create #{@@config_file} and #{@@session_dir}, go on?[Y/N] "
|
45
|
+
answer = $stdin.gets.chomp
|
46
|
+
return if answer == 'N' || answer == 'n'
|
47
|
+
FileUtils.mkpath @@session_dir
|
48
|
+
configure = {
|
49
|
+
'terminal' => support_term[choice]
|
50
|
+
}
|
51
|
+
IO.write @@config_file, YAML.dump(configure)
|
52
|
+
puts "Ok, the configure is initialized now. Happy coding!"
|
53
|
+
return
|
54
|
+
end
|
55
|
+
end
|
56
|
+
puts "Incorrect choice received!"
|
57
|
+
end
|
58
|
+
|
59
|
+
# Save the session into session file.
|
60
|
+
# If +print_stdout+ is true, print to stdout instead of saving it.
|
61
|
+
# If +exclude_current_pty+ is true, save all pty instead of the current one
|
62
|
+
def save session_name, print_stdout, exclude_current_pty
|
63
|
+
# TODO rewrite it once posixpsutil is mature
|
64
|
+
this_pid = ::Process.pid.to_s
|
65
|
+
pts = Hash.new {|k, v| []}
|
66
|
+
this_terminal_pid = nil
|
67
|
+
|
68
|
+
IO.popen('ps -eo pid,ppid,stat,tty,command').readlines.each do |entry|
|
69
|
+
pid, ppid, stat, tty, command = entry.rstrip.split(' ', 5)
|
70
|
+
if tty.start_with?('pts/')
|
71
|
+
tty.sub!('pts/', '')
|
72
|
+
# order by pid asc
|
73
|
+
case pts[tty].size
|
74
|
+
when 0
|
75
|
+
pts[tty] = [Process.new(pid, ppid, stat, command)]
|
76
|
+
else
|
77
|
+
session_leader = pts[tty].first
|
78
|
+
p = Process.new(pid, ppid, stat, command)
|
79
|
+
|
80
|
+
# this_terminal_pid -> session_leader_pids ->
|
81
|
+
# foreground_pids(exclude this_pid)
|
82
|
+
if p.pid == this_pid
|
83
|
+
this_terminal_pid = session_leader.ppid
|
84
|
+
elsif p.is_child_of(session_leader) && p.is_in_foreground
|
85
|
+
pts[tty].push(p)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
if exclude_current_pty
|
93
|
+
this_ppid = ::Process.ppid.to_s
|
94
|
+
pts.reject! do |tty, processes|
|
95
|
+
session_leader = processes.first
|
96
|
+
session_leader.ppid != this_terminal_pid || session_leader.pid == this_ppid
|
97
|
+
end
|
98
|
+
else
|
99
|
+
pts.reject! {|tty, processes| processes.first.ppid != this_terminal_pid }
|
100
|
+
end
|
101
|
+
return if pts.empty?
|
102
|
+
|
103
|
+
# get cwd for each session
|
104
|
+
session_cwd = {}
|
105
|
+
session_leader_pids = pts.values.map { |processes|
|
106
|
+
processes.first.pid
|
107
|
+
}.join(' ')
|
108
|
+
IO.popen("pwdx #{session_leader_pids}") do |f|
|
109
|
+
f.readlines.each do |entry|
|
110
|
+
# 1234: /home/xxx/yyy...
|
111
|
+
pid, cwd = entry.split(" ", 2)
|
112
|
+
pid = pid[0...-1]
|
113
|
+
session_cwd[pid] = cwd.rstrip
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
paths = session_cwd.values
|
118
|
+
path_prefix = {}
|
119
|
+
common_prefix = exact_commom_prefix(paths)
|
120
|
+
if common_prefix != ''
|
121
|
+
path_prefix['$PROJECT'] = common_prefix
|
122
|
+
session_cwd.each_value do |path|
|
123
|
+
path.sub!(common_prefix, '${PROJECT}') if path.start_with?(common_prefix)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
ptree = Hash.new {|k, v| {}}
|
128
|
+
pts.each_value do |processes|
|
129
|
+
session_leader = processes[0]
|
130
|
+
session = ptree[session_leader.ppid]
|
131
|
+
processes[0] = session_cwd[session_leader.pid]
|
132
|
+
session[session_leader.pid] = processes
|
133
|
+
ptree[session_leader.ppid] = session
|
134
|
+
end
|
135
|
+
|
136
|
+
if print_stdout
|
137
|
+
print_result ptree, path_prefix
|
138
|
+
else
|
139
|
+
result = dump ptree, path_prefix
|
140
|
+
save_to_file session_name, result
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# return the common prefix of given paths. If the common prefix is '/' or '~',
|
145
|
+
# or there is not common prefix at all, return ''
|
146
|
+
def exact_commom_prefix paths
|
147
|
+
home = Dir.home
|
148
|
+
paths = paths.map do |path|
|
149
|
+
new_path = path.sub!(home, '~') if path.start_with?(home)
|
150
|
+
# handle '/xxx'
|
151
|
+
new_path.nil? ? path.split('/') : new_path.split('/')
|
152
|
+
end
|
153
|
+
paths.sort! {|x, y| y.size <=> x.size }
|
154
|
+
# indicate if we have found common preifx or just ignore all the case
|
155
|
+
has_commom_prefix = false
|
156
|
+
common_prefix = paths.reduce do |prefix, path|
|
157
|
+
common_prefix = prefix
|
158
|
+
prefix.each_with_index do |v, i|
|
159
|
+
if v != path[i]
|
160
|
+
common_prefix = prefix[0...i]
|
161
|
+
break
|
162
|
+
end
|
163
|
+
end
|
164
|
+
# common_prefix should longer than '~/' and '/'
|
165
|
+
if common_prefix.size > 1
|
166
|
+
has_commom_prefix = true
|
167
|
+
common_prefix
|
168
|
+
else
|
169
|
+
# if there is not commom prefix between two path, just ignore it
|
170
|
+
prefix
|
171
|
+
end
|
172
|
+
end
|
173
|
+
has_commom_prefix && common_prefix.size > 1 ? common_prefix.join('/') : ''
|
174
|
+
end
|
175
|
+
|
176
|
+
# print the dumped result:
|
177
|
+
def print_result ptree, path_prefix
|
178
|
+
puts dump(ptree, path_prefix)
|
179
|
+
end
|
180
|
+
|
181
|
+
# dump the process tree in yml with such format:
|
182
|
+
# $0:xxx
|
183
|
+
# $1:...
|
184
|
+
# window0:
|
185
|
+
# cwd:
|
186
|
+
# tab0:
|
187
|
+
# cwd:
|
188
|
+
# command:comand
|
189
|
+
# window1...
|
190
|
+
#
|
191
|
+
# return a yml format string
|
192
|
+
def dump ptree, path_prefix
|
193
|
+
yml_tree = {}
|
194
|
+
path_prefix.each_pair do |k, v|
|
195
|
+
yml_tree[k] = v
|
196
|
+
end
|
197
|
+
|
198
|
+
win_order = 0
|
199
|
+
ptree.each_value do |session|
|
200
|
+
order = 0
|
201
|
+
tab_tree = {}
|
202
|
+
session.each_value do |v|
|
203
|
+
if order == 0
|
204
|
+
tab_tree['cwd'] = v[0]
|
205
|
+
tab_tree["command"] = v[1].command if v.size > 1
|
206
|
+
else
|
207
|
+
tab_node = {'cwd' => v[0]}
|
208
|
+
tab_node["command"] = v[1].command if v.size > 1
|
209
|
+
tab_tree["tab#{order-1}"] = tab_node
|
210
|
+
end
|
211
|
+
order += 1
|
212
|
+
end
|
213
|
+
yml_tree["window#{win_order}"] = tab_tree
|
214
|
+
win_order += 1
|
215
|
+
end
|
216
|
+
yml_tree.to_yaml
|
217
|
+
end
|
218
|
+
|
219
|
+
# return a Hash with two symbols:
|
220
|
+
# :exist => is session already existed
|
221
|
+
# :name => the absolute path of session
|
222
|
+
def search_session name
|
223
|
+
session_name = File.join(@@session_dir, name + ".yml")
|
224
|
+
{:exist => File.exist?(session_name), :name => session_name}
|
225
|
+
end
|
226
|
+
|
227
|
+
# save yml format string to a yml file in @@session_dir
|
228
|
+
def save_to_file session_name, result
|
229
|
+
begin
|
230
|
+
if session_name != ''
|
231
|
+
status = search_session session_name
|
232
|
+
overwrite = false
|
233
|
+
if status[:exist]
|
234
|
+
print "#{status[:name]} already exists, overwrite?[Y/N]:"
|
235
|
+
answer = $stdin.gets.chomp
|
236
|
+
overwrite = true if answer == 'Y' || answer == 'y'
|
237
|
+
end
|
238
|
+
if !status[:exist] || overwrite
|
239
|
+
IO.write status[:name], result
|
240
|
+
puts "Save session '#{session_name}' successfully"
|
241
|
+
return
|
242
|
+
end
|
243
|
+
end
|
244
|
+
print "Enter a new session name:"
|
245
|
+
name = $stdin.gets.chomp
|
246
|
+
save_to_file name, result
|
247
|
+
rescue Errno::ENOENT
|
248
|
+
FileUtils.mkpath(@@session_dir) unless File.exist?(@@session_dir)
|
249
|
+
save_to_file session_name, result # reentry
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
def list list_action
|
254
|
+
begin
|
255
|
+
Dir.chdir(@@session_dir)
|
256
|
+
sessions = Dir.glob('*.yml')
|
257
|
+
if sessions.empty?
|
258
|
+
puts "No session exists in #{@@session_dir}"
|
259
|
+
else
|
260
|
+
puts "order:\tsession name\tctime\t\tatime"
|
261
|
+
sessions.sort!{|x, y| File.atime(y) <=> File.atime(x) }
|
262
|
+
sessions.each_with_index do |f, i|
|
263
|
+
cdate = File.ctime(f).to_date.strftime
|
264
|
+
adate = File.atime(f).to_date.strftime
|
265
|
+
f.sub!(/\.yml$/, '')
|
266
|
+
puts format("[%d]: %15s\t%s\t%s", i, f, cdate, adate)
|
267
|
+
end
|
268
|
+
|
269
|
+
get_input_order = proc do |action, &handler|
|
270
|
+
print "Select one session to #{action}:"
|
271
|
+
order = $stdin.gets.chomp
|
272
|
+
if order.to_i != 0 || order == '0' # can order be an integer?
|
273
|
+
order = order.to_i
|
274
|
+
if 0 <= order && order < sessions.size
|
275
|
+
handler.call sessions[order]
|
276
|
+
return
|
277
|
+
end
|
278
|
+
end
|
279
|
+
puts "Received a wrong session order"
|
280
|
+
end
|
281
|
+
|
282
|
+
case list_action
|
283
|
+
when :load, :edit, :delete
|
284
|
+
get_input_order.call(list_action) { |session|
|
285
|
+
send("#{list_action}_session", session) }
|
286
|
+
end
|
287
|
+
end
|
288
|
+
rescue Errno::ENOENT
|
289
|
+
FileUtils.mkpath(@@session_dir) unless File.exist?(@@session_dir)
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
def delete_session name
|
294
|
+
status = search_session name
|
295
|
+
return puts "#{status[:name]} not found" unless status[:exist]
|
296
|
+
File.delete status[:name]
|
297
|
+
puts "Delete session '#{name}' successfully"
|
298
|
+
end
|
299
|
+
|
300
|
+
def edit_session name
|
301
|
+
FileUtils.mkpath(@@session_dir) unless File.exist?(@@session_dir)
|
302
|
+
status = search_session name
|
303
|
+
return puts "#{status[:name]} not found" unless status[:exist]
|
304
|
+
return puts "Set $EDITOR as your favourite editor to edit the session" unless ENV['EDITOR']
|
305
|
+
exec ENV['EDITOR'], status[:name]
|
306
|
+
end
|
307
|
+
|
308
|
+
def load_session name
|
309
|
+
ptree = load_file name
|
310
|
+
if ptree != {}
|
311
|
+
begin
|
312
|
+
ptree = check ptree
|
313
|
+
rescue SessionSyntaxError => e
|
314
|
+
puts "Parse session file error: #{e.message}"
|
315
|
+
exit 1
|
316
|
+
end
|
317
|
+
Session.new(@config).replay(ptree)
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
# load the process tree from yml format string
|
322
|
+
def load_file name
|
323
|
+
status = search_session name
|
324
|
+
unless status[:exist]
|
325
|
+
puts "#{status[:name]} not found"
|
326
|
+
{}
|
327
|
+
else
|
328
|
+
YAML.load(IO.read(status[:name]))
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
332
|
+
# Raise SessionSyntaxError if their is syntax error in session file.
|
333
|
+
# Session file format:
|
334
|
+
# $variables...
|
335
|
+
# window0:
|
336
|
+
# cwd:
|
337
|
+
# command:
|
338
|
+
# tab0:
|
339
|
+
# cwd:
|
340
|
+
# vsplit:
|
341
|
+
# hsplit:
|
342
|
+
# Restriction:
|
343
|
+
# 1. There are only four types of node: :window, :tab, :vsplit and :hsplit
|
344
|
+
# 2. node level: :window > :tab > :vsplit, :hsplit
|
345
|
+
# 3. The root node should be :window
|
346
|
+
# 4. The root node should have a cwd attributes.
|
347
|
+
# If a node itself does not have cwd attributes, it inherits its parent's
|
348
|
+
# 5. Each node has only one 'cwd' and at most one 'command'
|
349
|
+
# 6. the type of 'cwd' and 'command' is String
|
350
|
+
def check ptree
|
351
|
+
# (5) is ensured by yml syntax
|
352
|
+
parse_variables ptree
|
353
|
+
ptree.each_pair do |k, node|
|
354
|
+
check_node node, :window if k.start_with?("window")
|
355
|
+
end
|
356
|
+
ptree
|
357
|
+
end
|
358
|
+
|
359
|
+
def check_node node, node_type, parent_cwd=''
|
360
|
+
unless node.is_a?(Hash)
|
361
|
+
raise SessionSyntaxError.new("#{node_type} should be a node")
|
362
|
+
end
|
363
|
+
if node.has_key?('cwd')
|
364
|
+
path = node['cwd']
|
365
|
+
unless path.is_a?(String)
|
366
|
+
raise SessionSyntaxError.new("'cwd' should be a String")
|
367
|
+
end
|
368
|
+
unless Pathname.new(path).absolute?
|
369
|
+
if parent_cwd == ''
|
370
|
+
msg = "missing base working directory for relative path #{path}"
|
371
|
+
raise SessionSyntaxError.new(msg)
|
372
|
+
end
|
373
|
+
path = File.absolute_path(path, parent_cwd)
|
374
|
+
node['cwd'] = path
|
375
|
+
end
|
376
|
+
unless check_cwd_cd_able path
|
377
|
+
raise SessionSyntaxError.new("can't cd to #{path}")
|
378
|
+
end
|
379
|
+
else
|
380
|
+
if parent_cwd == ''
|
381
|
+
raise SessionSyntaxError.new("'cwd' not found in #{node_type}")
|
382
|
+
end
|
383
|
+
node['cwd'] = parent_cwd
|
384
|
+
end
|
385
|
+
cwd = node['cwd']
|
386
|
+
|
387
|
+
if node.has_key?('command')
|
388
|
+
unless node['command'].is_a?(String)
|
389
|
+
raise SessionSyntaxError.new("'command' should be a String")
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
remain_attributes = ['cwd', 'command']
|
394
|
+
case node_type
|
395
|
+
when :window
|
396
|
+
node.each_pair do |attr, value|
|
397
|
+
if attr.start_with?('window')
|
398
|
+
check_node value, :window, cwd
|
399
|
+
elsif attr.start_with?('tab')
|
400
|
+
check_node value, :tab, cwd
|
401
|
+
elsif attr.start_with?('vsplit')
|
402
|
+
check_node value, :vsplit, cwd
|
403
|
+
elsif attr.start_with?('hsplit')
|
404
|
+
check_node value, :hsplit, cwd
|
405
|
+
elsif !remain_attributes.include?(attr)
|
406
|
+
node.delete attr
|
407
|
+
end
|
408
|
+
end
|
409
|
+
when :tab
|
410
|
+
node.each_pair do |attr, value|
|
411
|
+
if attr.start_with?('tab')
|
412
|
+
check_node value, :tab, cwd
|
413
|
+
elsif attr.start_with?('vsplit')
|
414
|
+
check_node value, :vsplit, cwd
|
415
|
+
elsif attr.start_with?('hsplit')
|
416
|
+
check_node value, :hsplit, cwd
|
417
|
+
elsif !remain_attributes.include?(attr)
|
418
|
+
node.delete attr
|
419
|
+
end
|
420
|
+
end
|
421
|
+
when :vsplit, :hsplit
|
422
|
+
node.each_pair do |attr, value|
|
423
|
+
if attr.start_with?('vsplit')
|
424
|
+
check_node value, :vsplit, cwd
|
425
|
+
elsif attr.start_with?('hsplit')
|
426
|
+
check_node value, :hsplit, cwd
|
427
|
+
elsif !remain_attributes.include?(attr)
|
428
|
+
node.delete attr
|
429
|
+
end
|
430
|
+
end
|
431
|
+
end
|
432
|
+
end
|
433
|
+
|
434
|
+
# +path+ is absolute path
|
435
|
+
def check_cwd_cd_able path
|
436
|
+
Dir.exist?(path) && File.readable?(path)
|
437
|
+
end
|
438
|
+
|
439
|
+
def parse_variables ptree
|
440
|
+
# rewrite with tap for ruby > 1.9
|
441
|
+
variables = ptree.select {|k, v| k.start_with?('$')}
|
442
|
+
ptree.delete_if {|k, v| k.start_with?('$')}
|
443
|
+
var = Regexp.new(/(?<!\\) # don't trap in \$
|
444
|
+
\$\{
|
445
|
+
.*?[^\\] # parse the name until }(but not \})
|
446
|
+
\}/mix).freeze
|
447
|
+
scan_tree = proc do |node|
|
448
|
+
node.each_pair do |k, v|
|
449
|
+
if k == 'cwd'
|
450
|
+
cwd = node[k]
|
451
|
+
cwd.gsub!(var) do |match|
|
452
|
+
# match is sth like ${foo}
|
453
|
+
name = match[2...-1]
|
454
|
+
value = variables['$' + name]
|
455
|
+
# Enter value for unknown variables
|
456
|
+
if value.nil?
|
457
|
+
print "Enter the value of '#{name}':"
|
458
|
+
value = $stdin.gets.chomp
|
459
|
+
end
|
460
|
+
value
|
461
|
+
end
|
462
|
+
cwd.sub!('~', Dir.home) if cwd == '~' || cwd.start_with?('~/')
|
463
|
+
elsif v.is_a?(Hash)
|
464
|
+
scan_tree.call v
|
465
|
+
end
|
466
|
+
end
|
467
|
+
end
|
468
|
+
|
469
|
+
ptree.each_pair {|k, v| scan_tree.call v if k.start_with?('window')}
|
470
|
+
end
|
471
|
+
end
|
472
|
+
|
473
|
+
end
|
474
|
+
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module TermDump
|
2
|
+
class Process
|
3
|
+
attr_accessor :pid, :ppid, :stat, :command
|
4
|
+
def initialize pid, ppid, stat, command
|
5
|
+
@pid = pid
|
6
|
+
@ppid = ppid
|
7
|
+
@stat = stat
|
8
|
+
@command = command
|
9
|
+
end
|
10
|
+
|
11
|
+
def is_in_foreground
|
12
|
+
@stat.start_with?('S') && @stat.include?('+')
|
13
|
+
end
|
14
|
+
|
15
|
+
def is_child_of process
|
16
|
+
@ppid == process.pid
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module TermDump
|
2
|
+
Node = Struct.new(:type, :name, :cwd, :command)
|
3
|
+
|
4
|
+
class Session
|
5
|
+
# Optional Configures:
|
6
|
+
# * terminal: The type of terminal
|
7
|
+
# * new_window: The key sequence used to open a new window
|
8
|
+
# * new_tab: The key sequence used to open a new tab
|
9
|
+
# * new_vsplit: The key sequence used to open a new vsplit
|
10
|
+
# * new_hsplit: The key sequence used to open a new hsplit
|
11
|
+
def initialize config={}
|
12
|
+
if config.has_key?('terminal')
|
13
|
+
begin
|
14
|
+
terminal = config['terminal']
|
15
|
+
require_relative "terminal/#{terminal}"
|
16
|
+
rescue LoadError
|
17
|
+
puts "Load with terminal #{terminal} error:"
|
18
|
+
puts "Not support #{terminal} yet!"
|
19
|
+
exit 0
|
20
|
+
end
|
21
|
+
else
|
22
|
+
require_relative "terminal/base/default"
|
23
|
+
end
|
24
|
+
@terminal = Terminal.new(config)
|
25
|
+
@node_queue = []
|
26
|
+
@support_split = Terminal.public_method_defined?(:vsplit) &&
|
27
|
+
Terminal.public_method_defined?(:hsplit)
|
28
|
+
@support_tab = Terminal.public_method_defined?(:tab)
|
29
|
+
end
|
30
|
+
|
31
|
+
def replay task
|
32
|
+
scan task
|
33
|
+
fallback
|
34
|
+
exec
|
35
|
+
end
|
36
|
+
|
37
|
+
def enqueue type, name, attributes
|
38
|
+
@node_queue.push(Node.new(type, name,
|
39
|
+
attributes['cwd'], attributes['command']))
|
40
|
+
end
|
41
|
+
|
42
|
+
def scan node
|
43
|
+
node.each_pair do |k, v|
|
44
|
+
if k.start_with?('tab')
|
45
|
+
enqueue :tab, k, v
|
46
|
+
elsif k.start_with?('vsplit')
|
47
|
+
enqueue :vsplit, k, v
|
48
|
+
elsif k.start_with?('hsplit')
|
49
|
+
enqueue :hsplit, k, v
|
50
|
+
elsif k.start_with?('window')
|
51
|
+
enqueue :window, k, v
|
52
|
+
end
|
53
|
+
scan v if v.is_a?(Hash)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def fallback
|
58
|
+
unless @support_split
|
59
|
+
@node_queue.each_index do |i|
|
60
|
+
if @node_queue[i].type == :vsplit || @node_queue[i].type == :hsplit
|
61
|
+
@node_queue[i].type = :tab
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
unless @support_tab
|
66
|
+
@node_queue.each_index { |i|
|
67
|
+
@node_queue[i].type = :window if @node_queue[i].type == :tab }
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def exec
|
72
|
+
current_tab = @node_queue.first
|
73
|
+
@terminal.exec current_tab.cwd, current_tab.command
|
74
|
+
@node_queue.shift
|
75
|
+
@node_queue.each do |node|
|
76
|
+
case node.type
|
77
|
+
when :window, :tab, :vsplit, :hsplit
|
78
|
+
@terminal.method(node.type).call(node.name, node.cwd, node.command)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require_relative './terminal_helper'
|
2
|
+
|
3
|
+
module TermDump
|
4
|
+
class BasicTerminal
|
5
|
+
include TerminalHelper
|
6
|
+
|
7
|
+
def initialize config
|
8
|
+
@user_defined_config = config
|
9
|
+
@config = {}
|
10
|
+
@default_config = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
# Get user defined value/configure value/default value with a configure item.
|
14
|
+
# Raise keyError if value not found.
|
15
|
+
def configure configure_key
|
16
|
+
@user_defined_config.fetch(configure_key) {|key_in_config|
|
17
|
+
@config.fetch(key_in_config) {|default_key|
|
18
|
+
@default_config.fetch(default_key)}}
|
19
|
+
end
|
20
|
+
|
21
|
+
# run command in current window
|
22
|
+
# +cwd+ is the directory the command executed in
|
23
|
+
# +cmd+ if the cmd is nil, don't need to execute it; else execute the cmd
|
24
|
+
def exec cwd, cmd
|
25
|
+
raise NotImplementedError.new(
|
26
|
+
"exec should be implemented to execute cmd on current window")
|
27
|
+
end
|
28
|
+
|
29
|
+
# open a new window of this terminal automatically and focus on
|
30
|
+
# +name+ is the name of new window
|
31
|
+
# +cwd+ is the directory the command executed in
|
32
|
+
# +cmd+ if the cmd is nil, don't need to execute it; else execute the cmd
|
33
|
+
def window name, cwd, cmd
|
34
|
+
raise NotImplementedError.new(
|
35
|
+
"window should be implemented to open new window")
|
36
|
+
end
|
37
|
+
|
38
|
+
# open a new tab of this terminal automatically and focus on
|
39
|
+
# implement it if your terminal support tabs
|
40
|
+
# def tab(name, cwd, cmd); end
|
41
|
+
|
42
|
+
# open a new vertical split of this terminal automatically and focus on
|
43
|
+
# implement it if your terminal support tabs
|
44
|
+
# def vsplit(name, cwd, cmd); end
|
45
|
+
|
46
|
+
# open a new horizontal split of this terminal automatically and focus on
|
47
|
+
# implement it if your terminal support tabs
|
48
|
+
# def hsplit(name, cwd, cmd); end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
|
3
|
+
module TermDump
|
4
|
+
class Terminal < BasicTerminal
|
5
|
+
attr_reader :done_actions
|
6
|
+
def initialize config
|
7
|
+
@done_actions = []
|
8
|
+
end
|
9
|
+
|
10
|
+
def exec cwd, cmd
|
11
|
+
if cmd.nil?
|
12
|
+
@done_actions.push(cwd)
|
13
|
+
else
|
14
|
+
@done_actions.push(cwd, cmd)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def window name, cwd, cmd
|
19
|
+
if cmd.nil?
|
20
|
+
@done_actions.push(:window, name, cwd)
|
21
|
+
else
|
22
|
+
@done_actions.push(:window, name, cwd, cmd)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def tab name, cwd, cmd
|
27
|
+
if cmd.nil?
|
28
|
+
@done_actions.push(:tab, name, cwd)
|
29
|
+
else
|
30
|
+
@done_actions.push(:tab, name, cwd, cmd)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def vsplit name, cwd, cmd
|
35
|
+
if cmd.nil?
|
36
|
+
@done_actions.push(:vsplit, name, cwd)
|
37
|
+
else
|
38
|
+
@done_actions.push(:vsplit, name, cwd, cmd)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def hsplit name, cwd, cmd
|
43
|
+
if cmd.nil?
|
44
|
+
@done_actions.push(:hsplit, name, cwd)
|
45
|
+
else
|
46
|
+
@done_actions.push(:hsplit, name, cwd, cmd)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|