Class: Typr::Browser
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
Navigation Keys (interactive picker)
[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
Instance Attribute Summary collapse
-
#dir_history ⇒ Object
Open a directory history picker and cd to the chosen entry.
-
#directory ⇒ Object
Returns the value of attribute directory.
-
#mimetype_db ⇒ Object
Returns the value of attribute mimetype_db.
-
#mimetype_magic ⇒ Object
Returns the value of attribute mimetype_magic.
-
#pwd ⇒ Object
Returns the value of attribute pwd.
-
#user ⇒ Object
Returns the value of attribute user.
-
#view ⇒ Object
Returns the value of attribute view.
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
-
#back ⇒ Object
Navigate up one directory.
-
#bottom ⇒ Object
Height minus 2 rows reserved for path bar and command line.
-
#cd(dir, append = false) ⇒ Object
Change to
dir(absolute path,"..","~", or integer index into dir_history). -
#check(row) ⇒ Object
True when the row is a directory or passes base Grid filtering.
-
#confirm ⇒ Object
Returns the current directory path as the confirm result.
-
#directories ⇒ Object
Returns row ids for all directories in the current listing.
-
#draw ⇒ Object
Renders the grid with an info bar above showing the current directory path and colored status items for filter, sort, and view.
-
#filter_view ⇒ Object
Open a filter popup on the chosen row/column and apply the input.
-
#help ⇒ Object
Display a help grid listing all keybindings.
-
#home ⇒ Object
Navigate to the home directory.
-
#initialize(args = {}) ⇒ Browser
constructor
Construct a Browser widget.
-
#mark ⇒ Object
Prompts to select rows by index.
-
#mark_all ⇒ Object
Selects all non-directory rows.
-
#mark_invert ⇒ Object
Inverts selection across all non-directory rows.
-
#mark_range ⇒ Object
Toggles selection for every row within a "from..to" range.
-
#pick(type = 'file') ⇒ Object
Interactive file/directory picker.
-
#popup(args = {}) ⇒ Object
Create a small Grid popup under an info bar item for column selection.
-
#process(row) ⇒ Object
Colors symlink names green/red based on target existence and applies per-MIME-type foreground colors from
@colors[:types]. -
#recurse ⇒ Object
Recurse through the directory tree, adding subdirectory entries inline via
cd(path, true). -
#reset_view ⇒ Object
Reset display, selection, and position state then re-sort.
-
#root ⇒ Object
Navigate to the root directory.
-
#send(key) ⇒ Object
Resolves hint characters typed on a row.
-
#sort ⇒ Object
Sort by NAME by default; directories always appear before files within each prefix grouping.
-
#sort_view ⇒ Object
Open a sort-picker popup to choose a column to sort by.
-
#switch_to(view = nil) ⇒ Object
Switch to a named or indexed column view.
-
#top ⇒ Object
Top offset plus one row for the path info bar.
-
#views ⇒ Object
Open a view-picker popup and switch to the selected view.
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_history ⇒ Object
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 |
#directory ⇒ Object
Returns the value of attribute directory.
40 41 42 |
# File 'lib/browser.rb', line 40 def directory @directory end |
#mimetype_db ⇒ Object
Returns the value of attribute mimetype_db.
41 42 43 |
# File 'lib/browser.rb', line 41 def mimetype_db @mimetype_db end |
#mimetype_magic ⇒ Object
Returns the value of attribute mimetype_magic.
41 42 43 |
# File 'lib/browser.rb', line 41 def mimetype_magic @mimetype_magic end |
#pwd ⇒ Object
Returns the value of attribute pwd.
40 41 42 |
# File 'lib/browser.rb', line 40 def pwd @pwd end |
#user ⇒ Object
Returns the value of attribute user.
40 41 42 |
# File 'lib/browser.rb', line 40 def user @user end |
#view ⇒ Object
Returns the value of attribute view.
40 41 42 |
# File 'lib/browser.rb', line 40 def view @view end |
Instance Method Details
#back ⇒ Object
Navigate up one directory.
browser.back # equivalent to cd ".."
192 |
# File 'lib/browser.rb', line 192 def back; cd ".." end |
#bottom ⇒ Object
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.( 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 |
#confirm ⇒ Object
Returns the current directory path as the confirm result.
159 |
# File 'lib/browser.rb', line 159 def confirm; return @directory end |
#directories ⇒ Object
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 |
#draw ⇒ Object
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_view ⇒ Object
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 |
#help ⇒ Object
Display a help grid listing all keybindings.
208 |
# File 'lib/browser.rb', line 208 def help; @user.help end |
#home ⇒ Object
Navigate to the home directory.
browser.home # => cd "~"
204 |
# File 'lib/browser.rb', line 204 def home; cd ?~ end |
#mark ⇒ Object
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_all ⇒ Object
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_invert ⇒ Object
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_range ⇒ Object
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 |
#popup(args = {}) ⇒ Object
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 |
#recurse ⇒ Object
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_view ⇒ Object
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 |
#root ⇒ Object
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 |
#sort ⇒ Object
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_view ⇒ Object
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 |
#top ⇒ Object
Top offset plus one row for the path info bar.
49 |
# File 'lib/browser.rb', line 49 def top; super + 1 end |
#views ⇒ Object
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 |