typr 1.1.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/example/browser +45 -0
- data/example/grid +80 -0
- data/example/text +65 -0
- data/example/top500.csv +501 -0
- data/lib/browser.rb +431 -0
- data/lib/curses.rb +153 -0
- data/lib/frontend.rb +33 -0
- data/lib/graphical.rb +174 -0
- data/lib/grid.rb +431 -0
- data/lib/line.rb +99 -0
- data/lib/space.rb +85 -0
- data/lib/stack.rb +278 -0
- data/lib/terminal.rb +286 -0
- data/lib/text.rb +140 -0
- data/lib/typr.rb +55 -0
- data/share/mimetypes +1 -0
- metadata +73 -0
data/lib/browser.rb
ADDED
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# coding: utf-8
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
require 'etc'
|
|
6
|
+
require 'find'
|
|
7
|
+
require_relative 'grid.rb'
|
|
8
|
+
require_relative 'line.rb'
|
|
9
|
+
|
|
10
|
+
module Typr
|
|
11
|
+
|
|
12
|
+
MIMETYPES = eval File.read( __dir__ + "/../share/mimetypes" )
|
|
13
|
+
|
|
14
|
+
# A file-system directory browser built on Grid. Renders entries as rows
|
|
15
|
+
# with columns for name, symlink target, permissions, owner/group, size,
|
|
16
|
+
# timestamps, and MIME type. Directories sort to the top and appear in
|
|
17
|
+
# yellow; files get per-MIME-type foreground colors.
|
|
18
|
+
#
|
|
19
|
+
# == Column Views
|
|
20
|
+
#
|
|
21
|
+
# :simple - name
|
|
22
|
+
# :compact - name, size, type
|
|
23
|
+
# :stats - name, size, permissions, owner, group, modified
|
|
24
|
+
# :full - all columns
|
|
25
|
+
#
|
|
26
|
+
# == Navigation Keys (interactive picker)
|
|
27
|
+
#
|
|
28
|
+
# [h] | help
|
|
29
|
+
# [r] | reset view
|
|
30
|
+
# [d] | directory history
|
|
31
|
+
# [m] / [M] | mark / mark range
|
|
32
|
+
# [a] / [i] | mark all / invert mark
|
|
33
|
+
# [`] / [~] / [..] | root / home / back
|
|
34
|
+
# [v] | switch view
|
|
35
|
+
# [s] / [f] | sort / filter
|
|
36
|
+
# Return | confirm (cd on directories)
|
|
37
|
+
# Escape | quit, returns nil
|
|
38
|
+
|
|
39
|
+
class Browser < Grid
|
|
40
|
+
attr_accessor :directory, :user, :pwd, :view, :dir_history
|
|
41
|
+
attr_accessor :mimetype_db, :mimetype_magic
|
|
42
|
+
|
|
43
|
+
## Height minus 2 rows reserved for path bar and command line.
|
|
44
|
+
|
|
45
|
+
def bottom; super - 2 end
|
|
46
|
+
|
|
47
|
+
## Top offset plus one row for the path info bar.
|
|
48
|
+
|
|
49
|
+
def top; super + 1 end
|
|
50
|
+
|
|
51
|
+
##
|
|
52
|
+
# Change to +dir+ (absolute path, +".."+, +"~"+, or integer index into
|
|
53
|
+
# dir_history). When +append+ is false (default), clears data and colors
|
|
54
|
+
# before scanning. Pushes onto dir_history.
|
|
55
|
+
#
|
|
56
|
+
# browser.cd "/tmp" # => changes to /tmp
|
|
57
|
+
# browser.cd "~" # => changes to home
|
|
58
|
+
# browser.cd ".." # => parent directory
|
|
59
|
+
# browser.cd 2 # => jump to history entry 2
|
|
60
|
+
|
|
61
|
+
def cd dir, append=false
|
|
62
|
+
return unless dir
|
|
63
|
+
data, missing = [],{}
|
|
64
|
+
unless append
|
|
65
|
+
@colors[:fields] = {}
|
|
66
|
+
dir = @dir_history[dir+1] if dir.is_a? Integer
|
|
67
|
+
clear
|
|
68
|
+
Dir.chdir( @directory = File.expand_path( dir ) )
|
|
69
|
+
unless [?/, ENV["HOME"]].include? dir
|
|
70
|
+
@dir_history.unshift @directory
|
|
71
|
+
@dir_history.uniq!
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
data = Dir.new(@directory).children.map.with_index do |file,id|
|
|
75
|
+
if File.symlink? file
|
|
76
|
+
target = File.readlink( file ) rescue $!.to_s
|
|
77
|
+
next [file, target] + [??]*7 + ["inode","symlink"] unless File.exist?(file)
|
|
78
|
+
end
|
|
79
|
+
stat = File.stat(file)
|
|
80
|
+
type = stat.ftype.to_sym
|
|
81
|
+
|
|
82
|
+
mime = { "directory": "inode/directory",
|
|
83
|
+
"characterSpecial": "inode/chardevice",
|
|
84
|
+
"blockSpecial": "inode/blockdevice", "link": "inode/symlink",
|
|
85
|
+
"fifo": "inode/fifo", "socket": "inode/socket"}[type]
|
|
86
|
+
if type == :file and @mimetype_db
|
|
87
|
+
ext = file.match(/.+\.(\w+)$/)
|
|
88
|
+
mime ||= MIMETYPES[ext[1].to_sym] if ext
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
( missing[file] = id ; mime = "?/?" ) unless mime
|
|
92
|
+
[ file, target, stat.mode.to_s(8)[-3..-1].chars.map{|d|
|
|
93
|
+
"rwx".chars.map.with_index{|c,i| d.to_i[2-i]==0 ? ?- : c } }.join,
|
|
94
|
+
stat.uid, stat.gid, stat.size,
|
|
95
|
+
stat.atime.to_i, stat.mtime.to_i, stat.ctime.to_i,
|
|
96
|
+
*mime.to_s.split( ?/ ) ]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
`file --mime-type '#{missing.keys.join "' '"}'
|
|
100
|
+
`.split($/).each.with_index{ |line,id|
|
|
101
|
+
next unless line[/^.+:\s+\w+\/[\w\-\.]+$/]
|
|
102
|
+
file, mime = line.split(/:\s*/)
|
|
103
|
+
data[ missing[file] ][-2,2] = mime.rstrip.split( ?/ ) } if
|
|
104
|
+
@mimetype_magic and not missing.empty?
|
|
105
|
+
self << data
|
|
106
|
+
sort
|
|
107
|
+
filter
|
|
108
|
+
unless append
|
|
109
|
+
@directory += ?/ unless @directory == ?/
|
|
110
|
+
switch_to @view
|
|
111
|
+
reset :position
|
|
112
|
+
end
|
|
113
|
+
return
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
##
|
|
117
|
+
# Colors symlink names green/red based on target existence and applies
|
|
118
|
+
# per-MIME-type foreground colors from +@colors[:types]+.
|
|
119
|
+
#
|
|
120
|
+
# browser.process 0 # => sets colors for row 0
|
|
121
|
+
|
|
122
|
+
def process row; super
|
|
123
|
+
color = @colors[:types][@data[row][SUBTYPE].to_sym] ||
|
|
124
|
+
@colors[:types][@data[row][TYPE].to_sym]
|
|
125
|
+
@colors[:fields][[row,NAME]] = color if color
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
##
|
|
129
|
+
# Switch to a named or indexed column view. Resets format widths.
|
|
130
|
+
#
|
|
131
|
+
# browser.switch_to :full # all columns
|
|
132
|
+
# browser.switch_to 0 # first view key
|
|
133
|
+
# browser.switch_to :stats # name, size, perms, owner, group, modified
|
|
134
|
+
|
|
135
|
+
def switch_to view=nil
|
|
136
|
+
return unless view
|
|
137
|
+
view = @views.keys[ view ] if view.is_a? Integer
|
|
138
|
+
@sequence = @views[ view ].map{|title| @header.index title.to_s}
|
|
139
|
+
reset :format
|
|
140
|
+
@view = view
|
|
141
|
+
return
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
##
|
|
145
|
+
# Resolves hint characters typed on a row. Returns the matching row id
|
|
146
|
+
# for fast navigation, otherwise falls through to Stack#send.
|
|
147
|
+
#
|
|
148
|
+
# browser.send "1" # => row id if hint "1" is visible
|
|
149
|
+
|
|
150
|
+
def send key
|
|
151
|
+
if key.is_a?(String) and
|
|
152
|
+
id = @hints[0..[height, rows-1+headspace].min-@hints_start-1].index(key)
|
|
153
|
+
return page[id+@hints_start]
|
|
154
|
+
else super end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
## Returns the current directory path as the confirm result.
|
|
158
|
+
|
|
159
|
+
def confirm; return @directory end
|
|
160
|
+
|
|
161
|
+
## Prompts to select rows by index. Returns chosen ids or nil.
|
|
162
|
+
#
|
|
163
|
+
# browser.mark # => [0, 2, 5] or nil
|
|
164
|
+
|
|
165
|
+
def mark; return @user.ask( 'mark files': [self, :rows] ) end
|
|
166
|
+
|
|
167
|
+
## Toggles selection for every row within a "from..to" range.
|
|
168
|
+
#
|
|
169
|
+
# browser.mark_range # prompts for from/to, toggles selection
|
|
170
|
+
|
|
171
|
+
def mark_range; return ids[ ( eval @user.ask(
|
|
172
|
+
'mark from': [self, :relative_row], 'to': [self, :relative_row]
|
|
173
|
+
).join '..' ) ].each{|id| @selected[:rows].include?(id) ?
|
|
174
|
+
@selected[:rows].delete(id) : @selected[:rows] << id } end
|
|
175
|
+
|
|
176
|
+
## Selects all non-directory rows.
|
|
177
|
+
#
|
|
178
|
+
# browser.mark_all # => [0, 3, 7] (file ids only)
|
|
179
|
+
|
|
180
|
+
def mark_all; return @selected[:rows] = ids - directories end
|
|
181
|
+
|
|
182
|
+
## Inverts selection across all non-directory rows.
|
|
183
|
+
#
|
|
184
|
+
# browser.mark_invert # => toggled ids
|
|
185
|
+
|
|
186
|
+
def mark_invert; return @selected[:rows] = ids - @selected[:rows] end
|
|
187
|
+
|
|
188
|
+
## Navigate up one directory.
|
|
189
|
+
#
|
|
190
|
+
# browser.back # equivalent to cd ".."
|
|
191
|
+
|
|
192
|
+
def back; cd ".." end
|
|
193
|
+
|
|
194
|
+
## Navigate to the root directory.
|
|
195
|
+
#
|
|
196
|
+
# browser.root # => cd "/"
|
|
197
|
+
|
|
198
|
+
def root; cd ?/ end
|
|
199
|
+
|
|
200
|
+
## Navigate to the home directory.
|
|
201
|
+
#
|
|
202
|
+
# browser.home # => cd "~"
|
|
203
|
+
|
|
204
|
+
def home; cd ?~ end
|
|
205
|
+
|
|
206
|
+
## Display a help grid listing all keybindings.
|
|
207
|
+
|
|
208
|
+
def help; @user.help end
|
|
209
|
+
|
|
210
|
+
##
|
|
211
|
+
# Recurse through the directory tree, adding subdirectory entries inline
|
|
212
|
+
# via +cd(path, true)+. Sorts by name with the filetree processor.
|
|
213
|
+
#
|
|
214
|
+
# browser.recurse # flattens tree into leaf names
|
|
215
|
+
|
|
216
|
+
def recurse
|
|
217
|
+
sort_by NAME
|
|
218
|
+
@procs[NAME] = :filetree
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
## Reset display, selection, and position state then re-sort.
|
|
222
|
+
#
|
|
223
|
+
# browser.reset_view
|
|
224
|
+
|
|
225
|
+
def reset_view; reset [:display, :selection, :position]; sort end
|
|
226
|
+
|
|
227
|
+
## Open a filter popup on the chosen row/column and apply the input.
|
|
228
|
+
#
|
|
229
|
+
# browser.filter_view # prompts for filter text
|
|
230
|
+
|
|
231
|
+
def filter_view; add_filter *@user.ask(
|
|
232
|
+
filter: [ popup( type: :"[f]ilter" ), :row], with: :line ) end
|
|
233
|
+
|
|
234
|
+
## Open a sort-picker popup to choose a column to sort by.
|
|
235
|
+
#
|
|
236
|
+
# browser.sort_view # pick a column header to sort
|
|
237
|
+
|
|
238
|
+
def sort_view; sort_by @user.ask( "sort by": [ popup( type: :"[s]ort" ), :row] ) end
|
|
239
|
+
|
|
240
|
+
## Open a view-picker popup and switch to the selected view.
|
|
241
|
+
#
|
|
242
|
+
# browser.views # choose :simple, :compact, :stats, or :full
|
|
243
|
+
|
|
244
|
+
def views; switch_to( popup( type: :"[v]iews", input: @views.keys ).pick :row ) end
|
|
245
|
+
|
|
246
|
+
## Open a directory history picker and cd to the chosen entry.
|
|
247
|
+
#
|
|
248
|
+
# browser.dir_history # pick from previously visited dirs
|
|
249
|
+
|
|
250
|
+
def dir_history; cd( popup( { input: @dir_history[1..-1],
|
|
251
|
+
colors: { columns: [ colors[:dir_history] ] },
|
|
252
|
+
align: [:left], left: left } ).pick(:row) ) end
|
|
253
|
+
|
|
254
|
+
## True when the row is a directory or passes base Grid filtering.
|
|
255
|
+
#
|
|
256
|
+
# browser.check 0 # => true if navigable
|
|
257
|
+
|
|
258
|
+
def check row; super or @data[row][SUBTYPE] == "directory" end
|
|
259
|
+
|
|
260
|
+
##
|
|
261
|
+
# Interactive file/directory picker. Blocks until a selection or Escape.
|
|
262
|
+
# Return on a file wraps the path in single quotes. Enter on a directory
|
|
263
|
+
# triggers cd. +type+ optionally filters by MIME family (audio, video,
|
|
264
|
+
# image, text, application, inode).
|
|
265
|
+
#
|
|
266
|
+
# browser.pick # => '/home/user/file.txt'
|
|
267
|
+
# browser.pick 'image' # => '/path/to/photo.png' (images only)
|
|
268
|
+
# browser.pick nil # => anything, same as 'file'
|
|
269
|
+
|
|
270
|
+
def pick type='file'
|
|
271
|
+
type ||= 'file'
|
|
272
|
+
type = type.to_s
|
|
273
|
+
return super if type[/row|column|field|none/]
|
|
274
|
+
add_filter ( %w[audio video image text application inode].include?(
|
|
275
|
+
type) ? TYPE : SUBTYPE ), type if type != 'file'
|
|
276
|
+
loop do
|
|
277
|
+
draw
|
|
278
|
+
draw_hints :row
|
|
279
|
+
key = Typr.read( :key )
|
|
280
|
+
next unless choice = send( key )
|
|
281
|
+
case choice
|
|
282
|
+
when Array; choice = ?' + choice.map{ |id|
|
|
283
|
+
@directory+?/+@data[id][NAME] }.join("' '") + ?'
|
|
284
|
+
when Integer; file = @data[ choice ]
|
|
285
|
+
if file[SUBTYPE] == 'directory'; cd file[TARGET] || file[NAME]; next
|
|
286
|
+
else choice = ?'+@directory+?/+file[NAME]+?' end
|
|
287
|
+
end
|
|
288
|
+
reset :display
|
|
289
|
+
return choice
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
##
|
|
294
|
+
# Renders the grid with an info bar above showing the current directory
|
|
295
|
+
# path and colored status items for filter, sort, and view.
|
|
296
|
+
#
|
|
297
|
+
# browser.draw # draws browser with info bar
|
|
298
|
+
|
|
299
|
+
def draw; super
|
|
300
|
+
info = { "[f]ilter": @filters.empty? ? "no filter" : @filters.to_s[1..-2],
|
|
301
|
+
"[s]ort": sorted_by(true), "[v]iews": @view }
|
|
302
|
+
move left, top - 1
|
|
303
|
+
show prepare( @directory, width - info.values.join.size - 3, :left, :left, 5 )
|
|
304
|
+
@colors[:actions].each do |type,col|
|
|
305
|
+
color col
|
|
306
|
+
show ' ' + info[type].to_s
|
|
307
|
+
@positions[type] = Typr.column - (type==@positions.keys.last ? 0:1 )
|
|
308
|
+
end
|
|
309
|
+
@user.draw
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
## Returns row ids for all directories in the current listing.
|
|
313
|
+
#
|
|
314
|
+
# browser.directories # => [0, 1, 3]
|
|
315
|
+
|
|
316
|
+
def directories; ids.select{ |id| @data[id][SUBTYPE] == 'directory' } end
|
|
317
|
+
|
|
318
|
+
##
|
|
319
|
+
# Sort by NAME by default; directories always appear before files
|
|
320
|
+
# within each prefix grouping.
|
|
321
|
+
#
|
|
322
|
+
# browser.sort # re-sorts with dirs first
|
|
323
|
+
|
|
324
|
+
def sort
|
|
325
|
+
@sort ||= NAME
|
|
326
|
+
super
|
|
327
|
+
dirs = ids.select{ |id| @data[id][SUBTYPE] == 'directory' }
|
|
328
|
+
@map = (directories + ids).uniq
|
|
329
|
+
nil
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
##
|
|
333
|
+
# Create a small Grid popup under an info bar item for column selection.
|
|
334
|
+
# +args+ must include +:type+ matching an actions key for positioning.
|
|
335
|
+
#
|
|
336
|
+
# browser.popup type: :"[s]ort" # sort-picker popup
|
|
337
|
+
|
|
338
|
+
def popup args={}
|
|
339
|
+
Grid.new( { alternate:false, border: :light,
|
|
340
|
+
borders:{ top:nil, top_left:nil, top_right:nil },
|
|
341
|
+
top: top, align: [:right], input:@header,
|
|
342
|
+
left: left + ( @positions[args[:type]] || 0 ) -
|
|
343
|
+
( args[:input] || @header ).map( &:size ).max - 1,
|
|
344
|
+
colors:{ columns: [ @colors[:actions][args[:type]] ] } }.merge args )
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
##
|
|
348
|
+
# Construct a Browser widget. Keyword arguments:
|
|
349
|
+
#
|
|
350
|
+
# Option | Default | Description
|
|
351
|
+
# --------------------|---------------------------------|-----------------------------------
|
|
352
|
+
# +directory+ | +"~"+ | Starting directory to browse
|
|
353
|
+
# +mimetype_db+ | true | Load MIME from file extensions
|
|
354
|
+
# +mimetype_magic+ | true | Use +file --mime-type+ for unknowns
|
|
355
|
+
# +colors[:actions]+ | { "[f]ilter": :red, ... } | Status bar item colors
|
|
356
|
+
# +colors[:dir_history]+ | :yellow | History picker column color
|
|
357
|
+
# +colors[:types]+ | (see below) | Per MIME-type foreground color
|
|
358
|
+
#
|
|
359
|
+
# == Type Color Defaults
|
|
360
|
+
#
|
|
361
|
+
# image: :blue audio: :green video: :cyan
|
|
362
|
+
# text: 146 pdf: 136 inode: :magenta
|
|
363
|
+
# directory: :yellow symlink: 123 'x-empty': :grey70
|
|
364
|
+
# 'x-msdownload': :red 'x-pie-executable': :red
|
|
365
|
+
|
|
366
|
+
def initialize args={}
|
|
367
|
+
@dir_history, @mimetype_db, @mimetype_magic = [], true, true
|
|
368
|
+
@view, @sort, @views, @positions = :compact, 0, {}, {}
|
|
369
|
+
super
|
|
370
|
+
@directory ||= ?~
|
|
371
|
+
@colors[:actions] = { "[f]ilter": :red, "[s]ort": :blue, "[v]iews": :green
|
|
372
|
+
}.merge @colors[:actions] || {}
|
|
373
|
+
@colors[:dir_history] ||= :yellow
|
|
374
|
+
@colors[ :types ] = {
|
|
375
|
+
image: :blue,
|
|
376
|
+
audio: :green,
|
|
377
|
+
video: :cyan,
|
|
378
|
+
text: 146, pdf: 136, 'epub+zip': 136,
|
|
379
|
+
html: 150, xhtml: 150, javascript: 150,
|
|
380
|
+
'x-bzip2': 190, 'x-xz': 190, 'x-compressed': 190,
|
|
381
|
+
inode: :magenta, blockdevice:148, chardevice:144, symlink:123,
|
|
382
|
+
directory: :yellow,
|
|
383
|
+
'x-empty': :grey70, '?': :grey70,
|
|
384
|
+
'x-msdownload': :red, 'x-pie-executable': :red
|
|
385
|
+
}.merge( @colors[ :types ] || {} )
|
|
386
|
+
|
|
387
|
+
@keymap = { back: KEY_BACKSPACE, confirm: KEY_RETURN, help: ?h,
|
|
388
|
+
home: ?~, root: ?`, views: ?v, sort_view: ?s, dir_history: ?d,
|
|
389
|
+
mark: ?m, mark_range: ?M, mark_all: ?a, mark_invert: ?i,
|
|
390
|
+
filter_view: ?f, reset_view: ?r, recurse: ?R}.merge @keymap
|
|
391
|
+
|
|
392
|
+
self.header = %w[ name target permissions owner group size accessed
|
|
393
|
+
modified created type subtype ]
|
|
394
|
+
|
|
395
|
+
@format[NAME] = :max
|
|
396
|
+
@format[TYPE] = 5
|
|
397
|
+
@ignore[NAME] = @ignore[TARGET] = true
|
|
398
|
+
@hints = "1234567890"
|
|
399
|
+
[NAME, SIZE, ACCESSED, MODIFIED, CREATED].each{|col| @rawsort[col] = true}
|
|
400
|
+
|
|
401
|
+
@procs[ACCESSED] = @procs[MODIFIED] = @procs[CREATED] = :datetime
|
|
402
|
+
@procs[SIZE] = :magnitudes
|
|
403
|
+
@procs[OWNER] = Proc.new{|uid| Etc.getpwuid(uid).name rescue uid }
|
|
404
|
+
@procs[GROUP] = Proc.new{|gid| Etc.getgrgid(gid).name rescue gid }
|
|
405
|
+
|
|
406
|
+
@procs[NAME] = Proc.new{ |name, row| target = @data[row][TARGET]
|
|
407
|
+
name + ( target ? " > " + color_code(File.exist?(target) ? :green : :red) +
|
|
408
|
+
target : "" ) }
|
|
409
|
+
|
|
410
|
+
@views = { simple: %i[ name ],
|
|
411
|
+
compact: %i[ name size type],
|
|
412
|
+
stats: %i[ name size permissions owner group modified ],
|
|
413
|
+
full: %i[ name size permissions owner group
|
|
414
|
+
accessed modified created type subtype ] }.merge @views
|
|
415
|
+
@parent = self
|
|
416
|
+
@head ||= Grid.new( parent:self, left: ->{@parent.left}, top: ->{@parent.top-1},
|
|
417
|
+
input: [ [->{@parent.directory}, ->{@parent.filters.empty? ? "no filter" :
|
|
418
|
+
@parent.filters.to_s[1..-2] }, ->{@parent.sorted_by(true)},
|
|
419
|
+
->{@parent.view} ] ], format: [:max, :min, :min, :min],
|
|
420
|
+
colors: { columns: [@colors[:default]] + @colors[:actions].values } )
|
|
421
|
+
|
|
422
|
+
@user ||= Line.new( parent:self, left:->{@parent.left},top:->{@parent.bottom+1},
|
|
423
|
+
bindings: %w[ [h]elp ] + @colors[:actions].map{|name,color|
|
|
424
|
+
color_code( color ) + name.to_s + color_code(@colors[:default]) } +
|
|
425
|
+
%w[ [r]eset [d]irectories [m]ark [M]ark\ range mark\ [a]ll
|
|
426
|
+
[i]nvert\ mark [`]:root [~]:home [back]:.. [up] [down] [pageup] [pagedown] [home] [end] [esc]:quit [return]:confirm\ ])
|
|
427
|
+
cd @directory
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
end
|
data/lib/curses.rb
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/ruby
|
|
2
|
+
|
|
3
|
+
require 'ncursesw'
|
|
4
|
+
require_relative 'frontend'
|
|
5
|
+
|
|
6
|
+
module Visuals
|
|
7
|
+
|
|
8
|
+
include Ncurses
|
|
9
|
+
KEY_ESCAPE = 27
|
|
10
|
+
KEY_RETURN = 10
|
|
11
|
+
KEY_TAB = 9
|
|
12
|
+
KEY_PAGEDOWN = KEY_NPAGE
|
|
13
|
+
KEY_PAGEUP = KEY_PPAGE
|
|
14
|
+
KEY_INSERT = KEY_IC
|
|
15
|
+
KEY_DELETE = KEY_DC
|
|
16
|
+
|
|
17
|
+
$colors = COLORS.keys
|
|
18
|
+
$pairs = []
|
|
19
|
+
# COLORS = %w[ black red green yellow blue magenta cyan white grey]
|
|
20
|
+
# KEY_TAB = 9
|
|
21
|
+
# HEIGHT,WIDTH = IO.console.winsize
|
|
22
|
+
|
|
23
|
+
%w[color pair].each{ |name| method = <<STR
|
|
24
|
+
def %s_for obj; #return unless obj
|
|
25
|
+
if id = $%ss.index( obj ) then return id
|
|
26
|
+
else last = ( $%ss << obj.dup ).count - 1
|
|
27
|
+
Ncurses.init_%s( last, *obj )
|
|
28
|
+
return last
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
STR
|
|
32
|
+
eval( method % ([name]*4) )
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
def self.width; Ncurses.COLS end
|
|
36
|
+
def self.height; Ncurses.LINES end
|
|
37
|
+
def self.row; Ncurses.getcury $creen end
|
|
38
|
+
def self.column; Ncurses.getcurx $creen end
|
|
39
|
+
|
|
40
|
+
def self.exit
|
|
41
|
+
Ncurses.echo
|
|
42
|
+
Ncurses.nocbreak
|
|
43
|
+
Ncurses.nl
|
|
44
|
+
Ncurses.endwin
|
|
45
|
+
# system "reset"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def read input
|
|
49
|
+
case input
|
|
50
|
+
when :key
|
|
51
|
+
key = Ncurses.getch
|
|
52
|
+
key = key.chr if key.between? 32, 126
|
|
53
|
+
return key
|
|
54
|
+
when :line #string
|
|
55
|
+
Ncurses.echo
|
|
56
|
+
Ncurses.curs_set 1
|
|
57
|
+
# Ncurses.getstr str=""
|
|
58
|
+
str = Readline.readline
|
|
59
|
+
Ncurses.curs_set 0
|
|
60
|
+
Ncurses.noecho
|
|
61
|
+
return str
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def move x=0,y=0; Ncurses.move y, x end
|
|
66
|
+
def refresh; Ncurses.refresh end
|
|
67
|
+
def enable attr; Ncurses.attron attr end
|
|
68
|
+
def disable attr; Ncurses.attroff attr end
|
|
69
|
+
def clear type=:screen;
|
|
70
|
+
case type
|
|
71
|
+
when :screen; Ncurses.erase
|
|
72
|
+
when :line; Ncurses.clrtoeol
|
|
73
|
+
when :down; Ncurses.clrtobot
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
def change; Ncurses.color_set pair_for( $color ), 0 end
|
|
77
|
+
def show string; Ncurses.addstr string.to_s end
|
|
78
|
+
|
|
79
|
+
def get_background; $color[1] end
|
|
80
|
+
def get_foreground; $color[0] end
|
|
81
|
+
def background color;
|
|
82
|
+
$color[1] = (color ? color_for( color ) : $default[1]); change
|
|
83
|
+
end
|
|
84
|
+
def foreground color;
|
|
85
|
+
$color[0] = (color ? color_for( color ) : $default[0]); change
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def color c
|
|
89
|
+
c = [c] unless c.is_a? Array and c.count == 2
|
|
90
|
+
foreground c[0] if c[0]
|
|
91
|
+
background c[1] if c[1]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# def color_pair pair
|
|
95
|
+
# if id = $color_pairs.index( pair ) return id
|
|
96
|
+
# else last = ( $color_pairs << c ).count - 1
|
|
97
|
+
# Ncurses.init_pair( last, *pair )
|
|
98
|
+
# return last
|
|
99
|
+
# end
|
|
100
|
+
# pair = p.map{ |col|
|
|
101
|
+
# unless id = $colors.index( col )
|
|
102
|
+
# id = ($colors << col).count - 1
|
|
103
|
+
# Ncurses.init_color id, *col
|
|
104
|
+
# end
|
|
105
|
+
# id
|
|
106
|
+
# }
|
|
107
|
+
# unless ( id = $color_pairs.index( pair ) )
|
|
108
|
+
# id = ( $color_pairs << pair ).count - 1
|
|
109
|
+
# Ncurses.init_pair id, *pair
|
|
110
|
+
# end
|
|
111
|
+
# id
|
|
112
|
+
# end
|
|
113
|
+
|
|
114
|
+
def self.init args={}
|
|
115
|
+
# SCREEN = Ncurses.initscr
|
|
116
|
+
$creen = Ncurses.initscr
|
|
117
|
+
Ncurses.cbreak # provide unbuffered input
|
|
118
|
+
Ncurses.noecho # turn on input echoing
|
|
119
|
+
Ncurses.nl # turn on newline translation
|
|
120
|
+
# Ncurses.nonl # turn off newline translation
|
|
121
|
+
Ncurses.curs_set 0
|
|
122
|
+
Ncurses.start_color
|
|
123
|
+
Ncurses.stdscr.intrflush(false) # turn off flush-on-interrupt
|
|
124
|
+
Ncurses.stdscr.keypad(true) # turn on keypad mode
|
|
125
|
+
|
|
126
|
+
$default = [
|
|
127
|
+
color_for( args[:foreground] || :grey80 ),
|
|
128
|
+
color_for( args[:background] || :grey10 ) ]
|
|
129
|
+
$color = $default.dup
|
|
130
|
+
|
|
131
|
+
COLORS.each_with_index{ |c,i|
|
|
132
|
+
$pairs << [ i, $default[1] ]
|
|
133
|
+
Ncurses.init_color i, *c[1]
|
|
134
|
+
Ncurses.init_pair i, i, $default[1] }
|
|
135
|
+
|
|
136
|
+
Ncurses.bkgd Ncurses::COLOR_PAIR( pair_for $default )
|
|
137
|
+
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
if __FILE__ == $0
|
|
143
|
+
include Visuals
|
|
144
|
+
Visuals.init #background: :blue
|
|
145
|
+
characters = (32..126).map(&:chr).join
|
|
146
|
+
loop do
|
|
147
|
+
clear
|
|
148
|
+
$colors.each_with_index{ |c,i|
|
|
149
|
+
foreground c; move 0,i; show characters }
|
|
150
|
+
refresh
|
|
151
|
+
exit if read(:key) == KEY_ESCAPE
|
|
152
|
+
end
|
|
153
|
+
end
|
data/lib/frontend.rb
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
require 'readline'
|
|
2
|
+
|
|
3
|
+
module Visuals
|
|
4
|
+
|
|
5
|
+
# COLORS = {
|
|
6
|
+
# black: [ 0, 0, 0],
|
|
7
|
+
# red: [ 700, 400, 400],
|
|
8
|
+
# green: [ 400, 700, 400],
|
|
9
|
+
# yellow: [ 700, 700, 400],
|
|
10
|
+
# blue: [ 400, 400, 700],
|
|
11
|
+
# magenta: [ 700, 400, 700],
|
|
12
|
+
# cyan: [ 400, 700, 700],
|
|
13
|
+
# white: [ 999, 999, 999],
|
|
14
|
+
# grey: [ 618, 618, 618] }
|
|
15
|
+
|
|
16
|
+
COLORS = {
|
|
17
|
+
black: [ 0, 0, 0],
|
|
18
|
+
red: [ 180, 100, 100],
|
|
19
|
+
green: [ 100, 180, 100],
|
|
20
|
+
yellow: [ 180, 180, 100],
|
|
21
|
+
blue: [ 100, 100, 180],
|
|
22
|
+
magenta: [ 180, 100, 180],
|
|
23
|
+
cyan: [ 100, 180, 180],
|
|
24
|
+
white: [ 255, 255, 255],
|
|
25
|
+
grey: [ 158, 158, 158] }
|
|
26
|
+
|
|
27
|
+
COLORS.merge! 1.upto(9).map{ |i|
|
|
28
|
+
["grey#{ i*10 }".to_sym, [i*25]*3 ] }.to_h
|
|
29
|
+
|
|
30
|
+
# COLORS.merge! 1.upto(9).map{ |i|
|
|
31
|
+
# ["grey#{ i*10 }".to_sym, [i*100]*3 ] }.to_h
|
|
32
|
+
|
|
33
|
+
end
|