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.



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

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 1, "[o]pen"
  @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



276
277
278
279
280
281
282
# File 'lib/filecon.rb', line 276

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



103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/filecon.rb', line 103

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|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'
        @user.show "> #{prefix}#{cmd}#{$&}#{$'}#{reset}"
        return unless dir = pick(:directory)
        cmd += ?" + dir + ?"
      when '%file'
        @user.show "> #{prefix}#{cmd}#{$&}#{$'}#{reset}"
        return unless file = pick(:file)
        cmd += ?" + file + ?"
      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
# File 'lib/filecon.rb', line 12

def cd dir, append=false
  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

#cmd_historyObject



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

def cmd_history; history @cmd_history, :cmd end

#dir_historyObject



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

def dir_history; history @dir_history, :dir end

#executeObject



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

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

#flyObject



324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
# File 'lib/filecon.rb', line 324

def fly
  loop do
    show
    draw_hints :rows
    key = Typr.read_key
    next unless key
    case key
      when KEY_ESCAPE; exit
      else
        id = send( key )
        open_file( id, true) if id.is_a? Integer
        @user.reset
    end
  end
end

#helpObject



284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'lib/filecon.rb', line 284

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" \
    "       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



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
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'lib/filecon.rb', line 152

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]
  }

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

  entry = ordered[n][1]
  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



194
195
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
243
244
245
# File 'lib/filecon.rb', line 194

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



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

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

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



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

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
  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
    return unless cmd = 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, hints: "1234567890",
    colors: {columns: @colors[:commands] || [:green, :red] }  ).pick( :row, column: 0 )
    commands[ cmd ]
  end
  cmd = build pair.last, id, pair.first
  return unless cmd
  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}"
    Typr.read_key
  else
    record :cmd, cmd unless cmd[0..2] == '!cd'
    run( cmd )
    show
  end
end

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



247
248
249
250
251
252
253
254
# File 'lib/filecon.rb', line 247

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



256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
# File 'lib/filecon.rb', line 256

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

#run(cmd = nil) ⇒ Object



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

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

#switch_to(id = nil) ⇒ Object

switch_view



33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/filecon.rb', line 33

def switch_to 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
    io.read(width).to_s.scrub.gsub(/[\n\t]/, ?|)
  }.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