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.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +16 -1
  3. data/bin/rtfm +167 -38
  4. metadata +3 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fc43a3881cc5f944d08ca2f0aeaaf70f712b79b77e51cbccebf9df7a892282f3
4
- data.tar.gz: f7b1897e5accadf7d638b72bcd50ce8b88691162a60261baf7added43fe1cc09
3
+ metadata.gz: 506054b29faa54cff3af682e83d26356aefec7f631e99f5777a09f7321045816
4
+ data.tar.gz: 8276bf6d6271fa8364dc03cfb81b106761eb68597464597bd21f1a728cfb07cf
5
5
  SHA512:
6
- metadata.gz: e0b54e7a21bae71705dd1a3d4978cd435c0d6119f876c8de23709feb68483343eeabdf39a287de5dcd48a8ec5758ee467028f661e9bae9a4925ac240a2cdf53b
7
- data.tar.gz: bf57c7c14d8bdeb2ebc907b92fe373416c7f69b5691903115be61f90d89fdeeaf26a20445099251057b7c7e4f7e0714dec8583134cdabf9a38f1d86916942c7d
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.3' # Fixed permission change color update and broken symlink deletion
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 @tagged.empty?
1974
- @pB.say("No files tagged for bulk rename. Tag files first with 't'".fg(196))
1975
- return
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
- @tagged.each do |file|
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 = (@tagged + [@selected]).uniq.select { |p| File.exist?(p) || File.symlink?(p) }
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 old preview text to detect if it needs updating
3284
- old_preview_text = @pR.text
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
- gnm = Etc.getgrgid(File.stat(@selected).gid).name
3327
- 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
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(@selected).mode & 0o777
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
- File.chmod(mode, @selected)
3380
- @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 }
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
- @tagged.uniq!
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
- @tagged.each do |item|
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
- paths = (@tagged + [@selected]).uniq
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
- @h, @w = IO.console.winsize
5709
- @pT.update = @pL.update = @pR.update = @pB.update = true
5710
- refresh
5711
- render
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.1.3
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-21 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.3: Fixed permission change color update and broken symlink deletion.
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: