Class: Typr::Browser

Inherits:
Grid show all
Defined in:
lib/browser.rb

Overview

A file-system directory browser built on Grid. Renders entries as rows with columns for name, symlink target, permissions, owner/group, size, timestamps, and MIME type. Directories sort to the top and appear in yellow; files get per-MIME-type foreground colors.

Column Views

:simple   - name
:compact  - name, size, type
:stats    - name, size, permissions, owner, group, modified
:full     - all columns
[h]             | help
[r]             | reset view
[d]             | directory history
[m] / [M]       | mark / mark range
[a] / [i]       | mark all / invert mark
[`] / [~] / [..] | root / home / back
[v]             | switch view
[s] / [f]       | sort / filter
Return          | confirm (cd on directories)
Escape          | quit, returns nil

Constant Summary

Constants included from Typr

COLORS, INPUT, KEY_DELETE, KEY_ENTER, KEY_ESCAPE, KEY_INSERT, KEY_PAGEDOWN, KEY_PAGEUP, KEY_RETURN, KEY_TAB, MIMETYPES, MODES, OUTPUT

Instance Attribute Summary collapse

Attributes inherited from Grid

#align, #data, #filters, #format, #map, #procs, #rawfilter, #rawsort, #sequence

Attributes inherited from Stack

#header, #hints, #hints_start, #keymap, #selected, #separator, #start

Attributes inherited from Space

#borders, #colors, #interval, #left, #margin, #right

Instance Method Summary collapse

Methods inherited from Grid

#<<, #[], #[]=, #add_filter, #clear, #columns, #filter, #header=, #ids, #is_hash?, #page, #positions, #print, #reset, #rows, #sort_by, #sorted_by, #width_for

Methods inherited from Stack

#cycle, #down, #draw_hints, #headspace, #height, #page, #page_down, #page_up, #print, #reset, #to_bottom, #to_top, #up

Methods inherited from Space

#border=, #draw_border, #height, #start, #stop, #width

Methods included from Typr

#background, clear, #color, #color_code, column, exit, #fade, #foreground, #get_background, #get_foreground, height, init, #mode, #mode_code, #move, #move_code, position, #prepare, #printables, read, #real_size, row, #show, size, width

Constructor Details

#initialize(args = {}) ⇒ Browser

Construct a Browser widget. Keyword arguments:

Option               | Default                         | Description
--------------------|---------------------------------|-----------------------------------
+directory+          | +"~"+                           | Starting directory to browse
+mimetype_db+       | true                            | Load MIME from file extensions
+mimetype_magic+    | true                            | Use +file --mime-type+ for unknowns
+colors[:actions]+  | { "[f]ilter": :red, ... }       | Status bar item colors
+colors[:dir_history]+ | :yellow                      | History picker column color
+colors[:types]+     | (see below)                    | Per MIME-type foreground color

Type Color Defaults

image: :blue          audio: :green         video: :cyan
text: 146             pdf: 136              inode: :magenta
directory: :yellow    symlink: 123          'x-empty': :grey70
'x-msdownload': :red  'x-pie-executable': :red


366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
# File 'lib/browser.rb', line 366

def initialize args={}
  @dir_history, @mimetype_db, @mimetype_magic = [], true, true
  @view, @sort, @views, @positions = :compact, 0, {}, {}
  super
  @directory ||= ?~
  @colors[:actions] = { "[f]ilter": :red, "[s]ort": :blue, "[v]iews": :green
    }.merge @colors[:actions] || {}
  @colors[:dir_history] ||= :yellow
  @colors[ :types ] = {
        image: :blue,
        audio: :green,
        video: :cyan,
         text: 146, pdf: 136, 'epub+zip': 136,
         html: 150, xhtml: 150, javascript: 150,
    'x-bzip2': 190, 'x-xz': 190, 'x-compressed': 190,
        inode: :magenta, blockdevice:148, chardevice:144, symlink:123,
    directory: :yellow,
    'x-empty': :grey70, '?': :grey70,
    'x-msdownload': :red, 'x-pie-executable': :red
  }.merge( @colors[ :types ] || {} )

  @keymap = { back: KEY_BACKSPACE, confirm: KEY_RETURN, help: ?h,
    home: ?~, root: ?`, views: ?v, sort_view: ?s, dir_history: ?d,
    mark: ?m, mark_range: ?M, mark_all: ?a, mark_invert: ?i,
    filter_view: ?f, reset_view: ?r, recurse: ?R}.merge @keymap

  self.header = %w[ name target permissions owner group size accessed
    modified created type subtype ]

  @format[NAME] = :max
  @format[TYPE] = 5
  @ignore[NAME] = @ignore[TARGET] = true
  @hints = "1234567890"
  [NAME, SIZE, ACCESSED, MODIFIED, CREATED].each{|col| @rawsort[col] = true}

  @procs[ACCESSED] = @procs[MODIFIED] = @procs[CREATED] = :datetime
  @procs[SIZE] = :magnitudes
  @procs[OWNER] = Proc.new{|uid| Etc.getpwuid(uid).name rescue uid }
  @procs[GROUP] = Proc.new{|gid| Etc.getgrgid(gid).name rescue gid }

  @procs[NAME] = Proc.new{ |name, row| target = @data[row][TARGET]
    name + ( target ? " > " + color_code(File.exist?(target) ? :green : :red) +
      target : "" ) }

  @views = { simple: %i[ name ],
    compact: %i[ name size type],
    stats:   %i[ name size permissions owner group modified ],
    full:    %i[ name size permissions owner group
                 accessed modified created type subtype ] }.merge @views
  @parent = self
  @head ||= Grid.new( parent:self, left: ->{@parent.left}, top: ->{@parent.top-1},
    input: [ [->{@parent.directory}, ->{@parent.filters.empty? ? "no filter" :
      @parent.filters.to_s[1..-2] }, ->{@parent.sorted_by(true)},
      ->{@parent.view} ] ], format: [:max, :min, :min, :min],
    colors: { columns: [@colors[:default]] + @colors[:actions].values } )

  @user ||= Line.new( parent:self, left:->{@parent.left},top:->{@parent.bottom+1},
    bindings: %w[ [h]elp ] + @colors[:actions].map{|name,color|
      color_code( color ) + name.to_s + color_code(@colors[:default]) } +
        %w[ [r]eset [d]irectories [m]ark [M]ark\ range mark\ [a]ll
        [i]nvert\ mark [`]:root [~]:home [back]:.. [up] [down] [pageup] [pagedown] [home] [end] [esc]:quit [return]:confirm\  ])
  cd @directory
end

Instance Attribute Details

#dir_historyObject

Open a directory history picker and cd to the chosen entry.

browser.dir_history   # pick from previously visited dirs


250
251
252
# File 'lib/browser.rb', line 250

def dir_history
  @dir_history
end

#directoryObject

Returns the value of attribute directory.



40
41
42
# File 'lib/browser.rb', line 40

def directory
  @directory
end

#mimetype_dbObject

Returns the value of attribute mimetype_db.



41
42
43
# File 'lib/browser.rb', line 41

def mimetype_db
  @mimetype_db
end

#mimetype_magicObject

Returns the value of attribute mimetype_magic.



41
42
43
# File 'lib/browser.rb', line 41

def mimetype_magic
  @mimetype_magic
end

#pwdObject

Returns the value of attribute pwd.



40
41
42
# File 'lib/browser.rb', line 40

def pwd
  @pwd
end

#userObject

Returns the value of attribute user.



40
41
42
# File 'lib/browser.rb', line 40

def user
  @user
end

#viewObject

Returns the value of attribute view.



40
41
42
# File 'lib/browser.rb', line 40

def view
  @view
end

Instance Method Details

#backObject

Navigate up one directory.

browser.back   # equivalent to cd ".."


192
# File 'lib/browser.rb', line 192

def back; cd ".." end

#bottomObject

Height minus 2 rows reserved for path bar and command line.



45
# File 'lib/browser.rb', line 45

def bottom; super - 2 end

#cd(dir, append = false) ⇒ Object

Change to dir (absolute path, "..", "~", or integer index into dir_history). When append is false (default), clears data and colors before scanning. Pushes onto dir_history.

browser.cd "/tmp"        # => changes to /tmp
browser.cd "~"           # => changes to home
browser.cd ".."          # => parent directory
browser.cd 2             # => jump to history entry 2


61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/browser.rb', line 61

def cd dir, append=false
  return unless dir
  data, missing = [],{}
  unless append
    @colors[:fields] = {}
    dir = @dir_history[dir+1] if dir.is_a? Integer
    clear
    Dir.chdir( @directory = File.expand_path( dir ) )
    unless [?/, ENV["HOME"]].include? dir
      @dir_history.unshift @directory
      @dir_history.uniq!
    end
  end
  data = Dir.new(@directory).children.map.with_index do |file,id|
    if File.symlink? file
      target = File.readlink( file ) rescue $!.to_s
      next [file, target] + [??]*7 + ["inode","symlink"] unless File.exist?(file)
    end
    stat = File.stat(file)
    type = stat.ftype.to_sym

    mime = { "directory": "inode/directory",
      "characterSpecial": "inode/chardevice",
      "blockSpecial": "inode/blockdevice", "link": "inode/symlink",
      "fifo": "inode/fifo", "socket": "inode/socket"}[type]
    if type == :file and @mimetype_db
      ext = file.match(/.+\.(\w+)$/)
      mime ||= MIMETYPES[ext[1].to_sym] if ext
    end

    ( missing[file] = id ; mime = "?/?" ) unless mime
    [  file, target, stat.mode.to_s(8)[-3..-1].chars.map{|d|
      "rwx".chars.map.with_index{|c,i| d.to_i[2-i]==0 ? ?- : c } }.join,
      stat.uid, stat.gid, stat.size,
      stat.atime.to_i, stat.mtime.to_i, stat.ctime.to_i,
      *mime.to_s.split( ?/ ) ]
  end

  `file --mime-type '#{missing.keys.join "' '"}'
    `.split($/).each.with_index{ |line,id|
      next unless line[/^.+:\s+\w+\/[\w\-\.]+$/]
      file, mime = line.split(/:\s*/)
      data[ missing[file] ][-2,2] = mime.rstrip.split( ?/ ) } if
        @mimetype_magic and not missing.empty?
  self << data
  sort
  filter
  unless append
    @directory += ?/ unless @directory == ?/
    switch_to @view
    reset :position
  end
  return
end

#check(row) ⇒ Object

True when the row is a directory or passes base Grid filtering.

browser.check 0   # => true if navigable


258
# File 'lib/browser.rb', line 258

def check row; super or @data[row][SUBTYPE] == "directory" end

#confirmObject

Returns the current directory path as the confirm result.



159
# File 'lib/browser.rb', line 159

def confirm; return @directory end

#directoriesObject

Returns row ids for all directories in the current listing.

browser.directories   # => [0, 1, 3]


316
# File 'lib/browser.rb', line 316

def directories; ids.select{ |id| @data[id][SUBTYPE] == 'directory' } end

#drawObject

Renders the grid with an info bar above showing the current directory path and colored status items for filter, sort, and view.

browser.draw   # draws browser with info bar


299
300
301
302
303
304
305
306
307
308
309
310
# File 'lib/browser.rb', line 299

def draw; super
  info = { "[f]ilter": @filters.empty? ? "no filter" : @filters.to_s[1..-2],
    "[s]ort": sorted_by(true), "[v]iews": @view }
  move left, top - 1
  show prepare( @directory, width - info.values.join.size - 3, :left, :left, 5 )
  @colors[:actions].each do |type,col|
    color col
    show ' ' + info[type].to_s
    @positions[type] = Typr.column - (type==@positions.keys.last ? 0:1 )
  end
  @user.draw
end

#filter_viewObject

Open a filter popup on the chosen row/column and apply the input.

browser.filter_view   # prompts for filter text


231
232
# File 'lib/browser.rb', line 231

def filter_view; add_filter *@user.ask(
filter: [ popup( type: :"[f]ilter" ), :row], with: :line ) end

#helpObject

Display a help grid listing all keybindings.



208
# File 'lib/browser.rb', line 208

def help; @user.help end

#homeObject

Navigate to the home directory.

browser.home   # => cd "~"


204
# File 'lib/browser.rb', line 204

def home; cd ?~ end

#markObject

Prompts to select rows by index. Returns chosen ids or nil.

browser.mark   # => [0, 2, 5] or nil


165
# File 'lib/browser.rb', line 165

def mark; return @user.ask( 'mark files': [self, :rows] ) end

#mark_allObject

Selects all non-directory rows.

browser.mark_all   # => [0, 3, 7] (file ids only)


180
# File 'lib/browser.rb', line 180

def mark_all; return @selected[:rows] = ids - directories end

#mark_invertObject

Inverts selection across all non-directory rows.

browser.mark_invert   # => toggled ids


186
# File 'lib/browser.rb', line 186

def mark_invert; return @selected[:rows] = ids - @selected[:rows] end

#mark_rangeObject

Toggles selection for every row within a "from..to" range.

browser.mark_range   # prompts for from/to, toggles selection


171
172
173
174
# File 'lib/browser.rb', line 171

def mark_range; return ids[ ( eval @user.ask(
      'mark from': [self, :relative_row], 'to': [self, :relative_row]
      ).join '..' ) ].each{|id| @selected[:rows].include?(id) ?
@selected[:rows].delete(id) : @selected[:rows] << id } end

#pick(type = 'file') ⇒ Object

Interactive file/directory picker. Blocks until a selection or Escape. Return on a file wraps the path in single quotes. Enter on a directory triggers cd. type optionally filters by MIME family (audio, video, image, text, application, inode).

browser.pick              # => '/home/user/file.txt'
browser.pick 'image'      # => '/path/to/photo.png' (images only)
browser.pick nil          # => anything, same as 'file'


270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
# File 'lib/browser.rb', line 270

def pick type='file'
  type ||= 'file'
  type = type.to_s
  return super if type[/row|column|field|none/]
  add_filter ( %w[audio video image text application inode].include?(
    type) ? TYPE : SUBTYPE ), type if type != 'file'
  loop do
    draw
    draw_hints :row
    key = Typr.read( :key )
    next unless choice = send( key )
    case choice
      when Array; choice = ?' + choice.map{ |id|
        @directory+?/+@data[id][NAME] }.join("' '") + ?'
      when Integer; file = @data[ choice ]
        if file[SUBTYPE] == 'directory'; cd file[TARGET] || file[NAME]; next
        else choice = ?'+@directory+?/+file[NAME]+?' end
    end
    reset :display
    return choice
  end
end

Create a small Grid popup under an info bar item for column selection. args must include :type matching an actions key for positioning.

browser.popup type: :"[s]ort"   # sort-picker popup


338
339
340
341
342
343
344
345
# File 'lib/browser.rb', line 338

def popup args={}
  Grid.new( { alternate:false, border: :light,
    borders:{ top:nil, top_left:nil, top_right:nil },
    top: top, align: [:right], input:@header,
    left: left + ( @positions[args[:type]] || 0 ) -
      ( args[:input] || @header ).map( &:size ).max - 1,
    colors:{ columns: [ @colors[:actions][args[:type]] ] } }.merge args )
end

#process(row) ⇒ Object

Colors symlink names green/red based on target existence and applies per-MIME-type foreground colors from @colors[:types].

browser.process 0   # => sets colors for row 0


122
123
124
125
126
# File 'lib/browser.rb', line 122

def process row; super
  color = @colors[:types][@data[row][SUBTYPE].to_sym] ||
    @colors[:types][@data[row][TYPE].to_sym]
  @colors[:fields][[row,NAME]] = color if color
end

#recurseObject

Recurse through the directory tree, adding subdirectory entries inline via cd(path, true). Sorts by name with the filetree processor.

browser.recurse   # flattens tree into leaf names


216
217
218
219
# File 'lib/browser.rb', line 216

def recurse
  sort_by NAME
  @procs[NAME] = :filetree
end

#reset_viewObject

Reset display, selection, and position state then re-sort.

browser.reset_view


225
# File 'lib/browser.rb', line 225

def reset_view; reset [:display, :selection, :position]; sort end

#rootObject

Navigate to the root directory.

browser.root   # => cd "/"


198
# File 'lib/browser.rb', line 198

def root; cd ?/ end

#send(key) ⇒ Object

Resolves hint characters typed on a row. Returns the matching row id for fast navigation, otherwise falls through to Stack#send.

browser.send "1"   # => row id if hint "1" is visible


150
151
152
153
154
155
# File 'lib/browser.rb', line 150

def send key
  if key.is_a?(String) and
    id = @hints[0..[height, rows-1+headspace].min-@hints_start-1].index(key)
    return page[id+@hints_start]
  else super end
end

#sortObject

Sort by NAME by default; directories always appear before files within each prefix grouping.

browser.sort   # re-sorts with dirs first


324
325
326
327
328
329
330
# File 'lib/browser.rb', line 324

def sort
  @sort ||= NAME
  super
  dirs = ids.select{ |id| @data[id][SUBTYPE] == 'directory' }
  @map = (directories + ids).uniq
  nil
end

#sort_viewObject

Open a sort-picker popup to choose a column to sort by.

browser.sort_view   # pick a column header to sort


238
# File 'lib/browser.rb', line 238

def sort_view; sort_by @user.ask( "sort by": [ popup( type: :"[s]ort" ), :row] ) end

#switch_to(view = nil) ⇒ Object

Switch to a named or indexed column view. Resets format widths.

browser.switch_to :full      # all columns
browser.switch_to 0          # first view key
browser.switch_to :stats     # name, size, perms, owner, group, modified


135
136
137
138
139
140
141
142
# File 'lib/browser.rb', line 135

def switch_to view=nil
  return unless view
  view = @views.keys[ view ] if view.is_a? Integer
  @sequence = @views[ view ].map{|title| @header.index title.to_s}
  reset :format
  @view = view
  return
end

#topObject

Top offset plus one row for the path info bar.



49
# File 'lib/browser.rb', line 49

def top; super + 1 end

#viewsObject

Open a view-picker popup and switch to the selected view.

browser.views   # choose :simple, :compact, :stats, or :full


244
# File 'lib/browser.rb', line 244

def views; switch_to( popup( type: :"[v]iews", input: @views.keys ).pick :row )  end