filecon 0.1.2 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7999c20dbb6c7d75ff85343c955bb07794853b9bab8859132cf368b8f48735dd
4
- data.tar.gz: 41bcfea5119ff2ebe534305a3eda418e825d9980dd329fb2dbf51cf783b35e29
3
+ metadata.gz: 47e7fc518a42a7037832b88feb1e3c7d3b23c64968de970928104d1fe5c47ef1
4
+ data.tar.gz: e7cc5f0746c241fc6b3d3866b1efb3220b1274870c7446f7a6c48ac8abd0aa10
5
5
  SHA512:
6
- metadata.gz: e0e7cb9f038a8f91adabda6258643dd9e2a11557b471740b2adb7fa42a9f93a0a027a62a659719e5370de463974083d4e5f5f3ee509ae782def11620624b2936
7
- data.tar.gz: bcdc1bb5b7dfb36fe9bb43c4d8a50db175081d45aa548796e5482204056d4ae3e0d41fae0401ec547f32bd25ed840921c221e2c485d98a108285e5d21c0b5b81
6
+ metadata.gz: f34237e27ff2d45a8aee3e577afd2f6aa5e0bca8e1fbc03fcec77615500aab08b459575ac17715ed89a5c20eac959388e78d7afd0c446281e01fb502d8f41e5b
7
+ data.tar.gz: 9c639e9634aa690a09d4bad83732f16153b6852ffaee84967b55dc140e97ba27a151cd904ff93633c24d12b1dbeadd466c7021b8b75af7050bee87087f66facc
data/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ ![Filecon](logo_alpha_small.png)
2
+
1
3
  # Filecon
2
4
 
3
5
  A terminal-based file manager built on the [Typr](https://codeberg.org/typr/typr) terminal UI toolkit.
@@ -16,12 +18,6 @@ A terminal-based file manager built on the [Typr](https://codeberg.org/typr/typr
16
18
  gem install filecon
17
19
  ```
18
20
 
19
- Or add to your Gemfile:
20
-
21
- ```ruby
22
- gem "filecon", "~> 0.1"
23
- ```
24
-
25
21
  ## Usage
26
22
 
27
23
  ```bash
@@ -39,12 +35,13 @@ filecon [directory]
39
35
  **Actions**
40
36
  - `o` — Open list of all commands for selected file
41
37
  - `c` — Command history
38
+ - `x` — List all executables from $PATH directories
42
39
  - `h` — Help
43
40
  - `v` — Views
44
41
  - `s` — Sort view
45
42
  - `f` — Filter view
46
43
  - `r` — Reset view
47
- - `R` — Toggle recursive listing
44
+ - `R` — Recursive file listing
48
45
 
49
46
  **Marking**
50
47
  - `m` — Mark file
@@ -79,14 +76,13 @@ Config is stored at `~/.config/filecon/config.yaml` (YAML format) and includes:
79
76
 
80
77
  **Substitution variables:** `%name` (file path), `%dir` (directory picker), `%file` (file picker), `%str` (text input)
81
78
 
82
- See `filecon(1)` or the installed `config.yaml` for full details.
79
+ See `filecon(1)` or the installed `filecon.yaml` for full details.
83
80
 
84
81
  ## Development
85
82
 
86
83
  ```bash
87
84
  git clone https://codeberg.org/typr/filecon
88
85
  cd filecon
89
- bundle install
90
86
  rake test
91
87
  ```
92
88
 
data/bin/filecon CHANGED
@@ -1,39 +1,52 @@
1
1
  #!/usr/bin/ruby
2
2
 
3
+ require "fileutils"
4
+ require "yaml"
3
5
  require_relative "../lib/filecon"
4
6
 
5
7
  begin
6
- path = Filecon::PATH
7
- FileUtils.mkpath path unless File.exist? path
8
- FileUtils.cp File.join(__dir__, "../config.yaml"), path unless File.exist? File.join(path, "config.yaml")
9
- dir_history_path = File.join(path, "dir_history")
10
- cmd_history_path = File.join(path, "cmd_history")
11
- FileUtils.touch dir_history_path
12
- FileUtils.touch cmd_history_path
8
+ config_dir = File.expand_path("~/.config/filecon")
9
+ config_path = File.join(config_dir, "config.yaml")
10
+ unless File.exist?(config_path)
11
+ FileUtils.mkpath config_dir
12
+ FileUtils.cp File.join(__dir__, "../config.yaml"), config_path
13
+ end
14
+ state_dir = File.expand_path("~/.local/share/filecon")
15
+ FileUtils.mkpath state_dir
13
16
 
14
- config = YAML.safe_load(File.read(File.join(path, "config.yaml")), permitted_classes: [Symbol]) || {}
17
+ dh_path = File.join(state_dir, "dir_history")
18
+ ch_path = File.join(state_dir, "cmd_history")
19
+
20
+ [dh_path, ch_path].each { |f| FileUtils.touch f }
21
+
22
+ parse = ->(p) { File.readlines(p).map { |l| s = l.chomp.split(",", 2); [s[0].to_i, s[1]] } rescue [] }
23
+
24
+ config = YAML.safe_load(File.read(config_path), permitted_classes: [Symbol]) || {}
15
25
 
16
26
  term = config["term"] || ENV["TERM"] || ""
17
- term_hold = term + (config["term_hold"] || " --hold")
27
+ term += config["term_hold"] if config["term_hold"]
18
28
 
19
29
  Typr.init config["colors"]["default"]
20
30
  Typr.clear
21
31
 
22
32
  app = Filecon::App.new( right: -1, bottom: -1,
23
- dir_history: File.read(dir_history_path).split($/),
24
- cmd_history: File.read(cmd_history_path).split($/),
25
- directory: ARGV.first, keymap: { open_file: ?o, cmd_history: ?c },
26
- colors: { cmd_history: :red }.merge( config["colors"] || {} ),
33
+ dir_history: parse[dh_path],
34
+ cmd_history: parse[ch_path],
35
+ path: { config: config_path, state: state_dir, dir_history: dh_path,
36
+ cmd_history: ch_path },
37
+ directory: ARGV.first || ?., keymap: { open_file: ?o, cmd_history: ?c, dir_history: ?d, recurse: ?R, open_current: ?` },
38
+ colors: config["colors"] || {},
27
39
  views: config["views"] || {},
28
40
  commands: config["commands"] || {},
29
41
  term: term,
30
- term_hold: term_hold,
31
42
  max_history: config["max_history"] || 100,
32
- dir_history_path: dir_history_path,
33
- cmd_history_path: cmd_history_path )
34
- Typr.on_resize { app.draw }
43
+ frequency_limit: config["frequency_limit"] || 10,
44
+ filter_dirs: config["filter_dirs"] )
45
+ Typr.on_resize { app.show }
46
+ app.import_shell_history
35
47
  app.fly
36
48
  rescue => e
49
+ $stderr.puts "#{e.class}: #{e.message}\n#{e.backtrace.first(10).join("\n")}"
37
50
  File.write File.join(Dir.tmpdir, "filecon_error.log"),
38
51
  "#{e.class}: #{e.message}\n#{e.backtrace.first(10).join("\n")}"
39
52
  ensure
data/config.yaml CHANGED
@@ -3,7 +3,6 @@
3
3
  # =============================================================================
4
4
  #
5
5
  # This is the default configuration file for Filecon, a terminal file manager.
6
- # Copy this file to ~/.config/filecon/config.yaml and customize as needed.
7
6
  #
8
7
  # All settings are optional. Filecon will use sensible defaults if values
9
8
  # are not specified.
@@ -22,7 +21,11 @@ term_hold: " --hold"
22
21
 
23
22
  # Maximum number of history entries to retain.
24
23
  # Older entries beyond this limit are automatically pruned.
25
- max_history: 100
24
+ max_history: 10000
25
+
26
+ frequency_limit: 10
27
+ # When true, active filters also hide directories that don't match.
28
+ filter_dirs: true
26
29
 
27
30
 
28
31
  # =============================================================================
@@ -32,11 +35,28 @@ max_history: 100
32
35
  # Customize the color scheme for different file types in the browser view.
33
36
  #
34
37
  # Color values can be:
35
- # - Named colors: black, red, green, yellow, blue, magenta, cyan, white, grey
38
+ # - Named colors:
39
+ # Basic: black, red, green, yellow, blue, magenta, cyan, white, grey
40
+ # Shades: brown, orange, lime, pink, maroon, navy, teal, olive, coral, tan
41
+ # Dark: dark_red, dark_green, dark_yellow, dark_blue,
42
+ # dark_magenta, dark_cyan
36
43
  # - Grey shades: grey0 through grey100 (e.g., grey62 for 62% grey)
37
44
  # - 8-bit palette: 0 - 255 (maps to terminal's 256-color palette)
38
45
  # - RGB triplets: [0-255, 0-255, 0-255] (true color terminals)
39
46
  #
47
+ # Color settings passed to Typr spaces:
48
+ # default global default foreground/background [fg, bg]
49
+ # border widget borders
50
+ # header column/panel headers
51
+ # question prompt question text
52
+ # answer typed input text
53
+ # selected highlighted / selected row background
54
+ # alternate alternating row background in lists
55
+ # hints hint numbers/digits
56
+ # actions named action colors: "[f]ilter", "[s]ort", "[v]iews"
57
+ # commands command picker and status line: [name, args] colors
58
+ # search search-match highlight (text viewer)
59
+ #
40
60
  # Usage:
41
61
  # Foreground color only: "blue"
42
62
  # Foreground + background: ["white", "black"]
@@ -45,12 +65,16 @@ max_history: 100
45
65
 
46
66
  colors:
47
67
  default: ["white", "black"]
68
+ # [name, args] colors for the command picker and the status line
69
+ # shown while building/running a command
70
+ commands: ["green", "red"]
48
71
  types:
49
72
  image: "blue"
50
73
  audio: "green"
51
74
  video: "cyan"
52
75
  directory: "yellow"
53
76
  "x-pie-executable": "red"
77
+ # executable: "red"
54
78
 
55
79
 
56
80
  # =============================================================================
@@ -91,39 +115,58 @@ colors:
91
115
  #
92
116
  # %str - Prompts for text input, inserts the string
93
117
  #
118
+ # SPECIAL CATEGORIES:
119
+ #
120
+ # executable - Added to the command picker for any file whose
121
+ # executable bit is set for the current user, regardless
122
+ # of its MIME type (e.g. shell scripts with shebangs).
123
+ #
94
124
  # =============================================================================
95
125
 
96
126
  commands:
97
127
  default:
98
- open: "|xdg-open %name"
99
- disk_use: "|du -hs %name"
128
+ # copy: "|rsync --progress %name %dir"
100
129
  info: "=file %name"
101
- copy: "|rsync --progress %name %dir"
130
+ copy: "|cp -R %name %dir"
102
131
  move: "|mv %name %dir"
103
- delete: "|rm %name"
132
+ rename: "|mv %name %str"
133
+ delete: "|rm -rf %name"
134
+ open_with: "|%file %name %str"
135
+ open_with_terminal: "+%file %name %str"
136
+ xdg_open: "|xdg-open %name"
137
+
138
+ executable:
139
+ run: "|%name %str"
140
+ run_terminal: "+%name %str"
141
+ help: "=%name --help"
142
+ manual: "+man %name"
104
143
 
105
144
  directory:
106
145
  cd: "!cd %name"
107
- new_file: "|micro %str"
108
- new_dir: "|mkdir %str"
146
+ new_file: "|touch %name/%str"
147
+ new_dir: "|mkdir %name/%str"
148
+ shell: "+$SHELL -c 'cd %name && exec $SHELL'"
149
+ # zsh: "+zsh -c 'cd %name && exec zsh'"
150
+ disk_use: "|du -hs %name | cut -wf1"
109
151
 
110
152
  text:
111
153
  view: "+moor %name"
112
154
  edit: "+micro %name"
113
- count: "|wc -l %name"
155
+ count: "|wc -l %name | cut -wf1"
114
156
 
115
157
  troff:
116
158
  manual: "+man %name"
117
159
 
118
160
  "x-ruby":
119
- run: "+ruby %name"
161
+ run: "|ruby %name"
162
+ run_terminal: "+ruby %name"
120
163
 
121
164
  html:
122
165
  terminal: "+cha %name"
123
166
  graphics: "|vimb %name"
124
167
 
125
168
  image:
126
- play: "|mpv %name"
169
+ view: "|pqiv %name"
127
170
 
128
171
  video:
129
172
  play: "|mpv %name"
@@ -134,12 +177,6 @@ commands:
134
177
  pdf:
135
178
  view: "|mupdf %name"
136
179
 
137
- "x-pie-executable":
138
- run: "|%name"
139
- run_terminal: "+%name"
140
- help: "=%name --help"
141
- manual: "+man %name"
142
-
143
180
  "x-python":
144
181
  run: "+python3 %name"
145
182
 
@@ -178,23 +215,23 @@ commands:
178
215
  view: "=cat %name | jq ."
179
216
 
180
217
  "zip":
181
- list: "|unzip -l %name"
218
+ list: "=unzip -l %name"
182
219
  extract: "|unzip %name -d %dir"
183
220
 
184
221
  "x-gzip":
185
- list: "|tar -tzf %name"
222
+ list: "=tar -tzf %name"
186
223
  extract: "|tar -xzf %name -C %dir"
187
224
 
188
225
  "x-bzip2":
189
- list: "|tar -tjf %name"
226
+ list: "=tar -tjf %name"
190
227
  extract: "|tar -xjf %name -C %dir"
191
228
 
192
229
  "x-xz":
193
- list: "|tar -tJf %name"
230
+ list: "=tar -tJf %name"
194
231
  extract: "|tar -xJf %name -C %dir"
195
232
 
196
233
  "x-tar":
197
- list: "|tar -tf %name"
234
+ list: "=tar -tf %name"
198
235
  extract: "|tar -xf %name -C %dir"
199
236
 
200
237
  "epub+zip":
@@ -219,10 +256,10 @@ commands:
219
256
  # - A string Command to execute (with %name substitution)
220
257
  # - A symbol Reference to a system field (e.g., :size, :type, :subtype)
221
258
  #
222
- # Prefix ! to evaluate as Ruby, prefix | for shell commands.
259
+ # Prefix ! to evaluate as Ruby
223
260
  #
224
261
  # Available system fields:
225
- # target, permissions, owner, group, size,
262
+ # target, permissions, executable, owner, group, size,
226
263
  # accessed, modified, created, type, subtype
227
264
  #
228
265
  # When a view is activated, its columns are appended to the default file
@@ -234,15 +271,18 @@ views:
234
271
  content:
235
272
  content: "head -n 50 %name"
236
273
 
237
- filetype:
274
+ types:
238
275
  type: true
239
276
  subtype: true
240
277
  description: "file -b %name"
241
278
 
279
+ disk_use:
280
+ du: "du -hs %name | cut -wf1"
281
+
242
282
  count:
243
- chars: "wc -m %name"
244
- words: "wc -w %name"
245
- lines: "wc -l %name"
283
+ chars: "wc -m %name | cut -wf1"
284
+ words: "wc -w %name | cut -wf1"
285
+ lines: "wc -l %name | cut -wf1"
246
286
 
247
287
  whatis:
248
- whatis: "!open('|whatis %name 2>&1').read.match(/[-:]\\s(.*)$/)[1]"
288
+ whatis: "whatis $(basename %name) 2>&1 | sed 's/.* - //'"
data/filecon.1 CHANGED
@@ -77,6 +77,12 @@ Open list of all commands for selected file
77
77
  .TP
78
78
  .B c
79
79
  Browse command history
80
+ .PP
81
+ Directory and command history popups list the most frequently used entries
82
+ first, then the remaining entries in most-recent order. The
83
+ .B frequency_limit
84
+ setting in config.yaml (default 10) controls how many frequent entries lead
85
+ the list.
80
86
  .SH CONFIGURATION
81
87
  On first run, the default
82
88
  .B config.yaml
@@ -1,3 +1,3 @@
1
1
  module Filecon
2
- VERSION = "0.1.2"
2
+ VERSION = "0.2.0"
3
3
  end
data/lib/filecon.rb CHANGED
@@ -1,6 +1,3 @@
1
- require "fileutils"
2
- require "yaml"
3
-
4
1
  begin
5
2
  require_relative "../../typr/lib/typr"
6
3
  rescue LoadError
@@ -9,19 +6,30 @@ end
9
6
 
10
7
  require_relative "filecon/version"
11
8
 
12
- module Filecon
13
- PATH = File.join(ENV["HOME"], ".config/filecon/")
14
- end
15
-
16
9
  class Filecon::App < Typr::Browser
17
10
  include Typr
18
11
 
19
- def cd dir, append=false; super
20
- File.write @dir_history_path,
21
- @dir_history[0..@max_history].join($/)
22
- return
12
+ def cd dir, append=false
13
+ if dir.is_a? Array
14
+ dirs = dir.filter_map { |d| d = File.expand_path(d.to_s); Dir.exist?(d) ? d : nil }
15
+ dirs.each_with_index { |d, i|
16
+ @directory = d if i.positive?
17
+ Dir.chdir(d) if i.positive?
18
+ super d, i.positive?
19
+ }
20
+ @directory = dirs
21
+ nil
22
+ else
23
+ dir = @dir_history[dir+1][1] if dir.is_a? Integer
24
+ record :dir, File.expand_path(dir) unless append || [?/, ENV["HOME"]].include?(dir)
25
+ super dir, append
26
+ nil
27
+ end
23
28
  end
24
29
 
30
+ def execute; cd ENV["PATH"].split(":"); add_path_column end
31
+
32
+ #switch_view
25
33
  def switch_to id=nil
26
34
  return unless id
27
35
  id = @views.keys[id] if id.is_a? Integer
@@ -30,9 +38,13 @@ class Filecon::App < Typr::Browser
30
38
  data = @data.map.with_index{ |row, id| row + view.values.map{ |cmd|
31
39
  next unless cmd.is_a?(String)
32
40
  built = build cmd, id
33
- io = built[0] == ?! ?
34
- StringIO.new( eval( built[1..-1] ).to_s ) :
35
- open( ?| + built + " 2>&1" )
41
+ next unless built
42
+ io = if built[0] == ?!
43
+ StringIO.new eval( built[1..-1] ).to_s
44
+ else
45
+ cmd = built[0] == ?| ? built : ?| + built
46
+ open cmd + " 2>&1"
47
+ end
36
48
  io.read(width).to_s.scrub.gsub(/[\n\t]/, ?|)
37
49
  }.compact }
38
50
  @sequence = [ NAME ] + view.keys.map{ |head|
@@ -40,55 +52,79 @@ class Filecon::App < Typr::Browser
40
52
  clear
41
53
  self << data
42
54
  @current = id
55
+ @view = id
43
56
  end
44
57
 
45
58
  def open_file id=nil, default=false, types=nil
46
59
  unless id || types
47
60
  return unless id ||= @selected[:rows].empty? ?
48
- @user.ask( open:[self, :row] ) : @selected[:rows]
61
+ @user.ask( open:[self, :row, NAME] ) : @selected[:rows]
49
62
  end
50
63
  types ||= if id.is_a? Array
51
64
  eval id.map{ |id| @data[id].values_at(SUBTYPE,TYPE).to_s }.join(?&)
52
65
  else @data[id].values_at(SUBTYPE, TYPE)
53
66
  end - [??] + ["default"]
67
+ if id && (id.is_a?(Array) ? id.all?{ |i| @data[i][EXECUTABLE] } : @data[id][EXECUTABLE])
68
+ types.insert(types.index("default") || types.size, "executable")
69
+ end
54
70
 
55
- commands = @commands.values_at( *types ).map(&:to_a).flatten 1
56
- cmd = if default; commands.first.last
71
+ commands = @commands.values_at( *types ).map(&:to_a).flatten( 1 ).uniq
72
+ pair = if default; commands.first
57
73
  else move left+1,top+1
58
- if id.is_a?(Array); show "#{id.count.to_s} files: (#{
74
+ if id.is_a?(Array); draw "#{id.count.to_s} files: (#{
59
75
  types.join ?, })".ljust width
60
76
  else print id end
61
- return unless cmd = Grid.new( left:1, top:3, right: -1,
77
+ pop = id ? top+2 : top
78
+ return unless cmd = Grid.new( left:left+1, top: pop, right: -1,
79
+ bottom: [ pop + commands.count - 1, Typr.height ].min,
62
80
  input:commands, border: :light, format: [:min, :max],
63
- borders: { top: nil }, alternate: false,
64
- colors: {columns: [:green, :red] } ).pick( :row )
65
- commands[ cmd ].last
81
+ borders: { top: nil }, alternate: false, hints: "1234567890",
82
+ colors: {columns: @colors[:commands] || [:green, :red] } ).pick( :row, column: 0 )
83
+ commands[ cmd ]
66
84
  end
67
- cmd = build cmd, id
85
+ cmd = build pair.last, id, pair.first
86
+ return unless cmd
68
87
  if cmd[0] == ?|
69
88
  display = cmd[1..-1]
70
- io = run( cmd, true )
89
+ record :cmd, cmd
90
+ io = run( cmd )
71
91
  output = io.read.to_s.scrub.strip
72
- @user.draw "> #{display} : #{output.lines.last}"
73
- Typr.read :key
92
+ nc, ac = @colors[:commands] || [:green, :red]
93
+ @user.show "> #{color_code(nc)}#{pair.first}#{color_code(ac)}:#{display}" \
94
+ "#{color_code(@colors[:default])} : #{output.lines.last}"
95
+ Typr.read_key
74
96
  else
75
- run( cmd, cmd[0..2] != '!cd' )
76
- draw
97
+ record :cmd, cmd unless cmd[0..2] == '!cd'
98
+ run( cmd )
99
+ show
77
100
  end
78
101
  end
79
102
 
80
- def build str, id
103
+ def build str, id, name=nil
104
+ nc, ac = @colors[:commands] || [:green, :red]
105
+ reset = color_code( @colors[:default] )
106
+ prefix = name ? "#{color_code(nc)}#{name}#{color_code(ac)}:" : ""
81
107
  cmd = ""
82
108
  while str[/\%(name|dir|file|str)/]
83
109
  cmd += $`
84
110
  case $&
85
111
  when '%name'; cmd += ?"+( id.nil? ? @directory :
86
112
  id.is_a?( Array ) ? id.map{ |id|
87
- @directory + @data[id][NAME] }.join('" "') :
88
- @directory + @data[id][NAME] )+?"
89
- when '%dir'; cmd += ?"+(pick :directory)+?"
90
- when '%file'; cmd += ?"+(pick :file)+?"
91
- when '%str'; cmd += Typr.read :line
113
+ @data[id][NAME] }.join('" "') :
114
+ @data[id][NAME] )+?"
115
+ when '%dir'
116
+ @user.show "> #{prefix}#{cmd}#{$&}#{$'}#{reset}"
117
+ return unless dir = pick(:directory)
118
+ cmd += ?" + dir + ?"
119
+ when '%file'
120
+ @user.show "> #{prefix}#{cmd}#{$&}#{$'}#{reset}"
121
+ return unless file = pick(:file)
122
+ cmd += ?" + file + ?"
123
+ when '%str'
124
+ prompt = cmd.empty? || cmd.end_with?(' ') ? cmd : "#{cmd} "
125
+ query = Typr.read_line "> #{prefix}#{prompt}", left: left, top: @user.top
126
+ return unless query
127
+ cmd += query
92
128
  end
93
129
  str = str[($`+$&).size..-1]
94
130
  end
@@ -96,32 +132,168 @@ class Filecon::App < Typr::Browser
96
132
  return cmd
97
133
  end
98
134
 
99
- def run cmd=nil, store=false
135
+ def run cmd=nil
100
136
  return unless cmd
101
- cmd = @cmd_history[cmd] if cmd.is_a? Integer
137
+ cmd = @cmd_history[cmd][1] if cmd.is_a? Integer
102
138
  case cmd[0]
103
139
  when ?! then io = StringIO.new eval( cmd[1..-1] ).to_s
104
140
  when ?| then io = open( cmd + " 2>&1" )
105
141
  when ?+ then io = open( ?| + @term +' '+ cmd[1..-1] + " 2>&1" )
106
142
  when ?= then Text.new(top: 2, right:-1, bottom:-2, border: :light,
107
143
  input:open( ?|+cmd[1..-1]+" 2>&1" ) ).pick :none
108
- end
109
- if store
110
- @cmd_history.unshift cmd
111
- @cmd_history.uniq!
112
- File.write @cmd_history_path, @cmd_history[0..@max_history].join($/)
144
+ else io = open( ?| + @term +' '+ cmd + " 2>&1" )
113
145
  end
114
146
  return io
115
147
  end
116
148
 
117
- def cmd_history; run popup( { input: @cmd_history, align:[:left], left:left,
118
- colors: { columns: [ @colors[:cmd_history] ] } } ).pick(:row), true
149
+ def cmd_history; history @cmd_history, :cmd end
150
+ def dir_history; history @dir_history, :dir end
151
+
152
+ def history data, type
153
+ is_cmd = type == :cmd
154
+ data = data.drop 1 unless is_cmd
155
+ return unless data.any?
156
+
157
+ top = data.max_by(@frequency_limit) { |cnt, _| cnt }
158
+ top_set = top.filter_map { |_, e| e }.to_h { |e| [e, true] }
159
+ ordered = top + data.reject { |_, e| top_set[e] }
160
+
161
+ pairs = ordered.map { |_, e|
162
+ is_cmd ? [e.split(' ', 2)[0], e.split(' ', 2)[1] || ""] :
163
+ [File.basename(e), e]
164
+ }
165
+
166
+ fc = is_cmd ? [:red, :blue] : [:yellow, :brown]
167
+ fields = {}
168
+ ordered.each_with_index { |(cnt, e), i|
169
+ next unless top_set[e]
170
+ fields[[i,0]] = [fc[0], :grey20]
171
+ fields[[i,1]] = [fc[1], :grey20]
172
+ }
173
+
174
+ n = popup(input: pairs, hints:"1234567890", format:[:min,:max],
175
+ alternate: true, left: left, bottom: -2,
176
+ colors: { columns: fc, fields: fields }).pick(:row, column: :all)
177
+ return unless n
178
+
179
+ entry = ordered[n][1]
180
+ record type, entry
181
+
182
+ if is_cmd
183
+ begin; run entry; rescue => e; Text.new(top: 2, right:-1, bottom:-2, border: :light,
184
+ input: StringIO.new(e.message + "\n" + e.backtrace.first(3).join("\n"))).pick :none; end
185
+ else
186
+ unless Dir.exist? entry
187
+ @user.show "no such directory: #{entry}"; Typr.read_key; return
188
+ end
189
+ cd entry
190
+ nil
191
+ end
192
+ end
193
+
194
+ def import_shell_history
195
+ sync = File.join(@path[:state], "shell_sync"); return if File.exist?(sync)
196
+
197
+ @user.show "importing shell history..."
198
+ valid = ENV["PATH"].split(":").each_with_object({}) { |d, v|
199
+ Dir.entries(d).each { |e| v[e] = true } rescue nil }
200
+
201
+ builtins = %w[cd . alias bg break builtin command continue declare dirs echo
202
+ eval exec exit export false fc fg getopts hash history jobs kill let limit
203
+ local logoff logout mapfile popd printf pushd pwd read readonly return set
204
+ shift shopt source suspend test times trap true type typeset ulimit umask
205
+ unalias unset wait whence which]
206
+
207
+ aliases = {}
208
+ [".zshrc", ".bashrc"].each { |rc|
209
+ File.readlines(File.join(ENV["HOME"], rc)).each { |l|
210
+ l =~ /\A\s*alias\s+(\w+)=(?:'([^']*)'|"([^"]*)"|(\S+))/ &&
211
+ aliases[$1] = ($2 || $3 || $4 || "").split.first } rescue nil }
212
+
213
+ last = 0
214
+ lines = 0
215
+ [File.expand_path("~/.bash_history"), File.expand_path("~/.zsh_history")].each { |p|
216
+ next unless File.exist?(p)
217
+ @user.show "importing shell history: #{File.basename(p)}..."
218
+ File.readlines(p).each { |l|
219
+ c = l.chomp.scrub(''); next if c.empty? || c[0] == '#'
220
+ if p.end_with?('zsh_history')
221
+ ts = c[/\A: (\d+):\d+;(.*)/, 1]&.to_i
222
+ c = c[/\A: \d+:\d+;(.*)/, 1] || c
223
+ last = ts if ts && ts > last
224
+ end
225
+ next if c.empty?
226
+ first = c.split.first; next unless first
227
+ r = first; 10.times { break unless aliases[r]; r = aliases[r] }
228
+ c = r + c[first.size..] if r != first
229
+ next unless valid[r] || builtins.include?(r)
230
+ record :cmd, c, write: false unless %w[cd pushd].include?(r)
231
+ c.split.each { |t|
232
+ t = t.delete_prefix('"').delete_prefix("'").delete_suffix('"').delete_suffix("'")
233
+ next unless t.start_with?('/', '~')
234
+ t = File.expand_path(t) rescue nil
235
+ record :dir, t, write: false if t && Dir.exist?(t)
236
+ }
237
+ lines += 1
238
+ @user.show "importing shell history: #{lines} lines..." if lines % 500 == 0
239
+ }
240
+ }
241
+ File.write(@path[:cmd_history], @cmd_history.map { |c, e| "#{c},#{e}" }.join($/))
242
+ File.write(@path[:dir_history], @dir_history.map { |c, e| "#{c},#{e}" }.join($/))
243
+ File.write(sync, last.to_s)
244
+ @user.reset
245
+ end
246
+
247
+ def record type, entry, write: true
248
+ h = type == :cmd ? @cmd_history : @dir_history
249
+ idx = h.index { |_, e| e == entry }
250
+ cnt = idx ? (h.delete_at(idx)[0] + 1) : 1
251
+ h.unshift [cnt, entry]
252
+ h.slice!(@max_history..) if h.size > @max_history
253
+ File.write @path[:"#{type}_history"], h.map { |c, e| "#{c},#{e}" }.join($/) if write
254
+ end
255
+
256
+ def recurse
257
+ return if @directory.is_a?(Array)
258
+ root = File.expand_path(@directory)
259
+ clear
260
+ cd root
261
+ Dir.glob("**/*", File::FNM_DOTMATCH, base: root).sort.each { |p|
262
+ next if !@show_hidden && p.split(?/).any? { |c| c.start_with?(?.) }
263
+ next unless File.directory?(File.join(root, p))
264
+ d = File.expand_path(p, root)
265
+ @directory = d
266
+ Dir.chdir d
267
+ cd d, true
268
+ }
269
+ Dir.chdir root
270
+ @directory = root
271
+ add_path_column
272
+ @current = 0
273
+ nil
274
+ end
275
+
276
+ def add_path_column
277
+ Typr.const_set :PATH, @header.size unless Typr.const_defined?(:PATH)
278
+ @header << "path" unless @header.include?("path")
279
+ @data.each { |row| row[PATH] = File.dirname(row[NAME]) }
280
+ @sequence.insert(@sequence.index(NAME) || 0, PATH) unless @sequence.include?(PATH)
281
+ # @align[@header.size-1] = :right
119
282
  end
120
283
 
121
284
  def help
122
285
  info = "\nFILECON - a terminal file manager\n\n" \
123
286
  "HINTS: press a number (1-9,0) to open that file with its default action.\n" \
124
287
  " TAB cycles pages of hints.\n\n" \
288
+ "COMMANDS:\n" \
289
+ " e[x]ecute browse all executables on your PATH\n" \
290
+ " [R]ecurse expand the current directory tree (subdirectories listed inline)\n" \
291
+ " [d]irectories jump to a recently visited directory\n" \
292
+ " [c]ommands re-run a recent command\n\n" \
293
+ " Directory and command history popups list the most frequently used\n" \
294
+ " entries first, then the rest in most-recent order. The frequency_limit\n" \
295
+ " setting in config.yaml (default 10) controls how many frequent entries\n" \
296
+ " lead the list.\n\n" \
125
297
  "CONFIG: ~/.config/filecon/config.yaml\n\n" \
126
298
  "KEYBINDINGS:\n\n" + @user.bindings.join(?\n)
127
299
  Text.new( input: info, top:2, left:3, right: -3, bottom: -3, header: "HELP",
@@ -131,24 +303,35 @@ class Filecon::App < Typr::Browser
131
303
 
132
304
  def initialize args={}
133
305
  super
306
+ @rawsort[NAME] = false
307
+ @cmd_history ||= []
308
+ @dir_history ||= []
309
+ @path ||= {}
310
+ @frequency_limit = args[:frequency_limit] || 10
311
+ @keymap[:open_current] = ?`
312
+ @keymap[:execute] = ?x
313
+ @keymap[:recurse] = ?R
134
314
  @user.bindings.insert 1, "[o]pen"
135
- @user.bindings.insert 7, "[c]ommands"
136
- @user.draw
137
-
315
+ @user.bindings.insert 7, "[R]ecurse"
316
+ @user.bindings.insert 8, "[c]ommands"
317
+ @user.bindings.insert 9, "e[x]ecute"
318
+ @user.bindings.insert 11, "[`]:open_current"
319
+ @user.reset
138
320
  end
139
321
 
322
+ def open_current; open_file nil, false, ["directory", "inode"] - [??] + ["default"] end
323
+
140
324
  def fly
141
325
  loop do
142
- draw
326
+ show
143
327
  draw_hints :rows
144
- key = Typr.read :key
328
+ key = Typr.read_key
145
329
  next unless key
146
330
  case key
147
331
  when KEY_ESCAPE; exit
148
- when ?`; open_file nil, false, ["directory", "inode"] - [??] + ["default"]
149
332
  else
150
333
  id = send( key )
151
- open_file id, true if id.is_a? Integer
334
+ open_file( id, true) if id.is_a? Integer
152
335
  @user.reset
153
336
  end
154
337
  end
Binary file
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: filecon
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kilian Reitmayr
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2026-07-28 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: typr
@@ -16,14 +15,14 @@ dependencies:
16
15
  requirements:
17
16
  - - "~>"
18
17
  - !ruby/object:Gem::Version
19
- version: '1.0'
18
+ version: '1.2'
20
19
  type: :runtime
21
20
  prerelease: false
22
21
  version_requirements: !ruby/object:Gem::Requirement
23
22
  requirements:
24
23
  - - "~>"
25
24
  - !ruby/object:Gem::Version
26
- version: '1.0'
25
+ version: '1.2'
27
26
  - !ruby/object:Gem::Dependency
28
27
  name: unicode-display_width
29
28
  requirement: !ruby/object:Gem::Requirement
@@ -82,11 +81,11 @@ files:
82
81
  - filecon.1
83
82
  - lib/filecon.rb
84
83
  - lib/filecon/version.rb
84
+ - logo_alpha_small.png
85
85
  homepage: https://codeberg.org/typr/filecon
86
86
  licenses:
87
87
  - GPL-2.0-only
88
88
  metadata: {}
89
- post_install_message:
90
89
  rdoc_options: []
91
90
  require_paths:
92
91
  - lib
@@ -101,8 +100,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
101
100
  - !ruby/object:Gem::Version
102
101
  version: '0'
103
102
  requirements: []
104
- rubygems_version: 3.3.15
105
- signing_key:
103
+ rubygems_version: 3.6.9
106
104
  specification_version: 4
107
105
  summary: Terminal file manager built on Typr
108
106
  test_files: []