rtfm-filemanager 5.8.2 → 5.10.2
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 +25 -4
- data/bin/rtfm +990 -111
- metadata +5 -5
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 = '5.
|
21
|
+
@version = '5.10.2' # Enhanced tab management, eliminated dual-pane, fixed flickering, restored image redraw
|
22
22
|
|
23
23
|
# SAVE & STORE TERMINAL {{{1
|
24
24
|
ORIG_STTY = `stty -g`.chomp
|
@@ -51,6 +51,32 @@ require 'bootsnap/setup' # Speed up subsequent requires
|
|
51
51
|
# $last_checkpoint = now
|
52
52
|
#end
|
53
53
|
|
54
|
+
def check_image_redraw # {{{2
|
55
|
+
# Only check periodically to avoid performance impact
|
56
|
+
now = Time.now
|
57
|
+
return if now - @last_focus_check < @focus_check_interval
|
58
|
+
@last_focus_check = now
|
59
|
+
|
60
|
+
# Only check if we have an image currently displayed
|
61
|
+
return unless @image && @current_image_path
|
62
|
+
|
63
|
+
begin
|
64
|
+
# Check if terminal has focus and if image needs redrawing
|
65
|
+
active_window = `xdotool getactivewindow 2>/dev/null`.chomp
|
66
|
+
return if active_window.empty?
|
67
|
+
|
68
|
+
# Simple heuristic: if we can't find any w3mimgdisplay processes,
|
69
|
+
# the image overlay was probably cleared and needs redrawing
|
70
|
+
img_processes = `pgrep w3mimgdisplay 2>/dev/null`.chomp
|
71
|
+
if img_processes.empty? && File.exist?(@current_image_path)
|
72
|
+
# Redraw the image
|
73
|
+
showimage(Shellwords.escape(@current_image_path))
|
74
|
+
end
|
75
|
+
rescue
|
76
|
+
# Silently fail - we don't want focus checking to break anything
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
54
80
|
# LOAD LIBRARIES {{{1
|
55
81
|
begin
|
56
82
|
require 'rcurses'
|
@@ -63,12 +89,14 @@ rescue StandardError => e
|
|
63
89
|
exit 1
|
64
90
|
end
|
65
91
|
require 'tmpdir'
|
92
|
+
require 'set' # For symlink loop detection
|
66
93
|
# Lazy-load to speed up startup
|
67
94
|
autoload :Shellwords, 'shellwords'
|
68
95
|
autoload :Timeout, 'timeout'
|
69
96
|
autoload :Open3, 'open3'
|
70
97
|
autoload :PTY, 'pty'
|
71
98
|
autoload :OpenAI, 'ruby/openai'
|
99
|
+
autoload :Tempfile, 'tempfile'
|
72
100
|
#checkpoint("Libraries loaded")
|
73
101
|
|
74
102
|
# FIX TERMINAL MESSAGE BLEED-THROUGH {{{1
|
@@ -91,15 +119,47 @@ $stderr.reopen(logfile)
|
|
91
119
|
|
92
120
|
# RCURSES CLASS EXTENSION {{{1
|
93
121
|
module Rcurses
|
94
|
-
# Add
|
122
|
+
# Add location attribute for compatibility
|
95
123
|
class Pane
|
96
|
-
attr_accessor :
|
124
|
+
attr_accessor :locate
|
125
|
+
|
126
|
+
# Compatibility layer for the new update control system
|
127
|
+
def update=(value)
|
128
|
+
# TEMPORARILY DISABLED: If rcurses 4.9.0+ is available, use new suspend/resume system
|
129
|
+
# if self.respond_to?(:suspend_updates)
|
130
|
+
# if value
|
131
|
+
# resume_updates
|
132
|
+
# else
|
133
|
+
# suspend_updates
|
134
|
+
# end
|
135
|
+
# else
|
136
|
+
# Fallback for older rcurses versions
|
137
|
+
@update = value
|
138
|
+
# end
|
139
|
+
end
|
140
|
+
|
141
|
+
def update
|
142
|
+
# For older rcurses versions compatibility
|
143
|
+
@update.nil? ? true : @update
|
144
|
+
end
|
145
|
+
|
146
|
+
# Restore original say method override for compatibility
|
97
147
|
alias original_say say
|
98
148
|
def say(text)
|
99
149
|
original_say(text)
|
100
150
|
self.update = false
|
101
151
|
end
|
102
152
|
end
|
153
|
+
|
154
|
+
# Batch update helper for rcurses 4.9.0+
|
155
|
+
def self.batch_refresh(&block)
|
156
|
+
if Rcurses.respond_to?(:batch_updates)
|
157
|
+
Rcurses.batch_updates(&block)
|
158
|
+
else
|
159
|
+
# Fallback for older versions - just execute the block
|
160
|
+
block.call
|
161
|
+
end
|
162
|
+
end
|
103
163
|
end
|
104
164
|
|
105
165
|
# CREATE DIRS & SET FILE CONSTS {{{1
|
@@ -170,6 +230,15 @@ CONFIG_FILE = File.join(RTFM_HOME, 'conf')
|
|
170
230
|
T = Show currently tagged items in right pane
|
171
231
|
u = Untag all tagged items
|
172
232
|
|
233
|
+
TAB MANAGEMENT
|
234
|
+
] = Create new tab in current directory
|
235
|
+
[ = Close current tab (keeps at least one tab open)
|
236
|
+
J = Previous tab (wraps around)
|
237
|
+
K = Next tab (wraps around)
|
238
|
+
} = Duplicate current tab (creates a copy with same directory)
|
239
|
+
{ = Rename current tab
|
240
|
+
1-9 = Switch to tab by number (1 = first tab, 2 = second tab, etc.)
|
241
|
+
|
173
242
|
MANIPULATE ITEMS
|
174
243
|
p = Put (copy) tagged items here
|
175
244
|
P = PUT (move) tagged items here
|
@@ -411,11 +480,28 @@ else
|
|
411
480
|
end
|
412
481
|
@showimage = false unless cmd?('xwininfo') && cmd?('xdotool')
|
413
482
|
|
483
|
+
# Image auto-redraw variables
|
484
|
+
@current_image_path = nil # Path of currently displayed image
|
485
|
+
@last_focus_check = Time.now
|
486
|
+
@focus_check_interval = 0.5 # Check focus every 500ms
|
487
|
+
|
414
488
|
@bat = cmd?('bat') ? 'bat' : 'batcat'
|
415
489
|
|
416
490
|
## Set encoding for $stdin to utf-8 {{{2
|
417
491
|
$stdin.set_encoding(Encoding::UTF_8)
|
418
492
|
|
493
|
+
## Initialize rendering optimization variables {{{2
|
494
|
+
@last_render_state = nil
|
495
|
+
|
496
|
+
# Custom caching system - could be replaced with rcurses 4.9.0+ built-in cache
|
497
|
+
# when Rcurses.cache_set/cache_get becomes available
|
498
|
+
@dir_cache = {}
|
499
|
+
@dir_cache_size = 0
|
500
|
+
@max_cache_entries = 50
|
501
|
+
@metadata_cache = {}
|
502
|
+
@metadata_cache_size = 0
|
503
|
+
@max_metadata_entries = 100
|
504
|
+
|
419
505
|
# INITIALIZE VARIABLES {{{1
|
420
506
|
## These can be set by user in ~/.rtfm/conf
|
421
507
|
### Saved on quit ('q')
|
@@ -459,6 +545,211 @@ $stdin.set_encoding(Encoding::UTF_8)
|
|
459
545
|
@index = 0 # Set chosen item to first on startup
|
460
546
|
@tagsize = 0 # Size (in MB) of tagged items
|
461
547
|
@navi = '' # Navi result when navi is invoked
|
548
|
+
# @dual_pane removed - using tabs for multi-directory navigation
|
549
|
+
|
550
|
+
## Dual-pane directory navigation variables
|
551
|
+
# Dual-pane variables removed - using tabs for multi-directory navigation
|
552
|
+
# @active_pane, @index_left/right, @files_left/right, @selected_left/right
|
553
|
+
# @directory_left/right, @pwd_left/right all removed
|
554
|
+
|
555
|
+
## Tab system variables
|
556
|
+
@tabs = [] # Array of tab objects
|
557
|
+
@current_tab = 0 # Index of currently active tab
|
558
|
+
@tab_counter = 0 # Counter for generating unique tab IDs
|
559
|
+
@tab_bar_visible = false # Whether tab bar is currently shown
|
560
|
+
@tab_bar_hide_time = 0 # Time when tab bar should be hidden
|
561
|
+
|
562
|
+
# TAB MANAGEMENT FUNCTIONS {{{1
|
563
|
+
def create_tab(directory = Dir.pwd, name = nil) # {{{2
|
564
|
+
@tab_counter += 1
|
565
|
+
tab = {
|
566
|
+
id: @tab_counter,
|
567
|
+
name: name || File.basename(directory),
|
568
|
+
directory: directory,
|
569
|
+
index: 0,
|
570
|
+
tagged: [],
|
571
|
+
searched: '',
|
572
|
+
lsfiles: '',
|
573
|
+
lsmatch: '',
|
574
|
+
directory_memory: {},
|
575
|
+
files: []
|
576
|
+
}
|
577
|
+
@tabs << tab
|
578
|
+
tab
|
579
|
+
end
|
580
|
+
|
581
|
+
def current_tab # {{{2
|
582
|
+
@tabs[@current_tab] || create_tab
|
583
|
+
end
|
584
|
+
|
585
|
+
def switch_to_tab(tab_index) # {{{2
|
586
|
+
return if tab_index < 0 || tab_index >= @tabs.size
|
587
|
+
|
588
|
+
# Save current tab state
|
589
|
+
save_tab_state
|
590
|
+
|
591
|
+
# Switch to new tab
|
592
|
+
@current_tab = tab_index
|
593
|
+
restore_tab_state
|
594
|
+
|
595
|
+
# Update display
|
596
|
+
@pL.update = @pR.update = @pT.update = @pTab.update = true
|
597
|
+
end
|
598
|
+
|
599
|
+
def save_tab_state # {{{2
|
600
|
+
return unless @tabs[@current_tab]
|
601
|
+
|
602
|
+
tab = @tabs[@current_tab]
|
603
|
+
tab[:directory] = Dir.pwd
|
604
|
+
tab[:name] = File.basename(Dir.pwd) # Update tab name to current directory
|
605
|
+
tab[:index] = @index
|
606
|
+
tab[:tagged] = @tagged.dup
|
607
|
+
tab[:searched] = @searched
|
608
|
+
tab[:lsfiles] = @lsfiles
|
609
|
+
tab[:lsmatch] = @lsmatch
|
610
|
+
tab[:directory_memory] = @directory.dup
|
611
|
+
tab[:files] = @files.dup if defined?(@files)
|
612
|
+
|
613
|
+
# Save dual-pane state if in dual-pane mode
|
614
|
+
# Dual-pane tab state removed - using single-pane mode only
|
615
|
+
end
|
616
|
+
|
617
|
+
def restore_tab_state # {{{2
|
618
|
+
tab = current_tab
|
619
|
+
|
620
|
+
begin
|
621
|
+
Dir.chdir(tab[:directory]) if Dir.exist?(tab[:directory])
|
622
|
+
rescue
|
623
|
+
# If directory doesn't exist, go to home
|
624
|
+
Dir.chdir
|
625
|
+
tab[:directory] = Dir.pwd
|
626
|
+
end
|
627
|
+
|
628
|
+
@index = tab[:index]
|
629
|
+
@tagged = tab[:tagged].dup
|
630
|
+
@searched = tab[:searched]
|
631
|
+
@lsfiles = tab[:lsfiles]
|
632
|
+
@lsmatch = tab[:lsmatch]
|
633
|
+
@directory = tab[:directory_memory].dup
|
634
|
+
@files = tab[:files].dup if tab[:files]
|
635
|
+
|
636
|
+
# Dual-pane state restoration removed - using single-pane mode only
|
637
|
+
end
|
638
|
+
|
639
|
+
def close_tab(tab_index = @current_tab) # {{{2
|
640
|
+
return if @tabs.size <= 1 # Always keep at least one tab
|
641
|
+
|
642
|
+
@tabs.delete_at(tab_index)
|
643
|
+
|
644
|
+
# Adjust current tab index if necessary
|
645
|
+
if @current_tab >= @tabs.size
|
646
|
+
@current_tab = @tabs.size - 1
|
647
|
+
elsif tab_index < @current_tab
|
648
|
+
@current_tab -= 1
|
649
|
+
end
|
650
|
+
|
651
|
+
restore_tab_state
|
652
|
+
@pL.update = @pR.update = @pT.update = true
|
653
|
+
end
|
654
|
+
|
655
|
+
def new_tab # {{{2
|
656
|
+
save_tab_state
|
657
|
+
tab = create_tab(Dir.pwd)
|
658
|
+
@current_tab = @tabs.size - 1
|
659
|
+
@pL.update = @pR.update = @pT.update = true
|
660
|
+
end
|
661
|
+
|
662
|
+
def duplicate_tab # {{{2
|
663
|
+
# Duplicate current tab with same directory and state
|
664
|
+
save_tab_state
|
665
|
+
current = current_tab
|
666
|
+
# Create a smart copy name
|
667
|
+
base_name = current[:name].gsub(/ Copy.*$/, '') # Remove existing " Copy" suffixes
|
668
|
+
copy_name = "#{base_name} Copy"
|
669
|
+
tab = create_tab(current[:directory], copy_name)
|
670
|
+
@current_tab = @tabs.size - 1
|
671
|
+
@pL.update = @pR.update = @pT.update = true
|
672
|
+
end
|
673
|
+
|
674
|
+
def rename_tab # {{{2
|
675
|
+
@pB.say("Enter new tab name: ".fg(156))
|
676
|
+
name = gets_custom("Tab name: ", current_tab[:name])
|
677
|
+
return if name.nil? || name.strip.empty?
|
678
|
+
|
679
|
+
current_tab[:name] = name.strip
|
680
|
+
@pT.update = true # Update tab bar display
|
681
|
+
@pB.clear
|
682
|
+
end
|
683
|
+
|
684
|
+
|
685
|
+
# Keyboard handler functions for tabs {{{2
|
686
|
+
def prev_tab # {{{2
|
687
|
+
new_index = @current_tab > 0 ? @current_tab - 1 : @tabs.size - 1
|
688
|
+
switch_to_tab(new_index)
|
689
|
+
end
|
690
|
+
|
691
|
+
def next_tab # {{{2
|
692
|
+
new_index = @current_tab < @tabs.size - 1 ? @current_tab + 1 : 0
|
693
|
+
switch_to_tab(new_index)
|
694
|
+
end
|
695
|
+
|
696
|
+
def switch_tab_1; switch_to_tab(0); end
|
697
|
+
def switch_tab_2; switch_to_tab(1); end
|
698
|
+
def switch_tab_3; switch_to_tab(2); end
|
699
|
+
def switch_tab_4; switch_to_tab(3); end
|
700
|
+
def switch_tab_5; switch_to_tab(4); end
|
701
|
+
def switch_tab_6; switch_to_tab(5); end
|
702
|
+
def switch_tab_7; switch_to_tab(6); end
|
703
|
+
def switch_tab_8; switch_to_tab(7); end
|
704
|
+
def switch_tab_9; switch_to_tab(8); end
|
705
|
+
|
706
|
+
def show_tab_overlay # {{{2
|
707
|
+
# Show tab overlay for 2 seconds, then restore top pane
|
708
|
+
@tab_bar_visible = true
|
709
|
+
@tab_bar_hide_time = Time.now + 2
|
710
|
+
|
711
|
+
# Show tab overlay immediately
|
712
|
+
@pTab.text = render_tab_bar
|
713
|
+
@pTab.refresh
|
714
|
+
end
|
715
|
+
|
716
|
+
def update_tab_overlay # {{{2
|
717
|
+
# Check if we should hide the tab overlay and restore top pane
|
718
|
+
if @tab_bar_visible && Time.now >= @tab_bar_hide_time
|
719
|
+
@tab_bar_visible = false
|
720
|
+
@pT.refresh # Restore the top pane display
|
721
|
+
end
|
722
|
+
end
|
723
|
+
|
724
|
+
def render_tab_bar # {{{2
|
725
|
+
tab_display = []
|
726
|
+
@tabs.each_with_index do |tab, i|
|
727
|
+
name = tab[:name]
|
728
|
+
# Show current directory in tab if different from name
|
729
|
+
dir = File.basename(tab[:directory] || '')
|
730
|
+
display_name = (name == "Main" && dir != name) ? "#{name}:#{dir}" : name
|
731
|
+
display_name = display_name[0..12] + '…' if display_name.length > 14 # Truncate long names
|
732
|
+
|
733
|
+
if i == @current_tab
|
734
|
+
# Active tab with bright background and bold text
|
735
|
+
tab_display << " #{i+1}:#{display_name} ".bg(226).fg(0).b # Yellow background, black text, bold
|
736
|
+
else
|
737
|
+
# Inactive tab with subtle styling
|
738
|
+
tab_display << " #{i+1}:#{display_name} ".fg(244) # Gray text
|
739
|
+
end
|
740
|
+
end
|
741
|
+
|
742
|
+
# Enhanced tab bar with better shortcuts
|
743
|
+
if @tabs.size == 1
|
744
|
+
# Show enhanced help for single tab
|
745
|
+
"#{tab_display.join('')} | ]:new }:dup {:rename | Auto-hiding in #{(@tab_bar_hide_time - Time.now).ceil}s"
|
746
|
+
else
|
747
|
+
# Show all available tab shortcuts
|
748
|
+
"#{tab_display.join('')} | ]:new }:dup {:rename [:close J/K:nav 1-9:switch"
|
749
|
+
end
|
750
|
+
end
|
751
|
+
|
752
|
+
# DUAL-PANE HELPER FUNCTIONS REMOVED - using tabs for multi-directory navigation
|
462
753
|
|
463
754
|
# LOAD CONFIG {{{1
|
464
755
|
## Get variables from config file (written back to ~/.rtfm/conf when exit via 'q')
|
@@ -480,6 +771,9 @@ load_config
|
|
480
771
|
# Handle start dir {{{2
|
481
772
|
Dir.chdir(ARGV.shift) if ARGV[0] && File.directory?(ARGV[0])
|
482
773
|
|
774
|
+
# Initialize first tab {{{2
|
775
|
+
create_tab(Dir.pwd, "Main")
|
776
|
+
|
483
777
|
# OPENAI SETUP {{{1
|
484
778
|
def chat_history # {{{2
|
485
779
|
@chat_history ||= [
|
@@ -561,6 +855,7 @@ KEYMAP = { # {{{2
|
|
561
855
|
'-' => :toggle_preview,
|
562
856
|
'_' => :toggle_image,
|
563
857
|
'b' => :toggle_syntax,
|
858
|
+
# '{' key available for future use (dual-pane mode removed)
|
564
859
|
|
565
860
|
# MOTION {{{3
|
566
861
|
'DOWN' => :move_down,
|
@@ -580,6 +875,7 @@ KEYMAP = { # {{{2
|
|
580
875
|
'PgUP' => :page_up,
|
581
876
|
'END' => :go_last,
|
582
877
|
'HOME' => :go_first,
|
878
|
+
# '}' key available for future use (dual-pane mode removed)
|
583
879
|
|
584
880
|
# MARKS & JUMPING {{{3
|
585
881
|
'm' => :set_mark,
|
@@ -601,6 +897,23 @@ KEYMAP = { # {{{2
|
|
601
897
|
'T' => :show_tagged,
|
602
898
|
'u' => :clear_tagged,
|
603
899
|
|
900
|
+
# TAB MANAGEMENT {{{3
|
901
|
+
']' => :new_tab,
|
902
|
+
'[' => :close_tab,
|
903
|
+
'J' => :prev_tab,
|
904
|
+
'K' => :next_tab,
|
905
|
+
'}' => :duplicate_tab, # Duplicate current tab
|
906
|
+
'{' => :rename_tab, # Rename current tab
|
907
|
+
'1' => :switch_tab_1,
|
908
|
+
'2' => :switch_tab_2,
|
909
|
+
'3' => :switch_tab_3,
|
910
|
+
'4' => :switch_tab_4,
|
911
|
+
'5' => :switch_tab_5,
|
912
|
+
'6' => :switch_tab_6,
|
913
|
+
'7' => :switch_tab_7,
|
914
|
+
'8' => :switch_tab_8,
|
915
|
+
'9' => :switch_tab_9,
|
916
|
+
|
604
917
|
# MANIPULATE ITEMS {{{3
|
605
918
|
'p' => :copy_items,
|
606
919
|
'P' => :move_items,
|
@@ -645,8 +958,8 @@ KEYMAP = { # {{{2
|
|
645
958
|
'S-TAB' => :page_up_right,
|
646
959
|
|
647
960
|
# CLIPBOARD COPY {{{3
|
648
|
-
'y' => :
|
649
|
-
'Y' => :
|
961
|
+
'y' => :copy_path_primary,
|
962
|
+
'Y' => :copy_path_clipboard,
|
650
963
|
'C-Y' => :copy_right,
|
651
964
|
|
652
965
|
# SYSTEM SHORTCUTS {{{3
|
@@ -675,6 +988,10 @@ end
|
|
675
988
|
# MAIN GETKEY FOR USER INPUT {{{2
|
676
989
|
def getkey # {{{3
|
677
990
|
chr = getchr(1)
|
991
|
+
|
992
|
+
# Check for image redraw on focus regain (even if no key was pressed)
|
993
|
+
check_image_redraw if @showimage
|
994
|
+
|
678
995
|
return unless chr
|
679
996
|
|
680
997
|
showimage('clear') if @image
|
@@ -685,6 +1002,7 @@ def getkey # {{{3
|
|
685
1002
|
end
|
686
1003
|
rescue Errno::EIO
|
687
1004
|
# ignore transient TTY/read errors when upon focus switch
|
1005
|
+
# Note: rcurses 4.9.0+ has enhanced error handling, but keeping this for compatibility
|
688
1006
|
return
|
689
1007
|
rescue StandardError => e
|
690
1008
|
errormsg('⚷ Error in getkey', e)
|
@@ -727,13 +1045,28 @@ end
|
|
727
1045
|
def change_width # {{{3
|
728
1046
|
@width += 1
|
729
1047
|
@width = 2 if @width == 8
|
1048
|
+
|
1049
|
+
if @dual_pane
|
1050
|
+
# Show width setting info for dual-pane mode using the new ratio calculation
|
1051
|
+
dir_panes_ratio = [0.5 - (@width - 2) * 0.034, 0.33].max
|
1052
|
+
preview_ratio = 1.0 - dir_panes_ratio
|
1053
|
+
@pB.say("Width: #{@width} (Dir panes: #{(dir_panes_ratio * 100).to_i}%, Preview: #{(preview_ratio * 100).to_i}%)")
|
1054
|
+
else
|
1055
|
+
@pB.say("Width: #{@width}")
|
1056
|
+
end
|
1057
|
+
|
730
1058
|
refresh
|
731
|
-
@pR.update = @pB.update = true
|
1059
|
+
@pL.update = @pR.update = @pT.update = @pB.update = true
|
1060
|
+
# Also update dual-pane objects if they exist
|
1061
|
+
if @dual_pane && @pLeft && @pRight && @pPreview
|
1062
|
+
@pLeft.update = @pRight.update = @pPreview.update = true
|
1063
|
+
end
|
732
1064
|
end
|
733
1065
|
|
734
1066
|
def toggle_border # {{{3
|
735
1067
|
@border = (@border + 1) % 4
|
736
1068
|
setborder
|
1069
|
+
@pL.update = @pR.update = @pT.update = @pB.update = true
|
737
1070
|
end
|
738
1071
|
|
739
1072
|
def toggle_preview # {{{3
|
@@ -754,14 +1087,18 @@ def toggle_syntax # {{{3
|
|
754
1087
|
@pR.update = true
|
755
1088
|
end
|
756
1089
|
|
1090
|
+
# toggle_dual_pane function removed - using tabs for multi-directory navigation
|
1091
|
+
|
757
1092
|
# MOTION {{{2
|
758
1093
|
def move_down # {{{3
|
759
1094
|
@index = @index >= @max_index ? @min_index : @index + 1
|
1095
|
+
@pL.update = true
|
760
1096
|
@pR.update = @pB.update = true
|
761
1097
|
end
|
762
1098
|
|
763
1099
|
def move_up # {{{3
|
764
1100
|
@index = @index <= @min_index ? @max_index : @index - 1
|
1101
|
+
@pL.update = true
|
765
1102
|
@pR.update = @pB.update = true
|
766
1103
|
end
|
767
1104
|
|
@@ -776,9 +1113,12 @@ def move_left # {{{3
|
|
776
1113
|
@directory[parent] = child_idx
|
777
1114
|
mark_latest
|
778
1115
|
Dir.chdir(parent)
|
779
|
-
@pL.update =
|
1116
|
+
@pL.update = true
|
1117
|
+
@pR.update = @pB.update = true
|
780
1118
|
end
|
781
1119
|
|
1120
|
+
# dirlist_simple function removed - was only for dual-pane navigation
|
1121
|
+
|
782
1122
|
def move_right # {{{3
|
783
1123
|
@directory[Dir.pwd] = @index
|
784
1124
|
mark_latest
|
@@ -816,6 +1156,8 @@ def go_first # {{{3
|
|
816
1156
|
@pR.update = @pB.update = true
|
817
1157
|
end
|
818
1158
|
|
1159
|
+
# switch_pane function removed - using tabs for multi-directory navigation
|
1160
|
+
|
819
1161
|
# MARKS & JUMPING {{{2
|
820
1162
|
def set_mark # {{{3
|
821
1163
|
marks_info
|
@@ -852,9 +1194,46 @@ def jump_to_mark # {{{3
|
|
852
1194
|
end
|
853
1195
|
|
854
1196
|
def go_home # {{{3
|
855
|
-
|
856
|
-
|
857
|
-
|
1197
|
+
if @dual_pane
|
1198
|
+
# In dual-pane mode, navigate the active pane to home directory
|
1199
|
+
home_dir = Dir.home
|
1200
|
+
|
1201
|
+
# Get home directory listing
|
1202
|
+
newfiles = command(
|
1203
|
+
"ls #{Shellwords.escape(home_dir)} #{@lsbase} #{@lsall} #{@lsorder} #{@lsinvert} #{@lsuser}"
|
1204
|
+
).pure.split("\n")
|
1205
|
+
|
1206
|
+
# Update the active pane's state
|
1207
|
+
if @active_pane == :left
|
1208
|
+
@directory_left[@pwd_left] = @index_left
|
1209
|
+
@pwd_left = home_dir
|
1210
|
+
@index_left = 0
|
1211
|
+
@files_left = newfiles
|
1212
|
+
@selected_left = newfiles[0] if newfiles.length > 0
|
1213
|
+
@directory_left[home_dir] = 0
|
1214
|
+
@pLeft.update = true
|
1215
|
+
else
|
1216
|
+
@directory_right[@pwd_right] = @index_right
|
1217
|
+
@pwd_right = home_dir
|
1218
|
+
@index_right = 0
|
1219
|
+
@files_right = newfiles
|
1220
|
+
@selected_right = newfiles[0] if newfiles.length > 0
|
1221
|
+
@directory_right[home_dir] = 0
|
1222
|
+
@pRight.update = true
|
1223
|
+
end
|
1224
|
+
|
1225
|
+
# Update compatibility variables
|
1226
|
+
@index = @active_pane == :left ? @index_left : @index_right
|
1227
|
+
@files = @active_pane == :left ? @files_left : @files_right
|
1228
|
+
@selected = @active_pane == :left ? @selected_left : @selected_right
|
1229
|
+
|
1230
|
+
@pPreview.update = true if @pPreview
|
1231
|
+
else
|
1232
|
+
# Original single-pane logic
|
1233
|
+
@directory[Dir.pwd] = @index
|
1234
|
+
mark_latest
|
1235
|
+
Dir.chdir
|
1236
|
+
end
|
858
1237
|
@pR.update = @pB.update = true
|
859
1238
|
end
|
860
1239
|
|
@@ -862,11 +1241,25 @@ def follow_symlink # {{{3
|
|
862
1241
|
@directory[Dir.pwd] = @index; mark_latest
|
863
1242
|
if File.symlink?(@selected)
|
864
1243
|
begin
|
1244
|
+
# Track visited symlinks to prevent loops
|
1245
|
+
@symlink_history ||= Set.new
|
1246
|
+
resolved_path = File.realpath(@selected)
|
1247
|
+
|
1248
|
+
if @symlink_history.include?(resolved_path)
|
1249
|
+
@pB.say("Error: Symlink loop detected".fg(196))
|
1250
|
+
return
|
1251
|
+
end
|
1252
|
+
|
1253
|
+
@symlink_history.add(resolved_path)
|
865
1254
|
target = File.readlink(@selected)
|
866
1255
|
target = File.expand_path(target, File.dirname(@selected)) unless target.start_with?('/')
|
867
1256
|
Dir.chdir(target)
|
1257
|
+
|
1258
|
+
# Clear history after successful navigation
|
1259
|
+
@symlink_history.clear
|
868
1260
|
rescue => e
|
869
1261
|
@pB.say("Error following symlink: #{e}")
|
1262
|
+
@symlink_history&.clear
|
870
1263
|
end
|
871
1264
|
end
|
872
1265
|
@pB.update = true
|
@@ -910,15 +1303,55 @@ end
|
|
910
1303
|
|
911
1304
|
# TAGGING {{{2
|
912
1305
|
def tag_current # {{{3
|
913
|
-
|
914
|
-
|
915
|
-
|
1306
|
+
if @dual_pane
|
1307
|
+
# In dual-pane mode, get the correct selected item and construct full path
|
1308
|
+
current_dir = @active_pane == :left ? @pwd_left : @pwd_right
|
1309
|
+
current_index = @active_pane == :left ? @index_left : @index_right
|
1310
|
+
current_files = @active_pane == :left ? @files_left : @files_right
|
1311
|
+
|
1312
|
+
if current_files && current_index < current_files.length
|
1313
|
+
selected_item = current_files[current_index]
|
1314
|
+
item = File.join(current_dir, selected_item)
|
1315
|
+
|
1316
|
+
# Tag/untag the item
|
1317
|
+
if @tagged.include?(item)
|
1318
|
+
@tagged.delete(item); @tagsize -= File.size(item) rescue 0
|
1319
|
+
else
|
1320
|
+
@tagged.push(item); @tagsize += File.size(item) rescue 0
|
1321
|
+
end
|
1322
|
+
|
1323
|
+
# Advance to next item in the active pane
|
1324
|
+
max_index = current_files.size - 1
|
1325
|
+
if @active_pane == :left
|
1326
|
+
@index_left = [@index_left + 1, max_index].min
|
1327
|
+
@selected_left = current_files[@index_left] if current_files[@index_left]
|
1328
|
+
@pLeft.update = true
|
1329
|
+
else
|
1330
|
+
@index_right = [@index_right + 1, max_index].min
|
1331
|
+
@selected_right = current_files[@index_right] if current_files[@index_right]
|
1332
|
+
@pRight.update = true
|
1333
|
+
end
|
1334
|
+
|
1335
|
+
# Update compatibility variables
|
1336
|
+
@index = @active_pane == :left ? @index_left : @index_right
|
1337
|
+
@selected = @active_pane == :left ? @selected_left : @selected_right
|
1338
|
+
|
1339
|
+
@pPreview.update = true if @pPreview
|
1340
|
+
end
|
916
1341
|
else
|
917
|
-
|
1342
|
+
# Original single-pane logic
|
1343
|
+
item = @selected
|
1344
|
+
if @tagged.include?(item)
|
1345
|
+
@tagged.delete(item); @tagsize -= File.size(item) rescue 0
|
1346
|
+
else
|
1347
|
+
@tagged.push(item); @tagsize += File.size(item) rescue 0
|
1348
|
+
end
|
1349
|
+
@index = [@index + 1, (@files.size - 1)].min
|
1350
|
+
@pL.update = true
|
918
1351
|
end
|
919
|
-
|
1352
|
+
|
920
1353
|
@pB.say(" Tagged #{@tagged.size} files (#{(@tagsize.to_f / 1_000_000).round(2)}MB)".fg(204))
|
921
|
-
@pB.update = false; @pR.update = true
|
1354
|
+
@pB.update = false; @pR.update = true
|
922
1355
|
end
|
923
1356
|
|
924
1357
|
def tag_pattern # {{{3
|
@@ -949,11 +1382,13 @@ end
|
|
949
1382
|
# MANIPULATE ITEMS {{{2
|
950
1383
|
def copy_items # {{{3
|
951
1384
|
copy_move_link('copy')
|
1385
|
+
# Dual-pane refresh is handled in copy_move_link function
|
952
1386
|
@pR.update = true
|
953
1387
|
end
|
954
1388
|
|
955
1389
|
def move_items # {{{3
|
956
1390
|
copy_move_link('move')
|
1391
|
+
# Dual-pane refresh is handled in copy_move_link function
|
957
1392
|
@pR.update = true
|
958
1393
|
end
|
959
1394
|
|
@@ -979,6 +1414,7 @@ end
|
|
979
1414
|
|
980
1415
|
def link_items # {{{3
|
981
1416
|
copy_move_link('link')
|
1417
|
+
# Dual-pane refresh is handled in copy_move_link function
|
982
1418
|
@pR.update = true
|
983
1419
|
end
|
984
1420
|
|
@@ -991,18 +1427,22 @@ def delete_items # {{{3
|
|
991
1427
|
past_action = @trash ? 'Moved' : 'Deleted'
|
992
1428
|
@pB.say(" #{action} selected and tagged? (press 'y')")
|
993
1429
|
if getchr == 'y'
|
994
|
-
# collect & escape every path
|
995
|
-
paths = (@tagged + [@selected]).uniq
|
996
|
-
|
997
|
-
|
998
|
-
esc_trash = Shellwords.escape(TRASH_DIR)
|
999
|
-
command("mv -f #{esc} #{esc_trash}")
|
1430
|
+
# collect & escape every path with existence verification
|
1431
|
+
paths = (@tagged + [@selected]).uniq.select { |p| File.exist?(p) }
|
1432
|
+
if paths.empty?
|
1433
|
+
@pB.say("No valid items to #{action.downcase}".fg(196))
|
1000
1434
|
else
|
1001
|
-
|
1435
|
+
esc = paths.map { |p| Shellwords.escape(p) }.join(' ')
|
1436
|
+
if @trash
|
1437
|
+
esc_trash = Shellwords.escape(TRASH_DIR)
|
1438
|
+
command("mv -f #{esc} #{esc_trash}")
|
1439
|
+
else
|
1440
|
+
command("rm -rf #{esc}")
|
1441
|
+
end
|
1442
|
+
@tagged.clear
|
1443
|
+
refresh_right
|
1444
|
+
@pB.say("#{past_action} #{paths.size} items#{@trash ? " to #{TRASH_DIR}" : ''}".fg(204))
|
1002
1445
|
end
|
1003
|
-
@tagged.clear
|
1004
|
-
refresh_right
|
1005
|
-
@pR.say("#{past_action} #{paths.size} items#{@trash ? " to #{TRASH_DIR}" : ''}".fg(204))
|
1006
1446
|
else
|
1007
1447
|
@pB.update = true
|
1008
1448
|
end
|
@@ -1327,16 +1767,76 @@ def page_up_right # {{{3
|
|
1327
1767
|
end
|
1328
1768
|
|
1329
1769
|
# CLIPBOARD COPY {{{2
|
1330
|
-
def
|
1770
|
+
def copy_path_primary # {{{3
|
1331
1771
|
if @selected
|
1332
|
-
|
1333
|
-
|
1334
|
-
|
1772
|
+
# Get the correct path in dual-pane mode
|
1773
|
+
path = get_selected_full_path
|
1774
|
+
if path
|
1775
|
+
copy_to_clipboard(path, 'primary')
|
1776
|
+
@pB.say(' Path copied to primary selection (middle-click to paste)')
|
1777
|
+
else
|
1778
|
+
@pB.say(' No selected item path to copy')
|
1779
|
+
end
|
1335
1780
|
else
|
1336
1781
|
@pB.say(' No selected item path to copy')
|
1337
1782
|
end
|
1338
1783
|
end
|
1339
1784
|
|
1785
|
+
def copy_path_clipboard # {{{3
|
1786
|
+
if @selected
|
1787
|
+
# Get the correct path in dual-pane mode
|
1788
|
+
path = get_selected_full_path
|
1789
|
+
if path
|
1790
|
+
copy_to_clipboard(path, 'clipboard')
|
1791
|
+
@pB.say(' Path copied to clipboard (Ctrl+V to paste)')
|
1792
|
+
else
|
1793
|
+
@pB.say(' No selected item path to copy')
|
1794
|
+
end
|
1795
|
+
else
|
1796
|
+
@pB.say(' No selected item path to copy')
|
1797
|
+
end
|
1798
|
+
end
|
1799
|
+
|
1800
|
+
def copy_to_clipboard(text, selection = 'clipboard') # {{{3
|
1801
|
+
# Robust clipboard copying with fallback mechanisms
|
1802
|
+
escaped_text = text.gsub("'", "'\"'\"'") # Escape single quotes for shell
|
1803
|
+
|
1804
|
+
# Try xsel first (often more reliable), then fall back to xclip
|
1805
|
+
success = false
|
1806
|
+
|
1807
|
+
if cmd?('xsel')
|
1808
|
+
cmd = case selection
|
1809
|
+
when 'primary' then "echo -n '#{escaped_text}' | xsel --primary --input"
|
1810
|
+
else "echo -n '#{escaped_text}' | xsel --clipboard --input"
|
1811
|
+
end
|
1812
|
+
success = system(cmd + ' 2>/dev/null')
|
1813
|
+
end
|
1814
|
+
|
1815
|
+
unless success
|
1816
|
+
# Fallback to xclip with timeout to prevent hanging
|
1817
|
+
cmd = case selection
|
1818
|
+
when 'primary' then "echo -n '#{escaped_text}' | timeout 2 xclip -selection primary -in"
|
1819
|
+
else "echo -n '#{escaped_text}' | timeout 2 xclip -selection clipboard -in"
|
1820
|
+
end
|
1821
|
+
success = system(cmd + ' 2>/dev/null')
|
1822
|
+
end
|
1823
|
+
|
1824
|
+
unless success
|
1825
|
+
@pB.say(' Warning: Clipboard copy may have failed')
|
1826
|
+
end
|
1827
|
+
end
|
1828
|
+
|
1829
|
+
def get_selected_full_path # {{{3
|
1830
|
+
# Helper function to get the correct full path in both single and dual-pane modes
|
1831
|
+
if @dual_pane
|
1832
|
+
active_dir = @active_pane == :left ? @pwd_left : @pwd_right
|
1833
|
+
active_selected = @active_pane == :left ? @selected_left : @selected_right
|
1834
|
+
return active_selected ? File.join(active_dir, active_selected) : nil
|
1835
|
+
else
|
1836
|
+
return @selected
|
1837
|
+
end
|
1838
|
+
end
|
1839
|
+
|
1340
1840
|
def copy_right # {{{3
|
1341
1841
|
clip = 'xclip -selection clipboard'
|
1342
1842
|
@pB.say(' Right pane copied to clipboard')
|
@@ -1530,26 +2030,133 @@ def ruby_debug # {{{3
|
|
1530
2030
|
end
|
1531
2031
|
|
1532
2032
|
# GENERIC FUNCTIONS {{{1
|
1533
|
-
def
|
1534
|
-
|
1535
|
-
|
2033
|
+
def get_cached_dirlist(dir, ls_options) # {{{2
|
2034
|
+
# TEMPORARILY DISABLED: Always bypass cache to debug flickering
|
2035
|
+
return nil
|
2036
|
+
|
2037
|
+
# Create cache key from directory and options
|
2038
|
+
# cache_key = "#{dir}:#{ls_options}"
|
2039
|
+
|
2040
|
+
# begin
|
2041
|
+
# dir_stat = File.stat(dir)
|
2042
|
+
# cache_with_time = "#{cache_key}:#{dir_stat.mtime.to_i}"
|
2043
|
+
# rescue
|
2044
|
+
# # Directory might not exist or be accessible
|
2045
|
+
# return nil
|
2046
|
+
# end
|
2047
|
+
|
2048
|
+
# Check if we have cached results for this directory
|
2049
|
+
# cached_result = @dir_cache[cache_with_time]
|
2050
|
+
# return cached_result if cached_result
|
2051
|
+
|
2052
|
+
# Clean old cache entries if cache is getting too large
|
2053
|
+
if @dir_cache_size >= @max_cache_entries
|
2054
|
+
@dir_cache.clear
|
2055
|
+
@dir_cache_size = 0
|
2056
|
+
end
|
2057
|
+
|
2058
|
+
# Generate new directory listing
|
2059
|
+
begin
|
2060
|
+
purels = command("ls #{Shellwords.escape(dir)} #{ls_options}").pure.split("\n")
|
2061
|
+
colorls = command("ls --color #{Shellwords.escape(dir)} #{ls_options} #{@lslong}").split("\n")
|
2062
|
+
|
2063
|
+
result = { purels: purels, colorls: colorls }
|
2064
|
+
@dir_cache[cache_with_time] = result
|
2065
|
+
@dir_cache_size += 1
|
2066
|
+
|
2067
|
+
# Clean up old entries for this directory
|
2068
|
+
@dir_cache.delete_if { |key, _| key.start_with?("#{dir}:") && key != cache_with_time }
|
2069
|
+
|
2070
|
+
result
|
2071
|
+
rescue => e
|
2072
|
+
# Return empty result on error
|
2073
|
+
{ purels: [], colorls: [] }
|
2074
|
+
end
|
2075
|
+
end
|
2076
|
+
|
2077
|
+
def get_cached_file_metadata(file_path) # {{{2
|
2078
|
+
return nil unless file_path && File.exist?(file_path)
|
2079
|
+
|
2080
|
+
begin
|
2081
|
+
file_stat = File.stat(file_path)
|
2082
|
+
cache_key = "#{file_path}:#{file_stat.mtime.to_i}:#{file_stat.size}"
|
2083
|
+
rescue
|
2084
|
+
return nil
|
2085
|
+
end
|
2086
|
+
|
2087
|
+
# Check if we have cached metadata
|
2088
|
+
cached_metadata = @metadata_cache[cache_key]
|
2089
|
+
return cached_metadata if cached_metadata
|
2090
|
+
|
2091
|
+
# Clean old cache entries if cache is getting too large
|
2092
|
+
if @metadata_cache_size >= @max_metadata_entries
|
2093
|
+
@metadata_cache.clear
|
2094
|
+
@metadata_cache_size = 0
|
2095
|
+
end
|
2096
|
+
|
2097
|
+
# Generate new metadata
|
2098
|
+
metadata = nil
|
2099
|
+
begin
|
2100
|
+
if file_path.match(@imagefile) && cmd?('identify')
|
2101
|
+
metadata = `identify -format " [%wx%h %m %[colorspace] %[bit-depth]-bit]" #{Shellwords.escape(file_path)} 2>/dev/null`.strip
|
2102
|
+
elsif file_path.match(@pdffile)
|
2103
|
+
info = `pdfinfo #{Shellwords.escape(file_path)} 2>/dev/null`
|
2104
|
+
pages = info[/^Pages:\s+(\d+)/, 1]
|
2105
|
+
metadata = pages ? " [#{pages} pages]" : nil
|
2106
|
+
end
|
2107
|
+
|
2108
|
+
# Cache the result (even if nil)
|
2109
|
+
@metadata_cache[cache_key] = metadata
|
2110
|
+
@metadata_cache_size += 1
|
2111
|
+
|
2112
|
+
# Clean up old entries for this file
|
2113
|
+
@metadata_cache.delete_if { |key, _| key.start_with?("#{file_path}:") && key != cache_key }
|
2114
|
+
|
2115
|
+
metadata
|
2116
|
+
rescue
|
2117
|
+
nil
|
2118
|
+
end
|
2119
|
+
end
|
2120
|
+
|
2121
|
+
def dirlist(left: true, directory: nil) # LIST DIRECTORIES {{{2
|
2122
|
+
current_index = @index || 0
|
2123
|
+
current_index = current_index.to_i
|
2124
|
+
|
1536
2125
|
if left
|
1537
|
-
dir
|
2126
|
+
dir = directory || Dir.pwd
|
1538
2127
|
width = @pL.w
|
1539
2128
|
else
|
1540
|
-
dir = if
|
2129
|
+
dir = if directory
|
2130
|
+
directory
|
2131
|
+
elsif @selected && File.directory?(@selected)
|
1541
2132
|
File.symlink?(@selected) ? File.realpath(@selected) : @selected.to_s
|
1542
2133
|
else
|
1543
2134
|
File.dirname(@selected)
|
1544
2135
|
end
|
1545
2136
|
width = @pR.w
|
1546
2137
|
end
|
1547
|
-
#
|
1548
|
-
|
1549
|
-
|
2138
|
+
# Use cached directory listing
|
2139
|
+
ls_options = "#{@lsbase} #{@lsall} #{@lsorder} #{@lsinvert} #{@lsuser}"
|
2140
|
+
cached = get_cached_dirlist(dir, ls_options)
|
2141
|
+
|
2142
|
+
if cached
|
2143
|
+
purels = cached[:purels]
|
2144
|
+
colorls = cached[:colorls]
|
2145
|
+
else
|
2146
|
+
# No cache available, generate fresh directory listing
|
2147
|
+
begin
|
2148
|
+
color_output = command("ls #{Shellwords.escape(dir)} --color=always #{ls_options}")
|
2149
|
+
pure_output = command("ls #{Shellwords.escape(dir)} #{ls_options}")
|
2150
|
+
purels = pure_output.pure.split("\n")
|
2151
|
+
colorls = color_output.split("\n")
|
2152
|
+
rescue => e
|
2153
|
+
purels = []
|
2154
|
+
colorls = []
|
2155
|
+
end
|
2156
|
+
end
|
1550
2157
|
colorls.shift if colorls[0]&.strip == "#{dir}:"
|
1551
2158
|
colorls.shift if colorls[0]&.match?(/^total/)
|
1552
|
-
if left && @orderchange # Keep the same @selected even when we re-sort
|
2159
|
+
if left && @orderchange && !directory # Keep the same @selected even when we re-sort (only for single-pane)
|
1553
2160
|
basename = File.basename(@selected.to_s)
|
1554
2161
|
new_idx = purels.index(basename)
|
1555
2162
|
@index = new_idx if new_idx
|
@@ -1568,37 +2175,113 @@ def dirlist(left: true) # LIST DIRECTORIES {{{2
|
|
1568
2175
|
t = entries.transpose
|
1569
2176
|
purels = t[0] || []
|
1570
2177
|
ls = t[1] || []
|
1571
|
-
|
1572
|
-
|
1573
|
-
|
1574
|
-
@selected
|
1575
|
-
|
1576
|
-
|
1577
|
-
|
1578
|
-
|
1579
|
-
|
1580
|
-
|
1581
|
-
|
1582
|
-
|
2178
|
+
# Only update global variables in single-pane mode
|
2179
|
+
if left && !directory
|
2180
|
+
@files = purels
|
2181
|
+
# Update @selected & @fileattr for left pane
|
2182
|
+
if purels[@index]
|
2183
|
+
@selected = Dir.pwd + '/' + purels[@index]
|
2184
|
+
sfile = @selected.dup
|
2185
|
+
sfile += '/' if File.directory?(@selected)
|
2186
|
+
slsl_cmd = "ls -ldHlh --time-style=long-iso #{Shellwords.escape(sfile)}"
|
2187
|
+
slsl = command(slsl_cmd)
|
2188
|
+
a = slsl.split
|
2189
|
+
@fileattr = a.size >= 7 ? "#{a[2]}:#{a[3]} #{a[0]} #{a[4]} #{a[5]} #{a[6]}" : ''
|
2190
|
+
end
|
2191
|
+
end
|
2192
|
+
# Map & decorate each colored line - optimized version
|
2193
|
+
base_dir = left ? (directory || Dir.pwd) : dir
|
2194
|
+
search_regex = @searched.empty? ? nil : /#{@searched}/
|
2195
|
+
|
1583
2196
|
ls.map!.with_index do |el, i|
|
1584
2197
|
n = el.to_s.clean_ansi
|
1585
2198
|
n = n.shorten(width - 5).inject('…', -1) if n.pure.length > width - 6
|
1586
2199
|
raw_name = (purels[i] || '').strip
|
1587
|
-
|
1588
|
-
|
1589
|
-
|
1590
|
-
|
1591
|
-
|
2200
|
+
next n if raw_name.empty?
|
2201
|
+
|
2202
|
+
fullpath = "#{base_dir}/#{raw_name}"
|
2203
|
+
|
2204
|
+
# Batch file system checks to reduce system calls
|
2205
|
+
is_symlink = File.symlink?(fullpath)
|
2206
|
+
is_directory = File.directory?(fullpath)
|
2207
|
+
|
2208
|
+
n = n.inject('@', -1) if is_symlink
|
2209
|
+
n = n.inject('/', -1) if is_directory
|
2210
|
+
n = n.bg(238) if search_regex && raw_name.match(search_regex)
|
1592
2211
|
n = n.r if @tagged.include?(fullpath)
|
2212
|
+
|
1593
2213
|
if left
|
1594
|
-
|
2214
|
+
if i == current_index
|
2215
|
+
n = '→ ' + n.u # Default terminal color
|
2216
|
+
else
|
2217
|
+
n = ' ' + n
|
2218
|
+
end
|
1595
2219
|
end
|
1596
2220
|
n
|
1597
2221
|
end
|
1598
2222
|
ls.join("\n")
|
1599
2223
|
end
|
1600
2224
|
|
2225
|
+
def current_render_state # {{{2
|
2226
|
+
# Create a hash representing current state for conditional rendering
|
2227
|
+
# Note: pane_updates removed to avoid oscillation - panes update themselves when needed
|
2228
|
+
if @dual_pane
|
2229
|
+
# Dual-pane specific state tracking
|
2230
|
+
{
|
2231
|
+
dual_pane: true,
|
2232
|
+
active_pane: @active_pane,
|
2233
|
+
pwd_left: @pwd_left,
|
2234
|
+
pwd_right: @pwd_right,
|
2235
|
+
index_left: @index_left,
|
2236
|
+
index_right: @index_right,
|
2237
|
+
selected_left: @selected_left,
|
2238
|
+
selected_right: @selected_right,
|
2239
|
+
tagged_count: @tagged.size,
|
2240
|
+
preview: @preview,
|
2241
|
+
showimage: @showimage,
|
2242
|
+
searched: @searched,
|
2243
|
+
filters: [@filter, @filtered].join,
|
2244
|
+
tabs_state: [@tabs.size, @current_tab, @tabs.map { |t| t[:name] }.join('|')]
|
2245
|
+
}
|
2246
|
+
else
|
2247
|
+
# Single-pane state tracking
|
2248
|
+
{
|
2249
|
+
dual_pane: false,
|
2250
|
+
dir: Dir.pwd,
|
2251
|
+
index: @index,
|
2252
|
+
files_mtime: File.mtime(Dir.pwd).to_i,
|
2253
|
+
selected: @selected,
|
2254
|
+
tagged_count: @tagged.size,
|
2255
|
+
preview: @preview,
|
2256
|
+
showimage: @showimage,
|
2257
|
+
searched: @searched,
|
2258
|
+
filters: [@filter, @filtered].join,
|
2259
|
+
tabs_state: [@tabs.size, @current_tab, @tabs.map { |t| t[:name] }.join('|')]
|
2260
|
+
}
|
2261
|
+
end
|
2262
|
+
end
|
2263
|
+
|
2264
|
+
def needs_render? # {{{2
|
2265
|
+
current_state = current_render_state
|
2266
|
+
state_changed = @last_render_state != current_state
|
2267
|
+
|
2268
|
+
# Also check if any panes need updating (for refresh operations)
|
2269
|
+
panes_need_update = @pL&.update || @pR&.update || @pT&.update || @pB&.update
|
2270
|
+
if @dual_pane
|
2271
|
+
panes_need_update ||= @pLeft&.update || @pRight&.update || @pPreview&.update
|
2272
|
+
end
|
2273
|
+
|
2274
|
+
needs_update = state_changed || panes_need_update
|
2275
|
+
@last_render_state = current_state if state_changed
|
2276
|
+
needs_update
|
2277
|
+
end
|
2278
|
+
|
1601
2279
|
def render # RENDER ALL PANES {{{2
|
2280
|
+
return unless needs_render?
|
2281
|
+
|
2282
|
+
# TEMPORARILY DISABLED: Use batch updates for rcurses 4.9.0+ performance improvement
|
2283
|
+
# Rcurses.batch_refresh do
|
2284
|
+
|
1602
2285
|
# LEFT pane {{{3
|
1603
2286
|
if @pL.update
|
1604
2287
|
lefttext = @pL.text
|
@@ -1623,16 +2306,49 @@ def render # RENDER ALL PANES {{{2
|
|
1623
2306
|
@index = @max_index if @index > @max_index
|
1624
2307
|
@pL.refresh unless @pL.text == lefttext
|
1625
2308
|
end
|
2309
|
+
|
2310
|
+
# DUAL-PANE render section removed - using tabs for multi-directory navigation
|
1626
2311
|
|
1627
|
-
# RIGHT pane {{{3
|
2312
|
+
# RIGHT pane (or Preview pane in dual-pane mode) {{{3
|
1628
2313
|
if @pR.update && @preview
|
1629
2314
|
showimage('clear') if @image; @image = false
|
1630
2315
|
righttext = @pR.text
|
1631
|
-
|
1632
|
-
|
2316
|
+
|
2317
|
+
# In dual-pane mode, construct full path from active pane
|
2318
|
+
if @dual_pane
|
2319
|
+
active_dir = @active_pane == :left ? @pwd_left : @pwd_right
|
2320
|
+
active_selected = @active_pane == :left ? @selected_left : @selected_right
|
2321
|
+
|
2322
|
+
if active_selected && !active_selected.empty?
|
2323
|
+
# Ensure we're only using the basename for the selected file
|
2324
|
+
selected_basename = File.basename(active_selected)
|
2325
|
+
full_selected_path = File.join(active_dir, selected_basename)
|
2326
|
+
|
2327
|
+
# Temporarily set @selected for existing preview functions to work
|
2328
|
+
old_selected = @selected
|
2329
|
+
@selected = full_selected_path
|
2330
|
+
|
2331
|
+
if File.exist?(full_selected_path) && File.directory?(full_selected_path)
|
2332
|
+
@pR.text = dirlist(left: false)
|
2333
|
+
else
|
2334
|
+
# Use existing showcontent function for files
|
2335
|
+
showcontent
|
2336
|
+
end
|
2337
|
+
|
2338
|
+
# Restore original @selected
|
2339
|
+
@selected = old_selected
|
2340
|
+
else
|
2341
|
+
@pR.text = "No file selected"
|
2342
|
+
end
|
1633
2343
|
else
|
1634
|
-
|
2344
|
+
# Original single-pane logic
|
2345
|
+
if @selected && File.directory?(@selected)
|
2346
|
+
@pR.text = dirlist(left: false)
|
2347
|
+
else
|
2348
|
+
showcontent
|
2349
|
+
end
|
1635
2350
|
end
|
2351
|
+
|
1636
2352
|
@pR.full_refresh unless @pR.text == righttext || @image
|
1637
2353
|
end
|
1638
2354
|
|
@@ -1646,14 +2362,17 @@ def render # RENDER ALL PANES {{{2
|
|
1646
2362
|
end
|
1647
2363
|
# File attributes
|
1648
2364
|
text += " (#{@fileattr})" if defined?(@fileattr)
|
1649
|
-
# Image or PDF metadata
|
2365
|
+
# Image or PDF metadata using cache
|
1650
2366
|
begin
|
1651
|
-
|
1652
|
-
|
1653
|
-
|
1654
|
-
|
1655
|
-
|
2367
|
+
cached_meta = get_cached_file_metadata(@selected)
|
2368
|
+
if cached_meta
|
2369
|
+
text += cached_meta
|
2370
|
+
elsif @selected&.match(@imagefile) && cmd?('identify')
|
2371
|
+
# Fallback for non-cached image metadata
|
2372
|
+
meta = `identify -format " [%wx%h %m %[colorspace] %[bit-depth]-bit]" #{Shellwords.escape(@selected)} 2>/dev/null`
|
2373
|
+
text += meta
|
1656
2374
|
elsif @selected&.match(@pdffile)
|
2375
|
+
# Fallback for non-cached PDF metadata
|
1657
2376
|
info = `pdfinfo #{Shellwords.escape(@selected)} 2>/dev/null`
|
1658
2377
|
pages = info[/^Pages:\s+(\d+)/, 1]
|
1659
2378
|
size = info[/^Page size:.*\((.*)\)/, 1]
|
@@ -1672,7 +2391,22 @@ def render # RENDER ALL PANES {{{2
|
|
1672
2391
|
text += ' [Denied]'
|
1673
2392
|
end
|
1674
2393
|
end
|
2394
|
+
|
2395
|
+
# Add tab indicator to the right side if there are multiple tabs
|
2396
|
+
if @tabs.size > 1
|
2397
|
+
tab_indicator = "[#{@current_tab + 1}/#{@tabs.size}]"
|
2398
|
+
# Calculate available space for the main text (including space for right-justification)
|
2399
|
+
available_width = @pT.w - tab_indicator.length - 1 # -1 for space between text and indicator
|
2400
|
+
if text.length > available_width
|
2401
|
+
text = text[0, available_width - 3] + "..."
|
2402
|
+
end
|
2403
|
+
# Right-justify the tab indicator by padding with spaces
|
2404
|
+
padding = @pT.w - text.length - tab_indicator.length
|
2405
|
+
text += " " * padding + tab_indicator
|
2406
|
+
end
|
2407
|
+
|
1675
2408
|
@pT.text = text.b
|
2409
|
+
|
1676
2410
|
@pT.bg = @topmatch.find { |name, _| name.empty? || Dir.pwd.include?(name) }&.last
|
1677
2411
|
@pT.refresh unless @pT.text == toptext
|
1678
2412
|
end
|
@@ -1688,6 +2422,8 @@ def render # RENDER ALL PANES {{{2
|
|
1688
2422
|
@pB.text = info
|
1689
2423
|
@pB.refresh unless @pB.text == bottomtext
|
1690
2424
|
end
|
2425
|
+
|
2426
|
+
# end # Rcurses.batch_refresh - TEMPORARILY DISABLED
|
1691
2427
|
end
|
1692
2428
|
|
1693
2429
|
def refresh # {{{2
|
@@ -1696,16 +2432,41 @@ def refresh # {{{2
|
|
1696
2432
|
@p0.clear
|
1697
2433
|
@pT.w = @w
|
1698
2434
|
@pT.clear; @pT.update = true
|
2435
|
+
@pTab.w = @w # Keep tab overlay same width as top pane
|
1699
2436
|
@pB.w = @w
|
1700
2437
|
@pB.y = @h
|
1701
2438
|
@pB.clear; @pB.update = true
|
1702
|
-
@
|
1703
|
-
|
1704
|
-
|
1705
|
-
|
1706
|
-
|
1707
|
-
|
1708
|
-
|
2439
|
+
if @dual_pane && @pLeft && @pRight && @pPreview
|
2440
|
+
# Update dual-pane layout with width control via @width variable
|
2441
|
+
total_width = @w - 6 # Account for spacing and borders (2 + 2 + 2 spacing)
|
2442
|
+
# Map @width (2-7) to dir_panes_ratio (0.5 to 0.33) - balanced default
|
2443
|
+
dir_panes_ratio = [0.5 - (@width - 2) * 0.034, 0.33].max
|
2444
|
+
dir_panes_width = (total_width * dir_panes_ratio).to_i
|
2445
|
+
left_width = dir_panes_width / 2
|
2446
|
+
preview_width = total_width - dir_panes_width
|
2447
|
+
|
2448
|
+
@pLeft.w = left_width
|
2449
|
+
@pLeft.h = @h - 4
|
2450
|
+
@pRight.x = left_width + 4 # 2 columns spacing
|
2451
|
+
@pRight.w = left_width
|
2452
|
+
@pRight.h = @h - 4
|
2453
|
+
@pPreview.x = 2*left_width + 6 # 2 columns spacing
|
2454
|
+
@pPreview.w = preview_width
|
2455
|
+
@pPreview.h = @h - 4
|
2456
|
+
|
2457
|
+
@pLeft.clear; @pLeft.update = true
|
2458
|
+
@pRight.clear; @pRight.update = true
|
2459
|
+
@pPreview.clear; @pPreview.update = true
|
2460
|
+
else
|
2461
|
+
# Update single-pane layout
|
2462
|
+
@pL.w = (@w - 4) * @width / 10
|
2463
|
+
@pL.h = @h - 4
|
2464
|
+
@pR.x = @pL.w + 4
|
2465
|
+
@pR.w = @w - @pL.w - 4
|
2466
|
+
@pR.h = @h - 4
|
2467
|
+
@pL.clear; @pL.update = true
|
2468
|
+
@pR.clear; @pR.update = true
|
2469
|
+
end
|
1709
2470
|
@pCmd.y = @h
|
1710
2471
|
@pCmd.w = @w
|
1711
2472
|
@pRuby.y = @h
|
@@ -1784,25 +2545,31 @@ def command(cmd, timeout: 5, return_both: false) # {{{2
|
|
1784
2545
|
# Drain both pipes in background threads
|
1785
2546
|
out_reader = Thread.new { out_buf << stdout.read until stdout.eof? }
|
1786
2547
|
err_reader = Thread.new { err_buf << stderr.read until stderr.eof? }
|
1787
|
-
|
1788
|
-
|
1789
|
-
|
1790
|
-
|
1791
|
-
|
1792
|
-
|
1793
|
-
|
1794
|
-
|
1795
|
-
|
1796
|
-
|
1797
|
-
|
1798
|
-
|
2548
|
+
begin
|
2549
|
+
if timeout
|
2550
|
+
unless wait_thr.join(timeout)
|
2551
|
+
# Timed out → kill everything
|
2552
|
+
Process.kill('TERM', pid) rescue nil
|
2553
|
+
sleep 0.1
|
2554
|
+
Process.kill('KILL', pid) rescue nil
|
2555
|
+
Process.wait(pid) rescue nil
|
2556
|
+
out_reader.kill
|
2557
|
+
err_reader.kill
|
2558
|
+
showimage('clear') if @image
|
2559
|
+
@pR.say('Error: Command timed out.'.fg(196))
|
2560
|
+
return return_both ? ['', "Error: Command timed out.\n"] : ''
|
2561
|
+
end
|
2562
|
+
else
|
2563
|
+
wait_thr.join
|
1799
2564
|
end
|
1800
|
-
|
1801
|
-
|
2565
|
+
# Ensure we've captured all output
|
2566
|
+
out_reader.join(1) # Add timeout to prevent hanging
|
2567
|
+
err_reader.join(1) # Add timeout to prevent hanging
|
2568
|
+
ensure
|
2569
|
+
# Clean up threads if they're still running
|
2570
|
+
out_reader.kill if out_reader.alive?
|
2571
|
+
err_reader.kill if err_reader.alive?
|
1802
2572
|
end
|
1803
|
-
# Ensure we've captured all output
|
1804
|
-
out_reader.join
|
1805
|
-
err_reader.join
|
1806
2573
|
end
|
1807
2574
|
if return_both
|
1808
2575
|
[out_buf, err_buf]
|
@@ -1832,8 +2599,16 @@ end
|
|
1832
2599
|
|
1833
2600
|
def copy_move_link(type) # COPY OR MOVE TAGGED ITEMS {{{2
|
1834
2601
|
@tagged.uniq!
|
2602
|
+
|
2603
|
+
# Determine destination directory based on mode
|
2604
|
+
dest_dir = if @dual_pane
|
2605
|
+
@active_pane == :left ? @pwd_left : @pwd_right
|
2606
|
+
else
|
2607
|
+
Dir.pwd
|
2608
|
+
end
|
2609
|
+
|
1835
2610
|
@tagged.each do |item|
|
1836
|
-
dest = File.join(
|
2611
|
+
dest = File.join(dest_dir, File.basename(item))
|
1837
2612
|
dest += '1' if File.exist?(dest)
|
1838
2613
|
while File.exist?(dest)
|
1839
2614
|
# Replace the last character (presumed to be a digit) by incrementing it
|
@@ -1856,6 +2631,19 @@ def copy_move_link(type) # COPY OR MOVE TAGGED ITEMS {{{2
|
|
1856
2631
|
end
|
1857
2632
|
end
|
1858
2633
|
@tagged = []
|
2634
|
+
|
2635
|
+
# Set update flags for proper refresh - let render system handle the actual refresh
|
2636
|
+
if @dual_pane
|
2637
|
+
# Update the destination pane
|
2638
|
+
if @active_pane == :left
|
2639
|
+
@pLeft.update = true
|
2640
|
+
else
|
2641
|
+
@pRight.update = true
|
2642
|
+
end
|
2643
|
+
# Also update the preview pane
|
2644
|
+
@pPreview.update = true if @pPreview
|
2645
|
+
end
|
2646
|
+
|
1859
2647
|
render
|
1860
2648
|
end
|
1861
2649
|
|
@@ -1977,8 +2765,10 @@ def open_selected(html = nil) # OPEN SELECTED FILE {{{2
|
|
1977
2765
|
Cursor.show
|
1978
2766
|
editor = ENV.fetch('EDITOR', 'vi') # Launch $EDITOR on the real TTY
|
1979
2767
|
system("#{editor} #{Shellwords.escape(@selected)}")
|
2768
|
+
system('stty raw -echo isig < /dev/tty')
|
1980
2769
|
$stdin.raw!
|
1981
2770
|
$stdin.echo = false
|
2771
|
+
Rcurses.init! # Reinitialize rcurses to fix input handling
|
1982
2772
|
Cursor.hide
|
1983
2773
|
Rcurses.clear_screen # Redraw RTFM
|
1984
2774
|
refresh
|
@@ -2091,9 +2881,20 @@ def showcontent # SHOW CONTENTS IN THE RIGHT WINDOW {{{2
|
|
2091
2881
|
elsif pattern # Nil template → image or video
|
2092
2882
|
case @selected
|
2093
2883
|
when /\.(?:png|jpe?g|bmp|gif|webp|tiff?)$/i
|
2094
|
-
|
2095
|
-
|
2096
|
-
|
2884
|
+
# Allow larger image files up to 50MB
|
2885
|
+
begin
|
2886
|
+
file_size = File.size(@selected)
|
2887
|
+
if file_size > 50_000_000 # 50MB limit for images
|
2888
|
+
@pR.say("Image too large for preview (#{(file_size / 1024.0 / 1024.0).round(1)}MB > 50MB limit)")
|
2889
|
+
else
|
2890
|
+
showimage(Shellwords.escape(@selected))
|
2891
|
+
@image = true
|
2892
|
+
end
|
2893
|
+
rescue => e
|
2894
|
+
@pR.say("Error checking image size: #{e}")
|
2895
|
+
end
|
2896
|
+
when /\.(?:mpg|mpeg|avi|mov|mkv|mp4|webm|flv|wmv|m4v)$/i
|
2897
|
+
# Generate video thumbnail regardless of size
|
2097
2898
|
tn = '/tmp/rtfm_video_tn.jpg'
|
2098
2899
|
showcommand("ffmpegthumbnailer -s 1200 -i #{Shellwords.escape(@selected)} -o #{Shellwords.escape(tn)} 2>/dev/null")
|
2099
2900
|
showimage(tn)
|
@@ -2106,20 +2907,76 @@ def showcontent # SHOW CONTENTS IN THE RIGHT WINDOW {{{2
|
|
2106
2907
|
if @selected&.match(@pdffile)
|
2107
2908
|
@pR.say("No preview available for #{@selected}")
|
2108
2909
|
else
|
2109
|
-
text
|
2110
|
-
|
2111
|
-
|
2112
|
-
|
2113
|
-
|
2114
|
-
|
2115
|
-
|
2910
|
+
# Enhanced text file preview with partial loading for large files
|
2911
|
+
begin
|
2912
|
+
file_size = File.size(@selected)
|
2913
|
+
|
2914
|
+
# For text files, we can preview them partially even if large
|
2915
|
+
if file_size > 1_000_000 # Files larger than 1MB
|
2916
|
+
# Calculate how many lines fit in the preview pane
|
2917
|
+
preview_lines = @pR.h * 3 # Allow for 3 screens worth of preview
|
2918
|
+
|
2919
|
+
# Read only the first portion of the file
|
2920
|
+
preview_content = nil
|
2921
|
+
File.open(@selected, 'r:UTF-8') do |file|
|
2922
|
+
lines = []
|
2923
|
+
line_count = 0
|
2924
|
+
|
2925
|
+
file.each_line do |line|
|
2926
|
+
lines << line
|
2927
|
+
line_count += 1
|
2928
|
+
break if line_count >= preview_lines
|
2929
|
+
end
|
2930
|
+
|
2931
|
+
preview_content = lines.join
|
2932
|
+
end
|
2933
|
+
|
2934
|
+
if preview_content && preview_content.valid_encoding?
|
2935
|
+
# Show preview with truncation notice
|
2936
|
+
@pR.say("=== Showing first #{preview_lines} lines of large file (#{(file_size / 1024.0 / 1024.0).round(1)}MB) ===\n\n")
|
2937
|
+
|
2938
|
+
if @batuse
|
2939
|
+
# Write temp file for bat to process
|
2940
|
+
require 'tempfile'
|
2941
|
+
temp = Tempfile.new(['rtfm_preview', File.extname(@selected)])
|
2942
|
+
temp.write(preview_content)
|
2943
|
+
temp.close
|
2944
|
+
|
2945
|
+
begin
|
2946
|
+
showcommand("#{@bat} -n --color=always #{Shellwords.escape(temp.path)}")
|
2947
|
+
rescue
|
2948
|
+
@pR.say(preview_content)
|
2949
|
+
ensure
|
2950
|
+
temp.unlink
|
2951
|
+
end
|
2952
|
+
else
|
2953
|
+
@pR.say(preview_content)
|
2954
|
+
end
|
2955
|
+
|
2956
|
+
@pR.say("\n\n=== File truncated for preview ===")
|
2957
|
+
else
|
2958
|
+
@pR.say("File appears to be binary or has invalid encoding")
|
2116
2959
|
end
|
2117
2960
|
else
|
2118
|
-
|
2961
|
+
# Small files - read entirely as before
|
2962
|
+
text = File.read(@selected).force_encoding('UTF-8') rescue ''
|
2963
|
+
if text.valid_encoding?
|
2964
|
+
if @batuse
|
2965
|
+
begin
|
2966
|
+
showcommand("#{@bat} -n --color=always #{Shellwords.escape(@selected)}")
|
2967
|
+
rescue
|
2968
|
+
showcommand("cat #{Shellwords.escape(@selected)}")
|
2969
|
+
end
|
2970
|
+
else
|
2971
|
+
showcommand("cat #{Shellwords.escape(@selected)}")
|
2972
|
+
end
|
2973
|
+
else
|
2974
|
+
@pR.say("No preview available for #{@selected}")
|
2975
|
+
end
|
2119
2976
|
end
|
2120
|
-
|
2121
|
-
|
2122
|
-
|
2977
|
+
rescue => e
|
2978
|
+
@pR.say("Error previewing file: #{e}")
|
2979
|
+
end
|
2123
2980
|
end
|
2124
2981
|
end
|
2125
2982
|
end
|
@@ -2149,6 +3006,7 @@ def showimage(image) # SHOW THE SELECTED IMAGE IN THE RIGHT WINDOW {{{2
|
|
2149
3006
|
img_max_w += char_w + 2
|
2150
3007
|
img_max_h += 2
|
2151
3008
|
`echo "6;#{img_x};#{img_y};#{img_max_w};#{img_max_h};\n4;\n3;" | #{@imgdisplay} 2>/dev/null`
|
3009
|
+
@current_image_path = nil # Clear tracking when image is cleared
|
2152
3010
|
else
|
2153
3011
|
# Use the already-escaped image for shell commands
|
2154
3012
|
dimensions = `identify -format "%wx%h" #{image} 2>/dev/null`
|
@@ -2167,6 +3025,7 @@ def showimage(image) # SHOW THE SELECTED IMAGE IN THE RIGHT WINDOW {{{2
|
|
2167
3025
|
# Fix: Unescape the filename for w3mimgdisplay protocol, then quote it
|
2168
3026
|
unescaped = image.gsub(/\\(.)/, '\1') # Remove shell escaping
|
2169
3027
|
`echo "0;1;#{img_x};#{img_y};#{img_w};#{img_h};;;;;\"#{unescaped}\"\n4;\n3;" | #{@imgdisplay} 2>/dev/null`
|
3028
|
+
@current_image_path = unescaped # Track currently displayed image
|
2170
3029
|
end
|
2171
3030
|
rescue
|
2172
3031
|
@pR.text = 'Error showing image'
|
@@ -2204,9 +3063,25 @@ end
|
|
2204
3063
|
# p = Pane.new( x, y, width, height, fg, bg)
|
2205
3064
|
@p0 = Pane.new( 1, 1, @w, @h, 0, 0)
|
2206
3065
|
@pT = Pane.new( 1, 1, @w, 1, 0, @topcolor)
|
3066
|
+
@pTab = Pane.new( 1, 1, @w, 1, 255, 238) # Tab overlay pane (same coords as @pT)
|
2207
3067
|
@pB = Pane.new( 1, @h, @w, 1, 252, @bottomcolor)
|
2208
|
-
|
2209
|
-
|
3068
|
+
# Create panes based on mode
|
3069
|
+
if @dual_pane
|
3070
|
+
# Dual-pane layout: two directory panes + one preview pane
|
3071
|
+
left_width = (@w - 6) / 3 # Each directory pane gets 1/3 of width
|
3072
|
+
@pLeft = Pane.new( 2, 3, left_width, @h - 4, 226, 0) # Yellow fg for active
|
3073
|
+
@pRight = Pane.new(left_width + 4, 3, left_width, @h - 4, 15, 0) # Normal fg for inactive
|
3074
|
+
@pPreview = Pane.new(2*left_width + 6, 3, @w - 2*left_width - 6, @h - 4, 255, 0)
|
3075
|
+
@pLeft.border = true
|
3076
|
+
@pRight.border = true
|
3077
|
+
@pPreview.border = false
|
3078
|
+
@pL = @pLeft
|
3079
|
+
@pR = @pPreview
|
3080
|
+
else
|
3081
|
+
# Single-pane layout (default)
|
3082
|
+
@pL = Pane.new( 2, 3, (@w - 4)*@width/10, @h - 4, 15, 0)
|
3083
|
+
@pR = Pane.new(@pL.w + 4, 3, @w - @pL.w - 4, @h - 4, 255, 0)
|
3084
|
+
end
|
2210
3085
|
## Create special panes
|
2211
3086
|
@pCmd = Pane.new( 1, @h, @w, 1, 255, @cmdcolor)
|
2212
3087
|
@pSearch = Pane.new( 1, @h, @w, 1, 255, @searchcolor)
|
@@ -2216,6 +3091,7 @@ end
|
|
2216
3091
|
#checkpoint("Panes created")
|
2217
3092
|
|
2218
3093
|
## Set pane properties {{{2
|
3094
|
+
@pTab.update = true
|
2219
3095
|
@pT.update = true
|
2220
3096
|
@pL.update = true
|
2221
3097
|
@pR.update = true
|
@@ -2250,17 +3126,18 @@ $stdin.getc while $stdin.wait_readable(0)
|
|
2250
3126
|
#checkpoint("Program started")
|
2251
3127
|
loop do
|
2252
3128
|
@dir_old = Dir.pwd
|
3129
|
+
|
2253
3130
|
# redraw, but ignore TTY‐focus errors
|
2254
3131
|
begin
|
2255
3132
|
render
|
2256
3133
|
rescue Errno::EIO
|
2257
|
-
#
|
3134
|
+
# Note: rcurses 4.9.0+ has enhanced error handling, reducing need for this
|
2258
3135
|
end
|
2259
3136
|
# read key, but ignore TTY-focus errors
|
2260
3137
|
begin
|
2261
3138
|
getkey
|
2262
3139
|
rescue Errno::EIO
|
2263
|
-
#
|
3140
|
+
# Note: rcurses 4.9.0+ has enhanced error handling, reducing need for this
|
2264
3141
|
end
|
2265
3142
|
# If cwd was deleted externally, jump home
|
2266
3143
|
begin
|
@@ -2269,7 +3146,9 @@ loop do
|
|
2269
3146
|
Dir.chdir
|
2270
3147
|
end
|
2271
3148
|
# restore index if we cd'd
|
2272
|
-
|
3149
|
+
if Dir.pwd != @dir_old
|
3150
|
+
@index = @directory[Dir.pwd] || 0
|
3151
|
+
end
|
2273
3152
|
unless @navi.empty?
|
2274
3153
|
command(@navi)
|
2275
3154
|
@navi = ''
|