file_tree_visualizer 0.1.0 → 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: 487ea44c103965a0bf633fc0d618c50fb4a0c7d8ddbbee53b61f4d99a959d3f9
4
- data.tar.gz: 89a961a923ab5ae1a9f83ada18f1951905e5ccde53219de8ba83fe5bd34b14f1
3
+ metadata.gz: b43d301f84619b7a620a0cbcef1d135d4906073efaecf03b737b43572ebd6d89
4
+ data.tar.gz: 65de782e8d5094d981f0bedd23e6307d1b6cd4eedebd6b8bbb412a9b303e330e
5
5
  SHA512:
6
- metadata.gz: fd1eddbd5b1ae2b077e521f8ddd09dad953c12679aac80da81253cb18673a2f300d5146f86e244da85888b4872e59e823174c7a35e47f58aaa76cfa859e23cea
7
- data.tar.gz: 7e91afaea1510c5c3ea03695666a1edac1390bd1be55c15c5bd9a6092bdc10a35f981bce7289f0a9a1e05ef5f0823c84a4da54161929a42868150dda92d90729
6
+ metadata.gz: f6a58da21e94d26fd62fa5a05bc5d0946a16525c187f021e7184b010515051f346b2c8786067879c278c34ca3abfbe61dc416f1a2bd170201654c71d477bffd0
7
+ data.tar.gz: ba3cb73876856ee460c66f1b34a61f1965cb6deda2021f52dba5904a8500b995e8cd4c28fd91bb1f388462a88c2d62e381024e94d1430dfaf591555344c674a8
data/README.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # file_tree_visualizer
2
2
 
3
+ [![CI](https://github.com/JohnBidwellB/file-tree-visualizer/actions/workflows/release.yml/badge.svg)](https://github.com/JohnBidwellB/file-tree-visualizer/actions/workflows/release.yml)
4
+ [![RubyGems](https://badge.fury.io/rb/file_tree_visualizer.svg)](https://rubygems.org/gems/file_tree_visualizer)
5
+
3
6
  Ruby gem to scan file trees from the terminal and generate a fully offline interactive HTML report with the items that consume the most disk space.
4
7
 
5
8
  ## What it does
@@ -30,7 +33,7 @@ file-tree-visualizer /path/to/scan -o report.html
30
33
  Install a specific version:
31
34
 
32
35
  ```bash
33
- gem install file_tree_visualizer -v 0.1.0
36
+ gem install file_tree_visualizer -v 0.2.0
34
37
  ```
35
38
 
36
39
  ## Local development install
@@ -38,7 +41,7 @@ gem install file_tree_visualizer -v 0.1.0
38
41
  ```bash
39
42
  bundle install
40
43
  gem build file_tree_visualizer.gemspec
41
- gem install ./file_tree_visualizer-0.1.0.gem
44
+ gem install ./file_tree_visualizer-0.2.0.gem
42
45
  file-tree-visualizer /path/to/scan -o report.html
43
46
  ```
44
47
 
@@ -55,6 +58,10 @@ During scanning, an interactive terminal shows a progress bar with processed fil
55
58
  -t, --top N Number of largest files to keep in ranking (default: 200)
56
59
  --follow-symlinks Follow symbolic links
57
60
  --no-progress Disable progress bar
61
+ --exclude PATTERNS Exclude patterns (comma-separated or repeated)
62
+ --summary Print summary to terminal
63
+ --json FILE Export full result as JSON
64
+ --csv FILE Export top files as CSV
58
65
  --lang LANG Language for CLI and report (en|es)
59
66
  -h, --help Show help
60
67
 
@@ -64,12 +71,62 @@ Language resolution priority:
64
71
  - fallback to English (`en`)
65
72
  ```
66
73
 
74
+ ## Common workflows
75
+
76
+ Summary only (no HTML file generated unless `--output` is explicitly provided):
77
+
78
+ ```bash
79
+ file-tree-visualizer --summary
80
+ ```
81
+
82
+ Summary + HTML report:
83
+
84
+ ```bash
85
+ file-tree-visualizer . --summary --output report.html
86
+ ```
87
+
88
+ Exclude directories and files by pattern:
89
+
90
+ ```bash
91
+ file-tree-visualizer . --exclude "node_modules,.git,*.log" --exclude "tmp/**"
92
+ ```
93
+
94
+ Export JSON and CSV in one run:
95
+
96
+ ```bash
97
+ file-tree-visualizer . --summary --json scan.json --csv top-files.csv
98
+ ```
99
+
100
+ ## Contributing
101
+
102
+ 1. Create a feature branch from `main`.
103
+ 2. Implement your change and run tests locally.
104
+ 3. Push your branch and open a Pull Request.
105
+ 4. Wait for CI to pass and at least one approval.
106
+
107
+ ```bash
108
+ git checkout -b feat/short-description
109
+ bundle exec rake test
110
+ git push origin feat/short-description
111
+ ```
112
+
113
+ Branch policy:
114
+ - For collaborators, `main` is protected and requires PR + CI + approval.
115
+ - Repository admins can bypass these rules when needed.
116
+
67
117
  ## Automatic release to RubyGems
68
118
 
69
- - This repository publishes the gem automatically when pushing a tag with format `v*` (for example `v0.0.4`).
119
+ - This repository publishes the gem automatically when pushing a tag with format `v*` (for example `v0.2.0`).
70
120
  - Before using it, configure the `RUBYGEMS_API_KEY` secret in GitHub Actions.
121
+ - You can also trigger the workflow manually from GitHub Actions (`workflow_dispatch`).
122
+
123
+ ```bash
124
+ git tag v0.2.0
125
+ git push origin v0.2.0
126
+ ```
127
+
128
+ Reusable release notes template:
71
129
 
72
130
  ```bash
73
- git tag v0.0.4
74
- git push origin v0.0.4
131
+ gh release create v0.2.0 --title "v0.2.0" --notes-file .github/release-template.md
75
132
  ```
@@ -5,8 +5,8 @@ require_relative 'lib/file_tree_visualizer/version'
5
5
  Gem::Specification.new do |spec|
6
6
  spec.name = 'file_tree_visualizer'
7
7
  spec.version = FileTreeVisualizer::VERSION
8
- spec.authors = ['jbidwell1']
9
- spec.email = ['devnull@example.com']
8
+ spec.authors = ['John Bidwell']
9
+ spec.email = ['johnbidwellb@gmail.com']
10
10
 
11
11
  spec.summary = 'CLI Ruby tool to analyze file sizes and generate an interactive HTML report.'
12
12
  spec.description = 'Scans a directory recursively, computes file/directory sizes, and generates an HTML report rendered with React.'
@@ -14,7 +14,8 @@ Gem::Specification.new do |spec|
14
14
  spec.license = 'MIT'
15
15
  spec.required_ruby_version = '>= 2.7'
16
16
 
17
- spec.metadata['source_code_uri'] = spec.homepage
17
+ spec.metadata['source_code_uri'] = "#{spec.homepage}/tree/main"
18
+ spec.metadata['bug_tracker_uri'] = "#{spec.homepage}/issues"
18
19
  spec.metadata['changelog_uri'] = "#{spec.homepage}/blob/main/CHANGELOG.md"
19
20
 
20
21
  spec.files = Dir.chdir(__dir__) do
@@ -30,4 +31,5 @@ Gem::Specification.new do |spec|
30
31
  spec.bindir = 'bin'
31
32
  spec.executables = ['file-tree-visualizer']
32
33
  spec.require_paths = ['lib']
34
+ spec.add_dependency 'csv', '~> 3.3'
33
35
  end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'optparse'
4
+ require 'csv'
5
+ require 'json'
4
6
 
5
7
  module FileTreeVisualizer
6
8
  class CLI
@@ -24,22 +26,30 @@ module FileTreeVisualizer
24
26
  t(locale, 'cli.errors.invalid_directory', path: expanded_path)
25
27
  end
26
28
 
27
- scanner = Scanner.new(top_limit: options[:top], follow_symlinks: options[:follow_symlinks])
29
+ scanner = Scanner.new(
30
+ top_limit: options[:top],
31
+ follow_symlinks: options[:follow_symlinks],
32
+ excludes: options[:exclude_patterns]
33
+ )
28
34
  progress = ProgressRenderer.new(enabled: options[:progress], locale: locale)
29
35
  callback = options[:progress] ? progress.method(:render) : nil
30
36
  result = scanner.scan(expanded_path, progress: callback)
31
37
  progress.finish
32
- report = ReportBuilder.new(result: result, source_path: expanded_path, locale: locale).build
33
-
34
- output_path = File.expand_path(options[:output])
35
- File.write(output_path, report)
36
-
37
- puts t(locale, 'cli.messages.report_generated', path: output_path)
38
- puts t(locale, 'cli.messages.scanned',
39
- directories: result.stats[:directories],
40
- files: result.stats[:files],
41
- skipped: result.stats[:skipped])
42
- puts t(locale, 'cli.messages.total_size', size: format_bytes(result.stats[:bytes]))
38
+ generated_at = Time.now.utc
39
+ output_path = maybe_write_report(options, expanded_path, result, locale, generated_at)
40
+ json_path = write_json_export(options[:json_output], expanded_path, result, locale, generated_at)
41
+ csv_path = write_csv_export(options[:csv_output], result)
42
+ print_summary(expanded_path, result, locale) if options[:summary]
43
+ puts t(locale, 'cli.messages.report_generated', path: output_path) if output_path
44
+ puts t(locale, 'cli.messages.json_generated', path: json_path) if json_path
45
+ puts t(locale, 'cli.messages.csv_generated', path: csv_path) if csv_path
46
+ unless options[:summary]
47
+ puts t(locale, 'cli.messages.scanned',
48
+ directories: result.stats[:directories],
49
+ files: result.stats[:files],
50
+ skipped: result.stats[:skipped])
51
+ end
52
+ puts t(locale, 'cli.messages.total_size', size: format_bytes(result.stats[:bytes])) unless options[:summary]
43
53
  0
44
54
  rescue OptionParser::ParseError, ArgumentError => e
45
55
  locale = error_locale
@@ -133,6 +143,11 @@ module FileTreeVisualizer
133
143
  top: Scanner::DEFAULT_TOP_LIMIT,
134
144
  follow_symlinks: false,
135
145
  progress: true,
146
+ summary: false,
147
+ output_given: false,
148
+ json_output: nil,
149
+ csv_output: nil,
150
+ exclude_patterns: [],
136
151
  lang: nil,
137
152
  help: false
138
153
  }
@@ -164,6 +179,7 @@ module FileTreeVisualizer
164
179
 
165
180
  opts.on('-o', '--output FILE', t(locale, 'cli.output_option', default: DEFAULT_OUTPUT)) do |value|
166
181
  options[:output] = value
182
+ options[:output_given] = true
167
183
  end
168
184
 
169
185
  opts.on('-t', '--top N', Integer, t(locale, 'cli.top_option', default: Scanner::DEFAULT_TOP_LIMIT)) do |value|
@@ -178,6 +194,22 @@ module FileTreeVisualizer
178
194
  options[:progress] = false
179
195
  end
180
196
 
197
+ opts.on('--exclude PATTERNS', t(locale, 'cli.exclude_option')) do |value|
198
+ options[:exclude_patterns].concat(split_patterns(value))
199
+ end
200
+
201
+ opts.on('--summary', t(locale, 'cli.summary_option')) do
202
+ options[:summary] = true
203
+ end
204
+
205
+ opts.on('--json FILE', t(locale, 'cli.json_option')) do |value|
206
+ options[:json_output] = value
207
+ end
208
+
209
+ opts.on('--csv FILE', t(locale, 'cli.csv_option')) do |value|
210
+ options[:csv_output] = value
211
+ end
212
+
181
213
  opts.on('--lang LANG', t(locale, 'cli.lang_option')) do |value|
182
214
  options[:lang] = value
183
215
  end
@@ -198,6 +230,10 @@ module FileTreeVisualizer
198
230
  nil
199
231
  end
200
232
 
233
+ def split_patterns(value)
234
+ value.to_s.split(',').map(&:strip).reject(&:empty?)
235
+ end
236
+
201
237
  def t(locale, key, **vars)
202
238
  I18n.t(locale, key, **vars)
203
239
  end
@@ -221,5 +257,72 @@ module FileTreeVisualizer
221
257
  precision = value < 10 ? 2 : 1
222
258
  format("%.#{precision}f %s", value, units[unit_index])
223
259
  end
260
+
261
+ def maybe_write_report(options, source_path, result, locale, generated_at)
262
+ should_write = !options[:summary] || options[:output_given]
263
+ return nil unless should_write
264
+
265
+ report = ReportBuilder.new(
266
+ result: result,
267
+ source_path: source_path,
268
+ locale: locale,
269
+ generated_at: generated_at
270
+ ).build
271
+ output_path = File.expand_path(options[:output])
272
+ File.write(output_path, report)
273
+ output_path
274
+ end
275
+
276
+ def write_json_export(target_path, source_path, result, locale, generated_at)
277
+ return nil unless target_path
278
+
279
+ output_path = File.expand_path(target_path)
280
+ payload = {
281
+ source_path: source_path,
282
+ generated_at: generated_at.iso8601,
283
+ locale: locale,
284
+ version: FileTreeVisualizer::VERSION,
285
+ stats: result.stats,
286
+ root: result.root,
287
+ top_files: result.top_files
288
+ }
289
+ File.write(output_path, JSON.pretty_generate(payload))
290
+ output_path
291
+ end
292
+
293
+ def write_csv_export(target_path, result)
294
+ return nil unless target_path
295
+
296
+ output_path = File.expand_path(target_path)
297
+ CSV.open(output_path, 'w') do |csv|
298
+ csv << %w[rank path name size_bytes size_human]
299
+ result.top_files.each_with_index do |item, index|
300
+ csv << [index + 1, item[:path], item[:name], item[:size], format_bytes(item[:size])]
301
+ end
302
+ end
303
+ output_path
304
+ end
305
+
306
+ def print_summary(source_path, result, locale)
307
+ puts t(locale, 'cli.summary.heading')
308
+ puts t(locale, 'cli.summary.source', path: source_path)
309
+ puts t(locale, 'cli.summary.total_size', size: format_bytes(result.stats[:bytes]))
310
+ puts t(locale, 'cli.summary.files', count: result.stats[:files])
311
+ puts t(locale, 'cli.summary.directories', count: result.stats[:directories])
312
+ puts t(locale, 'cli.summary.skipped', count: result.stats[:skipped])
313
+ puts t(locale, 'cli.summary.top_files')
314
+
315
+ if result.top_files.empty?
316
+ puts " #{t(locale, 'cli.summary.none')}"
317
+ return
318
+ end
319
+
320
+ result.top_files.each_with_index do |item, index|
321
+ puts format(' %<rank>d. %<size>s %<path>s',
322
+ rank: index + 1,
323
+ size: format_bytes(item[:size]),
324
+ path: item[:path])
325
+ end
326
+ end
224
327
  end
225
328
  end
@@ -13,10 +13,16 @@ module FileTreeVisualizer
13
13
  'top_option' => 'Number of largest files in ranking (default: %<default>s)',
14
14
  'follow_symlinks_option' => 'Follow symbolic links during scan',
15
15
  'no_progress_option' => 'Disable progress bar',
16
+ 'exclude_option' => 'Exclude patterns (comma-separated or repeated)',
17
+ 'summary_option' => 'Print terminal summary',
18
+ 'json_option' => 'Export scan result as JSON',
19
+ 'csv_option' => 'Export top files as CSV',
16
20
  'lang_option' => 'Language for CLI and report (en|es)',
17
21
  'help_option' => 'Show this help',
18
22
  'messages' => {
19
23
  'report_generated' => 'Report generated at: %<path>s',
24
+ 'json_generated' => 'JSON exported at: %<path>s',
25
+ 'csv_generated' => 'CSV exported at: %<path>s',
20
26
  'scanned' => 'Scanned: %<directories>s directories, %<files>s files, %<skipped>s skipped.',
21
27
  'total_size' => 'Total size: %<size>s'
22
28
  },
@@ -30,6 +36,16 @@ module FileTreeVisualizer
30
36
  'progress' => {
31
37
  'preparing' => 'Preparing scan %<spinner>s Files:%<files>s Dirs:%<dirs>s',
32
38
  'scanning' => 'Scanning [%<bar>s] %<percent>s%% Files:%<pf>s/%<tf>s Dirs:%<pd>s/%<td>s'
39
+ },
40
+ 'summary' => {
41
+ 'heading' => 'Summary',
42
+ 'source' => 'Source: %<path>s',
43
+ 'total_size' => 'Total size: %<size>s',
44
+ 'files' => 'Files: %<count>s',
45
+ 'directories' => 'Directories: %<count>s',
46
+ 'skipped' => 'Skipped entries: %<count>s',
47
+ 'top_files' => 'Top files:',
48
+ 'none' => '(none)'
33
49
  }
34
50
  },
35
51
  'report' => {
@@ -72,10 +88,16 @@ module FileTreeVisualizer
72
88
  'top_option' => 'Cantidad de archivos pesados en ranking (default: %<default>s)',
73
89
  'follow_symlinks_option' => 'Sigue enlaces simbólicos durante el escaneo',
74
90
  'no_progress_option' => 'Desactiva la barra de progreso',
91
+ 'exclude_option' => 'Excluye patrones (separados por coma o repetidos)',
92
+ 'summary_option' => 'Imprime resumen en terminal',
93
+ 'json_option' => 'Exporta resultado del escaneo como JSON',
94
+ 'csv_option' => 'Exporta top de archivos como CSV',
75
95
  'lang_option' => 'Idioma para CLI y reporte (en|es)',
76
96
  'help_option' => 'Muestra esta ayuda',
77
97
  'messages' => {
78
98
  'report_generated' => 'Reporte generado en: %<path>s',
99
+ 'json_generated' => 'JSON exportado en: %<path>s',
100
+ 'csv_generated' => 'CSV exportado en: %<path>s',
79
101
  'scanned' => 'Escaneado: %<directories>s directorios, %<files>s archivos, %<skipped>s omitidos.',
80
102
  'total_size' => 'Tamano total: %<size>s'
81
103
  },
@@ -89,6 +111,16 @@ module FileTreeVisualizer
89
111
  'progress' => {
90
112
  'preparing' => 'Preparando escaneo %<spinner>s Arch:%<files>s Dir:%<dirs>s',
91
113
  'scanning' => 'Escaneando [%<bar>s] %<percent>s%% Arch:%<pf>s/%<tf>s Dir:%<pd>s/%<td>s'
114
+ },
115
+ 'summary' => {
116
+ 'heading' => 'Resumen',
117
+ 'source' => 'Origen: %<path>s',
118
+ 'total_size' => 'Tamano total: %<size>s',
119
+ 'files' => 'Archivos: %<count>s',
120
+ 'directories' => 'Directorios: %<count>s',
121
+ 'skipped' => 'Entradas omitidas: %<count>s',
122
+ 'top_files' => 'Top archivos:',
123
+ 'none' => '(ninguno)'
92
124
  }
93
125
  },
94
126
  'report' => {
@@ -5,9 +5,10 @@ module FileTreeVisualizer
5
5
  DEFAULT_TOP_LIMIT = 200
6
6
  Result = Struct.new(:root, :top_files, :stats, keyword_init: true)
7
7
 
8
- def initialize(top_limit: DEFAULT_TOP_LIMIT, follow_symlinks: false)
8
+ def initialize(top_limit: DEFAULT_TOP_LIMIT, follow_symlinks: false, excludes: [])
9
9
  @top_limit = top_limit
10
10
  @follow_symlinks = follow_symlinks
11
+ @excludes = Array(excludes).map(&:to_s).reject(&:empty?)
11
12
  reset!
12
13
  end
13
14
 
@@ -16,6 +17,8 @@ module FileTreeVisualizer
16
17
  expanded_path = File.expand_path(path.to_s)
17
18
  raise ArgumentError, "Path is not a directory: #{expanded_path}" unless File.directory?(expanded_path)
18
19
 
20
+ @root_path = expanded_path
21
+
19
22
  @progress_callback = progress
20
23
  notify_counting_progress(force: true)
21
24
  totals = count_totals(expanded_path)
@@ -57,6 +60,7 @@ module FileTreeVisualizer
57
60
  @processed_directories = 0
58
61
  @counted_files = 0
59
62
  @counted_directories = 0
63
+ @root_path = nil
60
64
  end
61
65
 
62
66
  def scan_directory(path)
@@ -78,6 +82,11 @@ module FileTreeVisualizer
78
82
  entries = Dir.children(path)
79
83
  entries.each do |entry_name|
80
84
  child_path = File.join(path, entry_name)
85
+ if excluded_path?(child_path)
86
+ @skipped_count += 1
87
+ next
88
+ end
89
+
81
90
  process_child_node(child_path, entry_name, node)
82
91
  rescue StandardError
83
92
  @skipped_count += 1
@@ -142,6 +151,7 @@ module FileTreeVisualizer
142
151
  entries = Dir.children(path)
143
152
  entries.each do |entry_name|
144
153
  child_path = File.join(path, entry_name)
154
+ next if excluded_path?(child_path)
145
155
 
146
156
  begin
147
157
  if File.symlink?(child_path) && !@follow_symlinks
@@ -165,6 +175,22 @@ module FileTreeVisualizer
165
175
  totals
166
176
  end
167
177
 
178
+ def excluded_path?(path)
179
+ return false if @excludes.empty?
180
+
181
+ basename = File.basename(path)
182
+ relative = relative_path(path)
183
+ flags = File::FNM_PATHNAME | File::FNM_DOTMATCH | File::FNM_EXTGLOB
184
+
185
+ @excludes.any? do |pattern|
186
+ File.fnmatch?(pattern, basename, flags) || File.fnmatch?(pattern, relative, flags)
187
+ end
188
+ end
189
+
190
+ def relative_path(path)
191
+ path.delete_prefix("#{@root_path}/")
192
+ end
193
+
168
194
  def notify_progress
169
195
  return unless @progress_callback
170
196
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FileTreeVisualizer
4
- VERSION = '0.1.0'
4
+ VERSION = '0.2.0'
5
5
  end
metadata CHANGED
@@ -1,19 +1,33 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: file_tree_visualizer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
- - jbidwell1
7
+ - John Bidwell
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
  date: 2026-04-04 00:00:00.000000000 Z
12
- dependencies: []
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: csv
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.3'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.3'
13
27
  description: Scans a directory recursively, computes file/directory sizes, and generates
14
28
  an HTML report rendered with React.
15
29
  email:
16
- - devnull@example.com
30
+ - johnbidwellb@gmail.com
17
31
  executables:
18
32
  - file-tree-visualizer
19
33
  extensions: []
@@ -34,7 +48,8 @@ homepage: https://github.com/JohnBidwellB/file-tree-visualizer
34
48
  licenses:
35
49
  - MIT
36
50
  metadata:
37
- source_code_uri: https://github.com/JohnBidwellB/file-tree-visualizer
51
+ source_code_uri: https://github.com/JohnBidwellB/file-tree-visualizer/tree/main
52
+ bug_tracker_uri: https://github.com/JohnBidwellB/file-tree-visualizer/issues
38
53
  changelog_uri: https://github.com/JohnBidwellB/file-tree-visualizer/blob/main/CHANGELOG.md
39
54
  post_install_message:
40
55
  rdoc_options: []