rtfm-filemanager 7.1.4 → 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 +142 -33
- 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
|
|
@@ -1646,6 +1646,10 @@ def undo_last_operation # {{{3
|
|
|
1646
1646
|
undo_link(operation)
|
|
1647
1647
|
when 'bulk_rename'
|
|
1648
1648
|
undo_bulk_rename(operation)
|
|
1649
|
+
when 'permissions'
|
|
1650
|
+
undo_permissions(operation)
|
|
1651
|
+
when 'ownership'
|
|
1652
|
+
undo_ownership(operation)
|
|
1649
1653
|
else
|
|
1650
1654
|
@pB.say("Unknown operation type: #{operation[:type]}".fg(196))
|
|
1651
1655
|
return
|
|
@@ -1738,7 +1742,7 @@ def undo_bulk_rename(operation) # {{{3
|
|
|
1738
1742
|
operation[:renames].reverse.each do |rename_info|
|
|
1739
1743
|
old_path = rename_info[:old_path]
|
|
1740
1744
|
new_path = rename_info[:new_path]
|
|
1741
|
-
|
|
1745
|
+
|
|
1742
1746
|
if File.exist?(new_path)
|
|
1743
1747
|
FileUtils.mv(new_path, old_path)
|
|
1744
1748
|
else
|
|
@@ -1747,6 +1751,38 @@ def undo_bulk_rename(operation) # {{{3
|
|
|
1747
1751
|
end
|
|
1748
1752
|
end
|
|
1749
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
|
+
|
|
1750
1786
|
# RECENTLY ACCESSED FILES {{{2
|
|
1751
1787
|
def track_file_access(file_path) # {{{3
|
|
1752
1788
|
return unless File.exist?(file_path)
|
|
@@ -1981,23 +2017,21 @@ end
|
|
|
1981
2017
|
|
|
1982
2018
|
# BULK RENAME {{{2
|
|
1983
2019
|
def bulk_rename # {{{3
|
|
1984
|
-
if
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
end
|
|
1988
|
-
|
|
2020
|
+
# Use tagged items if any exist, otherwise use selected item
|
|
2021
|
+
items = @tagged.empty? ? [@selected] : @tagged
|
|
2022
|
+
|
|
1989
2023
|
@pB.say("Bulk rename pattern: ".fg(156))
|
|
1990
2024
|
@pR.say(build_pattern_help)
|
|
1991
2025
|
@pR.update = false
|
|
1992
|
-
|
|
2026
|
+
|
|
1993
2027
|
pattern = @pCmd.ask('Pattern: ', '')
|
|
1994
2028
|
return if pattern.nil? || pattern.strip.empty?
|
|
1995
|
-
|
|
2029
|
+
|
|
1996
2030
|
# Parse and preview renames
|
|
1997
2031
|
rename_operations = []
|
|
1998
2032
|
errors = []
|
|
1999
|
-
|
|
2000
|
-
|
|
2033
|
+
|
|
2034
|
+
items.each do |file|
|
|
2001
2035
|
next unless File.exist?(file)
|
|
2002
2036
|
|
|
2003
2037
|
old_name = File.basename(file)
|
|
@@ -3254,9 +3288,11 @@ def delete_items # {{{3
|
|
|
3254
3288
|
prompt_text = @trash ? "Move to trash? (y/n)" : "⚠️ PERMANENTLY DELETE? (y/n)"
|
|
3255
3289
|
@pB.say(" #{prompt_text}".fg(action_color))
|
|
3256
3290
|
if getchr == 'y'
|
|
3291
|
+
# Use tagged items if any exist, otherwise use selected item
|
|
3292
|
+
items = @tagged.empty? ? [@selected] : @tagged
|
|
3257
3293
|
# Collect paths - include broken symlinks since they can be moved/deleted
|
|
3258
3294
|
# File.exist? returns false for broken symlinks, but File.symlink? still works
|
|
3259
|
-
paths =
|
|
3295
|
+
paths = items.select { |p| File.exist?(p) || File.symlink?(p) }
|
|
3260
3296
|
if paths.empty?
|
|
3261
3297
|
@pB.say("No valid items to #{action.downcase}".fg(196))
|
|
3262
3298
|
else
|
|
@@ -3290,14 +3326,20 @@ def delete_items # {{{3
|
|
|
3290
3326
|
# Cannot undo permanent deletion, so don't add to undo history
|
|
3291
3327
|
end
|
|
3292
3328
|
@tagged.clear
|
|
3293
|
-
|
|
3294
|
-
# Store the
|
|
3295
|
-
|
|
3296
|
-
|
|
3329
|
+
|
|
3330
|
+
# Store the currently selected item to restore selection after refresh
|
|
3331
|
+
previously_selected = @selected ? File.basename(@selected) : nil
|
|
3332
|
+
|
|
3297
3333
|
# Refresh the file list to reflect deletions
|
|
3298
3334
|
@pL.update = true
|
|
3299
3335
|
render # This will update @files and @selected
|
|
3300
|
-
|
|
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
|
+
|
|
3301
3343
|
# After render, check if we still have files
|
|
3302
3344
|
if @files.empty?
|
|
3303
3345
|
# No files left in directory - clear the preview
|
|
@@ -3334,8 +3376,11 @@ end
|
|
|
3334
3376
|
|
|
3335
3377
|
def change_ownership # {{{3
|
|
3336
3378
|
require 'etc'
|
|
3337
|
-
|
|
3338
|
-
|
|
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
|
|
3339
3384
|
ans = @pB.ask('Change ownership (user:group): ', "#{unm}:#{gnm}")
|
|
3340
3385
|
# Reset bottom pane after input
|
|
3341
3386
|
@pB.clear; @pB.update = true
|
|
@@ -3343,16 +3388,37 @@ def change_ownership # {{{3
|
|
|
3343
3388
|
user, group = ans.split(':')
|
|
3344
3389
|
uid = Etc.getpwnam(user).uid
|
|
3345
3390
|
gid = Etc.getgrnam(group).gid
|
|
3346
|
-
File.chown(uid, gid, @selected)
|
|
3347
|
-
@tagged.each { |t| File.chown(uid, gid, t) rescue nil }
|
|
3348
3391
|
if user == unm && group == gnm
|
|
3349
3392
|
@pB.say('No change in ownership')
|
|
3350
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 }
|
|
3351
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
|
+
})
|
|
3352
3416
|
end
|
|
3353
3417
|
end
|
|
3354
3418
|
|
|
3355
3419
|
def change_permissions # {{{3
|
|
3420
|
+
# Use tagged items if any exist, otherwise use selected item
|
|
3421
|
+
items = @tagged.empty? ? [@selected] : @tagged
|
|
3356
3422
|
# strip leading "-" off e.g. "-rwxr-xr-x" → "rwxr-xr-x"
|
|
3357
3423
|
default = @fileattr.split[1][1..]
|
|
3358
3424
|
ans = @pB.ask('Permissions: ', default)
|
|
@@ -3361,6 +3427,25 @@ def change_permissions # {{{3
|
|
|
3361
3427
|
return if ans.nil? || ans.strip.empty?
|
|
3362
3428
|
mode = if ans =~ /^\d{3}$/ # "755"
|
|
3363
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
|
|
3364
3449
|
elsif ans.length == 3 # "rwx" → "rwxrwxrwx"
|
|
3365
3450
|
# compute the single octal digit
|
|
3366
3451
|
digit = ans.chars.map do |c|
|
|
@@ -3383,13 +3468,24 @@ def change_permissions # {{{3
|
|
|
3383
3468
|
if mode.nil?
|
|
3384
3469
|
@pB.say('Invalid mode')
|
|
3385
3470
|
else
|
|
3386
|
-
current = File.stat(
|
|
3387
|
-
if mode == current
|
|
3471
|
+
current = File.stat(items.first).mode & 0o777
|
|
3472
|
+
if mode == current && items.size == 1
|
|
3388
3473
|
@pB.say("No change needed (already #{mode.to_s(8)})")
|
|
3389
3474
|
else
|
|
3390
|
-
|
|
3391
|
-
|
|
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 }
|
|
3392
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
|
+
|
|
3393
3489
|
# Invalidate cache for current directory to show updated colors
|
|
3394
3490
|
@dir_cache.delete_if { |key, _| key.start_with?("#{Dir.pwd}:") }
|
|
3395
3491
|
@pL.update = true
|
|
@@ -4986,19 +5082,20 @@ def command(cmd, timeout: 5, return_both: false) # {{{2
|
|
|
4986
5082
|
end
|
|
4987
5083
|
|
|
4988
5084
|
def copy_move_link(type) # COPY OR MOVE TAGGED ITEMS {{{2
|
|
4989
|
-
|
|
4990
|
-
|
|
5085
|
+
# Use tagged items if any exist, otherwise use selected item
|
|
5086
|
+
items = @tagged.empty? ? [@selected] : @tagged.uniq
|
|
5087
|
+
|
|
4991
5088
|
# Determine destination directory based on mode
|
|
4992
5089
|
dest_dir = if @dual_pane
|
|
4993
5090
|
@active_pane == :left ? @pwd_left : @pwd_right
|
|
4994
5091
|
else
|
|
4995
5092
|
Dir.pwd
|
|
4996
5093
|
end
|
|
4997
|
-
|
|
5094
|
+
|
|
4998
5095
|
# Track operations for undo
|
|
4999
5096
|
operations = []
|
|
5000
|
-
|
|
5001
|
-
|
|
5097
|
+
|
|
5098
|
+
items.each do |item|
|
|
5002
5099
|
dest = File.join(dest_dir, File.basename(item))
|
|
5003
5100
|
dest += '1' if File.exist?(dest)
|
|
5004
5101
|
while File.exist?(dest)
|
|
@@ -5050,7 +5147,12 @@ def copy_move_link(type) # COPY OR MOVE TAGGED ITEMS {{{2
|
|
|
5050
5147
|
end
|
|
5051
5148
|
|
|
5052
5149
|
@tagged = []
|
|
5053
|
-
|
|
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
|
+
|
|
5054
5156
|
# Set update flags for proper refresh - let render system handle the actual refresh
|
|
5055
5157
|
if @dual_pane
|
|
5056
5158
|
# Update the destination pane
|
|
@@ -5062,8 +5164,14 @@ def copy_move_link(type) # COPY OR MOVE TAGGED ITEMS {{{2
|
|
|
5062
5164
|
# Also update the preview pane
|
|
5063
5165
|
@pPreview.update = true if @pPreview
|
|
5064
5166
|
end
|
|
5065
|
-
|
|
5167
|
+
|
|
5066
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
|
|
5067
5175
|
end
|
|
5068
5176
|
|
|
5069
5177
|
def mark_latest # UPDATE MARKS LIST {{{2
|
|
@@ -5173,7 +5281,8 @@ def open_selected(html = nil) # OPEN SELECTED FILE {{{2
|
|
|
5173
5281
|
return
|
|
5174
5282
|
end
|
|
5175
5283
|
tmpfile = File.join(Dir.tmpdir, 'rtfm_err.log')
|
|
5176
|
-
|
|
5284
|
+
# Use tagged items if any exist, otherwise use selected item
|
|
5285
|
+
paths = @tagged.empty? ? [@selected] : @tagged
|
|
5177
5286
|
if html # html mode - open in HTML-browser
|
|
5178
5287
|
esc = paths.map { |p| Shellwords.escape(p) }.join(' ')
|
|
5179
5288
|
shell("xdg-open #{esc} &", err: tmpfile)
|
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:
|