filecon 0.1.2 → 0.2.1

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: d62cae7fca7f8ef2348b79ac2df27edfafbc5da7ada9523a2fad5b38de27e5a9
4
+ data.tar.gz: 20fe942d398a0dd68e27a5cdc5b40d16d296e33399327494aaa608d847401e47
5
5
  SHA512:
6
- metadata.gz: e0e7cb9f038a8f91adabda6258643dd9e2a11557b471740b2adb7fa42a9f93a0a027a62a659719e5370de463974083d4e5f5f3ee509ae782def11620624b2936
7
- data.tar.gz: bcdc1bb5b7dfb36fe9bb43c4d8a50db175081d45aa548796e5482204056d4ae3e0d41fae0401ec547f32bd25ed840921c221e2c485d98a108285e5d21c0b5b81
6
+ metadata.gz: 1d2ac9423eecd422db9ac518ba264ef56b3f9d17823950f648e56eff3de0dd08326daba18bfd8eeeba51d76e072a060f4be5c6542a50c709623adac370f080f0
7
+ data.tar.gz: 198c5fa607c36cbc86808d42e3b0a806d83832ba5432827c871526d8ebaebaaa8cc3864f7a486e586b00ce2e983086e81184d08fe993f93ca2fa58fd7b0c9fd6
data/README.md CHANGED
@@ -1,14 +1,33 @@
1
+ ![Filecon](logo_alpha_small.png)
2
+
1
3
  # Filecon
2
4
 
3
- A terminal-based file manager built on the [Typr](https://codeberg.org/typr/typr) terminal UI toolkit.
5
+ ## Description
6
+
7
+ Filecon is a terminal-based file manager built on the [Typr](https://codeberg.org/typr/typr) terminal UI toolkit. It packs everything a file manager should do into a keyboard-driven terminal program: every file, command, and directory is reachable in a few keystrokes, and the most common operations take the fewest key presses.
8
+
9
+ Filecon is designed to be **intuitive**, **complete**, **powerful**, and the **fastest possible** file manager. The whole interface is built around that principle:
10
+
11
+ - **Hint navigation** — every visible row carries a number hint (1–9, 0); pressing it acts on that row immediately, no cursor movement needed.
12
+ - **Live search in popups** — command and history popups open in `/` search mode; type to distill the list, then pick one of the few remaining results with a number.
13
+ - **Frequency-ordered history** — directory and command history are ranked by use count, so the most likely choice is always near the top.
14
+
15
+ Filecon's long-term goal is to become an **interactive shell**: a separate companion program will handle command building, and combined with Filecon the two will form a full interactive shell that competes with interactive CLI shells.
16
+
17
+ Filecon runs on Typr, whose self-contained terminal layer ships its own precompiled terminfo key table and MIME-type database in the gem — no ncurses or external tooling required — so Filecon works on any raw, ANSI-capable terminal (including Termux).
4
18
 
5
19
  ## Features
6
20
 
7
- - Directory browsing with MIME-type detection
8
- - Customizable views (content preview, file types, line counts, etc.)
9
- - Command execution with pipe support
10
- - Directory and command history
11
- - Configurable keybindings and colors
21
+ - **Directory browsing with MIME-type detection** — files are typed from a packaged MIME database and colored accordingly, with optional `file --mime-type` magic detection for unknowns
22
+ - **Fastest possible navigation** hint numbers (1–9, 0) open rows directly; TAB pages hints beyond the first ten
23
+ - **Default `[/]:search` in popups** — command and history popups open in search mode: start typing to distill the list down to the matches, then press a number hint to pick from the few remaining results. The listing itself searches the same way
24
+ - **Mouse support** click a row to open it with its default action; the mouse wheel scrolls the listing (pointer-only mode is planned)
25
+ - **Command execution modes** — commands run in one of four ways, chosen by prefix: `|` status line, `=` text viewer, `+` new terminal window, `!` Ruby eval; per-MIME-type command sets with `%name`, `%dir`, `%file`, `%str` substitution
26
+ - **Customizable column views** — view columns run shell commands with `%name` substitution (content preview, char/word/line counts, type info, disk usage, man-page descriptions)
27
+ - **Directory and command history with a frequency counter** — entries stored as `[count, entry]` pairs, most-used first then most-recent; `frequency_limit` controls how many frequent entries lead the list
28
+ - **Shell history import** — on first run, `~/.bash_history` and `~/.zsh_history` are validated, alias-resolved, and merged into the command history; directory paths referenced by those commands are collected into the directory history
29
+ - **Power browsing** — recursive directory listing, `execute` ($PATH browser), marking, filtering, sorting, and switchable column views
30
+ - **Fully documented configuration** — a self-documenting `config.yaml` covers colors, commands, views, history, and terminal settings
12
31
 
13
32
  ## Installation
14
33
 
@@ -16,12 +35,6 @@ A terminal-based file manager built on the [Typr](https://codeberg.org/typr/typr
16
35
  gem install filecon
17
36
  ```
18
37
 
19
- Or add to your Gemfile:
20
-
21
- ```ruby
22
- gem "filecon", "~> 0.1"
23
- ```
24
-
25
38
  ## Usage
26
39
 
27
40
  ```bash
@@ -39,12 +52,14 @@ filecon [directory]
39
52
  **Actions**
40
53
  - `o` — Open list of all commands for selected file
41
54
  - `c` — Command history
55
+ - `x` — List all executables from $PATH directories
42
56
  - `h` — Help
43
57
  - `v` — Views
44
58
  - `s` — Sort view
45
59
  - `f` — Filter view
46
60
  - `r` — Reset view
47
- - `R` — Toggle recursive listing
61
+ - `R` — Recursive file listing
62
+ - `/` — Search the listing by name
48
63
 
49
64
  **Marking**
50
65
  - `m` — Mark file
@@ -61,32 +76,46 @@ filecon [directory]
61
76
  - Enter — Select / confirm
62
77
  - Esc — Quit
63
78
 
79
+ ### Mouse
80
+
81
+ Click a row to open it with its default action; the mouse wheel scrolls the listing. Mouse reporting is enabled automatically when Filecon starts.
82
+
83
+ ### Popups and search
84
+
85
+ Command and history popups open in `/` search mode. Type a few characters to **distill** the list down to the matching entries; when only a handful remain, press the number hint of the one you want. Large lists can be narrowed in a keystroke or two. The main listing searches the same way when you press `/`.
86
+
64
87
  ### Configuration
65
88
 
66
- Config is stored at `~/.config/filecon/config.yaml` (YAML format) and includes:
89
+ Config is stored at `~/.config/filecon/config.yaml` (YAML). On first run the bundled, fully documented `config.yaml` is copied there. It includes:
67
90
 
68
- - Color settings (named colors, 8-bit palette, RGB triplets)
69
- - Command definitions per MIME type
70
- - View configurations
91
+ - Color settings (named colors, 8-bit palette, RGB triplets) — including the `colors.commands` pair used by the command picker and status line
92
+ - Command definitions per MIME type, with execution-mode prefixes
93
+ - View configurations (columns that run shell commands)
71
94
  - History limits and terminal settings
72
95
 
73
- **Command prefixes:**
96
+ State and history files live under `~/.local/share/filecon/`.
97
+
98
+ **Command execution modes:**
74
99
 
75
- - `|` — pipe output to status line
100
+ - `|` — pipe output to the status line
76
101
  - `+` — run in a new terminal window
77
102
  - `=` — display output in the text viewer
78
103
  - `!` — evaluate as Ruby code
79
104
 
80
105
  **Substitution variables:** `%name` (file path), `%dir` (directory picker), `%file` (file picker), `%str` (text input)
81
106
 
82
- See `filecon(1)` or the installed `config.yaml` for full details.
107
+ See `filecon(1)` for the full manual.
108
+
109
+ ## Roadmap
110
+
111
+ - **Pointer-only mode** — full mouse-driven operation of the entire interface, building on the click/wheel mouse support already present
112
+ - **Interactive shell** — a separate command-building program that combines with Filecon into an interactive shell competing with interactive CLI shells
83
113
 
84
114
  ## Development
85
115
 
86
116
  ```bash
87
117
  git clone https://codeberg.org/typr/filecon
88
118
  cd filecon
89
- bundle install
90
119
  rake test
91
120
  ```
92
121
 
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,59 @@ 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"
109
-
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
+ play: "+mpv %name"
152
+
110
153
  text:
111
154
  view: "+moor %name"
112
155
  edit: "+micro %name"
113
- count: "|wc -l %name"
156
+ count: "|wc -l %name | cut -wf1"
114
157
 
115
158
  troff:
116
159
  manual: "+man %name"
117
160
 
118
161
  "x-ruby":
119
- run: "+ruby %name"
162
+ run: "|ruby %name"
163
+ run_terminal: "+ruby %name"
120
164
 
121
165
  html:
122
166
  terminal: "+cha %name"
123
167
  graphics: "|vimb %name"
124
168
 
125
169
  image:
126
- play: "|mpv %name"
170
+ view: "|pqiv %name"
127
171
 
128
172
  video:
129
173
  play: "|mpv %name"
@@ -134,12 +178,6 @@ commands:
134
178
  pdf:
135
179
  view: "|mupdf %name"
136
180
 
137
- "x-pie-executable":
138
- run: "|%name"
139
- run_terminal: "+%name"
140
- help: "=%name --help"
141
- manual: "+man %name"
142
-
143
181
  "x-python":
144
182
  run: "+python3 %name"
145
183
 
@@ -178,23 +216,23 @@ commands:
178
216
  view: "=cat %name | jq ."
179
217
 
180
218
  "zip":
181
- list: "|unzip -l %name"
219
+ list: "=unzip -l %name"
182
220
  extract: "|unzip %name -d %dir"
183
221
 
184
222
  "x-gzip":
185
- list: "|tar -tzf %name"
223
+ list: "=tar -tzf %name"
186
224
  extract: "|tar -xzf %name -C %dir"
187
225
 
188
226
  "x-bzip2":
189
- list: "|tar -tjf %name"
227
+ list: "=tar -tjf %name"
190
228
  extract: "|tar -xjf %name -C %dir"
191
229
 
192
230
  "x-xz":
193
- list: "|tar -tJf %name"
231
+ list: "=tar -tJf %name"
194
232
  extract: "|tar -xJf %name -C %dir"
195
233
 
196
234
  "x-tar":
197
- list: "|tar -tf %name"
235
+ list: "=tar -tf %name"
198
236
  extract: "|tar -xf %name -C %dir"
199
237
 
200
238
  "epub+zip":
@@ -219,10 +257,10 @@ commands:
219
257
  # - A string Command to execute (with %name substitution)
220
258
  # - A symbol Reference to a system field (e.g., :size, :type, :subtype)
221
259
  #
222
- # Prefix ! to evaluate as Ruby, prefix | for shell commands.
260
+ # Prefix ! to evaluate as Ruby
223
261
  #
224
262
  # Available system fields:
225
- # target, permissions, owner, group, size,
263
+ # target, permissions, executable, owner, group, size,
226
264
  # accessed, modified, created, type, subtype
227
265
  #
228
266
  # When a view is activated, its columns are appended to the default file
@@ -234,15 +272,18 @@ views:
234
272
  content:
235
273
  content: "head -n 50 %name"
236
274
 
237
- filetype:
275
+ types:
238
276
  type: true
239
277
  subtype: true
240
278
  description: "file -b %name"
241
279
 
280
+ disk_use:
281
+ du: "du -hs %name | cut -wf1"
282
+
242
283
  count:
243
- chars: "wc -m %name"
244
- words: "wc -w %name"
245
- lines: "wc -l %name"
284
+ chars: "wc -m %name | cut -wf1"
285
+ words: "wc -w %name | cut -wf1"
286
+ lines: "wc -l %name | cut -wf1"
246
287
 
247
288
  whatis:
248
- whatis: "!open('|whatis %name 2>&1').read.match(/[-:]\\s(.*)$/)[1]"
289
+ 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.1"
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,43 @@ 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"]
332
+ when Typr::Mouse
333
+ if key.wheel? and key.press?
334
+ send( key.wheel_up? ? @keymap[:up] : @keymap[:down] )
335
+ elsif key.press? and key.left?
336
+ id = hit( key.y, key.x )
337
+ open_file( id, true ) if id.is_a? Integer
338
+ end
339
+ @user.reset
149
340
  else
150
341
  id = send( key )
151
- open_file id, true if id.is_a? Integer
342
+ open_file( id, true) if id.is_a? Integer
152
343
  @user.reset
153
344
  end
154
345
  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.1
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.3'
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.3'
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: []