filecon 0.1.1 → 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: 7f656a074b7160a9c362539e004b02f9e5d474cfafa6dd009df97040cc4398bf
4
- data.tar.gz: 28f6a13ebf1aa1d536b69928f0f0e279cb0a5f60adadbf17e6bfbeb30d0213e1
3
+ metadata.gz: 47e7fc518a42a7037832b88feb1e3c7d3b23c64968de970928104d1fe5c47ef1
4
+ data.tar.gz: e7cc5f0746c241fc6b3d3866b1efb3220b1274870c7446f7a6c48ac8abd0aa10
5
5
  SHA512:
6
- metadata.gz: aee107ce9e33a5529ce0b574079d22471e7f96b5e42b246bb8b547730b40676a87d5f99f8f9988d108610af0d2c7d922bdefd0d8593b22cf8ce8027942304d7b
7
- data.tar.gz: b4ca16e0cb4b8b5de25f0cc8088db0178894afe92f270176f6cb39cac7edd2a2f168470376357a85eba183fbfa7c827f547971840322f42bb10e8003b0b87f6a
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
@@ -54,7 +51,7 @@ filecon [directory]
54
51
 
55
52
  **Directory**
56
53
  - `~` — Go to home directory
57
- - `` ` `` — Go to root directory
54
+ - `` ` `` — Open commands for current directory
58
55
  - `d` — Directory history
59
56
 
60
57
  **Other**
@@ -63,19 +60,29 @@ filecon [directory]
63
60
 
64
61
  ### Configuration
65
62
 
66
- Config is stored at `~/.config/filecon/config.yaml` and includes:
63
+ Config is stored at `~/.config/filecon/config.yaml` (YAML format) and includes:
67
64
 
68
- - Color settings
69
- - Command definitions per file type
65
+ - Color settings (named colors, 8-bit palette, RGB triplets)
66
+ - Command definitions per MIME type
70
67
  - View configurations
71
- - History limits
68
+ - History limits and terminal settings
69
+
70
+ **Command prefixes:**
71
+
72
+ - `|` — pipe output to status line
73
+ - `+` — run in a new terminal window
74
+ - `=` — display output in the text viewer
75
+ - `!` — evaluate as Ruby code
76
+
77
+ **Substitution variables:** `%name` (file path), `%dir` (directory picker), `%file` (file picker), `%str` (text input)
78
+
79
+ See `filecon(1)` or the installed `filecon.yaml` for full details.
72
80
 
73
81
  ## Development
74
82
 
75
83
  ```bash
76
84
  git clone https://codeberg.org/typr/filecon
77
85
  cd filecon
78
- bundle install
79
86
  rake test
80
87
  ```
81
88
 
data/bin/filecon CHANGED
@@ -1,29 +1,52 @@
1
1
  #!/usr/bin/ruby
2
2
 
3
+ require "fileutils"
4
+ require "yaml"
5
+ require_relative "../lib/filecon"
6
+
3
7
  begin
4
- require "filecon"
5
- rescue LoadError
6
- require_relative "../lib/filecon"
7
- end
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
8
16
 
9
- Filecon.setup_config
17
+ dh_path = File.join(state_dir, "dir_history")
18
+ ch_path = File.join(state_dir, "cmd_history")
10
19
 
11
- begin
12
- config = Filecon.config
13
- term = config[:term] || ENV["TERM"] || ""
14
- term_hold = term + (config[:term_hold] || " --hold")
15
- Typr.init( color: config[:colors][:default] )
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]) || {}
25
+
26
+ term = config["term"] || ENV["TERM"] || ""
27
+ term += config["term_hold"] if config["term_hold"]
28
+
29
+ Typr.init config["colors"]["default"]
16
30
  Typr.clear
17
- Filecon::App.new( right: -1, bottom: -1,
18
- dir_history: File.read(Filecon.dir_history_path).split($/),
19
- cmd_history: File.read(Filecon.cmd_history_path).split($/),
20
- directory: ARGV.first, keymap: { open_file: ?o, cmd_history: ?c },
21
- colors: { cmd_history: :red }.merge( config[:colors] || {} ),
22
- views: config[:views] || {},
23
- commands: config[:commands] || {},
31
+
32
+ app = Filecon::App.new( right: -1, bottom: -1,
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"] || {},
39
+ views: config["views"] || {},
40
+ commands: config["commands"] || {},
24
41
  term: term,
25
- term_hold: term_hold ).fly
42
+ max_history: config["max_history"] || 100,
43
+ frequency_limit: config["frequency_limit"] || 10,
44
+ filter_dirs: config["filter_dirs"] )
45
+ Typr.on_resize { app.show }
46
+ app.import_shell_history
47
+ app.fly
26
48
  rescue => e
49
+ $stderr.puts "#{e.class}: #{e.message}\n#{e.backtrace.first(10).join("\n")}"
27
50
  File.write File.join(Dir.tmpdir, "filecon_error.log"),
28
51
  "#{e.class}: #{e.message}\n#{e.backtrace.first(10).join("\n")}"
29
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,37 +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
- diskuse: "|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
-
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"
151
+
108
152
  text:
109
153
  view: "+moor %name"
110
154
  edit: "+micro %name"
111
- count: "|wc -l %name"
155
+ count: "|wc -l %name | cut -wf1"
112
156
 
113
157
  troff:
114
158
  manual: "+man %name"
115
159
 
116
160
  "x-ruby":
117
- run: "+ruby %name"
161
+ run: "|ruby %name"
162
+ run_terminal: "+ruby %name"
118
163
 
119
164
  html:
120
165
  terminal: "+cha %name"
121
166
  graphics: "|vimb %name"
122
167
 
123
168
  image:
124
- play: "|mpv %name"
169
+ view: "|pqiv %name"
125
170
 
126
171
  video:
127
172
  play: "|mpv %name"
@@ -132,12 +177,6 @@ commands:
132
177
  pdf:
133
178
  view: "|mupdf %name"
134
179
 
135
- "x-pie-executable":
136
- run: "|%name"
137
- run_terminal: "+%name"
138
- help: "=%name --help"
139
- manual: "+man %name"
140
-
141
180
  "x-python":
142
181
  run: "+python3 %name"
143
182
 
@@ -176,23 +215,23 @@ commands:
176
215
  view: "=cat %name | jq ."
177
216
 
178
217
  "zip":
179
- list: "|unzip -l %name"
218
+ list: "=unzip -l %name"
180
219
  extract: "|unzip %name -d %dir"
181
220
 
182
221
  "x-gzip":
183
- list: "|tar -tzf %name"
222
+ list: "=tar -tzf %name"
184
223
  extract: "|tar -xzf %name -C %dir"
185
224
 
186
225
  "x-bzip2":
187
- list: "|tar -tjf %name"
226
+ list: "=tar -tjf %name"
188
227
  extract: "|tar -xjf %name -C %dir"
189
228
 
190
229
  "x-xz":
191
- list: "|tar -tJf %name"
230
+ list: "=tar -tJf %name"
192
231
  extract: "|tar -xJf %name -C %dir"
193
232
 
194
233
  "x-tar":
195
- list: "|tar -tf %name"
234
+ list: "=tar -tf %name"
196
235
  extract: "|tar -xf %name -C %dir"
197
236
 
198
237
  "epub+zip":
@@ -217,10 +256,10 @@ commands:
217
256
  # - A string Command to execute (with %name substitution)
218
257
  # - A symbol Reference to a system field (e.g., :size, :type, :subtype)
219
258
  #
220
- # Prefix ! to evaluate as Ruby, prefix | for shell commands.
259
+ # Prefix ! to evaluate as Ruby
221
260
  #
222
261
  # Available system fields:
223
- # target, permissions, owner, group, size,
262
+ # target, permissions, executable, owner, group, size,
224
263
  # accessed, modified, created, type, subtype
225
264
  #
226
265
  # When a view is activated, its columns are appended to the default file
@@ -232,13 +271,18 @@ views:
232
271
  content:
233
272
  content: "head -n 50 %name"
234
273
 
235
- filetype:
274
+ types:
236
275
  type: true
237
276
  subtype: true
238
277
  description: "file -b %name"
239
278
 
240
- lines:
241
- lines: "wc -l %name"
279
+ disk_use:
280
+ du: "du -hs %name | cut -wf1"
281
+
282
+ count:
283
+ chars: "wc -m %name | cut -wf1"
284
+ words: "wc -w %name | cut -wf1"
285
+ lines: "wc -l %name | cut -wf1"
242
286
 
243
287
  whatis:
244
- whatis: "!open('|whatis %name 2>&1').read.match(/[-:]\\s(.*)$/)[1]"
288
+ whatis: "whatis $(basename %name) 2>&1 | sed 's/.* - //'"
data/filecon.1 CHANGED
@@ -1,4 +1,4 @@
1
- .TH FILECON 1 "2026-07-26" "filecon" "User Commands"
1
+ .TH FILECON 1 "2026-07-27" "filecon 0.1.1" "User Commands"
2
2
  .SH NAME
3
3
  filecon \- interactive terminal file manager with hint-based navigation
4
4
  .SH SYNOPSIS
@@ -60,7 +60,7 @@ Filter rows
60
60
  Recurse directory tree
61
61
  .TP
62
62
  .B `
63
- Navigate to root directory
63
+ Open commands for current directory
64
64
  .TP
65
65
  .B ~
66
66
  Navigate to home directory
@@ -77,10 +77,16 @@ 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
83
- is copied from the installation directory to
89
+ (YAML format) is copied from the installation directory to
84
90
  .BR ~/.config/filecon/ .
85
91
  .SS Config File Locations
86
92
  .TP
@@ -92,6 +98,19 @@ Directory navigation history
92
98
  .TP
93
99
  .B ~/.config/filecon/cmd_history
94
100
  Command execution history
101
+ .SS Command Prefixes
102
+ .TP
103
+ .B |
104
+ Pipe output to status line
105
+ .TP
106
+ .B +
107
+ Run in a new terminal window
108
+ .TP
109
+ .B =
110
+ Display output in the text viewer
111
+ .TP
112
+ .B !
113
+ Evaluate as Ruby code
95
114
  .SS Command Substitutions
96
115
  .TP
97
116
  .B %name
@@ -1,3 +1,3 @@
1
1
  module Filecon
2
- VERSION = "0.1.1"
2
+ VERSION = "0.2.0"
3
3
  end
data/lib/filecon.rb CHANGED
@@ -1,7 +1,3 @@
1
- require "fileutils"
2
- require "open3"
3
- require "yaml"
4
-
5
1
  begin
6
2
  require_relative "../../typr/lib/typr"
7
3
  rescue LoadError
@@ -10,54 +6,30 @@ end
10
6
 
11
7
  require_relative "filecon/version"
12
8
 
13
- module Filecon
14
- PATH = File.join(ENV["HOME"], ".config/filecon/")
15
-
16
- def self.dir_history_path
17
- File.join(PATH, "dir_history")
18
- end
19
-
20
- def self.cmd_history_path
21
- File.join(PATH, "cmd_history")
22
- end
23
-
24
- def self.setup_config
25
- FileUtils.mkpath PATH unless File.exist? PATH
26
- FileUtils.cp File.join(__dir__, "../config.yaml"), PATH unless File.exist? File.join(PATH, "config.yaml")
27
- FileUtils.touch dir_history_path
28
- FileUtils.touch cmd_history_path
29
- end
30
-
31
- def self.deep_symbolize_keys(obj)
32
- case obj
33
- when Hash
34
- obj.each_with_object({}) { |(k, v), h| h[k.to_sym] = deep_symbolize_keys(v) }
35
- when Array
36
- obj.map { |v| deep_symbolize_keys(v) }
37
- else
38
- obj
39
- end
40
- end
9
+ class Filecon::App < Typr::Browser
10
+ include Typr
41
11
 
42
- def self.config
43
- config_path = File.join(PATH, "config.yaml")
44
- if File.exist?(config_path)
45
- deep_symbolize_keys(YAML.safe_load(File.read(config_path), permitted_classes: [Symbol]))
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
46
22
  else
47
- {}
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
48
27
  end
49
28
  end
50
- end
51
29
 
52
- class Filecon::App < Typr::Browser
53
- include Typr
54
-
55
- def cd dir, append=false; super
56
- File.write Filecon.dir_history_path,
57
- @dir_history[0..Filecon.config["max_history"]].join($/)
58
- return
59
- end
30
+ def execute; cd ENV["PATH"].split(":"); add_path_column end
60
31
 
32
+ #switch_view
61
33
  def switch_to id=nil
62
34
  return unless id
63
35
  id = @views.keys[id] if id.is_a? Integer
@@ -66,9 +38,13 @@ class Filecon::App < Typr::Browser
66
38
  data = @data.map.with_index{ |row, id| row + view.values.map{ |cmd|
67
39
  next unless cmd.is_a?(String)
68
40
  built = build cmd, id
69
- io = built[0] == ?! ?
70
- StringIO.new( eval( built[1..-1] ).to_s ) :
71
- 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
72
48
  io.read(width).to_s.scrub.gsub(/[\n\t]/, ?|)
73
49
  }.compact }
74
50
  @sequence = [ NAME ] + view.keys.map{ |head|
@@ -76,51 +52,79 @@ class Filecon::App < Typr::Browser
76
52
  clear
77
53
  self << data
78
54
  @current = id
55
+ @view = id
79
56
  end
80
57
 
81
- def open_file id=nil, default=false
82
- return unless id ||= @selected[:rows].empty? ?
83
- @user.ask( open:[self, :row] ) : @selected[:rows]
84
- types = if id.is_a? Array
58
+ def open_file id=nil, default=false, types=nil
59
+ unless id || types
60
+ return unless id ||= @selected[:rows].empty? ?
61
+ @user.ask( open:[self, :row, NAME] ) : @selected[:rows]
62
+ end
63
+ types ||= if id.is_a? Array
85
64
  eval id.map{ |id| @data[id].values_at(SUBTYPE,TYPE).to_s }.join(?&)
86
65
  else @data[id].values_at(SUBTYPE, TYPE)
87
- end - [??] + [:default]
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
88
70
 
89
- commands = @commands.values_at( *types.map(&:to_sym) ).map(&:to_a).flatten 1
90
- cmd = if default; commands.first.last
71
+ commands = @commands.values_at( *types ).map(&:to_a).flatten( 1 ).uniq
72
+ pair = if default; commands.first
91
73
  else move left+1,top+1
92
- if id.is_a?(Array); show "#{id.count.to_s} files: (#{
74
+ if id.is_a?(Array); draw "#{id.count.to_s} files: (#{
93
75
  types.join ?, })".ljust width
94
76
  else print id end
95
- 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,
96
80
  input:commands, border: :light, format: [:min, :max],
97
- borders: { top: nil }, alternate: false,
98
- colors: {columns: [:green, :red] } ).pick( :row )
99
- 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 ]
84
+ end
85
+ cmd = build pair.last, id, pair.first
86
+ return unless cmd
87
+ if cmd[0] == ?|
88
+ display = cmd[1..-1]
89
+ record :cmd, cmd
90
+ io = run( cmd )
91
+ output = io.read.to_s.scrub.strip
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
96
+ else
97
+ record :cmd, cmd unless cmd[0..2] == '!cd'
98
+ run( cmd )
99
+ show
100
100
  end
101
- cmd = build cmd, id
102
- if cmd.start_with? ?|
103
- fork { io = run( cmd, true )
104
- until io.eof?
105
- @user.draw io.readline.strip.scrub
106
- sleep 0.1
107
- end
108
- }
109
- else run( cmd, cmd[0..2] != '!cd' ) end
110
- draw
111
101
  end
112
102
 
113
- 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)}:" : ""
114
107
  cmd = ""
115
108
  while str[/\%(name|dir|file|str)/]
116
- @user.draw( cmd += $` )
109
+ cmd += $`
117
110
  case $&
118
- when '%name'; cmd += ?"+( id.is_a?( Array ) ? id.map{ |id|
119
- @directory + @data[id][NAME] }.join('" "') :
120
- @directory + @data[id][NAME] )+?"
121
- when '%dir'; cmd += ?"+(pick :directory)+?"
122
- when '%file'; cmd += ?"+(pick :file)+?"
123
- when '%str'; cmd += Typr.read :line
111
+ when '%name'; cmd += ?"+( id.nil? ? @directory :
112
+ id.is_a?( Array ) ? id.map{ |id|
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
124
128
  end
125
129
  str = str[($`+$&).size..-1]
126
130
  end
@@ -128,46 +132,207 @@ class Filecon::App < Typr::Browser
128
132
  return cmd
129
133
  end
130
134
 
131
- def run cmd=nil, store=false
135
+ def run cmd=nil
132
136
  return unless cmd
133
- cmd = @cmd_history[cmd] if cmd.is_a? Integer
137
+ cmd = @cmd_history[cmd][1] if cmd.is_a? Integer
134
138
  case cmd[0]
135
139
  when ?! then io = StringIO.new eval( cmd[1..-1] ).to_s
136
140
  when ?| then io = open( cmd + " 2>&1" )
137
141
  when ?+ then io = open( ?| + @term +' '+ cmd[1..-1] + " 2>&1" )
138
142
  when ?= then Text.new(top: 2, right:-1, bottom:-2, border: :light,
139
143
  input:open( ?|+cmd[1..-1]+" 2>&1" ) ).pick :none
140
- end
141
- if store
142
- @cmd_history.unshift cmd
143
- @cmd_history.uniq!
144
- File.write Filecon.cmd_history_path, @cmd_history[0..Filecon.config["max_history"]].join($/)
144
+ else io = open( ?| + @term +' '+ cmd + " 2>&1" )
145
145
  end
146
146
  return io
147
147
  end
148
148
 
149
- def cmd_history; run popup( { input: @cmd_history, align:[:left], left:left,
150
- 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
282
+ end
283
+
284
+ def help
285
+ info = "\nFILECON - a terminal file manager\n\n" \
286
+ "HINTS: press a number (1-9,0) to open that file with its default action.\n" \
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" \
297
+ "CONFIG: ~/.config/filecon/config.yaml\n\n" \
298
+ "KEYBINDINGS:\n\n" + @user.bindings.join(?\n)
299
+ Text.new( input: info, top:2, left:3, right: -3, bottom: -3, header: "HELP",
300
+ colors: { header: [:black,:white], border:[:white,:grey10] },
301
+ border: :light ).pick :none
151
302
  end
152
303
 
153
304
  def initialize args={}
154
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
155
314
  @user.bindings.insert 1, "[o]pen"
156
- @user.bindings.insert 7, "[c]ommands"
157
- @user.draw
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
158
320
  end
159
321
 
322
+ def open_current; open_file nil, false, ["directory", "inode"] - [??] + ["default"] end
323
+
160
324
  def fly
161
325
  loop do
162
- draw
326
+ show
163
327
  draw_hints :rows
164
- key = Typr.read :key
165
- @user.reset
328
+ key = Typr.read_key
329
+ next unless key
166
330
  case key
167
331
  when KEY_ESCAPE; exit
168
332
  else
169
333
  id = send( key )
170
- open_file id, true if id.is_a? Integer
334
+ open_file( id, true) if id.is_a? Integer
335
+ @user.reset
171
336
  end
172
337
  end
173
338
  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.1
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-26 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: []