rtfm-filemanager 7.1.3 → 7.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 +4 -4
- data/README.md +16 -1
- data/bin/rtfm +167 -38
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 506054b29faa54cff3af682e83d26356aefec7f631e99f5777a09f7321045816
|
|
4
|
+
data.tar.gz: 8276bf6d6271fa8364dc03cfb81b106761eb68597464597bd21f1a728cfb07cf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 49917f6e6f31fe5d21575829565da7720fe7a76d9ea369451d4b4789c139971ee804bbcb1880f58585c987063b6552661c37a95715c9572fe71b355bb0f0f65a
|
|
7
|
+
data.tar.gz: '09254076afa868509902e490e417050e6dbdb09f3b2d47417ae197d1179ef314e013e6aa52465f5c2ee7a074559eecaa8c908478ad6a0e7a5b6e4e68c057a7a5'
|
data/README.md
CHANGED
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
# RTFM - Ruby Terminal File Manager
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
 [](https://badge.fury.io/rb/rtfm-filemanager)  
|
|
4
4
|
|
|
5
|
+
## Version 7.2
|
|
6
|
+
|
|
7
|
+
**BREAKING CHANGE** - Batch Operation Behavior:
|
|
8
|
+
|
|
9
|
+
Version 7.2 introduces a **breaking change** to how batch operations work with tagged items:
|
|
10
|
+
|
|
11
|
+
- **Previous behavior**: Operations affected tagged items + selected item (always)
|
|
12
|
+
- **New behavior**: Operations affect tagged items OR selected item (not both)
|
|
13
|
+
- When items ARE tagged → operate ONLY on tagged items
|
|
14
|
+
- When NO items are tagged → operate on selected item only
|
|
15
|
+
|
|
16
|
+
This provides consistent, predictable behavior across all operations including: delete, copy, move, symlink, bulk rename, change permissions, change ownership, and open.
|
|
17
|
+
|
|
18
|
+
**Migration**: If you want the selected item included in a batch operation, explicitly tag it first with 't'.
|
|
19
|
+
|
|
5
20
|
## Version 7.1
|
|
6
21
|
|
|
7
22
|
Version 7.1 adds comprehensive compressed archive viewing capabilities:
|
data/bin/rtfm
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
# get a great understanding of the code itself by simply sending
|
|
19
19
|
# or pasting this whole file into you favorite AI for coding with
|
|
20
20
|
# a prompt like this: "Help me understand every part of this code".
|
|
21
|
-
@version = '7.
|
|
21
|
+
@version = '7.2.0' # BREAKING: Batch operations now work on tagged OR selected, not both
|
|
22
22
|
|
|
23
23
|
# SAVE & STORE TERMINAL {{{1
|
|
24
24
|
ORIG_STTY = `stty -g`.chomp
|
|
@@ -1124,6 +1124,17 @@ def show_version # {{{3
|
|
|
1124
1124
|
end
|
|
1125
1125
|
|
|
1126
1126
|
def refresh_all # {{{3
|
|
1127
|
+
# Re-read terminal size to handle window resizing
|
|
1128
|
+
begin
|
|
1129
|
+
new_h, new_w = IO.console.winsize
|
|
1130
|
+
# Validate terminal size (minimum 10x20 to be usable)
|
|
1131
|
+
if new_h && new_w && new_h >= 10 && new_w >= 20
|
|
1132
|
+
@h, @w = new_h, new_w
|
|
1133
|
+
end
|
|
1134
|
+
rescue
|
|
1135
|
+
# Keep current size if we can't read new size
|
|
1136
|
+
end
|
|
1137
|
+
|
|
1127
1138
|
# Clear remote directory cache if in remote mode
|
|
1128
1139
|
if @remote_mode
|
|
1129
1140
|
@remote_files_cache = []
|
|
@@ -1635,6 +1646,10 @@ def undo_last_operation # {{{3
|
|
|
1635
1646
|
undo_link(operation)
|
|
1636
1647
|
when 'bulk_rename'
|
|
1637
1648
|
undo_bulk_rename(operation)
|
|
1649
|
+
when 'permissions'
|
|
1650
|
+
undo_permissions(operation)
|
|
1651
|
+
when 'ownership'
|
|
1652
|
+
undo_ownership(operation)
|
|
1638
1653
|
else
|
|
1639
1654
|
@pB.say("Unknown operation type: #{operation[:type]}".fg(196))
|
|
1640
1655
|
return
|
|
@@ -1727,7 +1742,7 @@ def undo_bulk_rename(operation) # {{{3
|
|
|
1727
1742
|
operation[:renames].reverse.each do |rename_info|
|
|
1728
1743
|
old_path = rename_info[:old_path]
|
|
1729
1744
|
new_path = rename_info[:new_path]
|
|
1730
|
-
|
|
1745
|
+
|
|
1731
1746
|
if File.exist?(new_path)
|
|
1732
1747
|
FileUtils.mv(new_path, old_path)
|
|
1733
1748
|
else
|
|
@@ -1736,6 +1751,38 @@ def undo_bulk_rename(operation) # {{{3
|
|
|
1736
1751
|
end
|
|
1737
1752
|
end
|
|
1738
1753
|
|
|
1754
|
+
def undo_permissions(operation) # {{{3
|
|
1755
|
+
# Restore old permissions
|
|
1756
|
+
operation[:items].each do |item_info|
|
|
1757
|
+
path = item_info[:path]
|
|
1758
|
+
old_mode = item_info[:mode]
|
|
1759
|
+
|
|
1760
|
+
if File.exist?(path)
|
|
1761
|
+
File.chmod(old_mode, path)
|
|
1762
|
+
else
|
|
1763
|
+
raise "Cannot undo permissions: #{path} not found"
|
|
1764
|
+
end
|
|
1765
|
+
end
|
|
1766
|
+
# Invalidate cache to show updated colors
|
|
1767
|
+
@dir_cache.delete_if { |key, _| key.start_with?("#{Dir.pwd}:") }
|
|
1768
|
+
@pL.update = true
|
|
1769
|
+
end
|
|
1770
|
+
|
|
1771
|
+
def undo_ownership(operation) # {{{3
|
|
1772
|
+
# Restore old ownership
|
|
1773
|
+
operation[:items].each do |item_info|
|
|
1774
|
+
path = item_info[:path]
|
|
1775
|
+
old_uid = item_info[:uid]
|
|
1776
|
+
old_gid = item_info[:gid]
|
|
1777
|
+
|
|
1778
|
+
if File.exist?(path)
|
|
1779
|
+
File.chown(old_uid, old_gid, path)
|
|
1780
|
+
else
|
|
1781
|
+
raise "Cannot undo ownership: #{path} not found"
|
|
1782
|
+
end
|
|
1783
|
+
end
|
|
1784
|
+
end
|
|
1785
|
+
|
|
1739
1786
|
# RECENTLY ACCESSED FILES {{{2
|
|
1740
1787
|
def track_file_access(file_path) # {{{3
|
|
1741
1788
|
return unless File.exist?(file_path)
|
|
@@ -1970,23 +2017,21 @@ end
|
|
|
1970
2017
|
|
|
1971
2018
|
# BULK RENAME {{{2
|
|
1972
2019
|
def bulk_rename # {{{3
|
|
1973
|
-
if
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
end
|
|
1977
|
-
|
|
2020
|
+
# Use tagged items if any exist, otherwise use selected item
|
|
2021
|
+
items = @tagged.empty? ? [@selected] : @tagged
|
|
2022
|
+
|
|
1978
2023
|
@pB.say("Bulk rename pattern: ".fg(156))
|
|
1979
2024
|
@pR.say(build_pattern_help)
|
|
1980
2025
|
@pR.update = false
|
|
1981
|
-
|
|
2026
|
+
|
|
1982
2027
|
pattern = @pCmd.ask('Pattern: ', '')
|
|
1983
2028
|
return if pattern.nil? || pattern.strip.empty?
|
|
1984
|
-
|
|
2029
|
+
|
|
1985
2030
|
# Parse and preview renames
|
|
1986
2031
|
rename_operations = []
|
|
1987
2032
|
errors = []
|
|
1988
|
-
|
|
1989
|
-
|
|
2033
|
+
|
|
2034
|
+
items.each do |file|
|
|
1990
2035
|
next unless File.exist?(file)
|
|
1991
2036
|
|
|
1992
2037
|
old_name = File.basename(file)
|
|
@@ -3243,9 +3288,11 @@ def delete_items # {{{3
|
|
|
3243
3288
|
prompt_text = @trash ? "Move to trash? (y/n)" : "⚠️ PERMANENTLY DELETE? (y/n)"
|
|
3244
3289
|
@pB.say(" #{prompt_text}".fg(action_color))
|
|
3245
3290
|
if getchr == 'y'
|
|
3291
|
+
# Use tagged items if any exist, otherwise use selected item
|
|
3292
|
+
items = @tagged.empty? ? [@selected] : @tagged
|
|
3246
3293
|
# Collect paths - include broken symlinks since they can be moved/deleted
|
|
3247
3294
|
# File.exist? returns false for broken symlinks, but File.symlink? still works
|
|
3248
|
-
paths =
|
|
3295
|
+
paths = items.select { |p| File.exist?(p) || File.symlink?(p) }
|
|
3249
3296
|
if paths.empty?
|
|
3250
3297
|
@pB.say("No valid items to #{action.downcase}".fg(196))
|
|
3251
3298
|
else
|
|
@@ -3279,14 +3326,20 @@ def delete_items # {{{3
|
|
|
3279
3326
|
# Cannot undo permanent deletion, so don't add to undo history
|
|
3280
3327
|
end
|
|
3281
3328
|
@tagged.clear
|
|
3282
|
-
|
|
3283
|
-
# Store the
|
|
3284
|
-
|
|
3285
|
-
|
|
3329
|
+
|
|
3330
|
+
# Store the currently selected item to restore selection after refresh
|
|
3331
|
+
previously_selected = @selected ? File.basename(@selected) : nil
|
|
3332
|
+
|
|
3286
3333
|
# Refresh the file list to reflect deletions
|
|
3287
3334
|
@pL.update = true
|
|
3288
3335
|
render # This will update @files and @selected
|
|
3289
|
-
|
|
3336
|
+
|
|
3337
|
+
# Try to restore the previous selection if it still exists
|
|
3338
|
+
if previously_selected && @files
|
|
3339
|
+
restored_index = @files.index(previously_selected)
|
|
3340
|
+
@index = restored_index if restored_index
|
|
3341
|
+
end
|
|
3342
|
+
|
|
3290
3343
|
# After render, check if we still have files
|
|
3291
3344
|
if @files.empty?
|
|
3292
3345
|
# No files left in directory - clear the preview
|
|
@@ -3323,8 +3376,11 @@ end
|
|
|
3323
3376
|
|
|
3324
3377
|
def change_ownership # {{{3
|
|
3325
3378
|
require 'etc'
|
|
3326
|
-
|
|
3327
|
-
|
|
3379
|
+
# Use tagged items if any exist, otherwise use selected item
|
|
3380
|
+
items = @tagged.empty? ? [@selected] : @tagged
|
|
3381
|
+
first_item = items.first
|
|
3382
|
+
gnm = Etc.getgrgid(File.stat(first_item).gid).name
|
|
3383
|
+
unm = Etc.getpwuid(File.stat(first_item).uid).name
|
|
3328
3384
|
ans = @pB.ask('Change ownership (user:group): ', "#{unm}:#{gnm}")
|
|
3329
3385
|
# Reset bottom pane after input
|
|
3330
3386
|
@pB.clear; @pB.update = true
|
|
@@ -3332,16 +3388,37 @@ def change_ownership # {{{3
|
|
|
3332
3388
|
user, group = ans.split(':')
|
|
3333
3389
|
uid = Etc.getpwnam(user).uid
|
|
3334
3390
|
gid = Etc.getgrnam(group).gid
|
|
3335
|
-
File.chown(uid, gid, @selected)
|
|
3336
|
-
@tagged.each { |t| File.chown(uid, gid, t) rescue nil }
|
|
3337
3391
|
if user == unm && group == gnm
|
|
3338
3392
|
@pB.say('No change in ownership')
|
|
3339
3393
|
else
|
|
3394
|
+
# Record old ownership for undo
|
|
3395
|
+
old_ownership = items.map do |item|
|
|
3396
|
+
stat = File.stat(item)
|
|
3397
|
+
{
|
|
3398
|
+
path: item,
|
|
3399
|
+
uid: stat.uid,
|
|
3400
|
+
gid: stat.gid,
|
|
3401
|
+
user: Etc.getpwuid(stat.uid).name,
|
|
3402
|
+
group: Etc.getgrgid(stat.gid).name
|
|
3403
|
+
}
|
|
3404
|
+
end
|
|
3405
|
+
|
|
3406
|
+
# Change ownership
|
|
3407
|
+
items.each { |item| File.chown(uid, gid, item) rescue nil }
|
|
3340
3408
|
@pB.say("Ownership changed to #{user}:#{group}")
|
|
3409
|
+
|
|
3410
|
+
# Add to undo history
|
|
3411
|
+
add_undo_operation({
|
|
3412
|
+
type: 'ownership',
|
|
3413
|
+
items: old_ownership,
|
|
3414
|
+
timestamp: Time.now
|
|
3415
|
+
})
|
|
3341
3416
|
end
|
|
3342
3417
|
end
|
|
3343
3418
|
|
|
3344
3419
|
def change_permissions # {{{3
|
|
3420
|
+
# Use tagged items if any exist, otherwise use selected item
|
|
3421
|
+
items = @tagged.empty? ? [@selected] : @tagged
|
|
3345
3422
|
# strip leading "-" off e.g. "-rwxr-xr-x" → "rwxr-xr-x"
|
|
3346
3423
|
default = @fileattr.split[1][1..]
|
|
3347
3424
|
ans = @pB.ask('Permissions: ', default)
|
|
@@ -3350,6 +3427,25 @@ def change_permissions # {{{3
|
|
|
3350
3427
|
return if ans.nil? || ans.strip.empty?
|
|
3351
3428
|
mode = if ans =~ /^\d{3}$/ # "755"
|
|
3352
3429
|
ans.to_i(8)
|
|
3430
|
+
elsif ans =~ /^[+-][rwx]+$/ # "+x", "-w", "+rw", etc.
|
|
3431
|
+
# Modify current permissions
|
|
3432
|
+
current = File.stat(items.first).mode & 0o777
|
|
3433
|
+
operation = ans[0]
|
|
3434
|
+
chars = ans[1..-1].chars
|
|
3435
|
+
# Calculate permission bits to add/remove
|
|
3436
|
+
bits = chars.map do |c|
|
|
3437
|
+
case c
|
|
3438
|
+
when 'r' then 0o444 # r for all (user, group, other)
|
|
3439
|
+
when 'w' then 0o222 # w for all
|
|
3440
|
+
when 'x' then 0o111 # x for all
|
|
3441
|
+
else 0
|
|
3442
|
+
end
|
|
3443
|
+
end.sum
|
|
3444
|
+
if operation == '+'
|
|
3445
|
+
current | bits # Add permissions
|
|
3446
|
+
else
|
|
3447
|
+
current & ~bits # Remove permissions
|
|
3448
|
+
end
|
|
3353
3449
|
elsif ans.length == 3 # "rwx" → "rwxrwxrwx"
|
|
3354
3450
|
# compute the single octal digit
|
|
3355
3451
|
digit = ans.chars.map do |c|
|
|
@@ -3372,13 +3468,24 @@ def change_permissions # {{{3
|
|
|
3372
3468
|
if mode.nil?
|
|
3373
3469
|
@pB.say('Invalid mode')
|
|
3374
3470
|
else
|
|
3375
|
-
current = File.stat(
|
|
3376
|
-
if mode == current
|
|
3471
|
+
current = File.stat(items.first).mode & 0o777
|
|
3472
|
+
if mode == current && items.size == 1
|
|
3377
3473
|
@pB.say("No change needed (already #{mode.to_s(8)})")
|
|
3378
3474
|
else
|
|
3379
|
-
|
|
3380
|
-
|
|
3475
|
+
# Record old permissions for undo
|
|
3476
|
+
old_permissions = items.map { |item| { path: item, mode: File.stat(item).mode & 0o777 } }
|
|
3477
|
+
|
|
3478
|
+
# Change permissions
|
|
3479
|
+
items.each { |item| File.chmod(mode, item) rescue nil }
|
|
3381
3480
|
@pB.say("Permissions changed to: #{mode.to_s(8)}")
|
|
3481
|
+
|
|
3482
|
+
# Add to undo history
|
|
3483
|
+
add_undo_operation({
|
|
3484
|
+
type: 'permissions',
|
|
3485
|
+
items: old_permissions,
|
|
3486
|
+
timestamp: Time.now
|
|
3487
|
+
})
|
|
3488
|
+
|
|
3382
3489
|
# Invalidate cache for current directory to show updated colors
|
|
3383
3490
|
@dir_cache.delete_if { |key, _| key.start_with?("#{Dir.pwd}:") }
|
|
3384
3491
|
@pL.update = true
|
|
@@ -4975,19 +5082,20 @@ def command(cmd, timeout: 5, return_both: false) # {{{2
|
|
|
4975
5082
|
end
|
|
4976
5083
|
|
|
4977
5084
|
def copy_move_link(type) # COPY OR MOVE TAGGED ITEMS {{{2
|
|
4978
|
-
|
|
4979
|
-
|
|
5085
|
+
# Use tagged items if any exist, otherwise use selected item
|
|
5086
|
+
items = @tagged.empty? ? [@selected] : @tagged.uniq
|
|
5087
|
+
|
|
4980
5088
|
# Determine destination directory based on mode
|
|
4981
5089
|
dest_dir = if @dual_pane
|
|
4982
5090
|
@active_pane == :left ? @pwd_left : @pwd_right
|
|
4983
5091
|
else
|
|
4984
5092
|
Dir.pwd
|
|
4985
5093
|
end
|
|
4986
|
-
|
|
5094
|
+
|
|
4987
5095
|
# Track operations for undo
|
|
4988
5096
|
operations = []
|
|
4989
|
-
|
|
4990
|
-
|
|
5097
|
+
|
|
5098
|
+
items.each do |item|
|
|
4991
5099
|
dest = File.join(dest_dir, File.basename(item))
|
|
4992
5100
|
dest += '1' if File.exist?(dest)
|
|
4993
5101
|
while File.exist?(dest)
|
|
@@ -5039,7 +5147,12 @@ def copy_move_link(type) # COPY OR MOVE TAGGED ITEMS {{{2
|
|
|
5039
5147
|
end
|
|
5040
5148
|
|
|
5041
5149
|
@tagged = []
|
|
5042
|
-
|
|
5150
|
+
|
|
5151
|
+
# Store the currently selected item to restore selection after refresh (for move operations)
|
|
5152
|
+
if type == 'move'
|
|
5153
|
+
previously_selected = @selected ? File.basename(@selected) : nil
|
|
5154
|
+
end
|
|
5155
|
+
|
|
5043
5156
|
# Set update flags for proper refresh - let render system handle the actual refresh
|
|
5044
5157
|
if @dual_pane
|
|
5045
5158
|
# Update the destination pane
|
|
@@ -5051,8 +5164,14 @@ def copy_move_link(type) # COPY OR MOVE TAGGED ITEMS {{{2
|
|
|
5051
5164
|
# Also update the preview pane
|
|
5052
5165
|
@pPreview.update = true if @pPreview
|
|
5053
5166
|
end
|
|
5054
|
-
|
|
5167
|
+
|
|
5055
5168
|
render
|
|
5169
|
+
|
|
5170
|
+
# Restore selection after move operations
|
|
5171
|
+
if type == 'move' && previously_selected && @files
|
|
5172
|
+
restored_index = @files.index(previously_selected)
|
|
5173
|
+
@index = restored_index if restored_index
|
|
5174
|
+
end
|
|
5056
5175
|
end
|
|
5057
5176
|
|
|
5058
5177
|
def mark_latest # UPDATE MARKS LIST {{{2
|
|
@@ -5162,7 +5281,8 @@ def open_selected(html = nil) # OPEN SELECTED FILE {{{2
|
|
|
5162
5281
|
return
|
|
5163
5282
|
end
|
|
5164
5283
|
tmpfile = File.join(Dir.tmpdir, 'rtfm_err.log')
|
|
5165
|
-
|
|
5284
|
+
# Use tagged items if any exist, otherwise use selected item
|
|
5285
|
+
paths = @tagged.empty? ? [@selected] : @tagged
|
|
5166
5286
|
if html # html mode - open in HTML-browser
|
|
5167
5287
|
esc = paths.map { |p| Shellwords.escape(p) }.join(' ')
|
|
5168
5288
|
shell("xdg-open #{esc} &", err: tmpfile)
|
|
@@ -5700,15 +5820,24 @@ end
|
|
|
5700
5820
|
setborder
|
|
5701
5821
|
|
|
5702
5822
|
## Catch change in terminal resize, redraw {{{2
|
|
5703
|
-
Signal.trap('WINCH') do
|
|
5823
|
+
Signal.trap('WINCH') do
|
|
5704
5824
|
# Don't refresh/render if an external interactive program is running
|
|
5705
5825
|
# This prevents RTFM from painting over programs like HyperList when
|
|
5706
5826
|
# switching terminals in window managers like i3-wm
|
|
5707
5827
|
unless @external_program_running
|
|
5708
|
-
|
|
5709
|
-
|
|
5710
|
-
|
|
5711
|
-
|
|
5828
|
+
begin
|
|
5829
|
+
new_h, new_w = IO.console.winsize
|
|
5830
|
+
# Validate terminal size (minimum 10x20 to be usable)
|
|
5831
|
+
if new_h && new_w && new_h >= 10 && new_w >= 20
|
|
5832
|
+
@h, @w = new_h, new_w
|
|
5833
|
+
@pT.update = @pL.update = @pR.update = @pB.update = true
|
|
5834
|
+
refresh
|
|
5835
|
+
render
|
|
5836
|
+
end
|
|
5837
|
+
rescue => e
|
|
5838
|
+
# Silently ignore resize errors to prevent crashes
|
|
5839
|
+
# User can manually refresh with 'r' key if needed
|
|
5840
|
+
end
|
|
5712
5841
|
end
|
|
5713
5842
|
end
|
|
5714
5843
|
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rtfm-filemanager
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 7.
|
|
4
|
+
version: 7.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Geir Isene
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-10-
|
|
11
|
+
date: 2025-10-24 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rcurses
|
|
@@ -53,7 +53,7 @@ dependencies:
|
|
|
53
53
|
- !ruby/object:Gem::Version
|
|
54
54
|
version: '7.4'
|
|
55
55
|
description: |-
|
|
56
|
-
RTFM v7.
|
|
56
|
+
RTFM v7.2.0: BREAKING CHANGE - Batch operations now work ONLY on tagged items when items are tagged (not tagged + selected). If no items are tagged, operations work on the selected item only.
|
|
57
57
|
A full featured terminal browser with syntax highlighted files, images shown in the terminal, videos thumbnailed, etc. Features include remote SSH/SFTP browsing, interactive SSH shell, comprehensive undo system, bookmarks, and much more. You can bookmark and jump around easily, delete, rename, copy, symlink and move files. RTFM is one of the most feature-packed terminal file managers.
|
|
58
58
|
email: g@isene.com
|
|
59
59
|
executables:
|