cetusql 0.0.1
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/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +52 -0
- data/Rakefile +2 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/cetusql.gemspec +34 -0
- data/exe/cetussql +975 -0
- data/lib/cetusql.rb +5 -0
- data/lib/cetusql/cli_sqlite.rb +285 -0
- data/lib/cetusql/cli_utils.rb +412 -0
- data/lib/cetusql/version.rb +3 -0
- metadata +106 -0
data/lib/cetusql.rb
ADDED
@@ -0,0 +1,285 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
# ----------------------------------------------------------------------------- #
|
3
|
+
# File: cli_sqlite.rb
|
4
|
+
# Description: common sqlite routines to be sourced in ruby shell programs
|
5
|
+
# Author: j kepler http://github.com/mare-imbrium/canis/
|
6
|
+
# Date: 2017-03-18 - 17:53
|
7
|
+
# License: MIT
|
8
|
+
# Last update: 2017-03-29 14:17
|
9
|
+
# ----------------------------------------------------------------------------- #
|
10
|
+
# YFF Copyright (C) 2012-2016 j kepler
|
11
|
+
# ----------------------------------------------------------------------------- #
|
12
|
+
require 'sqlite3'
|
13
|
+
require 'shellwords'
|
14
|
+
require 'pp'
|
15
|
+
|
16
|
+
def get_table_names db
|
17
|
+
#raise "No database file selected." unless $current_db
|
18
|
+
|
19
|
+
tables = get_data "select name from sqlite_master where type='table'"
|
20
|
+
tables.collect!{|x| x[0] } ## 1.9 hack, but will it run on 1.8 ??
|
21
|
+
tables
|
22
|
+
end
|
23
|
+
def get_column_names tbname
|
24
|
+
get_metadata tbname
|
25
|
+
end
|
26
|
+
# connect to given database, and if no name supplied then allow user to choose
|
27
|
+
def connect dbname=nil
|
28
|
+
dbname ||= getdbname
|
29
|
+
return nil unless dbname
|
30
|
+
#$log.debug "XXX: CONNECT got #{dbname} "
|
31
|
+
$current_db = dbname
|
32
|
+
$db = SQLite3::Database.new(dbname) if dbname
|
33
|
+
return $db
|
34
|
+
end
|
35
|
+
def get_data db, sql
|
36
|
+
#$log.debug "SQL: #{sql} "
|
37
|
+
$columns, *rows = db.execute2(sql)
|
38
|
+
#$log.debug "XXX COLUMNS #{sql}, #{rows.count} "
|
39
|
+
content = rows
|
40
|
+
return nil if content.nil? or content[0].nil?
|
41
|
+
$datatypes = content[0].types #if @datatypes.nil?
|
42
|
+
return content
|
43
|
+
end
|
44
|
+
def get_metadata table
|
45
|
+
get_data "select * from #{table} limit 1"
|
46
|
+
return $columns
|
47
|
+
end
|
48
|
+
# TODO option of headers
|
49
|
+
# run query and put into a temp table and view it using vim
|
50
|
+
# If no outputfile name passed, then use temp table
|
51
|
+
# What about STDOUT
|
52
|
+
# TODO use temp file, format it there and append to given file only after termtable
|
53
|
+
def OLDview_data db, sql, options
|
54
|
+
outputfile = options[:output_to]
|
55
|
+
formatting = options[:formatting]
|
56
|
+
headers = options[:headers]
|
57
|
+
#str = db.get_data sql
|
58
|
+
rs = db.execute_query sql
|
59
|
+
str = rs.content
|
60
|
+
columns = rs.columns
|
61
|
+
#puts "SQL: #{sql}.\nstr: #{str.size}"
|
62
|
+
data = []
|
63
|
+
if headers
|
64
|
+
data << columns.join("\t")
|
65
|
+
end
|
66
|
+
str.each {|line| data << line.join("\t"); }
|
67
|
+
#puts "Rows: #{data.size}"
|
68
|
+
require 'tempfile'
|
69
|
+
tmpfile = Tempfile.new('SQL.XXXXXX')
|
70
|
+
filename = tmpfile.path
|
71
|
+
filename = Shellwords.escape(filename)
|
72
|
+
#puts "Writing to #{filename}"
|
73
|
+
tmpfile.write(data.join("\n"))
|
74
|
+
tmpfile.close # need to flush, otherwise write is buffered
|
75
|
+
headerstr=nil
|
76
|
+
if formatting
|
77
|
+
headerstr = "-H" unless headers
|
78
|
+
# sometimes this can be slow, and it can fault on UTF-8 chars
|
79
|
+
system("cat #{filename} | term-table.rb #{headerstr} | sponge #{filename}")
|
80
|
+
end
|
81
|
+
if outputfile
|
82
|
+
#puts "comes here"
|
83
|
+
system("cp #{filename} #{outputfile}")
|
84
|
+
filename = outputfile
|
85
|
+
end
|
86
|
+
system "wc -l #{filename}" if $opt_debug
|
87
|
+
|
88
|
+
#system "$EDITOR #{filename}"
|
89
|
+
system "vim -c ':set nowrap' #{filename}"
|
90
|
+
tmpfile.close
|
91
|
+
tmpfile.unlink
|
92
|
+
end
|
93
|
+
def view_data db, sql, options
|
94
|
+
outputfile = options[:output_to]
|
95
|
+
formatting = options[:formatting]
|
96
|
+
headers = options[:headers]
|
97
|
+
#str = db.get_data sql
|
98
|
+
rs = db.execute_query sql
|
99
|
+
str = rs.content
|
100
|
+
columns = rs.columns
|
101
|
+
#puts "SQL: #{sql}.\nstr: #{str.size}"
|
102
|
+
#data = []
|
103
|
+
#if headers
|
104
|
+
#data << columns.join("\t")
|
105
|
+
#end
|
106
|
+
#str.each {|line| data << line.join("\t"); }
|
107
|
+
#puts "Rows: #{data.size}"
|
108
|
+
headerstr=nil
|
109
|
+
tmpfile = nil
|
110
|
+
if formatting
|
111
|
+
if headers
|
112
|
+
str.unshift(columns)
|
113
|
+
end
|
114
|
+
filename = tabulate2 str, options
|
115
|
+
else
|
116
|
+
data = []
|
117
|
+
if headers
|
118
|
+
data << columns.join("\t")
|
119
|
+
end
|
120
|
+
str.each {|line| data << line.join("\t"); }
|
121
|
+
tmpfile = mktemp()
|
122
|
+
tmpfile.write(data.join("\n"))
|
123
|
+
tmpfile.close # need to flush, otherwise write is buffered
|
124
|
+
filename = tmpfile.path
|
125
|
+
end
|
126
|
+
if outputfile
|
127
|
+
#puts "comes here"
|
128
|
+
system("cp #{filename} #{outputfile}")
|
129
|
+
filename = outputfile
|
130
|
+
end
|
131
|
+
|
132
|
+
#system "$EDITOR #{filename}"
|
133
|
+
system "vim -c ':set nowrap' #{filename}"
|
134
|
+
if tmpfile
|
135
|
+
tmpfile.close
|
136
|
+
tmpfile.unlink
|
137
|
+
end
|
138
|
+
end
|
139
|
+
def mktemp
|
140
|
+
require 'tempfile'
|
141
|
+
tmpfile = Tempfile.new('SQL.XXXXXX')
|
142
|
+
filename = tmpfile.path
|
143
|
+
filename = Shellwords.escape(filename)
|
144
|
+
#puts "Writing to #{filename}"
|
145
|
+
#tmpfile.write(data.join("\n"))
|
146
|
+
#tmpfile.close # need to flush, otherwise write is buffered
|
147
|
+
return tmpfile
|
148
|
+
end
|
149
|
+
# given content returned by get_data, formats and returns in a file
|
150
|
+
def tabulate content, options
|
151
|
+
data = []
|
152
|
+
content.each {|line| data << line.join("\t"); }
|
153
|
+
puts "Rows: #{data.size}" if $opt_verbose
|
154
|
+
require 'tempfile'
|
155
|
+
tmpfile = Tempfile.new('SQL.XXXXXX')
|
156
|
+
filename = tmpfile.path
|
157
|
+
#filename = Shellwords.escape(filename)
|
158
|
+
#puts "Writing to #{filename}"
|
159
|
+
tmpfile.write(data.join("\n"))
|
160
|
+
tmpfile.close # need to flush, otherwise write is buffered
|
161
|
+
if options[:formatting]
|
162
|
+
system("term-table.rb < #{filename} | sponge #{filename}")
|
163
|
+
end
|
164
|
+
return filename
|
165
|
+
end
|
166
|
+
# rather than use external program with dependencies,
|
167
|
+
# we generate tabular format for an array with headings
|
168
|
+
# returns filename
|
169
|
+
# TODO check for headings true
|
170
|
+
def tabulate2 content, options
|
171
|
+
widths = calculate_column_widths(content, 99)
|
172
|
+
str = "| "
|
173
|
+
sep = "+"
|
174
|
+
widths.each do |w|
|
175
|
+
str << "%-#{w}s | "
|
176
|
+
sep << ("-"*(w+2)) + "+"
|
177
|
+
end
|
178
|
+
data = []
|
179
|
+
data << sep
|
180
|
+
content.each_with_index {|line, ix|
|
181
|
+
data << str % line
|
182
|
+
data << sep if ix == 0
|
183
|
+
}
|
184
|
+
data << sep
|
185
|
+
require 'tempfile'
|
186
|
+
tmpfile = Tempfile.new('SQL.XXXXXX')
|
187
|
+
filename = tmpfile.path
|
188
|
+
#filename = Shellwords.escape(filename)
|
189
|
+
#puts "Writing to #{filename}"
|
190
|
+
tmpfile.write(data.join("\n"))
|
191
|
+
tmpfile.close # need to flush, otherwise write is buffered
|
192
|
+
return filename
|
193
|
+
end
|
194
|
+
|
195
|
+
def calculate_column_widths content, maxrows=99
|
196
|
+
widths = []
|
197
|
+
content.first.each_with_index {|r,i| widths << calculate_column_width(content, i, maxrows) }
|
198
|
+
return widths
|
199
|
+
end
|
200
|
+
def calculate_column_width content, col, maxrows=99
|
201
|
+
ret = 1
|
202
|
+
ctr = 0
|
203
|
+
content.each_with_index { |r, i|
|
204
|
+
break if ctr > maxrows
|
205
|
+
ctr += 1
|
206
|
+
c = r[col]
|
207
|
+
x = c.to_s.length
|
208
|
+
ret = x if x > ret
|
209
|
+
}
|
210
|
+
ret
|
211
|
+
end
|
212
|
+
class Database
|
213
|
+
|
214
|
+
ResultSet = Struct.new(:content, :columns, :datatypes)
|
215
|
+
|
216
|
+
def initialize(name)
|
217
|
+
raise ArgumentError, "Database name cannot be nil" unless name
|
218
|
+
@name = name
|
219
|
+
connect name
|
220
|
+
end
|
221
|
+
def connect name
|
222
|
+
raise ArgumentError, "Database name cannot be nil" unless name
|
223
|
+
@tables = nil
|
224
|
+
@db = SQLite3::Database.new(name)
|
225
|
+
end
|
226
|
+
def tables
|
227
|
+
return @tables if @tables
|
228
|
+
tables = sql "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
|
229
|
+
tables.collect!{|x| x[0] } ## 1.9 hack, but will it run on 1.8 ??
|
230
|
+
@tables = tables
|
231
|
+
end
|
232
|
+
def columns table
|
233
|
+
raise ArgumentError, "#{$0}: table name cannot be nil" unless table
|
234
|
+
columns, ignore = self.get_metadata table
|
235
|
+
return columns
|
236
|
+
end
|
237
|
+
def indexed_columns table
|
238
|
+
indexes = sql %Q{select sql from sqlite_master where type='index' and tbl_name = "#{table}" }
|
239
|
+
return nil if indexes == []
|
240
|
+
#pp indexes
|
241
|
+
arr = []
|
242
|
+
indexes.each do |ind|
|
243
|
+
if ind.first.nil?
|
244
|
+
arr << "auto?"
|
245
|
+
next
|
246
|
+
end
|
247
|
+
m = ind.first.match /\((.*)\)/
|
248
|
+
arr << m[1] if m
|
249
|
+
end
|
250
|
+
return arr.join(",")
|
251
|
+
end
|
252
|
+
def get_metadata table
|
253
|
+
raise ArgumentError, "#{$0}: table name cannot be nil" unless table
|
254
|
+
sql = "select * from #{table} limit 1"
|
255
|
+
columns, *rows = @db.execute2(sql)
|
256
|
+
content = rows
|
257
|
+
return nil if content.nil? or content[0].nil?
|
258
|
+
datatypes = content[0].types
|
259
|
+
return columns, datatypes
|
260
|
+
end
|
261
|
+
# runs sql query and returns an array of arrays
|
262
|
+
def get_data sql
|
263
|
+
#$log.debug "SQL: #{sql} "
|
264
|
+
columns, *rows = @db.execute2(sql)
|
265
|
+
#$log.debug "XXX COLUMNS #{sql}, #{rows.count} "
|
266
|
+
content = rows
|
267
|
+
return content
|
268
|
+
end
|
269
|
+
alias :sql :get_data
|
270
|
+
def execute_query sql
|
271
|
+
columns, *rows = @db.execute2(sql)
|
272
|
+
content = rows
|
273
|
+
return nil if content.nil? or content[0].nil?
|
274
|
+
datatypes = content[0].types
|
275
|
+
rs = ResultSet.new(content, columns, datatypes)
|
276
|
+
return rs
|
277
|
+
end
|
278
|
+
def sql_recent_rows tablename
|
279
|
+
sql = "SELECT * from #{tablename} ORDER by rowid DESC LIMIT 100"
|
280
|
+
end
|
281
|
+
def close
|
282
|
+
@db = nil
|
283
|
+
@tables = nil
|
284
|
+
end
|
285
|
+
end
|
@@ -0,0 +1,412 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# ----------------------------------------------------------------------------- #
|
3
|
+
# File: cli_utils.rb
|
4
|
+
# Description: common stuff for command line utils such as menu, getting a single char
|
5
|
+
# inline variable editing
|
6
|
+
# Author: j kepler http://github.com/mare-imbrium/canis/
|
7
|
+
# Date: 2017-03-18 - 14:33
|
8
|
+
# License: MIT
|
9
|
+
# Last update: 2017-04-02 19:47
|
10
|
+
# ----------------------------------------------------------------------------- #
|
11
|
+
# YFF Copyright (C) 2012-2016 j kepler
|
12
|
+
|
13
|
+
|
14
|
+
# edit a variable inline like zsh's vared
|
15
|
+
def vared var, prompt=">"
|
16
|
+
Readline.pre_input_hook = -> do
|
17
|
+
Readline.insert_text var
|
18
|
+
Readline.redisplay
|
19
|
+
# Remove the hook right away.
|
20
|
+
Readline.pre_input_hook = nil
|
21
|
+
end
|
22
|
+
begin
|
23
|
+
input = Readline.readline(prompt, false)
|
24
|
+
rescue Exception => e
|
25
|
+
return nil
|
26
|
+
end
|
27
|
+
input
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
# What if we only want to allow the given keys and ignore others.
|
32
|
+
# In menu maybe ENTER and other such keys should be ignored, or atleast
|
33
|
+
# option should be there, so i don't accidentally hit enter.
|
34
|
+
# print in columns, but take into account size so we don't exceed COLS
|
35
|
+
# (Some entries like paths can be long)
|
36
|
+
def menu title, h
|
37
|
+
return unless h
|
38
|
+
|
39
|
+
pbold "#{title}"
|
40
|
+
ctr = 0
|
41
|
+
#h.each_pair { |k, v| puts " #{k}: #{v}" }
|
42
|
+
h.each_pair { |k, v|
|
43
|
+
print " #{k}: %-20s" % [v]
|
44
|
+
if ctr > 1
|
45
|
+
print "\n"
|
46
|
+
ctr = 0
|
47
|
+
else
|
48
|
+
ctr += 1
|
49
|
+
end
|
50
|
+
}
|
51
|
+
print "\n >"
|
52
|
+
#print "\r >"
|
53
|
+
ch = get_char
|
54
|
+
puts ch
|
55
|
+
#system("clear") # ???
|
56
|
+
binding = h[ch]
|
57
|
+
binding = h[ch.to_sym] unless binding
|
58
|
+
if binding
|
59
|
+
if respond_to?(binding, true)
|
60
|
+
# 2017-03-19 - we can't send return values from a method ??
|
61
|
+
send(binding)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
return ch, binding
|
65
|
+
end
|
66
|
+
## get a character from user and return as a string
|
67
|
+
# Adapted from:
|
68
|
+
#http://stackoverflow.com/questions/174933/how-to-get-a-single-character-without-pressing-enter/8274275#8274275
|
69
|
+
# Need to take complex keys and matc against a hash.
|
70
|
+
def get_char
|
71
|
+
begin
|
72
|
+
system("stty raw -echo 2>/dev/null") # turn raw input on
|
73
|
+
c = nil
|
74
|
+
#if $stdin.ready?
|
75
|
+
c = $stdin.getc
|
76
|
+
cn=c.ord
|
77
|
+
return "ENTER" if cn == 10 || cn == 13
|
78
|
+
return "BACKSPACE" if cn == 127
|
79
|
+
return "C-SPACE" if cn == 0
|
80
|
+
return "SPACE" if cn == 32
|
81
|
+
# next does not seem to work, you need to bind C-i
|
82
|
+
return "TAB" if cn == 8
|
83
|
+
if cn >= 0 && cn < 27
|
84
|
+
x= cn + 96
|
85
|
+
return "C-#{x.chr}"
|
86
|
+
end
|
87
|
+
if c == ''
|
88
|
+
buff=c.chr
|
89
|
+
while true
|
90
|
+
k = nil
|
91
|
+
if $stdin.ready?
|
92
|
+
k = $stdin.getc
|
93
|
+
#puts "got #{k}"
|
94
|
+
buff += k.chr
|
95
|
+
else
|
96
|
+
x=$kh[buff]
|
97
|
+
return x if x
|
98
|
+
#puts "returning with #{buff}"
|
99
|
+
if buff.size == 2
|
100
|
+
## possibly a meta/alt char
|
101
|
+
k = buff[-1]
|
102
|
+
return "M-#{k.chr}"
|
103
|
+
end
|
104
|
+
return buff
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
#end
|
109
|
+
return c.chr if c
|
110
|
+
ensure
|
111
|
+
#system "stty -raw echo" # turn raw input off
|
112
|
+
system("stty -raw echo 2>/dev/null") # turn raw input on
|
113
|
+
end
|
114
|
+
end
|
115
|
+
## clean this up a bit, copied from shell program and macro'd
|
116
|
+
$kh=Hash.new
|
117
|
+
$kh["OP"]="F1"
|
118
|
+
$kh["[A"]="UP"
|
119
|
+
$kh["[5~"]="PGUP"
|
120
|
+
$kh['']="ESCAPE"
|
121
|
+
KEY_PGDN="[6~"
|
122
|
+
KEY_PGUP="[5~"
|
123
|
+
## I needed to replace the O with a [ for this to work
|
124
|
+
# in Vim Home comes as ^[OH whereas on the command line it is correct as ^[[H
|
125
|
+
KEY_HOME='[H'
|
126
|
+
KEY_END="[F"
|
127
|
+
KEY_F1="OP"
|
128
|
+
KEY_UP="[A"
|
129
|
+
KEY_DOWN="[B"
|
130
|
+
|
131
|
+
$kh[KEY_PGDN]="PgDn"
|
132
|
+
$kh[KEY_PGUP]="PgUp"
|
133
|
+
$kh[KEY_HOME]="Home"
|
134
|
+
$kh[KEY_END]="End"
|
135
|
+
$kh[KEY_F1]="F1"
|
136
|
+
$kh[KEY_UP]="UP"
|
137
|
+
$kh[KEY_DOWN]="DOWN"
|
138
|
+
KEY_LEFT='[D'
|
139
|
+
KEY_RIGHT='[C'
|
140
|
+
$kh["OQ"]="F2"
|
141
|
+
$kh["OR"]="F3"
|
142
|
+
$kh["OS"]="F4"
|
143
|
+
$kh[KEY_LEFT] = "LEFT"
|
144
|
+
$kh[KEY_RIGHT]= "RIGHT"
|
145
|
+
KEY_F5='[15~'
|
146
|
+
KEY_F6='[17~'
|
147
|
+
KEY_F7='[18~'
|
148
|
+
KEY_F8='[19~'
|
149
|
+
KEY_F9='[20~'
|
150
|
+
KEY_F10='[21~'
|
151
|
+
KEY_S_F1='[1;2P'
|
152
|
+
$kh[KEY_F5]="F5"
|
153
|
+
$kh[KEY_F6]="F6"
|
154
|
+
$kh[KEY_F7]="F7"
|
155
|
+
$kh[KEY_F8]="F8"
|
156
|
+
$kh[KEY_F9]="F9"
|
157
|
+
$kh[KEY_F10]="F10"
|
158
|
+
# testing out shift+Function. these are the codes my kb generates
|
159
|
+
$kh[KEY_S_F1]="S-F1"
|
160
|
+
$kh['[1;2Q']="S-F2"
|
161
|
+
|
162
|
+
def pbold text
|
163
|
+
puts "#{BOLD}#{text}#{BOLD_OFF}"
|
164
|
+
end
|
165
|
+
def pgreen text
|
166
|
+
puts "#{GREEN}#{text}#{CLEAR}"
|
167
|
+
end
|
168
|
+
def perror text
|
169
|
+
pred text
|
170
|
+
get_char
|
171
|
+
end
|
172
|
+
def pred text
|
173
|
+
puts "#{RED}#{text}#{CLEAR}"
|
174
|
+
end
|
175
|
+
def pause text=" Press a key ..."
|
176
|
+
print text
|
177
|
+
get_char
|
178
|
+
end
|
179
|
+
# alternative of menu that takes an array and uses numbers as indices.
|
180
|
+
# Hey wait, if there aer more than 10 then we are screwed since we take one character
|
181
|
+
# I have handled this somewhere, should check, maybe we should use characters
|
182
|
+
# returns text, can be nil if selection not one of choices
|
183
|
+
# How do we communicate to caller, that user pressed C-c
|
184
|
+
def select_from title, array
|
185
|
+
h = {}
|
186
|
+
array.each_with_index {|e,ix| ix += 1; h[ix.to_s] = e }
|
187
|
+
ch, text = menu title, h
|
188
|
+
unless text
|
189
|
+
if ch == "ENTER"
|
190
|
+
return array.first
|
191
|
+
end
|
192
|
+
end
|
193
|
+
return text
|
194
|
+
end
|
195
|
+
# multiselect from an array using fzf
|
196
|
+
def multi_select title, array
|
197
|
+
arr = %x[ echo "#{array.join("\n")}" | fzf --multi --reverse --prompt="#{title} >"]
|
198
|
+
return arr.split("\n")
|
199
|
+
end
|
200
|
+
# single select from an array using fzf
|
201
|
+
# CAUTION: this messes with single and double quotes, so don't pass a query in
|
202
|
+
def single_select title, array
|
203
|
+
str = %x[ echo "#{array.join("\n")}" | fzf --reverse --prompt="#{title} >" -1 -0 ]
|
204
|
+
return str
|
205
|
+
end
|
206
|
+
def editline array
|
207
|
+
Readline::HISTORY.push(*array)
|
208
|
+
begin
|
209
|
+
command = Readline::readline('>', true)
|
210
|
+
rescue Exception => e
|
211
|
+
return nil
|
212
|
+
end
|
213
|
+
return command
|
214
|
+
end
|
215
|
+
|
216
|
+
# allows user to select from list, returning string if user pressed ENTER
|
217
|
+
# Aborts if user presses Q or C-c or ESCAPE
|
218
|
+
def ctrlp arr
|
219
|
+
patt = nil
|
220
|
+
curr = 0
|
221
|
+
while true
|
222
|
+
# clear required otherwise will keep drawing itself
|
223
|
+
system("clear")
|
224
|
+
if patt and patt != ""
|
225
|
+
# need fuzzy match here
|
226
|
+
view = arr.grep(/^#{patt}/)
|
227
|
+
view = view | arr.grep(/#{patt}/)
|
228
|
+
fuzzypatt = patt.split("").join(".*")
|
229
|
+
view = view | arr.grep(/#{fuzzypatt}/)
|
230
|
+
else
|
231
|
+
view = arr
|
232
|
+
end
|
233
|
+
curr = [view.size-1, curr].min
|
234
|
+
# if empty then curr becomes -1
|
235
|
+
curr = 0 if curr < 0
|
236
|
+
view.each_with_index do |a, i|
|
237
|
+
mark = " "
|
238
|
+
mark = ">" if curr == i
|
239
|
+
print "#{mark} #{a} \n"
|
240
|
+
end
|
241
|
+
#puts " "
|
242
|
+
print "\r#{patt} >"
|
243
|
+
ch = get_char
|
244
|
+
if ch =~ /^[a-z]$/
|
245
|
+
patt ||= ""
|
246
|
+
patt << ch
|
247
|
+
elsif ch == "BACKSPACE"
|
248
|
+
if patt && patt.size > 0
|
249
|
+
patt = patt[0..-2]
|
250
|
+
end
|
251
|
+
elsif ch == "Q" or ch == "C-c" or ch == "ESCAPE"
|
252
|
+
break
|
253
|
+
elsif ch == "UP"
|
254
|
+
curr -= 1
|
255
|
+
curr = 0 if curr < 0
|
256
|
+
elsif ch == "DOWN"
|
257
|
+
curr += 1
|
258
|
+
curr = [view.size-1, curr].min
|
259
|
+
# if empty then curr becomes -1
|
260
|
+
curr = 0 if curr < 0
|
261
|
+
elsif ch == "ENTER"
|
262
|
+
return view[curr]
|
263
|
+
else
|
264
|
+
# do right and left arrow
|
265
|
+
|
266
|
+
# get arrow keys here
|
267
|
+
end
|
268
|
+
|
269
|
+
end
|
270
|
+
end
|
271
|
+
## CONSTANTS
|
272
|
+
GMARK = '*'
|
273
|
+
CURMARK = '>'
|
274
|
+
SPACE = " "
|
275
|
+
CLEAR = "\e[0m"
|
276
|
+
BOLD = "\e[1m"
|
277
|
+
BOLD_OFF = "\e[22m"
|
278
|
+
RED = "\e[31m"
|
279
|
+
ON_RED = "\e[41m"
|
280
|
+
GREEN = "\e[32m"
|
281
|
+
YELLOW = "\e[33m"
|
282
|
+
BLUE = "\e[1;34m"
|
283
|
+
|
284
|
+
ON_BLUE = "\e[44m"
|
285
|
+
REVERSE = "\e[7m"
|
286
|
+
UNDERLINE = "\e[4m"
|
287
|
+
#CURSOR_COLOR = ON_BLUE
|
288
|
+
CURSOR_COLOR = CLEAR
|
289
|
+
|
290
|
+
# --- end constants
|
291
|
+
## check screen size and accordingly adjust some variables
|
292
|
+
#
|
293
|
+
def screen_settings
|
294
|
+
$glines=%x(tput lines).to_i
|
295
|
+
$gcols=%x(tput cols).to_i
|
296
|
+
$grows = $glines - 3
|
297
|
+
$pagesize = 60
|
298
|
+
#$gviscols = 3
|
299
|
+
$pagesize = $grows * $gviscols
|
300
|
+
end
|
301
|
+
|
302
|
+
# readline version of gets
|
303
|
+
def input(prompt="", newline=false)
|
304
|
+
prompt += "\n" if newline
|
305
|
+
ret = nil
|
306
|
+
begin
|
307
|
+
ret = Readline.readline(prompt, true).squeeze(" ").strip
|
308
|
+
rescue Exception => e
|
309
|
+
return nil
|
310
|
+
end
|
311
|
+
return ret
|
312
|
+
end
|
313
|
+
def agree(prompt="")
|
314
|
+
x = input(prompt)
|
315
|
+
return true if x.upcase == "Y"
|
316
|
+
false
|
317
|
+
end
|
318
|
+
alias :confirm :agree
|
319
|
+
##
|
320
|
+
#
|
321
|
+
# print in columns
|
322
|
+
# ary - array of data
|
323
|
+
# sz - lines in one column
|
324
|
+
#
|
325
|
+
def columnate ary, sz
|
326
|
+
buff=Array.new
|
327
|
+
return buff if ary.nil? || ary.size == 0
|
328
|
+
|
329
|
+
# determine width based on number of files to show
|
330
|
+
# if less than sz then 1 col and full width
|
331
|
+
#
|
332
|
+
wid = 30
|
333
|
+
ars = ary.size
|
334
|
+
ars = [$pagesize, ary.size].min
|
335
|
+
d = 0
|
336
|
+
if ars <= sz
|
337
|
+
wid = $gcols - d
|
338
|
+
else
|
339
|
+
tmp = (ars * 1.000/ sz).ceil
|
340
|
+
wid = $gcols / tmp - d
|
341
|
+
end
|
342
|
+
#elsif ars < sz * 2
|
343
|
+
#wid = $gcols/2 - d
|
344
|
+
#elsif ars < sz * 3
|
345
|
+
#wid = $gcols/3 - d
|
346
|
+
#else
|
347
|
+
#wid = $gcols/$gviscols - d
|
348
|
+
#end
|
349
|
+
|
350
|
+
# ix refers to the index in the complete file list, wherease we only show 60 at a time
|
351
|
+
ix=0
|
352
|
+
while true
|
353
|
+
## ctr refers to the index in the column
|
354
|
+
ctr=0
|
355
|
+
while ctr < sz
|
356
|
+
|
357
|
+
f = ary[ix]
|
358
|
+
fsz = f.size
|
359
|
+
if fsz > wid
|
360
|
+
f = f[0, wid-2]+"$ "
|
361
|
+
## we do the coloring after trunc so ANSI escpe seq does not get get
|
362
|
+
if ix + $sta == $cursor
|
363
|
+
f = "#{CURSOR_COLOR}#{f}#{CLEAR}"
|
364
|
+
end
|
365
|
+
else
|
366
|
+
## we do the coloring before padding so the entire line does not get padded, only file name
|
367
|
+
if ix + $sta == $cursor
|
368
|
+
f = "#{CURSOR_COLOR}#{f}#{CLEAR}"
|
369
|
+
end
|
370
|
+
#f = f.ljust(wid)
|
371
|
+
f << " " * (wid-fsz)
|
372
|
+
end
|
373
|
+
|
374
|
+
if buff[ctr]
|
375
|
+
buff[ctr] += f
|
376
|
+
else
|
377
|
+
buff[ctr] = f
|
378
|
+
end
|
379
|
+
|
380
|
+
ctr+=1
|
381
|
+
ix+=1
|
382
|
+
break if ix >= ary.size
|
383
|
+
end
|
384
|
+
break if ix >= ary.size
|
385
|
+
end
|
386
|
+
return buff
|
387
|
+
end
|
388
|
+
# print an array in columns
|
389
|
+
# default number of columns is 3 and can be supplied. I actually much prefer that this be derived, so we can get more
|
390
|
+
# columns if possible, and te width should be caclulated too
|
391
|
+
def print_in_cols a, noc=nil
|
392
|
+
unless noc
|
393
|
+
noc = 3
|
394
|
+
if a.size < 7
|
395
|
+
noc = 1
|
396
|
+
elsif a.size < 15
|
397
|
+
noc = 2
|
398
|
+
end
|
399
|
+
end
|
400
|
+
|
401
|
+
x = noc - 1
|
402
|
+
cols = a.each_slice((a.size+x)/noc).to_a
|
403
|
+
# todo width should be determined based on COLS of screen, and width of data
|
404
|
+
cols.first.zip( *cols[1..-1] ).each{|row| puts row.map{|e| e ? '%-30s' % e : ' '}.join(" ") }
|
405
|
+
end
|
406
|
+
## expects array of arrays
|
407
|
+
#class Array
|
408
|
+
#def to_table l = []
|
409
|
+
#self.each{|r|r.each_with_index{|f,i|l[i] = [l[i]||0, f.length].max}}
|
410
|
+
#self.each{|r|r.each_with_index{|f,i|print "#{f.ljust l[i]}|"};puts ""}
|
411
|
+
#end
|
412
|
+
#end
|