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.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +16 -1
  3. data/bin/rtfm +142 -33
  4. metadata +3 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e58a3ca5dea3c9a47fb684b59d6ca3e59e343e792da7f6bd920e19352a6ef304
4
- data.tar.gz: b39baa83754fae08f10da279055508ea09fa0912c65533a3989a97dee57f9fea
3
+ metadata.gz: 506054b29faa54cff3af682e83d26356aefec7f631e99f5777a09f7321045816
4
+ data.tar.gz: 8276bf6d6271fa8364dc03cfb81b106761eb68597464597bd21f1a728cfb07cf
5
5
  SHA512:
6
- metadata.gz: 4cc0c86a87b3c2262bc8f54a0ba5e0289d87aced4090cdf43443e84454bd28afadebbc5d5fc693f132fdb818bda663ff9d94a451e67859c9022adc4a6c1ccbee
7
- data.tar.gz: fdd25b0ea4a8683e6f13d07ad6e59ceeab7e9258f6ee54839f65e208d51eebe5e25ebd9fc923a0a145e8adee68a0bb7d4409f9a4ff40b0231b106ee07125cc49
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
  ![Ruby](https://img.shields.io/badge/language-Ruby-red) [![Gem Version](https://badge.fury.io/rb/rtfm-filemanager.svg)](https://badge.fury.io/rb/rtfm-filemanager) ![Unlicense](https://img.shields.io/badge/license-Unlicense-green) ![Stay Amazing](https://img.shields.io/badge/Stay-Amazing-important)
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.1.4' # Fixed terminal resizing issues in window managers
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 @tagged.empty?
1985
- @pB.say("No files tagged for bulk rename. Tag files first with 't'".fg(196))
1986
- return
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
- @tagged.each do |file|
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 = (@tagged + [@selected]).uniq.select { |p| File.exist?(p) || File.symlink?(p) }
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 old preview text to detect if it needs updating
3295
- old_preview_text = @pR.text
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
- gnm = Etc.getgrgid(File.stat(@selected).gid).name
3338
- unm = Etc.getpwuid(File.stat(@selected).uid).name
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(@selected).mode & 0o777
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
- File.chmod(mode, @selected)
3391
- @tagged.each { |t| File.chmod(mode, t) rescue nil }
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
- @tagged.uniq!
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
- @tagged.each do |item|
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
- paths = (@tagged + [@selected]).uniq
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.1.4
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-23 00:00:00.000000000 Z
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.1.4: Fixed terminal resizing issues in window managers like i3-wm.
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: