termdump 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|