Class: Filecon::App

Inherits:
Typr::Browser
  • Object
show all
Includes:
Typr
Defined in:
lib/filecon.rb

Instance Method Summary collapse

Constructor Details

#initialize(args = {}) ⇒ App

Returns a new instance of App.



357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
# File 'lib/filecon.rb', line 357

def initialize args={}
  super
  @rawsort[NAME] = false
  @cmd_history ||= []
  @dir_history ||= []
  @path ||= {}
  @frequency_limit = args[:frequency_limit] || 10
  @keymap[:open_current] = ?`
  @keymap[:execute] = ?x
  @keymap[:recurse] = ?R
  @user.bindings.insert 7, "[R]ecurse"
  @user.bindings.insert 8, "[c]ommands"
  @user.bindings.insert 9, "e[x]ecute"
  @user.bindings.insert 11, "[`]:open_current"
  @user.reset
end

Instance Method Details

#add_path_columnObject



326
327
328
329
330
331
332
# File 'lib/filecon.rb', line 326

def add_path_column
  Typr.const_set :PATH, @header.size unless Typr.const_defined?(:PATH)
  @header << "path" unless @header.include?("path")
  @data.each { |row| row[PATH] = File.dirname(row[NAME]) }
  @sequence.insert(@sequence.index(NAME) || 0, PATH) unless @sequence.include?(PATH)
  # @align[@header.size-1] = :right
end

#build(str, id, name = nil) ⇒ Object



150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# File 'lib/filecon.rb', line 150

def build str, id, name=nil
  nc, ac = @colors[:commands] || [:green, :red]
  reset = color_code( @colors[:default] )
  prefix = name ? "#{color_code(nc)}#{name}#{color_code(ac)}:" : ""
  cmd = ""
  while str[/\%(name|cmd|dir|file|str)/]
    cmd += $`
    case $&
      when '%name'; cmd += ?"+( id.nil? ? @directory :
        id.is_a?( Array ) ? id.map{ |id|
        @data[id][NAME] }.join('" "') :
        @data[id][NAME] )+?"
      when '%dir', '%file', '%cmd'
        @user.show "> #{prefix}#{cmd}#{$&}#{$'}#{reset}"
        result = pick( { '%dir' => :directory, '%file' => :file, '%cmd' => :cmd }[$&] )
        return unless result
        cmd += ?" + result + ?"
      when '%str'
        prompt = cmd.empty? || cmd.end_with?(' ') ? cmd : "#{cmd} "
        query = Typr.read_line "> #{prefix}#{prompt}", left: left, top: @user.top
        return unless query
        cmd += query
    end
    str = str[($`+$&).size..-1]
  end
  cmd += $' || str
  return cmd
end

#cd(dir, append = false) ⇒ Object



12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# File 'lib/filecon.rb', line 12

def cd dir, append=false
  @selected[:rows] = [] unless append
  if dir.is_a? Array
    dirs = dir.filter_map { |d| d = File.expand_path(d.to_s); Dir.exist?(d) ? d : nil }
    dirs.each_with_index { |d, i|
      @directory = d if i.positive?
      Dir.chdir(d) if i.positive?
      super d, i.positive?
    }
    @directory = dirs
    nil
  else
    dir = @dir_history[dir+1][1] if dir.is_a? Integer
    record :dir, File.expand_path(dir) unless append || [?/, ENV["HOME"]].include?(dir)
    super dir, append
    nil
  end
end

#change_view(id = nil) ⇒ Object



42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/filecon.rb', line 42

def change_view id=nil
  return unless id
  id = @views.keys[id] if id.is_a? Integer
  view = @views[id]
  return super if view.is_a? Array
  data = @data.map.with_index{ |row, id| row + view.values.map{ |cmd|
    next unless cmd.is_a?(String)
    built = build cmd, id
    next unless built
    io = if built[0] == ?!
      StringIO.new eval( built[1..-1] ).to_s
    else
      cmd = built[0] == ?| ? built : ?| + built
      open cmd + " 2>&1"
    end
    cell = io.read(width).to_s.scrub.strip
    value = coerce_type cell
    value.is_a?(String) || cell.empty? ? cell.gsub(/[\n\t]/, ?|) : value
  }.compact }
  @sequence = [ NAME ] + view.keys.map{ |head|
    @header.index(head.to_s) || (@header += [head.to_s]; @header.count-1) }
  clear
  self << data
  @current = id
  @view = id
end

#cmd_historyObject



193
# File 'lib/filecon.rb', line 193

def cmd_history; history @cmd_history, :cmd end

#confirmObject



374
375
376
377
# File 'lib/filecon.rb', line 374

def confirm
  return unless @selected[:rows].any?
  open_file @selected[:rows], false
end

#delete_row(id) ⇒ Object



136
137
138
139
140
141
142
143
144
145
146
147
148
# File 'lib/filecon.rb', line 136

def delete_row id
  @data.delete_at id
  @layer.delete_at id if @layer
  if @map
    @map.delete id
    @map.map!{ |i| i > id ? i - 1 : i }
  end
  @colors[:fields] = @colors[:fields].map { |(row,col),color|
    row == id ? nil : row > id ? [[row - 1, col], color] : [[row, col], color] }.compact.to_h
  @start = rows - height if @start > rows - height
  @start = 0 if @start < 0
  @selected[:rows] = []
end

#dir_historyObject



194
# File 'lib/filecon.rb', line 194

def dir_history; history @dir_history, :dir end

#executeObject



31
# File 'lib/filecon.rb', line 31

def execute; cd ENV["PATH"].split(":"); add_path_column end

#flyObject



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
# File 'lib/filecon.rb', line 381

def fly
  loop do
    show
    draw_hints :rows
    key = Typr.read_key
    next unless key
    case key
      when KEY_ESCAPE; exit
      when Typr::Mouse
        if key.wheel? and key.press?
          send( key.wheel_up? ? @keymap[:up] : @keymap[:down] )
        elsif key.press? and ( key.left? or key.right? )
          id = hit( key.y, key.x )
          open_file( id, !key.right? ) if id.is_a? Integer
        end
        @user.reset
      else
        alt = key.is_a?(String) && key[/\A\e(\d)\z/]
        key = key[1] if alt
        id = send( key )
        open_file( id, !alt ) if id.is_a? Integer
        @user.reset
    end
  end
end

#helpObject



334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
# File 'lib/filecon.rb', line 334

def help
  info = "\nFILECON - a terminal file manager\n\n" \
    "HINTS: press a number (1-9,0) to open that file with its default action.\n" \
    "       Alt+number opens the command list for that file instead.\n" \
    "       Right-click a file to open its command list.\n" \
    "       Mark files with m, then Return opens their command list.\n" \
    "       TAB cycles pages of hints.\n\n" \
    "COMMANDS:\n" \
    "  e[x]ecute      browse all executables on your PATH\n" \
    "  [R]ecurse      expand the current directory tree (subdirectories listed inline)\n" \
    "  [d]irectories  jump to a recently visited directory\n" \
    "  [c]ommands     re-run a recent command\n\n" \
    "  Directory and command history popups list the most frequently used\n" \
    "  entries first, then the rest in most-recent order. The frequency_limit\n" \
    "  setting in config.yaml (default 10) controls how many frequent entries\n" \
    "  lead the list.\n\n" \
    "CONFIG: ~/.config/filecon/config.yaml\n\n" \
    "KEYBINDINGS:\n\n" + @user.bindings.join(?\n)
  Text.new( input: info, top:2, left:3, right: -3, bottom: -3, header: "HELP",
    colors: { header: [:black,:white], border:[:white,:grey10] },
    border: :light ).pick :none
end

#history(data, type) ⇒ Object



196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
# File 'lib/filecon.rb', line 196

def history data, type
  is_cmd = type == :cmd
  data = data.drop 1 unless is_cmd
  return unless data.any?

  top = data.max_by(@frequency_limit) { |cnt, _| cnt }
  top_set = top.filter_map { |_, e| e }.to_h { |e| [e, true] }
  ordered = top + data.reject { |_, e| top_set[e] }

  pairs = ordered.map { |_, e|
    is_cmd ? [e.split(' ', 2)[0], e.split(' ', 2)[1] || ""] :
      [File.basename(e), e]
  }

  fc = is_cmd ? [:red, :blue] : [:yellow, :brown]
  fields = {}
  ordered.each_with_index { |(cnt, e), i|
    next unless top_set[e]
    fields[[i,0]] = [fc[0], :grey20]
    fields[[i,1]] = [fc[1], :grey20]
  }

  picker = popup(input: pairs, format:[:min,:max],
    alternate: true, left: left, bottom: -2,
    colors: { columns: fc, fields: fields })
  n = picker.pick(:row, column: :all)
  return unless n

  entry = ordered[n][1]
  if is_cmd and picker.modifier == :alt
    edited = Typr.read_line "> ", left: left, top: @user.top, initial: entry
    return unless edited
    entry = edited
  end
  record type, entry

  if is_cmd
    begin; run entry; rescue => e; Text.new(top: 2, right:-1, bottom:-2, border: :light,
      input: StringIO.new(e.message + "\n" + e.backtrace.first(3).join("\n"))).pick :none; end
  else
    unless Dir.exist? entry
      @user.show "no such directory: #{entry}"; Typr.read_key; return
    end
    cd entry
    nil
  end
end

#import_shell_historyObject



244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
# File 'lib/filecon.rb', line 244

def import_shell_history
  sync = File.join(@path[:state], "shell_sync"); return if File.exist?(sync)

  @user.show "importing shell history..."
  valid = ENV["PATH"].split(":").each_with_object({}) { |d, v|
    Dir.entries(d).each { |e| v[e] = true } rescue nil }

  builtins = %w[cd . alias bg break builtin command continue declare dirs echo
    eval exec exit export false fc fg getopts hash history jobs kill let limit
    local logoff logout mapfile popd printf pushd pwd read readonly return set
    shift shopt source suspend test times trap true type typeset ulimit umask
    unalias unset wait whence which]

  aliases = {}
  [".zshrc", ".bashrc"].each { |rc|
    File.readlines(File.join(ENV["HOME"], rc)).each { |l|
      l =~ /\A\s*alias\s+(\w+)=(?:'([^']*)'|"([^"]*)"|(\S+))/ &&
        aliases[$1] = ($2 || $3 || $4 || "").split.first } rescue nil }

  last = 0
  lines = 0
  [File.expand_path("~/.bash_history"), File.expand_path("~/.zsh_history")].each { |p|
    next unless File.exist?(p)
    @user.show "importing shell history: #{File.basename(p)}..."
    File.readlines(p).each { |l|
      c = l.chomp.scrub(''); next if c.empty? || c[0] == '#'
      if p.end_with?('zsh_history')
        ts = c[/\A: (\d+):\d+;(.*)/, 1]&.to_i
        c = c[/\A: \d+:\d+;(.*)/, 1] || c
        last = ts if ts && ts > last
      end
      next if c.empty?
      first = c.split.first; next unless first
      r = first; 10.times { break unless aliases[r]; r = aliases[r] }
      c = r + c[first.size..] if r != first
      next unless valid[r] || builtins.include?(r)
      record :cmd, c, write: false unless %w[cd pushd].include?(r)
      c.split.each { |t|
        t = t.delete_prefix('"').delete_prefix("'").delete_suffix('"').delete_suffix("'")
        next unless t.start_with?('/', '~')
        t = File.expand_path(t) rescue nil
        record :dir, t, write: false if t && Dir.exist?(t)
      }
      lines += 1
      @user.show "importing shell history: #{lines} lines..." if lines % 500 == 0
    }
  }
  File.write(@path[:cmd_history], @cmd_history.map { |c, e| "#{c},#{e}" }.join($/))
  File.write(@path[:dir_history], @dir_history.map { |c, e| "#{c},#{e}" }.join($/))
  File.write(sync, last.to_s)
  @user.reset
end

#open_currentObject



379
# File 'lib/filecon.rb', line 379

def open_current; open_file nil, false, ["directory", "inode"] - [??] + ["default"] end

#open_file(id = nil, default = false, types = nil) ⇒ Object



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
115
116
117
118
119
120
121
122
123
124
# File 'lib/filecon.rb', line 69

def open_file id=nil, default=false, types=nil
  unless id || types
    return unless id ||= @selected[:rows].empty? ?
      @user.ask( open:[self, :row, NAME] ) : @selected[:rows]
  end
  types ||= if id.is_a? Array
    eval id.map{ |id| @data[id].values_at(SUBTYPE,TYPE).to_s }.join(?&)
  else @data[id].values_at(SUBTYPE, TYPE)
  end - [??] + ["default"]
  if id && (id.is_a?(Array) ? id.all?{ |i| @data[i][EXECUTABLE] } : @data[id][EXECUTABLE])
    types.insert(types.index("default") || types.size, "executable")
  end

  commands = @commands.values_at( *types ).map(&:to_a).flatten( 1 ).uniq
  alt = false
  pair = if default; commands.first
  else move left+1,top+1
    if id.is_a?(Array); draw "#{id.count.to_s} files: (#{ 
      types.join ?, })".ljust width
    else print id end
    pop = id ? top+2 : top
    picker = Grid.new( left:left+1, top: pop, right: -1,
    bottom: [ pop + commands.count - 1, Typr.height ].min,
    input:commands, border: :light, format: [:min, :max],
    borders: { top: nil }, alternate: false,
    colors: {columns: @colors[:commands] || [:green, :red] } )
    return unless cmd = picker.pick( :row, column: 0 )
    alt = picker.modifier == :alt
    commands[ cmd ]
  end
  dir = @directory
  cmd = build pair.last, id, pair.first
  return unless cmd
  if alt
    edited = Typr.read_line "> ", left: left, top: @user.top, initial: cmd
    return unless edited
    cmd = edited
  end
  if cmd[0] == ?|
    display = cmd[1..-1]
    record :cmd, cmd
    io = run( cmd )
    output = io.read.to_s.scrub.strip
    nc, ac = @colors[:commands] || [:green, :red]
    @user.show "> #{color_code(nc)}#{pair.first}#{color_code(ac)}:#{display}" \
      "#{color_code(@colors[:default])} : #{output.lines.last}"
    show
    Typr.read_key
  else
    record :cmd, cmd unless cmd[0..2] == '!cd'
    run( cmd )
  end
  Array(id).sort.reverse.each { |i| refresh_row i } if id && cmd[0..2] != '!cd' && @directory == dir
  @selected[:rows] = [] if id && @directory != dir
  show #unless cmd[0] == ?|
end

#pick(type = 'file', **kw) ⇒ Object



33
34
35
36
37
38
39
40
# File 'lib/filecon.rb', line 33

def pick type='file', **kw
  return super unless type == :cmd
  dir = @directory
  execute
  result = super
  cd dir
  result
end

#record(type, entry, write: true) ⇒ Object



297
298
299
300
301
302
303
304
# File 'lib/filecon.rb', line 297

def record type, entry, write: true
  h = type == :cmd ? @cmd_history : @dir_history
  idx = h.index { |_, e| e == entry }
  cnt = idx ? (h.delete_at(idx)[0] + 1) : 1
  h.unshift [cnt, entry]
  h.slice!(@max_history..) if h.size > @max_history
  File.write @path[:"#{type}_history"], h.map { |c, e| "#{c},#{e}" }.join($/) if write
end

#recurseObject



306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
# File 'lib/filecon.rb', line 306

def recurse
  return if @directory.is_a?(Array)
  root = File.expand_path(@directory)
  clear
  cd root
  Dir.glob("**/*", File::FNM_DOTMATCH, base: root).sort.each { |p|
    next if !@show_hidden && p.split(?/).any? { |c| c.start_with?(?.) }
    next unless File.directory?(File.join(root, p))
    d = File.expand_path(p, root)
    @directory = d
    Dir.chdir d
    cd d, true
  }
  Dir.chdir root
  @directory = root
  add_path_column
  @current = 0
  nil
end

#refresh_row(id) ⇒ Object



126
127
128
129
130
131
132
133
134
# File 'lib/filecon.rb', line 126

def refresh_row id
  return unless row = @data[id]
  name = row[NAME]
  return delete_row id unless File.symlink?(name) || File.exist?(name)
  updated = scan_file name
  updated[-2,2] = row[-2,2]
  @data[id] = updated
  process id
end

#run(cmd = nil) ⇒ Object



179
180
181
182
183
184
185
186
187
188
189
190
191
# File 'lib/filecon.rb', line 179

def run cmd=nil
  return unless cmd
  cmd = @cmd_history[cmd][1] if cmd.is_a? Integer
  case cmd[0]
    when ?! then io = StringIO.new eval( cmd[1..-1] ).to_s
    when ?| then io = open( cmd + " 2>&1" )
    when ?+ then io = open( ?| + @term +' '+ cmd[1..-1] + " 2>&1" )
    when ?= then Text.new(top: 2, right:-1, bottom:-2, border: :light,
      input:open( ?|+cmd[1..-1]+" 2>&1" ) ).pick :none
    else io = open( ?| + @term +' '+ cmd + " 2>&1" )
  end
  return io
end