file_tree_visualizer 0.0.3 → 0.1.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 +4 -4
- data/Gemfile +2 -0
- data/README.md +54 -23
- data/Rakefile +8 -0
- data/file_tree_visualizer.gemspec +20 -20
- data/lib/file_tree_visualizer/cli.rb +80 -29
- data/lib/file_tree_visualizer/i18n.rb +173 -0
- data/lib/file_tree_visualizer/report_builder.rb +50 -20
- data/lib/file_tree_visualizer/scanner.rb +3 -3
- data/lib/file_tree_visualizer/version.rb +1 -1
- data/lib/file_tree_visualizer.rb +5 -4
- metadata +5 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 487ea44c103965a0bf633fc0d618c50fb4a0c7d8ddbbee53b61f4d99a959d3f9
|
|
4
|
+
data.tar.gz: 89a961a923ab5ae1a9f83ada18f1951905e5ccde53219de8ba83fe5bd34b14f1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fd1eddbd5b1ae2b077e521f8ddd09dad953c12679aac80da81253cb18673a2f300d5146f86e244da85888b4872e59e823174c7a35e47f58aaa76cfa859e23cea
|
|
7
|
+
data.tar.gz: 7e91afaea1510c5c3ea03695666a1edac1390bd1be55c15c5bd9a6092bdc10a35f981bce7289f0a9a1e05ef5f0823c84a4da54161929a42868150dda92d90729
|
data/Gemfile
CHANGED
data/README.md
CHANGED
|
@@ -1,44 +1,75 @@
|
|
|
1
1
|
# file_tree_visualizer
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
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
4
|
|
|
5
|
-
##
|
|
5
|
+
## What it does
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
- Ranking
|
|
13
|
-
-
|
|
7
|
+
- Recursively scans a directory.
|
|
8
|
+
- Computes cumulative size per directory and size per file.
|
|
9
|
+
- Generates an HTML report with:
|
|
10
|
+
- Expand/collapse file tree.
|
|
11
|
+
- Text and minimum-size filters.
|
|
12
|
+
- Ranking of the largest files.
|
|
13
|
+
- No external dependencies (works offline).
|
|
14
14
|
|
|
15
|
-
##
|
|
15
|
+
## Quick usage (without installing the gem)
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
ruby bin/file-tree-visualizer /
|
|
18
|
+
ruby bin/file-tree-visualizer /path/to/scan -o report.html
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
##
|
|
21
|
+
## Install from RubyGems
|
|
22
|
+
|
|
23
|
+
Gem page: https://rubygems.org/gems/file_tree_visualizer
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
gem install file_tree_visualizer
|
|
27
|
+
file-tree-visualizer /path/to/scan -o report.html
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Install a specific version:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
gem install file_tree_visualizer -v 0.1.0
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Local development install
|
|
22
37
|
|
|
23
38
|
```bash
|
|
24
39
|
bundle install
|
|
25
40
|
gem build file_tree_visualizer.gemspec
|
|
26
|
-
gem install ./file_tree_visualizer-0.0.
|
|
27
|
-
file-tree-visualizer /
|
|
41
|
+
gem install ./file_tree_visualizer-0.1.0.gem
|
|
42
|
+
file-tree-visualizer /path/to/scan -o report.html
|
|
28
43
|
```
|
|
29
44
|
|
|
30
|
-
##
|
|
45
|
+
## Options
|
|
31
46
|
|
|
32
47
|
```bash
|
|
33
|
-
file-tree-visualizer [
|
|
48
|
+
file-tree-visualizer [path] [options]
|
|
49
|
+
|
|
50
|
+
If no `path` is provided, the current directory is scanned by default.
|
|
34
51
|
|
|
35
|
-
|
|
52
|
+
During scanning, an interactive terminal shows a progress bar with processed files/directories vs discovered totals.
|
|
36
53
|
|
|
37
|
-
|
|
54
|
+
-o, --output FILE Output HTML file (default: file-tree-report.html)
|
|
55
|
+
-t, --top N Number of largest files to keep in ranking (default: 200)
|
|
56
|
+
--follow-symlinks Follow symbolic links
|
|
57
|
+
--no-progress Disable progress bar
|
|
58
|
+
--lang LANG Language for CLI and report (en|es)
|
|
59
|
+
-h, --help Show help
|
|
60
|
+
|
|
61
|
+
Language resolution priority:
|
|
62
|
+
- `--lang`
|
|
63
|
+
- `LANG` environment variable (for example `es_CL.UTF-8`)
|
|
64
|
+
- fallback to English (`en`)
|
|
65
|
+
```
|
|
38
66
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
67
|
+
## Automatic release to RubyGems
|
|
68
|
+
|
|
69
|
+
- This repository publishes the gem automatically when pushing a tag with format `v*` (for example `v0.0.4`).
|
|
70
|
+
- Before using it, configure the `RUBYGEMS_API_KEY` secret in GitHub Actions.
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
git tag v0.0.4
|
|
74
|
+
git push origin v0.0.4
|
|
44
75
|
```
|
data/Rakefile
CHANGED
|
@@ -1,33 +1,33 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative
|
|
3
|
+
require_relative 'lib/file_tree_visualizer/version'
|
|
4
4
|
|
|
5
5
|
Gem::Specification.new do |spec|
|
|
6
|
-
spec.name =
|
|
6
|
+
spec.name = 'file_tree_visualizer'
|
|
7
7
|
spec.version = FileTreeVisualizer::VERSION
|
|
8
|
-
spec.authors = [
|
|
9
|
-
spec.email = [
|
|
8
|
+
spec.authors = ['jbidwell1']
|
|
9
|
+
spec.email = ['devnull@example.com']
|
|
10
10
|
|
|
11
|
-
spec.summary =
|
|
12
|
-
spec.description =
|
|
13
|
-
spec.homepage =
|
|
14
|
-
spec.license =
|
|
15
|
-
spec.required_ruby_version =
|
|
11
|
+
spec.summary = 'CLI Ruby tool to analyze file sizes and generate an interactive HTML report.'
|
|
12
|
+
spec.description = 'Scans a directory recursively, computes file/directory sizes, and generates an HTML report rendered with React.'
|
|
13
|
+
spec.homepage = 'https://github.com/JohnBidwellB/file-tree-visualizer'
|
|
14
|
+
spec.license = 'MIT'
|
|
15
|
+
spec.required_ruby_version = '>= 2.7'
|
|
16
16
|
|
|
17
|
-
spec.metadata[
|
|
18
|
-
spec.metadata[
|
|
17
|
+
spec.metadata['source_code_uri'] = spec.homepage
|
|
18
|
+
spec.metadata['changelog_uri'] = "#{spec.homepage}/blob/main/CHANGELOG.md"
|
|
19
19
|
|
|
20
20
|
spec.files = Dir.chdir(__dir__) do
|
|
21
21
|
Dir[
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
'README.md',
|
|
23
|
+
'Gemfile',
|
|
24
|
+
'Rakefile',
|
|
25
|
+
'bin/*',
|
|
26
|
+
'lib/**/*.rb',
|
|
27
|
+
'file_tree_visualizer.gemspec'
|
|
28
28
|
]
|
|
29
29
|
end
|
|
30
|
-
spec.bindir =
|
|
31
|
-
spec.executables = [
|
|
32
|
-
spec.require_paths = [
|
|
30
|
+
spec.bindir = 'bin'
|
|
31
|
+
spec.executables = ['file-tree-visualizer']
|
|
32
|
+
spec.require_paths = ['lib']
|
|
33
33
|
end
|
|
@@ -1,42 +1,54 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
3
|
+
require 'optparse'
|
|
4
4
|
|
|
5
5
|
module FileTreeVisualizer
|
|
6
6
|
class CLI
|
|
7
|
-
DEFAULT_OUTPUT =
|
|
7
|
+
DEFAULT_OUTPUT = 'file-tree-report.html'
|
|
8
|
+
DEFAULT_LOCALE = 'en'
|
|
8
9
|
|
|
9
10
|
def initialize(argv)
|
|
10
11
|
@argv = argv
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
def run
|
|
14
|
-
options, parser = parse_options(@argv.dup)
|
|
15
|
+
options, parser, locale = parse_options(@argv.dup)
|
|
15
16
|
if options[:help]
|
|
16
17
|
puts parser
|
|
17
18
|
return 0
|
|
18
19
|
end
|
|
19
20
|
|
|
21
|
+
expanded_path = File.expand_path(options[:path])
|
|
22
|
+
unless File.directory?(expanded_path)
|
|
23
|
+
raise ArgumentError,
|
|
24
|
+
t(locale, 'cli.errors.invalid_directory', path: expanded_path)
|
|
25
|
+
end
|
|
26
|
+
|
|
20
27
|
scanner = Scanner.new(top_limit: options[:top], follow_symlinks: options[:follow_symlinks])
|
|
21
|
-
progress = ProgressRenderer.new(enabled: options[:progress])
|
|
28
|
+
progress = ProgressRenderer.new(enabled: options[:progress], locale: locale)
|
|
22
29
|
callback = options[:progress] ? progress.method(:render) : nil
|
|
23
|
-
result = scanner.scan(
|
|
30
|
+
result = scanner.scan(expanded_path, progress: callback)
|
|
24
31
|
progress.finish
|
|
25
|
-
report = ReportBuilder.new(result: result, source_path:
|
|
32
|
+
report = ReportBuilder.new(result: result, source_path: expanded_path, locale: locale).build
|
|
26
33
|
|
|
27
34
|
output_path = File.expand_path(options[:output])
|
|
28
35
|
File.write(output_path, report)
|
|
29
36
|
|
|
30
|
-
puts
|
|
31
|
-
puts
|
|
32
|
-
|
|
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]))
|
|
33
43
|
0
|
|
34
44
|
rescue OptionParser::ParseError, ArgumentError => e
|
|
45
|
+
locale = error_locale
|
|
35
46
|
warn e.message
|
|
36
|
-
warn
|
|
47
|
+
warn t(locale, 'cli.errors.use_help')
|
|
37
48
|
1
|
|
38
49
|
rescue StandardError => e
|
|
39
|
-
|
|
50
|
+
locale = error_locale
|
|
51
|
+
warn t(locale, 'cli.errors.unexpected', message: e.message)
|
|
40
52
|
1
|
|
41
53
|
end
|
|
42
54
|
|
|
@@ -45,9 +57,10 @@ module FileTreeVisualizer
|
|
|
45
57
|
class ProgressRenderer
|
|
46
58
|
BAR_WIDTH = 28
|
|
47
59
|
|
|
48
|
-
def initialize(io: $stdout, enabled: true)
|
|
60
|
+
def initialize(io: $stdout, enabled: true, locale: DEFAULT_LOCALE)
|
|
49
61
|
@io = io
|
|
50
62
|
@enabled = enabled && io.tty?
|
|
63
|
+
@locale = locale
|
|
51
64
|
@last_draw_at = nil
|
|
52
65
|
@has_rendered = false
|
|
53
66
|
@spinner_index = 0
|
|
@@ -82,7 +95,7 @@ module FileTreeVisualizer
|
|
|
82
95
|
def render_counting(data)
|
|
83
96
|
spinner = %w[| / - \\][@spinner_index % 4]
|
|
84
97
|
@spinner_index += 1
|
|
85
|
-
@io.print(format("\
|
|
98
|
+
@io.print(format("\r#{I18n.t(@locale, 'cli.progress.preparing')}",
|
|
86
99
|
spinner: spinner,
|
|
87
100
|
files: data.fetch(:discovered_files, 0),
|
|
88
101
|
dirs: data.fetch(:discovered_directories, 0)))
|
|
@@ -102,9 +115,9 @@ module FileTreeVisualizer
|
|
|
102
115
|
empty = BAR_WIDTH - filled
|
|
103
116
|
percent = progress_ratio * 100
|
|
104
117
|
|
|
105
|
-
@io.print(format("\
|
|
106
|
-
bar: (
|
|
107
|
-
percent: percent,
|
|
118
|
+
@io.print(format("\r#{I18n.t(@locale, 'cli.progress.scanning')}",
|
|
119
|
+
bar: ('#' * filled) + ('-' * empty),
|
|
120
|
+
percent: format('%6.2f', percent),
|
|
108
121
|
pf: processed_files,
|
|
109
122
|
tf: total_files,
|
|
110
123
|
pd: processed_directories,
|
|
@@ -113,46 +126,84 @@ module FileTreeVisualizer
|
|
|
113
126
|
end
|
|
114
127
|
|
|
115
128
|
def parse_options(argv)
|
|
129
|
+
locale = I18n.resolve_locale(cli_value: extract_lang_argument(argv), env_lang: ENV['LANG'])
|
|
116
130
|
options = {
|
|
117
131
|
path: Dir.pwd,
|
|
118
132
|
output: DEFAULT_OUTPUT,
|
|
119
133
|
top: Scanner::DEFAULT_TOP_LIMIT,
|
|
120
134
|
follow_symlinks: false,
|
|
121
135
|
progress: true,
|
|
136
|
+
lang: nil,
|
|
122
137
|
help: false
|
|
123
138
|
}
|
|
124
139
|
|
|
125
|
-
parser =
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
140
|
+
parser = build_parser(locale, options)
|
|
141
|
+
remaining = parser.parse(argv)
|
|
142
|
+
|
|
143
|
+
if options[:lang] && !I18n.supported_locale?(options[:lang])
|
|
144
|
+
raise ArgumentError, t(locale,
|
|
145
|
+
'cli.errors.invalid_lang',
|
|
146
|
+
lang: options[:lang],
|
|
147
|
+
supported: I18n::SUPPORTED_LOCALES.join(', '))
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
locale = I18n.resolve_locale(cli_value: options[:lang], env_lang: ENV['LANG'])
|
|
151
|
+
parser = build_parser(locale, options)
|
|
152
|
+
|
|
153
|
+
options[:path] = File.expand_path(remaining.first || options[:path])
|
|
154
|
+
raise ArgumentError, t(locale, 'cli.errors.invalid_top') unless options[:top].positive?
|
|
155
|
+
|
|
156
|
+
[options, parser, locale]
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def build_parser(locale, options)
|
|
160
|
+
OptionParser.new do |opts|
|
|
161
|
+
opts.banner = t(locale, 'cli.banner')
|
|
162
|
+
opts.separator ''
|
|
163
|
+
opts.separator t(locale, 'cli.options_header')
|
|
129
164
|
|
|
130
|
-
opts.on(
|
|
165
|
+
opts.on('-o', '--output FILE', t(locale, 'cli.output_option', default: DEFAULT_OUTPUT)) do |value|
|
|
131
166
|
options[:output] = value
|
|
132
167
|
end
|
|
133
168
|
|
|
134
|
-
opts.on(
|
|
169
|
+
opts.on('-t', '--top N', Integer, t(locale, 'cli.top_option', default: Scanner::DEFAULT_TOP_LIMIT)) do |value|
|
|
135
170
|
options[:top] = value
|
|
136
171
|
end
|
|
137
172
|
|
|
138
|
-
opts.on(
|
|
173
|
+
opts.on('--follow-symlinks', t(locale, 'cli.follow_symlinks_option')) do
|
|
139
174
|
options[:follow_symlinks] = true
|
|
140
175
|
end
|
|
141
176
|
|
|
142
|
-
opts.on(
|
|
177
|
+
opts.on('--no-progress', t(locale, 'cli.no_progress_option')) do
|
|
143
178
|
options[:progress] = false
|
|
144
179
|
end
|
|
145
180
|
|
|
146
|
-
opts.on(
|
|
181
|
+
opts.on('--lang LANG', t(locale, 'cli.lang_option')) do |value|
|
|
182
|
+
options[:lang] = value
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
opts.on('-h', '--help', t(locale, 'cli.help_option')) do
|
|
147
186
|
options[:help] = true
|
|
148
187
|
end
|
|
149
188
|
end
|
|
189
|
+
end
|
|
150
190
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
191
|
+
def extract_lang_argument(argv)
|
|
192
|
+
argv.each_with_index do |arg, index|
|
|
193
|
+
return arg.split('=', 2).last if arg.start_with?('--lang=')
|
|
194
|
+
next unless arg == '--lang'
|
|
195
|
+
|
|
196
|
+
return argv[index + 1]
|
|
197
|
+
end
|
|
198
|
+
nil
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def t(locale, key, **vars)
|
|
202
|
+
I18n.t(locale, key, **vars)
|
|
203
|
+
end
|
|
154
204
|
|
|
155
|
-
|
|
205
|
+
def error_locale
|
|
206
|
+
I18n.resolve_locale(cli_value: extract_lang_argument(@argv), env_lang: ENV['LANG'])
|
|
156
207
|
end
|
|
157
208
|
|
|
158
209
|
def format_bytes(bytes)
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FileTreeVisualizer
|
|
4
|
+
module I18n
|
|
5
|
+
SUPPORTED_LOCALES = %w[en es].freeze
|
|
6
|
+
|
|
7
|
+
TRANSLATIONS = {
|
|
8
|
+
'en' => {
|
|
9
|
+
'cli' => {
|
|
10
|
+
'banner' => 'Usage: file-tree-visualizer [path] [options]',
|
|
11
|
+
'options_header' => 'Options:',
|
|
12
|
+
'output_option' => 'Output HTML file (default: %<default>s)',
|
|
13
|
+
'top_option' => 'Number of largest files in ranking (default: %<default>s)',
|
|
14
|
+
'follow_symlinks_option' => 'Follow symbolic links during scan',
|
|
15
|
+
'no_progress_option' => 'Disable progress bar',
|
|
16
|
+
'lang_option' => 'Language for CLI and report (en|es)',
|
|
17
|
+
'help_option' => 'Show this help',
|
|
18
|
+
'messages' => {
|
|
19
|
+
'report_generated' => 'Report generated at: %<path>s',
|
|
20
|
+
'scanned' => 'Scanned: %<directories>s directories, %<files>s files, %<skipped>s skipped.',
|
|
21
|
+
'total_size' => 'Total size: %<size>s'
|
|
22
|
+
},
|
|
23
|
+
'errors' => {
|
|
24
|
+
'invalid_top' => '--top must be greater than 0',
|
|
25
|
+
'invalid_lang' => "Unsupported language '%<lang>s'. Supported values: %<supported>s",
|
|
26
|
+
'invalid_directory' => 'Path is not a directory: %<path>s',
|
|
27
|
+
'use_help' => 'Use --help to see available options.',
|
|
28
|
+
'unexpected' => 'Unexpected error: %<message>s'
|
|
29
|
+
},
|
|
30
|
+
'progress' => {
|
|
31
|
+
'preparing' => 'Preparing scan %<spinner>s Files:%<files>s Dirs:%<dirs>s',
|
|
32
|
+
'scanning' => 'Scanning [%<bar>s] %<percent>s%% Files:%<pf>s/%<tf>s Dirs:%<pd>s/%<td>s'
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
'report' => {
|
|
36
|
+
'page_title' => 'File Report',
|
|
37
|
+
'header_title' => 'Disk usage report',
|
|
38
|
+
'meta_source_prefix' => 'Source: ',
|
|
39
|
+
'meta_generated_prefix' => 'Generated: ',
|
|
40
|
+
'stats' => {
|
|
41
|
+
'total_size' => 'Total size',
|
|
42
|
+
'files' => 'Files',
|
|
43
|
+
'directories' => 'Directories',
|
|
44
|
+
'skipped' => 'Skipped entries'
|
|
45
|
+
},
|
|
46
|
+
'filters' => {
|
|
47
|
+
'search_label' => 'Search by name/path',
|
|
48
|
+
'search_placeholder' => 'eg: node_modules, .mp4, backup',
|
|
49
|
+
'min_size_label' => 'Minimum size (MB)',
|
|
50
|
+
'min_size_hint' => 'Filters tree nodes and ranking by minimum size.'
|
|
51
|
+
},
|
|
52
|
+
'panels' => {
|
|
53
|
+
'tree' => 'File tree',
|
|
54
|
+
'top_files' => 'Top largest files'
|
|
55
|
+
},
|
|
56
|
+
'table' => {
|
|
57
|
+
'index' => '#',
|
|
58
|
+
'size' => 'Size',
|
|
59
|
+
'path' => 'Path'
|
|
60
|
+
},
|
|
61
|
+
'empty' => {
|
|
62
|
+
'tree' => 'No results with current filters.',
|
|
63
|
+
'top_files' => 'No files to show with current filters.'
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
'es' => {
|
|
68
|
+
'cli' => {
|
|
69
|
+
'banner' => 'Uso: file-tree-visualizer [ruta] [opciones]',
|
|
70
|
+
'options_header' => 'Opciones:',
|
|
71
|
+
'output_option' => 'Archivo HTML de salida (default: %<default>s)',
|
|
72
|
+
'top_option' => 'Cantidad de archivos pesados en ranking (default: %<default>s)',
|
|
73
|
+
'follow_symlinks_option' => 'Sigue enlaces simbólicos durante el escaneo',
|
|
74
|
+
'no_progress_option' => 'Desactiva la barra de progreso',
|
|
75
|
+
'lang_option' => 'Idioma para CLI y reporte (en|es)',
|
|
76
|
+
'help_option' => 'Muestra esta ayuda',
|
|
77
|
+
'messages' => {
|
|
78
|
+
'report_generated' => 'Reporte generado en: %<path>s',
|
|
79
|
+
'scanned' => 'Escaneado: %<directories>s directorios, %<files>s archivos, %<skipped>s omitidos.',
|
|
80
|
+
'total_size' => 'Tamano total: %<size>s'
|
|
81
|
+
},
|
|
82
|
+
'errors' => {
|
|
83
|
+
'invalid_top' => '--top debe ser mayor que 0',
|
|
84
|
+
'invalid_lang' => "Idioma no soportado '%<lang>s'. Valores permitidos: %<supported>s",
|
|
85
|
+
'invalid_directory' => 'La ruta no es un directorio: %<path>s',
|
|
86
|
+
'use_help' => 'Usa --help para ver las opciones disponibles.',
|
|
87
|
+
'unexpected' => 'Error inesperado: %<message>s'
|
|
88
|
+
},
|
|
89
|
+
'progress' => {
|
|
90
|
+
'preparing' => 'Preparando escaneo %<spinner>s Arch:%<files>s Dir:%<dirs>s',
|
|
91
|
+
'scanning' => 'Escaneando [%<bar>s] %<percent>s%% Arch:%<pf>s/%<tf>s Dir:%<pd>s/%<td>s'
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
'report' => {
|
|
95
|
+
'page_title' => 'Reporte de archivos',
|
|
96
|
+
'header_title' => 'Reporte de espacio en disco',
|
|
97
|
+
'meta_source_prefix' => 'Origen: ',
|
|
98
|
+
'meta_generated_prefix' => 'Generado: ',
|
|
99
|
+
'stats' => {
|
|
100
|
+
'total_size' => 'Tamano total',
|
|
101
|
+
'files' => 'Archivos',
|
|
102
|
+
'directories' => 'Directorios',
|
|
103
|
+
'skipped' => 'Entradas omitidas'
|
|
104
|
+
},
|
|
105
|
+
'filters' => {
|
|
106
|
+
'search_label' => 'Buscar por nombre/ruta',
|
|
107
|
+
'search_placeholder' => 'ej: node_modules, .mp4, backup',
|
|
108
|
+
'min_size_label' => 'Tamano minimo (MB)',
|
|
109
|
+
'min_size_hint' => 'Filtra nodos y ranking por tamano minimo.'
|
|
110
|
+
},
|
|
111
|
+
'panels' => {
|
|
112
|
+
'tree' => 'Arbol de archivos',
|
|
113
|
+
'top_files' => 'Top archivos mas pesados'
|
|
114
|
+
},
|
|
115
|
+
'table' => {
|
|
116
|
+
'index' => '#',
|
|
117
|
+
'size' => 'Tamano',
|
|
118
|
+
'path' => 'Ruta'
|
|
119
|
+
},
|
|
120
|
+
'empty' => {
|
|
121
|
+
'tree' => 'Sin resultados con los filtros actuales.',
|
|
122
|
+
'top_files' => 'Sin archivos para mostrar con los filtros actuales.'
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}.freeze
|
|
127
|
+
|
|
128
|
+
module_function
|
|
129
|
+
|
|
130
|
+
def supported_locale?(value)
|
|
131
|
+
SUPPORTED_LOCALES.include?(normalize_locale(value))
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def resolve_locale(cli_value: nil, env_lang: nil)
|
|
135
|
+
normalize_locale(cli_value) || normalize_locale(env_lang) || 'en'
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def normalize_locale(value)
|
|
139
|
+
return nil if value.nil?
|
|
140
|
+
|
|
141
|
+
normalized = value.to_s.strip.downcase
|
|
142
|
+
return nil if normalized.empty?
|
|
143
|
+
|
|
144
|
+
return 'es' if normalized.start_with?('es')
|
|
145
|
+
return 'en' if normalized.start_with?('en')
|
|
146
|
+
|
|
147
|
+
nil
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def t(locale, key, **vars)
|
|
151
|
+
template = lookup(locale, key) || lookup('en', key)
|
|
152
|
+
return key unless template.is_a?(String)
|
|
153
|
+
|
|
154
|
+
return template if vars.empty?
|
|
155
|
+
|
|
156
|
+
template % vars.transform_keys(&:to_sym)
|
|
157
|
+
rescue KeyError
|
|
158
|
+
template
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def lookup(locale, key)
|
|
162
|
+
current = TRANSLATIONS[locale]
|
|
163
|
+
return nil unless current
|
|
164
|
+
|
|
165
|
+
key.to_s.split('.').each do |part|
|
|
166
|
+
return nil unless current.is_a?(Hash)
|
|
167
|
+
|
|
168
|
+
current = current[part]
|
|
169
|
+
end
|
|
170
|
+
current
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
@@ -1,25 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'time'
|
|
5
5
|
|
|
6
6
|
module FileTreeVisualizer
|
|
7
7
|
class ReportBuilder
|
|
8
|
-
def initialize(result:, source_path:, generated_at: Time.now.utc)
|
|
8
|
+
def initialize(result:, source_path:, generated_at: Time.now.utc, locale: 'en')
|
|
9
9
|
@result = result
|
|
10
10
|
@source_path = File.expand_path(source_path.to_s)
|
|
11
11
|
@generated_at = generated_at
|
|
12
|
+
@locale = I18n.resolve_locale(cli_value: locale)
|
|
12
13
|
end
|
|
13
14
|
|
|
14
15
|
def build
|
|
15
|
-
payload_json = JSON.generate(payload).gsub(
|
|
16
|
+
payload_json = JSON.generate(payload).gsub('</', '<\\/')
|
|
17
|
+
translations_json = JSON.generate(translation_payload).gsub('</', '<\\/')
|
|
16
18
|
<<~HTML
|
|
17
19
|
<!doctype html>
|
|
18
|
-
<html lang="
|
|
20
|
+
<html lang="#{@locale}">
|
|
19
21
|
<head>
|
|
20
22
|
<meta charset="utf-8" />
|
|
21
23
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
22
|
-
<title
|
|
24
|
+
<title>#{t('report.page_title')}</title>
|
|
23
25
|
<style>
|
|
24
26
|
:root {
|
|
25
27
|
color-scheme: dark;
|
|
@@ -215,6 +217,7 @@ module FileTreeVisualizer
|
|
|
215
217
|
<script>
|
|
216
218
|
(function () {
|
|
217
219
|
const data = JSON.parse(document.getElementById("scan-data").textContent);
|
|
220
|
+
const i18n = #{translations_json};
|
|
218
221
|
const app = document.getElementById("app");
|
|
219
222
|
|
|
220
223
|
const state = {
|
|
@@ -256,31 +259,31 @@ module FileTreeVisualizer
|
|
|
256
259
|
'<div class="container">',
|
|
257
260
|
' <header class="header">',
|
|
258
261
|
' <div>',
|
|
259
|
-
' <h1>
|
|
262
|
+
' <h1>' + i18n.headerTitle + '</h1>',
|
|
260
263
|
' <div class="meta" id="meta-source"></div>',
|
|
261
264
|
' <div class="meta" id="meta-generated"></div>',
|
|
262
265
|
' </div>',
|
|
263
266
|
' </header>',
|
|
264
267
|
' <section class="stats">',
|
|
265
|
-
' <div class="stat"><div class="stat-label">
|
|
266
|
-
' <div class="stat"><div class="stat-label">
|
|
267
|
-
' <div class="stat"><div class="stat-label">
|
|
268
|
-
' <div class="stat"><div class="stat-label">
|
|
268
|
+
' <div class="stat"><div class="stat-label">' + i18n.statsTotalSize + '</div><div class="stat-value" id="stat-bytes"></div></div>',
|
|
269
|
+
' <div class="stat"><div class="stat-label">' + i18n.statsFiles + '</div><div class="stat-value" id="stat-files"></div></div>',
|
|
270
|
+
' <div class="stat"><div class="stat-label">' + i18n.statsDirectories + '</div><div class="stat-value" id="stat-directories"></div></div>',
|
|
271
|
+
' <div class="stat"><div class="stat-label">' + i18n.statsSkipped + '</div><div class="stat-value" id="stat-skipped"></div></div>',
|
|
269
272
|
' </section>',
|
|
270
273
|
' <section class="filters">',
|
|
271
|
-
' <label>
|
|
272
|
-
' <label>
|
|
274
|
+
' <label>' + i18n.searchLabel + '<input id="query-input" type="text" placeholder="' + i18n.searchPlaceholder + '" /></label>',
|
|
275
|
+
' <label>' + i18n.minSizeLabel + '<input id="min-size-input" type="number" min="0" value="0" /><span class="hint">' + i18n.minSizeHint + '</span></label>',
|
|
273
276
|
' </section>',
|
|
274
277
|
' <section class="layout">',
|
|
275
278
|
' <article class="panel">',
|
|
276
|
-
' <h2
|
|
279
|
+
' <h2>' + i18n.treeTitle + '</h2>',
|
|
277
280
|
' <div class="panel-body" id="tree-panel"></div>',
|
|
278
281
|
' </article>',
|
|
279
282
|
' <article class="panel">',
|
|
280
|
-
' <h2>
|
|
283
|
+
' <h2>' + i18n.topFilesTitle + '</h2>',
|
|
281
284
|
' <div class="panel-body">',
|
|
282
285
|
' <table>',
|
|
283
|
-
' <thead><tr><th
|
|
286
|
+
' <thead><tr><th>' + i18n.tableIndex + '</th><th>' + i18n.tableSize + '</th><th>' + i18n.tablePath + '</th></tr></thead>',
|
|
284
287
|
' <tbody id="top-files-body"></tbody>',
|
|
285
288
|
' </table>',
|
|
286
289
|
' </div>',
|
|
@@ -294,8 +297,8 @@ module FileTreeVisualizer
|
|
|
294
297
|
const treePanel = document.getElementById("tree-panel");
|
|
295
298
|
const topFilesBody = document.getElementById("top-files-body");
|
|
296
299
|
|
|
297
|
-
document.getElementById("meta-source").textContent =
|
|
298
|
-
document.getElementById("meta-generated").textContent =
|
|
300
|
+
document.getElementById("meta-source").textContent = i18n.metaSourcePrefix + data.source_path;
|
|
301
|
+
document.getElementById("meta-generated").textContent = i18n.metaGeneratedPrefix + data.generated_at;
|
|
299
302
|
document.getElementById("stat-bytes").textContent = formatBytes(data.stats.bytes);
|
|
300
303
|
document.getElementById("stat-files").textContent = formatNumber(data.stats.files);
|
|
301
304
|
document.getElementById("stat-directories").textContent = formatNumber(data.stats.directories);
|
|
@@ -365,7 +368,7 @@ module FileTreeVisualizer
|
|
|
365
368
|
if (!nodeMatches(data.root, activeFilters.query, activeFilters.minBytes)) {
|
|
366
369
|
const empty = document.createElement("div");
|
|
367
370
|
empty.className = "empty-state";
|
|
368
|
-
empty.textContent =
|
|
371
|
+
empty.textContent = i18n.emptyTree;
|
|
369
372
|
treePanel.appendChild(empty);
|
|
370
373
|
return;
|
|
371
374
|
}
|
|
@@ -384,7 +387,7 @@ module FileTreeVisualizer
|
|
|
384
387
|
const cell = document.createElement("td");
|
|
385
388
|
cell.colSpan = 3;
|
|
386
389
|
cell.className = "empty-state";
|
|
387
|
-
cell.textContent =
|
|
390
|
+
cell.textContent = i18n.emptyTopFiles;
|
|
388
391
|
emptyRow.appendChild(cell);
|
|
389
392
|
topFilesBody.appendChild(emptyRow);
|
|
390
393
|
return;
|
|
@@ -439,5 +442,32 @@ module FileTreeVisualizer
|
|
|
439
442
|
top_files: @result.top_files
|
|
440
443
|
}
|
|
441
444
|
end
|
|
445
|
+
|
|
446
|
+
def translation_payload
|
|
447
|
+
{
|
|
448
|
+
headerTitle: t('report.header_title'),
|
|
449
|
+
metaSourcePrefix: t('report.meta_source_prefix'),
|
|
450
|
+
metaGeneratedPrefix: t('report.meta_generated_prefix'),
|
|
451
|
+
statsTotalSize: t('report.stats.total_size'),
|
|
452
|
+
statsFiles: t('report.stats.files'),
|
|
453
|
+
statsDirectories: t('report.stats.directories'),
|
|
454
|
+
statsSkipped: t('report.stats.skipped'),
|
|
455
|
+
searchLabel: t('report.filters.search_label'),
|
|
456
|
+
searchPlaceholder: t('report.filters.search_placeholder'),
|
|
457
|
+
minSizeLabel: t('report.filters.min_size_label'),
|
|
458
|
+
minSizeHint: t('report.filters.min_size_hint'),
|
|
459
|
+
treeTitle: t('report.panels.tree'),
|
|
460
|
+
topFilesTitle: t('report.panels.top_files'),
|
|
461
|
+
tableIndex: t('report.table.index'),
|
|
462
|
+
tableSize: t('report.table.size'),
|
|
463
|
+
tablePath: t('report.table.path'),
|
|
464
|
+
emptyTree: t('report.empty.tree'),
|
|
465
|
+
emptyTopFiles: t('report.empty.top_files')
|
|
466
|
+
}
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
def t(key)
|
|
470
|
+
I18n.t(@locale, key)
|
|
471
|
+
end
|
|
442
472
|
end
|
|
443
473
|
end
|
|
@@ -14,7 +14,7 @@ module FileTreeVisualizer
|
|
|
14
14
|
def scan(path, progress: nil)
|
|
15
15
|
reset!
|
|
16
16
|
expanded_path = File.expand_path(path.to_s)
|
|
17
|
-
raise ArgumentError, "
|
|
17
|
+
raise ArgumentError, "Path is not a directory: #{expanded_path}" unless File.directory?(expanded_path)
|
|
18
18
|
|
|
19
19
|
@progress_callback = progress
|
|
20
20
|
notify_counting_progress(force: true)
|
|
@@ -66,7 +66,7 @@ module FileTreeVisualizer
|
|
|
66
66
|
node = {
|
|
67
67
|
name: directory_name,
|
|
68
68
|
path: path,
|
|
69
|
-
type:
|
|
69
|
+
type: 'directory',
|
|
70
70
|
size: 0,
|
|
71
71
|
children: []
|
|
72
72
|
}
|
|
@@ -106,7 +106,7 @@ module FileTreeVisualizer
|
|
|
106
106
|
file_node = {
|
|
107
107
|
name: entry_name,
|
|
108
108
|
path: child_path,
|
|
109
|
-
type:
|
|
109
|
+
type: 'file',
|
|
110
110
|
size: file_size
|
|
111
111
|
}
|
|
112
112
|
parent_node[:children] << file_node
|
data/lib/file_tree_visualizer.rb
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative
|
|
4
|
-
require_relative
|
|
5
|
-
require_relative
|
|
6
|
-
require_relative
|
|
3
|
+
require_relative 'file_tree_visualizer/version'
|
|
4
|
+
require_relative 'file_tree_visualizer/i18n'
|
|
5
|
+
require_relative 'file_tree_visualizer/scanner'
|
|
6
|
+
require_relative 'file_tree_visualizer/report_builder'
|
|
7
|
+
require_relative 'file_tree_visualizer/cli'
|
|
7
8
|
|
|
8
9
|
module FileTreeVisualizer
|
|
9
10
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: file_tree_visualizer
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0
|
|
4
|
+
version: 0.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- jbidwell1
|
|
@@ -26,15 +26,16 @@ files:
|
|
|
26
26
|
- file_tree_visualizer.gemspec
|
|
27
27
|
- lib/file_tree_visualizer.rb
|
|
28
28
|
- lib/file_tree_visualizer/cli.rb
|
|
29
|
+
- lib/file_tree_visualizer/i18n.rb
|
|
29
30
|
- lib/file_tree_visualizer/report_builder.rb
|
|
30
31
|
- lib/file_tree_visualizer/scanner.rb
|
|
31
32
|
- lib/file_tree_visualizer/version.rb
|
|
32
|
-
homepage: https://
|
|
33
|
+
homepage: https://github.com/JohnBidwellB/file-tree-visualizer
|
|
33
34
|
licenses:
|
|
34
35
|
- MIT
|
|
35
36
|
metadata:
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
source_code_uri: https://github.com/JohnBidwellB/file-tree-visualizer
|
|
38
|
+
changelog_uri: https://github.com/JohnBidwellB/file-tree-visualizer/blob/main/CHANGELOG.md
|
|
38
39
|
post_install_message:
|
|
39
40
|
rdoc_options: []
|
|
40
41
|
require_paths:
|