hyperlist 1.1.0 → 1.1.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 +15 -0
- data/hyperlist +754 -373
- data/hyperlist.gemspec +1 -1
- data/test.hl +4 -19
- metadata +2 -2
data/hyperlist
CHANGED
@@ -70,7 +70,7 @@ class HyperListApp
|
|
70
70
|
include Rcurses::Input
|
71
71
|
include Rcurses::Cursor
|
72
72
|
|
73
|
-
VERSION = "1.1.
|
73
|
+
VERSION = "1.1.2"
|
74
74
|
|
75
75
|
def initialize(filename = nil)
|
76
76
|
@filename = filename ? File.expand_path(filename) : nil
|
@@ -105,6 +105,7 @@ class HyperListApp
|
|
105
105
|
@macro_key = nil # Key for current macro
|
106
106
|
@encryption_key = nil # Store derived encryption key
|
107
107
|
@encrypted_lines = {} # Track which lines are encrypted
|
108
|
+
@st_underline_mode = 0 # 0: none, 1: underline states, 2: underline transitions
|
108
109
|
@split_view = false
|
109
110
|
@split_items = [] # Second view items
|
110
111
|
@split_current = 0 # Second view cursor
|
@@ -149,22 +150,29 @@ class HyperListApp
|
|
149
150
|
if @split_view
|
150
151
|
# Split view layout - no header, more space for content
|
151
152
|
split_width = @cols / 2
|
152
|
-
# Main content panes -
|
153
|
-
|
154
|
-
@
|
153
|
+
# Main content panes - use nil colors to act as pass-through for pre-colored text
|
154
|
+
# This is how RTFM works - it passes colored command output to panes with nil colors
|
155
|
+
@main = Pane.new(1, 1, split_width - 1, @rows - 1, nil, nil)
|
156
|
+
@split_pane = Pane.new(split_width + 1, 1, split_width - 1, @rows - 1, nil, nil)
|
155
157
|
# Footer: Use @rows for y-position (this puts it at the actual bottom)
|
156
|
-
@footer = Pane.new(
|
158
|
+
@footer = Pane.new(1, @rows, @cols, 1, 15, 8)
|
157
159
|
|
158
|
-
# Add separator
|
159
|
-
@separator = Pane.new(split_width,
|
160
|
-
|
160
|
+
# Add separator with explicit background to overwrite emoji overflow
|
161
|
+
@separator = Pane.new(split_width, 1, 1, @rows - 1, 15, 0)
|
162
|
+
# Build separator with explicit clearing
|
163
|
+
separator_lines = []
|
164
|
+
(@rows - 1).times do
|
165
|
+
separator_lines << "│"
|
166
|
+
end
|
167
|
+
@separator.text = separator_lines.join("\n")
|
161
168
|
@separator.refresh
|
162
169
|
else
|
163
170
|
# Single view layout - no header, more space for content
|
164
|
-
#
|
165
|
-
|
171
|
+
# Use nil colors like RTFM to avoid ANSI wrapping issues
|
172
|
+
# This prevents the corruption that happens with narrow terminals
|
173
|
+
@main = Pane.new(1, 1, @cols, @rows - 1, nil, nil)
|
166
174
|
# Footer: Use @rows for y-position (this puts it at the actual bottom)
|
167
|
-
@footer = Pane.new(
|
175
|
+
@footer = Pane.new(1, @rows, @cols, 1, 15, 8)
|
168
176
|
end
|
169
177
|
end
|
170
178
|
|
@@ -558,38 +566,28 @@ class HyperListApp
|
|
558
566
|
level = item["level"]
|
559
567
|
indent = " " * level
|
560
568
|
|
561
|
-
#
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
#
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
#
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
# Italic /text/
|
585
|
-
text = text.gsub(/\/([^\/]+)\//) do
|
586
|
-
"\u0001ITALICSTART\u0002#{$1}\u0001ITALICEND\u0002"
|
587
|
-
end
|
588
|
-
|
589
|
-
# Underline _text_
|
590
|
-
text = text.gsub(/_([^_]+)_/) do
|
591
|
-
"\u0001UNDERSTART\u0002#{$1}\u0001UNDEREND\u0002"
|
592
|
-
end
|
569
|
+
# Process the line
|
570
|
+
# We'll apply formatting BEFORE escaping HTML entities
|
571
|
+
text = original_text
|
572
|
+
|
573
|
+
# Apply all formatting with placeholder markers
|
574
|
+
# Use unique markers that won't appear in normal text
|
575
|
+
|
576
|
+
# Markdown formatting MUST come first before other replacements
|
577
|
+
# Bold *text*
|
578
|
+
text = text.gsub(/\*([^*]+)\*/) do
|
579
|
+
"\u0001BOLDSTART\u0002#{$1}\u0001BOLDEND\u0002"
|
580
|
+
end
|
581
|
+
|
582
|
+
# Italic /text/
|
583
|
+
text = text.gsub(/\/([^\/]+)\//) do
|
584
|
+
"\u0001ITALICSTART\u0002#{$1}\u0001ITALICEND\u0002"
|
585
|
+
end
|
586
|
+
|
587
|
+
# Underline _text_
|
588
|
+
text = text.gsub(/_([^_]+)_/) do
|
589
|
+
"\u0001UNDERSTART\u0002#{$1}\u0001UNDEREND\u0002"
|
590
|
+
end
|
593
591
|
|
594
592
|
# Checkboxes at start
|
595
593
|
text = text.sub(/^\[X\]/i, "\u0001CBCHECKED\u0002")
|
@@ -663,7 +661,6 @@ class HyperListApp
|
|
663
661
|
.gsub("\u0001UNDEREND\u0002", '</u>')
|
664
662
|
|
665
663
|
html += indent + text + "\n"
|
666
|
-
end
|
667
664
|
end
|
668
665
|
|
669
666
|
html += "</body>\n</html>"
|
@@ -683,133 +680,129 @@ class HyperListApp
|
|
683
680
|
render_main
|
684
681
|
render_split_pane if @split_view
|
685
682
|
render_footer
|
683
|
+
# Force redraw separator column to fix emoji overflow
|
684
|
+
if @split_view
|
685
|
+
split_col = @cols / 2
|
686
|
+
# Clear and redraw the entire separator column
|
687
|
+
(@rows - 1).times do |row|
|
688
|
+
# Move to position and draw separator with background color to overwrite anything
|
689
|
+
print "\e[#{row + 1};#{split_col}H\e[48;5;0m│\e[49m"
|
690
|
+
end
|
691
|
+
end
|
686
692
|
end
|
687
693
|
|
688
694
|
|
689
695
|
def render_main
|
690
696
|
visible_items = get_visible_items
|
691
697
|
|
692
|
-
#
|
693
|
-
|
694
|
-
if @current < @offset
|
695
|
-
@offset = @current
|
696
|
-
elsif @current >= @offset + view_height
|
697
|
-
@offset = @current - view_height + 1
|
698
|
-
end
|
699
|
-
|
700
|
-
# Track if we're in a literal block
|
698
|
+
# Build ALL lines for the pane (like RTFM/IMDB do)
|
699
|
+
lines = []
|
701
700
|
in_literal_block = false
|
702
701
|
literal_start_level = -1
|
703
702
|
|
704
|
-
|
705
|
-
lines = []
|
706
|
-
start_idx = @offset
|
707
|
-
end_idx = [@offset + view_height, visible_items.length].min
|
708
|
-
|
709
|
-
# For very large files, limit cache size and clear old entries
|
710
|
-
if visible_items.length > 5000 && @processed_cache.size > 2000
|
711
|
-
# Keep only recent entries
|
712
|
-
@processed_cache.clear
|
713
|
-
end
|
714
|
-
|
715
|
-
# Only process visible items
|
716
|
-
(start_idx...end_idx).each do |idx|
|
717
|
-
item = visible_items[idx]
|
703
|
+
visible_items.each_with_index do |item, idx|
|
718
704
|
next unless item
|
719
705
|
|
720
|
-
|
721
|
-
cache_key = "#{item['text']}_#{item['level']}_#{item['fold']}"
|
706
|
+
line = " " * item["level"] # 4 spaces per level
|
722
707
|
|
723
|
-
#
|
724
|
-
|
725
|
-
|
708
|
+
# Add fold indicator
|
709
|
+
real_idx = @items.index(item)
|
710
|
+
if real_idx && has_children?(real_idx, @items) && item["fold"]
|
711
|
+
color = (@presentation_mode && !is_item_in_presentation_focus?(item)) ? "240" : "245"
|
712
|
+
line += "▶".fg(color) + " "
|
713
|
+
elsif real_idx && has_children?(real_idx, @items)
|
714
|
+
color = (@presentation_mode && !is_item_in_presentation_focus?(item)) ? "240" : "245"
|
715
|
+
line += "▷".fg(color) + " "
|
726
716
|
else
|
727
|
-
line
|
728
|
-
|
729
|
-
|
730
|
-
|
731
|
-
|
732
|
-
|
733
|
-
|
734
|
-
|
735
|
-
|
736
|
-
|
737
|
-
|
738
|
-
|
717
|
+
line += " "
|
718
|
+
end
|
719
|
+
|
720
|
+
# Handle literal blocks and syntax highlighting
|
721
|
+
if item["text"].strip == "\\"
|
722
|
+
if !in_literal_block
|
723
|
+
in_literal_block = true
|
724
|
+
literal_start_level = item["level"]
|
725
|
+
spaces = item["text"].match(/^(\s*)/)[1]
|
726
|
+
line += spaces + "\\".fg("3")
|
727
|
+
elsif item["level"] == literal_start_level
|
728
|
+
in_literal_block = false
|
729
|
+
literal_start_level = -1
|
730
|
+
spaces = item["text"].match(/^(\s*)/)[1]
|
731
|
+
line += spaces + "\\".fg("3")
|
739
732
|
else
|
740
|
-
line += " "
|
741
|
-
end
|
742
|
-
|
743
|
-
# Check for literal block markers
|
744
|
-
if item["text"].strip == "\\"
|
745
|
-
if !in_literal_block
|
746
|
-
# Starting a literal block
|
747
|
-
in_literal_block = true
|
748
|
-
literal_start_level = item["level"]
|
749
|
-
# Preserve leading spaces and color the backslash
|
750
|
-
spaces = item["text"].match(/^(\s*)/)[1]
|
751
|
-
line += spaces + "\\".fg("3") # Yellow for literal marker
|
752
|
-
elsif item["level"] == literal_start_level
|
753
|
-
# Ending a literal block
|
754
|
-
in_literal_block = false
|
755
|
-
literal_start_level = -1
|
756
|
-
# Preserve leading spaces and color the backslash
|
757
|
-
spaces = item["text"].match(/^(\s*)/)[1]
|
758
|
-
line += spaces + "\\".fg("3") # Yellow for literal marker
|
759
|
-
else
|
760
|
-
# Backslash inside literal block - no highlighting
|
761
|
-
line += item["text"]
|
762
|
-
end
|
763
|
-
elsif in_literal_block
|
764
|
-
# Inside literal block - no syntax highlighting
|
765
733
|
line += item["text"]
|
766
|
-
else
|
767
|
-
# Normal text - apply syntax highlighting with caching
|
768
|
-
# Check if this line has a search match
|
769
|
-
has_match = @search_matches.include?(idx) && @search && !@search.empty?
|
770
|
-
|
771
|
-
# Check if this is raw text (for help/documentation screens)
|
772
|
-
if item["raw"]
|
773
|
-
line += item["text"]
|
774
|
-
else
|
775
|
-
# Check if we should grey out this item in presentation mode
|
776
|
-
if @presentation_mode && !is_item_in_presentation_focus?(item)
|
777
|
-
# Grey out items not in focus
|
778
|
-
line += item["text"].fg("240") # Dark grey for unfocused items
|
779
|
-
else
|
780
|
-
processed_text_key = has_match ? "search_#{item['text']}_#{@search}" : "text_#{item['text']}"
|
781
|
-
if @processed_cache[processed_text_key] && !has_match && !@presentation_mode
|
782
|
-
line += @processed_cache[processed_text_key]
|
783
|
-
else
|
784
|
-
processed = process_text(item["text"], has_match)
|
785
|
-
@processed_cache[processed_text_key] = processed if @processed_cache.size < 1000 && !has_match && !@presentation_mode
|
786
|
-
line += processed
|
787
|
-
end
|
788
|
-
end
|
789
|
-
end
|
790
734
|
end
|
791
|
-
|
792
|
-
|
793
|
-
|
794
|
-
|
735
|
+
elsif in_literal_block
|
736
|
+
line += item["text"]
|
737
|
+
else
|
738
|
+
# Normal syntax highlighting
|
739
|
+
has_match = @search_matches.include?(idx) && @search && !@search.empty?
|
740
|
+
if @presentation_mode && !is_item_in_presentation_focus?(item)
|
741
|
+
line += item["text"].fg("240")
|
742
|
+
else
|
743
|
+
line += process_text(item["text"], has_match)
|
795
744
|
end
|
796
|
-
|
797
|
-
|
798
|
-
|
799
|
-
|
745
|
+
end
|
746
|
+
|
747
|
+
# Apply current item highlighting
|
748
|
+
if idx == @current
|
749
|
+
bg_color = (!@split_view || @active_pane == :main) ? "237" : "234"
|
750
|
+
if bg_color
|
751
|
+
bg_code = "\e[48;5;#{bg_color}m"
|
752
|
+
reset_bg = "\e[49m"
|
753
|
+
line = bg_code + line.gsub(/\e\[49m/, '') + reset_bg
|
800
754
|
end
|
801
|
-
|
802
|
-
|
755
|
+
end
|
756
|
+
|
757
|
+
lines << line
|
758
|
+
|
759
|
+
# Check if exiting literal block
|
760
|
+
if in_literal_block && item["level"] <= literal_start_level && !item["text"].strip == "\\"
|
761
|
+
in_literal_block = false
|
762
|
+
literal_start_level = -1
|
763
|
+
end
|
764
|
+
end
|
765
|
+
|
766
|
+
# Add a blank line at the bottom to show end of document
|
767
|
+
lines << ""
|
768
|
+
|
769
|
+
# Set the full content to the pane and let rcurses handle scrolling
|
770
|
+
@main.text = lines.join("\n")
|
771
|
+
|
772
|
+
# Calculate how many extra lines are created by wrapping
|
773
|
+
logical_lines = lines.length
|
774
|
+
# Estimate wrapped lines by checking line lengths against pane width
|
775
|
+
extra_wrapped_lines = 0
|
776
|
+
lines[0..-2].each do |line| # Exclude the blank line we added
|
777
|
+
# Remove ANSI codes for length calculation
|
778
|
+
clean_line = line.gsub(/\e\[[0-9;]*m/, '')
|
779
|
+
if clean_line.length > @main.w
|
780
|
+
# This line will wrap - calculate how many extra lines it creates
|
781
|
+
extra_lines = (clean_line.length.to_f / @main.w).ceil - 1
|
782
|
+
extra_wrapped_lines += extra_lines
|
803
783
|
end
|
804
784
|
end
|
805
785
|
|
806
|
-
#
|
807
|
-
|
808
|
-
|
809
|
-
|
810
|
-
|
811
|
-
|
786
|
+
# Calculate scroll position exactly like RTFM does, but account for wrapping
|
787
|
+
# Treat the content as having one extra line (the blank line at bottom)
|
788
|
+
scrolloff = 3
|
789
|
+
total = visible_items.length + 1 # +1 for the blank line
|
790
|
+
page = @main.h
|
791
|
+
|
792
|
+
if total <= page
|
793
|
+
# If everything fits, always start from the very top
|
794
|
+
@main.ix = 0
|
795
|
+
elsif @current - @main.ix < scrolloff
|
796
|
+
# If we're too close to the top of the pane, scroll up
|
797
|
+
@main.ix = [@current - scrolloff, 0].max
|
798
|
+
elsif (@main.ix + page - 1 - @current) < scrolloff
|
799
|
+
# If we're too close to the bottom of the pane, scroll down
|
800
|
+
# Account for wrapped lines dynamically
|
801
|
+
max_off = [total - page + extra_wrapped_lines, 0].max
|
802
|
+
@main.ix = [@current + scrolloff - page + 1, max_off].min
|
812
803
|
end
|
804
|
+
|
805
|
+
@main.refresh
|
813
806
|
end
|
814
807
|
|
815
808
|
def process_text(text, highlight_search = false)
|
@@ -822,6 +815,47 @@ class HyperListApp
|
|
822
815
|
return result
|
823
816
|
end
|
824
817
|
|
818
|
+
# Apply State and Transition underlining FIRST, before any coloring
|
819
|
+
if @st_underline_mode == 1
|
820
|
+
# Underline states
|
821
|
+
if result =~ /^(\s*)(S:\s+)(.*)$/ || result =~ /^(\s*)(\|\s+)(.*)$/
|
822
|
+
prefix = $1 || ""
|
823
|
+
marker = $2
|
824
|
+
content = $3 || ""
|
825
|
+
# Apply underline to content after the marker
|
826
|
+
result = prefix + marker + content.u
|
827
|
+
end
|
828
|
+
elsif @st_underline_mode == 2
|
829
|
+
# Underline transitions
|
830
|
+
if result =~ /^(\s*)(T:\s+)(.*)$/ || result =~ /^(\s*)(\/\s+)(.*)$/
|
831
|
+
prefix = $1 || ""
|
832
|
+
marker = $2
|
833
|
+
content = $3 || ""
|
834
|
+
# Apply underline to content after the marker
|
835
|
+
result = prefix + marker + content.u
|
836
|
+
end
|
837
|
+
end
|
838
|
+
|
839
|
+
# Helper method to safely apply regexes without corrupting ANSI sequences
|
840
|
+
def safe_regex_replace(text, pattern, &block)
|
841
|
+
# Find all ANSI sequences and replace with placeholders
|
842
|
+
ansi_sequences = []
|
843
|
+
placeholder_text = text.gsub(/\e\[[0-9;]*m/) do |match|
|
844
|
+
ansi_sequences << match
|
845
|
+
"⟨ANSI#{ansi_sequences.length - 1}⟩"
|
846
|
+
end
|
847
|
+
|
848
|
+
# Apply the regex to the placeholder text
|
849
|
+
result_text = placeholder_text.gsub(pattern, &block)
|
850
|
+
|
851
|
+
# Restore ANSI sequences
|
852
|
+
ansi_sequences.each_with_index do |ansi, index|
|
853
|
+
result_text.gsub!("⟨ANSI#{index}⟩", ansi)
|
854
|
+
end
|
855
|
+
|
856
|
+
result_text
|
857
|
+
end
|
858
|
+
|
825
859
|
# Check if this is an encrypted line
|
826
860
|
if result.start_with?("ENC:")
|
827
861
|
# Show encrypted indicator instead of the encrypted data
|
@@ -906,7 +940,7 @@ class HyperListApp
|
|
906
940
|
|
907
941
|
# Check if it's an operator (ALL-CAPS with optional _, -, (), /, =, spaces)
|
908
942
|
if text_part =~ /^[A-Z][A-Z_\-() \/=]*$/
|
909
|
-
prefix_space + text_part.fg("4") + colon_space.fg("4") # Blue for operators
|
943
|
+
prefix_space + text_part.fg("4") + colon_space.fg("4") # Blue for operators (including S: and T:)
|
910
944
|
elsif text_part.length >= 2 && space_after.include?(" ")
|
911
945
|
# It's a property (mixed case, at least 2 chars, has space after colon)
|
912
946
|
prefix_space + text_part.fg("1") + colon_space.fg("1") # Red for properties
|
@@ -916,16 +950,24 @@ class HyperListApp
|
|
916
950
|
end
|
917
951
|
end
|
918
952
|
|
953
|
+
|
954
|
+
# Color special state/transition markers (| and /) green
|
955
|
+
result.gsub!(/^(\s*)\|\s+/) { $1 + "| ".fg("2") } # Green for pipe (state marker)
|
956
|
+
result.gsub!(/^(\s*)\/\s+/) { $1 + "/ ".fg("2") } # Green for slash (transition marker)
|
957
|
+
|
919
958
|
# Handle OR: at the beginning of a line (with optional spaces)
|
920
959
|
result.sub!(/^(\s*)(OR):/) { $1 + "OR:".fg("4") } # Blue for OR: at line start
|
921
960
|
|
922
961
|
# Handle parentheses content (moved here to avoid conflicts with properties)
|
923
962
|
# Based on hyperlist.vim: '(.\{-})'
|
924
|
-
result
|
963
|
+
result = safe_regex_replace(result, /\(([^)]*)\)/) do |match|
|
964
|
+
content = match[1..-2] # Extract content between parentheses
|
965
|
+
"(".fg("6") + content.fg("6") + ")".fg("6")
|
966
|
+
end
|
925
967
|
|
926
|
-
# Handle semicolons as separators
|
927
|
-
#
|
928
|
-
result
|
968
|
+
# Handle semicolons as separators (they separate items on the same line)
|
969
|
+
# Semicolons are green like qualifiers
|
970
|
+
result = safe_regex_replace(result, /;/) { ";".fg("2") }
|
929
971
|
|
930
972
|
# Handle references - color entire reference including brackets
|
931
973
|
# Based on hyperlist.vim: '<\{1,2}[...]\+>\{1,2}'
|
@@ -934,19 +976,13 @@ class HyperListApp
|
|
934
976
|
# Handle special keywords SKIP and END
|
935
977
|
result.gsub!(/\b(SKIP|END)\b/) { $1.fg("5") } # Magenta for special keywords (like references)
|
936
978
|
|
937
|
-
# Handle quoted strings
|
979
|
+
# Handle quoted strings (only double quotes are special in HyperList)
|
938
980
|
# Based on hyperlist.vim: '".\{-}"'
|
939
981
|
result.gsub!(/"([^"]*)"/) do
|
940
982
|
content = $1
|
941
983
|
# Color any ## sequences inside the quotes as red
|
942
984
|
content.gsub!(/(##[<>-]+)/) { $1.fg("1") }
|
943
|
-
'"'.fg("6") + content + '"'.fg("6") # Cyan for
|
944
|
-
end
|
945
|
-
result.gsub!(/'([^']*)'/) do
|
946
|
-
content = $1
|
947
|
-
# Color any ## sequences inside the quotes as red
|
948
|
-
content.gsub!(/(##[<>-]+)/) { $1.fg("1") }
|
949
|
-
"'".fg("6") + content + "'".fg("6") # Cyan for single quotes
|
985
|
+
'"'.fg("6") + content.fg("6") + '"'.fg("6") # Cyan for quoted strings
|
950
986
|
end
|
951
987
|
|
952
988
|
# Handle change markup - all double-hashes should be red
|
@@ -1108,40 +1144,135 @@ class HyperListApp
|
|
1108
1144
|
false
|
1109
1145
|
end
|
1110
1146
|
|
1111
|
-
def
|
1112
|
-
|
1113
|
-
return if @
|
1147
|
+
def indent_split_right(with_children = false)
|
1148
|
+
visible_items = get_visible_split_items
|
1149
|
+
return if @split_current >= visible_items.length
|
1114
1150
|
|
1115
|
-
item =
|
1116
|
-
real_idx = @
|
1151
|
+
item = visible_items[@split_current]
|
1152
|
+
real_idx = @split_items.index(item)
|
1153
|
+
|
1154
|
+
# Can only indent if there's a previous item at same or higher level
|
1155
|
+
if real_idx && real_idx > 0
|
1156
|
+
save_undo_state # Save state before modification
|
1157
|
+
original_level = item["level"]
|
1158
|
+
item["level"] += 1 # Since item is a reference, this updates the object in both arrays
|
1159
|
+
|
1160
|
+
# Indent children if requested
|
1161
|
+
if with_children
|
1162
|
+
((real_idx + 1)...@split_items.length).each do |i|
|
1163
|
+
if @split_items[i]["level"] > original_level
|
1164
|
+
@split_items[i]["level"] += 1
|
1165
|
+
else
|
1166
|
+
break
|
1167
|
+
end
|
1168
|
+
end
|
1169
|
+
end
|
1170
|
+
|
1171
|
+
@modified = true
|
1172
|
+
@message = "Indented in split pane"
|
1173
|
+
end
|
1174
|
+
end
|
1175
|
+
|
1176
|
+
def indent_split_left(with_children = false)
|
1177
|
+
visible_items = get_visible_split_items
|
1178
|
+
return if @split_current >= visible_items.length
|
1179
|
+
|
1180
|
+
item = visible_items[@split_current]
|
1181
|
+
real_idx = @split_items.index(item)
|
1117
1182
|
|
1118
|
-
if real_idx &&
|
1119
|
-
|
1120
|
-
|
1183
|
+
if real_idx && item["level"] > 0
|
1184
|
+
save_undo_state # Save state before modification
|
1185
|
+
original_level = item["level"]
|
1186
|
+
item["level"] -= 1 # Since item is a reference, this updates the object in both arrays
|
1187
|
+
|
1188
|
+
# Unindent children if requested
|
1189
|
+
if with_children
|
1190
|
+
((real_idx + 1)...@split_items.length).each do |i|
|
1191
|
+
if @split_items[i]["level"] > original_level
|
1192
|
+
@split_items[i]["level"] -= 1
|
1193
|
+
else
|
1194
|
+
break
|
1195
|
+
end
|
1196
|
+
end
|
1197
|
+
end
|
1198
|
+
|
1199
|
+
@modified = true
|
1200
|
+
@message = "Unindented in split pane"
|
1201
|
+
end
|
1202
|
+
end
|
1203
|
+
|
1204
|
+
def toggle_fold
|
1205
|
+
if @split_view && @active_pane == :split
|
1206
|
+
# Handle fold toggle in split pane
|
1207
|
+
visible_items = get_visible_split_items
|
1208
|
+
return if @split_current >= visible_items.length
|
1209
|
+
|
1210
|
+
item = visible_items[@split_current]
|
1211
|
+
real_idx = @split_items.index(item)
|
1212
|
+
|
1213
|
+
if real_idx && has_children_in_array?(real_idx, @split_items)
|
1214
|
+
item["fold"] = !item["fold"] # Since item is a reference, this updates the object in both arrays
|
1215
|
+
end
|
1216
|
+
else
|
1217
|
+
# Handle fold toggle in main pane
|
1218
|
+
visible = get_visible_items
|
1219
|
+
return if @current >= visible.length
|
1220
|
+
|
1221
|
+
item = visible[@current]
|
1222
|
+
real_idx = @items.index(item)
|
1223
|
+
|
1224
|
+
if real_idx && has_children?(real_idx, @items)
|
1225
|
+
@items[real_idx]["fold"] = !@items[real_idx]["fold"]
|
1226
|
+
record_last_action(:toggle_fold, nil)
|
1227
|
+
end
|
1121
1228
|
end
|
1122
1229
|
end
|
1123
1230
|
|
1124
1231
|
def move_up
|
1125
|
-
|
1232
|
+
max_items = get_visible_items.length - 1
|
1233
|
+
|
1234
|
+
if @current == 0
|
1235
|
+
# Wrap around to last item
|
1236
|
+
@current = max_items
|
1237
|
+
else
|
1238
|
+
@current = [@current - 1, 0].max
|
1239
|
+
end
|
1240
|
+
|
1126
1241
|
update_presentation_focus if @presentation_mode
|
1127
1242
|
end
|
1128
1243
|
|
1129
1244
|
def move_down
|
1130
1245
|
max = get_visible_items.length - 1
|
1131
|
-
|
1246
|
+
|
1247
|
+
if @current == max
|
1248
|
+
# Wrap around to first item
|
1249
|
+
@current = 0
|
1250
|
+
else
|
1251
|
+
@current = [@current + 1, max].min
|
1252
|
+
end
|
1253
|
+
|
1132
1254
|
update_presentation_focus if @presentation_mode
|
1133
1255
|
end
|
1134
1256
|
|
1135
1257
|
def page_up
|
1136
|
-
|
1137
|
-
|
1138
|
-
|
1258
|
+
if @split_view && @active_pane == :split
|
1259
|
+
@split_current = [@split_current - (@split_pane.h - 1), 0].max
|
1260
|
+
else
|
1261
|
+
@current = [@current - (@main.h - 1), 0].max
|
1262
|
+
@offset = [@offset - (@main.h - 1), 0].max
|
1263
|
+
update_presentation_focus if @presentation_mode
|
1264
|
+
end
|
1139
1265
|
end
|
1140
1266
|
|
1141
1267
|
def page_down
|
1142
|
-
|
1143
|
-
|
1144
|
-
|
1268
|
+
if @split_view && @active_pane == :split
|
1269
|
+
max = get_visible_split_items.length - 1
|
1270
|
+
@split_current = [@split_current + (@split_pane.h - 1), max].min
|
1271
|
+
else
|
1272
|
+
max = get_visible_items.length - 1
|
1273
|
+
@current = [@current + (@main.h - 1), max].min
|
1274
|
+
update_presentation_focus if @presentation_mode
|
1275
|
+
end
|
1145
1276
|
end
|
1146
1277
|
|
1147
1278
|
def go_to_parent
|
@@ -2016,9 +2147,9 @@ class HyperListApp
|
|
2016
2147
|
help_lines << help_line("#{"i/Enter".fg("10")}", "Edit line", "#{"o".fg("10")}", "Insert line below")
|
2017
2148
|
help_lines << help_line("#{"O".fg("10")}", "Insert line above", "#{"a".fg("10")}", "Insert child")
|
2018
2149
|
help_lines << help_line("#{"D".fg("10")}", "Delete+yank line", "#{"C-D".fg("10")}", "Delete+yank item&descendants")
|
2019
|
-
help_lines << help_line("#{"y".fg("10")}
|
2150
|
+
help_lines << help_line("#{"y".fg("10")}" + "/".fg("10") + "#{"Y".fg("10")}", "Copy line/tree", "#{"p".fg("10")}", "Paste")
|
2020
2151
|
help_lines << help_line("#{"u".fg("10")}", "Undo", "#{".".fg("10")}", "Repeat last action")
|
2021
|
-
help_lines << help_line("#{"r".fg("10")}, #{"C-R".fg("10")}", "Redo")
|
2152
|
+
help_lines << help_line("#{"r".fg("10")}" + ", ".fg("10") + "#{"C-R".fg("10")}", "Redo")
|
2022
2153
|
help_lines << help_line("#{"S-UP".fg("10")}", "Move item up", "#{"S-DOWN".fg("10")}", "Move item down")
|
2023
2154
|
help_lines << help_line("#{"C-UP".fg("10")}", "Move item&descendants up", "#{"C-DOWN".fg("10")}", "Move item&descendants down")
|
2024
2155
|
help_lines << help_line("#{"Tab".fg("10")}", "Indent item+kids", "#{"S-Tab".fg("10")}", "Unindent item+kids")
|
@@ -2026,13 +2157,12 @@ class HyperListApp
|
|
2026
2157
|
help_lines << ""
|
2027
2158
|
help_lines << "#{"FEATURES".fg("14")}"
|
2028
2159
|
help_lines << help_line("#{"v".fg("10")}", "Toggle checkbox", "#{"V".fg("10")}", "Checkbox with date")
|
2029
|
-
help_lines << help_line("#{"C-E".fg("10")}", "Encrypt/decrypt line", "#{"
|
2030
|
-
help_lines << help_line("#{"
|
2031
|
-
help_lines << help_line("#{"P".fg("10")}", "Presentation mode")
|
2160
|
+
help_lines << help_line("#{"C-E".fg("10")}", "Encrypt/decrypt line", "#{"C-U".fg("10")}", "Toggle State/Trans underline")
|
2161
|
+
help_lines << help_line("#{"R".fg("10")}", "Go to reference", "#{"F".fg("10")}", "Open file")
|
2162
|
+
help_lines << help_line("#{"N".fg("10")}", "Next = marker", "#{"P".fg("10")}", "Presentation mode")
|
2032
2163
|
help_lines << help_line("#{"t".fg("10")}", "Insert template", "#{":template".fg("10")}", "Show templates")
|
2033
2164
|
help_lines << help_line("#{"Ma".fg("10")}", "Record macro 'a'", "#{"@a".fg("10")}", "Play macro 'a'")
|
2034
|
-
help_lines << help_line("#{":vsplit".fg("10")}", "Split view vertically", "#{"
|
2035
|
-
help_lines << help_line("#{"\\u".fg("10")}", "Toggle underline")
|
2165
|
+
help_lines << help_line("#{":vsplit".fg("10")}", "Split view vertically", "#{"w".fg("10")}", "Switch panes")
|
2036
2166
|
help_lines << ""
|
2037
2167
|
help_lines << "#{"FILE OPERATIONS".fg("14")}"
|
2038
2168
|
help_lines << help_line("#{":w".fg("10")}", "Save", "#{":q".fg("10")}", "Quit")
|
@@ -2048,8 +2178,8 @@ class HyperListApp
|
|
2048
2178
|
help_lines << help_line("#{"[ ]".fg("22")}", "Unchecked", "#{"[-]".fg("2")}", "Partial")
|
2049
2179
|
help_lines << help_line("#{"[?]".fg("2")}", "Conditionals", "#{"AND:".fg("4")}", "Operators")
|
2050
2180
|
help_lines << help_line("#{"Date:".fg("1")}", "Properties", "#{"<ref>".fg("5")}", "References")
|
2051
|
-
help_lines << help_line("#{"(
|
2052
|
-
help_lines << help_line("#{";
|
2181
|
+
help_lines << help_line("#{"(comment)".fg("6")}", "Comments", "#{'"text"'.fg("14")}", "Quoted strings")
|
2182
|
+
help_lines << help_line("#{";".fg("2")}", "Separator", "#{"#tag".fg("184")}", "Hash tags")
|
2053
2183
|
|
2054
2184
|
help = help_lines.join("\n")
|
2055
2185
|
|
@@ -2093,7 +2223,8 @@ class HyperListApp
|
|
2093
2223
|
@current = 0
|
2094
2224
|
@offset = 0
|
2095
2225
|
when "END"
|
2096
|
-
|
2226
|
+
visible = get_visible_items
|
2227
|
+
@current = [visible.length - 1, 0].max
|
2097
2228
|
else
|
2098
2229
|
# Any other key returns to main view
|
2099
2230
|
break
|
@@ -2274,13 +2405,14 @@ class HyperListApp
|
|
2274
2405
|
|
2275
2406
|
#{"ADDITIVES".b}
|
2276
2407
|
|
2277
|
-
#{"Comments".fg("6")} (in parentheses
|
2408
|
+
#{"Comments".fg("6")} (in parentheses):
|
2278
2409
|
(This is a comment) Not executed in transitions
|
2279
|
-
|
2410
|
+
|
2411
|
+
#{"Separators".fg("2")} (semicolon):
|
2412
|
+
Item 1; Item 2; Item 3 Multiple items on same line
|
2280
2413
|
|
2281
2414
|
#{"Quotes".fg("14")} (in quotation marks):
|
2282
2415
|
"Literal text" Not interpreted as HyperList
|
2283
|
-
'Also literal' Single quotes work too
|
2284
2416
|
|
2285
2417
|
#{"Tags".fg("184")} (hashtags):
|
2286
2418
|
#TODO #important Markers for categorization
|
@@ -2618,14 +2750,16 @@ class HyperListApp
|
|
2618
2750
|
@current = 0
|
2619
2751
|
@offset = 0
|
2620
2752
|
when "END"
|
2621
|
-
|
2753
|
+
visible = get_visible_items
|
2754
|
+
@current = [visible.length - 1, 0].max
|
2622
2755
|
when "g"
|
2623
2756
|
if getchr == "g"
|
2624
2757
|
@current = 0
|
2625
2758
|
@offset = 0
|
2626
2759
|
end
|
2627
2760
|
when "G"
|
2628
|
-
|
2761
|
+
visible = get_visible_items
|
2762
|
+
@current = [visible.length - 1, 0].max
|
2629
2763
|
end
|
2630
2764
|
end
|
2631
2765
|
|
@@ -2710,7 +2844,7 @@ class HyperListApp
|
|
2710
2844
|
when "autosave", "as"
|
2711
2845
|
status = @auto_save_enabled ? "enabled" : "disabled"
|
2712
2846
|
@message = "Auto-save is #{status} (interval: #{@auto_save_interval}s)"
|
2713
|
-
when "
|
2847
|
+
when "t"
|
2714
2848
|
show_templates
|
2715
2849
|
when "foldlevel"
|
2716
2850
|
level = @footer.ask("Fold to level (0-9): ", "")
|
@@ -2883,22 +3017,53 @@ class HyperListApp
|
|
2883
3017
|
|
2884
3018
|
password = ""
|
2885
3019
|
loop do
|
2886
|
-
|
2887
|
-
|
2888
|
-
|
2889
|
-
|
2890
|
-
|
2891
|
-
|
2892
|
-
|
2893
|
-
|
2894
|
-
else
|
2895
|
-
if c.length == 1 && c.ord >= 32 && c.ord <= 126
|
2896
|
-
password += c
|
3020
|
+
begin
|
3021
|
+
c = getchr
|
3022
|
+
|
3023
|
+
# Debug logging
|
3024
|
+
if ENV['DEBUG']
|
3025
|
+
File.open('/tmp/hyperlist_debug.log', 'a') do |f|
|
3026
|
+
f.puts "Password prompt key: #{c.inspect}"
|
3027
|
+
end
|
2897
3028
|
end
|
3029
|
+
|
3030
|
+
case c
|
3031
|
+
when "ENTER", "\r", "\n"
|
3032
|
+
break
|
3033
|
+
when "BACKSPACE", "BACK", "C-h", "\x7F", "\b"
|
3034
|
+
password.chop! unless password.empty?
|
3035
|
+
when "ESC", "C-c"
|
3036
|
+
return nil
|
3037
|
+
when nil
|
3038
|
+
# Skip nil input
|
3039
|
+
next
|
3040
|
+
else
|
3041
|
+
if c && c.length == 1 && c.ord >= 32 && c.ord <= 126
|
3042
|
+
password += c
|
3043
|
+
end
|
3044
|
+
end
|
3045
|
+
@footer.text = prompt + "*" * password.length
|
3046
|
+
@footer.refresh
|
3047
|
+
rescue => e
|
3048
|
+
# If there's an error, return nil
|
3049
|
+
@message = "Password input error: #{e.message}"
|
3050
|
+
return nil
|
2898
3051
|
end
|
2899
|
-
@footer.text = prompt + "*" * password.length
|
2900
|
-
@footer.refresh
|
2901
3052
|
end
|
3053
|
+
|
3054
|
+
# Clear the password prompt
|
3055
|
+
@footer.text = ""
|
3056
|
+
@footer.refresh
|
3057
|
+
|
3058
|
+
# Flush any remaining input to avoid accidental quit
|
3059
|
+
begin
|
3060
|
+
while IO.select([$stdin], nil, nil, 0)
|
3061
|
+
$stdin.read_nonblock(1024)
|
3062
|
+
end
|
3063
|
+
rescue IO::WaitReadable, EOFError
|
3064
|
+
# No more input to flush
|
3065
|
+
end
|
3066
|
+
|
2902
3067
|
password
|
2903
3068
|
end
|
2904
3069
|
|
@@ -2994,40 +3159,44 @@ class HyperListApp
|
|
2994
3159
|
def toggle_line_encryption
|
2995
3160
|
return if @items.empty?
|
2996
3161
|
|
2997
|
-
|
2998
|
-
|
2999
|
-
|
3000
|
-
|
3001
|
-
|
3002
|
-
|
3003
|
-
|
3004
|
-
|
3005
|
-
|
3006
|
-
|
3007
|
-
|
3008
|
-
|
3009
|
-
|
3010
|
-
|
3011
|
-
|
3012
|
-
|
3013
|
-
# Encrypt the line
|
3014
|
-
encrypted = encrypt_string(current_text)
|
3015
|
-
if encrypted
|
3016
|
-
save_state
|
3017
|
-
item["text"] = encrypted
|
3018
|
-
@encrypted_lines[@current] = true
|
3019
|
-
@modified = true
|
3020
|
-
@message = "Line encrypted"
|
3162
|
+
begin
|
3163
|
+
item = @items[@current]
|
3164
|
+
current_text = item["text"]
|
3165
|
+
|
3166
|
+
if current_text.start_with?("ENC:")
|
3167
|
+
# Decrypt the line
|
3168
|
+
decrypted = decrypt_string(current_text)
|
3169
|
+
if decrypted
|
3170
|
+
save_undo_state
|
3171
|
+
item["text"] = decrypted
|
3172
|
+
@encrypted_lines.delete(@current)
|
3173
|
+
@modified = true
|
3174
|
+
@message = "Line decrypted"
|
3175
|
+
else
|
3176
|
+
@message = "Decryption failed or cancelled"
|
3177
|
+
end
|
3021
3178
|
else
|
3022
|
-
|
3179
|
+
# Encrypt the line
|
3180
|
+
encrypted = encrypt_string(current_text)
|
3181
|
+
if encrypted
|
3182
|
+
save_undo_state
|
3183
|
+
item["text"] = encrypted
|
3184
|
+
@encrypted_lines[@current] = true
|
3185
|
+
@modified = true
|
3186
|
+
@message = "Line encrypted"
|
3187
|
+
else
|
3188
|
+
@message = "Encryption cancelled"
|
3189
|
+
end
|
3023
3190
|
end
|
3191
|
+
rescue => e
|
3192
|
+
@message = "Encryption error: #{e.message}"
|
3024
3193
|
end
|
3025
3194
|
end
|
3026
3195
|
|
3027
3196
|
def load_templates
|
3028
3197
|
{
|
3029
3198
|
"project" => [
|
3030
|
-
{"text" => "Project:
|
3199
|
+
{"text" => "Project: =Project Name=", "level" => 0},
|
3031
3200
|
{"text" => "[_] Define project scope", "level" => 1},
|
3032
3201
|
{"text" => "[_] Identify stakeholders", "level" => 1},
|
3033
3202
|
{"text" => "[_] Create timeline", "level" => 1},
|
@@ -3046,25 +3215,25 @@ class HyperListApp
|
|
3046
3215
|
{"text" => "[_] Archive project materials", "level" => 2}
|
3047
3216
|
],
|
3048
3217
|
"meeting" => [
|
3049
|
-
{"text" => "Meeting:
|
3218
|
+
{"text" => "Meeting: =Title=", "level" => 0},
|
3050
3219
|
{"text" => "Date: #{Time.now.strftime('%Y-%m-%d %H:%M')}", "level" => 1},
|
3051
|
-
{"text" => "Location:
|
3220
|
+
{"text" => "Location: =Conference Room/Online=", "level" => 1},
|
3052
3221
|
{"text" => "Attendees", "level" => 1},
|
3053
|
-
{"text" => "
|
3054
|
-
{"text" => "
|
3222
|
+
{"text" => "=Name 1=", "level" => 2},
|
3223
|
+
{"text" => "=Name 2=", "level" => 2},
|
3055
3224
|
{"text" => "Agenda", "level" => 1},
|
3056
3225
|
{"text" => "[_] Opening remarks", "level" => 2},
|
3057
3226
|
{"text" => "[_] Review previous action items", "level" => 2},
|
3058
3227
|
{"text" => "[_] Main topics", "level" => 2},
|
3059
|
-
{"text" => "Topic 1:
|
3060
|
-
{"text" => "Topic 2:
|
3228
|
+
{"text" => "Topic 1: =Description=", "level" => 3},
|
3229
|
+
{"text" => "Topic 2: =Description=", "level" => 3},
|
3061
3230
|
{"text" => "[_] Q&A session", "level" => 2},
|
3062
3231
|
{"text" => "[_] Next steps", "level" => 2},
|
3063
3232
|
{"text" => "Action Items", "level" => 1},
|
3064
|
-
{"text" => "[_]
|
3065
|
-
{"text" => "[_]
|
3233
|
+
{"text" => "[_] =Action 1= - Assigned to: =Name= - Due: =Date=", "level" => 2},
|
3234
|
+
{"text" => "[_] =Action 2= - Assigned to: =Name= - Due: =Date=", "level" => 2},
|
3066
3235
|
{"text" => "Notes", "level" => 1},
|
3067
|
-
{"text" => "
|
3236
|
+
{"text" => "=Add meeting notes here=", "level" => 2}
|
3068
3237
|
],
|
3069
3238
|
"daily" => [
|
3070
3239
|
{"text" => "Daily Plan: #{Time.now.strftime('%Y-%m-%d')}", "level" => 0},
|
@@ -3073,19 +3242,19 @@ class HyperListApp
|
|
3073
3242
|
{"text" => "[_] Check emails", "level" => 2},
|
3074
3243
|
{"text" => "[_] Plan priorities", "level" => 2},
|
3075
3244
|
{"text" => "Priority Tasks", "level" => 1},
|
3076
|
-
{"text" => "[_]
|
3077
|
-
{"text" => "[_]
|
3078
|
-
{"text" => "[_]
|
3245
|
+
{"text" => "[_] =High Priority Task 1=", "level" => 2},
|
3246
|
+
{"text" => "[_] =High Priority Task 2=", "level" => 2},
|
3247
|
+
{"text" => "[_] =High Priority Task 3=", "level" => 2},
|
3079
3248
|
{"text" => "Regular Tasks", "level" => 1},
|
3080
|
-
{"text" => "[_]
|
3081
|
-
{"text" => "[_]
|
3249
|
+
{"text" => "[_] =Task 1=", "level" => 2},
|
3250
|
+
{"text" => "[_] =Task 2=", "level" => 2},
|
3082
3251
|
{"text" => "Meetings/Appointments", "level" => 1},
|
3083
|
-
{"text" => "
|
3252
|
+
{"text" => "=Time= - =Meeting/Event=", "level" => 2},
|
3084
3253
|
{"text" => "Notes", "level" => 1},
|
3085
|
-
{"text" => "
|
3254
|
+
{"text" => "=Daily observations and reflections=", "level" => 2}
|
3086
3255
|
],
|
3087
3256
|
"checklist" => [
|
3088
|
-
{"text" => "Checklist:
|
3257
|
+
{"text" => "Checklist: =Title=", "level" => 0},
|
3089
3258
|
{"text" => "[_] Item 1", "level" => 1},
|
3090
3259
|
{"text" => "[_] Item 2", "level" => 1},
|
3091
3260
|
{"text" => "[_] Item 3", "level" => 1},
|
@@ -3093,9 +3262,9 @@ class HyperListApp
|
|
3093
3262
|
{"text" => "[_] Item 5", "level" => 1}
|
3094
3263
|
],
|
3095
3264
|
"brainstorm" => [
|
3096
|
-
{"text" => "Brainstorming:
|
3265
|
+
{"text" => "Brainstorming: =Topic=", "level" => 0},
|
3097
3266
|
{"text" => "Problem Statement", "level" => 1},
|
3098
|
-
{"text" => "
|
3267
|
+
{"text" => "=Define the problem or opportunity=", "level" => 2},
|
3099
3268
|
{"text" => "Ideas", "level" => 1},
|
3100
3269
|
{"text" => "Category 1", "level" => 2},
|
3101
3270
|
{"text" => "Idea A", "level" => 3},
|
@@ -3112,37 +3281,35 @@ class HyperListApp
|
|
3112
3281
|
{"text" => "[_] Create action plan", "level" => 2}
|
3113
3282
|
],
|
3114
3283
|
"recipe" => [
|
3115
|
-
{"text" => "Recipe:
|
3116
|
-
{"text" => "Servings:
|
3117
|
-
{"text" => "Prep Time:
|
3118
|
-
{"text" => "Cook Time:
|
3284
|
+
{"text" => "Recipe: =Name=", "level" => 0},
|
3285
|
+
{"text" => "Servings: =Number=", "level" => 1},
|
3286
|
+
{"text" => "Prep Time: =Time=", "level" => 1},
|
3287
|
+
{"text" => "Cook Time: =Time=", "level" => 1},
|
3119
3288
|
{"text" => "Ingredients", "level" => 1},
|
3120
|
-
{"text" => "
|
3121
|
-
{"text" => "
|
3122
|
-
{"text" => "
|
3289
|
+
{"text" => "=Amount= =Ingredient 1=", "level" => 2},
|
3290
|
+
{"text" => "=Amount= =Ingredient 2=", "level" => 2},
|
3291
|
+
{"text" => "=Amount= =Ingredient 3=", "level" => 2},
|
3123
3292
|
{"text" => "Instructions", "level" => 1},
|
3124
|
-
{"text" => "[_] Step 1:
|
3125
|
-
{"text" => "[_] Step 2:
|
3126
|
-
{"text" => "[_] Step 3:
|
3293
|
+
{"text" => "[_] Step 1: =Description=", "level" => 2},
|
3294
|
+
{"text" => "[_] Step 2: =Description=", "level" => 2},
|
3295
|
+
{"text" => "[_] Step 3: =Description=", "level" => 2},
|
3127
3296
|
{"text" => "Notes", "level" => 1},
|
3128
|
-
{"text" => "
|
3297
|
+
{"text" => "=Tips, variations, serving suggestions=", "level" => 2}
|
3129
3298
|
]
|
3130
3299
|
}
|
3131
3300
|
end
|
3132
3301
|
|
3133
3302
|
def show_templates
|
3134
|
-
#
|
3135
|
-
|
3136
|
-
|
3137
|
-
|
3138
|
-
|
3139
|
-
|
3140
|
-
|
3141
|
-
|
3142
|
-
@items = []
|
3143
|
-
@items << {"text" => "TEMPLATES (press Enter to insert, q to cancel)", "level" => 0, "fold" => false, "raw" => true}
|
3144
|
-
@items << {"text" => "="*50, "level" => 0, "fold" => false, "raw" => true}
|
3303
|
+
# Store original state in an array to ensure proper restoration
|
3304
|
+
original_state = {
|
3305
|
+
items: @items.dup,
|
3306
|
+
current: @current,
|
3307
|
+
offset: @offset,
|
3308
|
+
filename: @filename,
|
3309
|
+
modified: @modified
|
3310
|
+
}
|
3145
3311
|
|
3312
|
+
# Create template selection view
|
3146
3313
|
template_list = [
|
3147
3314
|
["project", "Project Plan - Complete project management template"],
|
3148
3315
|
["meeting", "Meeting Agenda - Structure for meeting notes"],
|
@@ -3152,61 +3319,102 @@ class HyperListApp
|
|
3152
3319
|
["recipe", "Recipe - Cooking recipe structure"]
|
3153
3320
|
]
|
3154
3321
|
|
3322
|
+
# Build template selection items
|
3323
|
+
@items = []
|
3324
|
+
@items << {"text" => "TEMPLATES (press Enter to insert, q to cancel)", "level" => 0, "fold" => false, "raw" => true}
|
3325
|
+
@items << {"text" => "="*50, "level" => 0, "fold" => false, "raw" => true}
|
3326
|
+
|
3155
3327
|
template_list.each_with_index do |(key, desc), idx|
|
3156
|
-
@items << {
|
3328
|
+
@items << {
|
3329
|
+
"text" => "#{idx+1}. #{key.capitalize}: #{desc}",
|
3330
|
+
"level" => 0,
|
3331
|
+
"fold" => false,
|
3332
|
+
"raw" => true,
|
3333
|
+
"template_key" => key
|
3334
|
+
}
|
3157
3335
|
end
|
3158
3336
|
|
3159
3337
|
@current = 2 # Start at first template
|
3160
3338
|
@offset = 0
|
3161
|
-
@modified = false
|
3162
3339
|
|
3163
3340
|
selected_template = nil
|
3341
|
+
exit_loop = false
|
3164
3342
|
|
3165
|
-
# Template
|
3166
|
-
|
3167
|
-
|
3168
|
-
|
3169
|
-
|
3170
|
-
|
3171
|
-
|
3172
|
-
|
3173
|
-
|
3174
|
-
#
|
3175
|
-
|
3176
|
-
|
3177
|
-
|
3178
|
-
|
3179
|
-
|
3180
|
-
|
3181
|
-
|
3182
|
-
|
3183
|
-
|
3184
|
-
|
3185
|
-
|
3186
|
-
|
3187
|
-
|
3188
|
-
|
3189
|
-
|
3190
|
-
|
3191
|
-
|
3192
|
-
|
3193
|
-
|
3194
|
-
|
3195
|
-
|
3343
|
+
# Template selection loop
|
3344
|
+
while !exit_loop
|
3345
|
+
begin
|
3346
|
+
render_main
|
3347
|
+
@footer.text = "Templates | Enter: insert | q: cancel | j/k: navigate"
|
3348
|
+
@footer.refresh
|
3349
|
+
|
3350
|
+
c = getchr
|
3351
|
+
|
3352
|
+
# Skip truly nil input
|
3353
|
+
next if c.nil?
|
3354
|
+
|
3355
|
+
# Debug logging to file
|
3356
|
+
File.open('/tmp/hyperlist_debug.log', 'a') do |f|
|
3357
|
+
f.puts "Template key received: #{c.inspect} (length: #{c.length}, ord: #{c.bytes.inspect})"
|
3358
|
+
end if ENV['DEBUG']
|
3359
|
+
|
3360
|
+
case c
|
3361
|
+
when "q", "ESC", "C-c", "Q"
|
3362
|
+
exit_loop = true
|
3363
|
+
when "j", "DOWN"
|
3364
|
+
@current = [@current + 1, @items.length - 1].min
|
3365
|
+
when "k", "UP"
|
3366
|
+
@current = [@current - 1, 2].max
|
3367
|
+
when "ENTER", "RETURN", "\n", "\r", "l"
|
3368
|
+
if @current >= 2 && @current < @items.length
|
3369
|
+
item = @items[@current]
|
3370
|
+
if item && item.is_a?(Hash) && item["template_key"]
|
3371
|
+
selected_template = item["template_key"]
|
3372
|
+
exit_loop = true
|
3373
|
+
end
|
3374
|
+
end
|
3375
|
+
when /^[1-6]$/
|
3376
|
+
idx = c.to_i - 1
|
3377
|
+
if idx >= 0 && idx < template_list.length
|
3378
|
+
selected_template = template_list[idx][0]
|
3379
|
+
exit_loop = true
|
3380
|
+
end
|
3196
3381
|
end
|
3382
|
+
|
3383
|
+
rescue => e
|
3384
|
+
# Log error but continue
|
3385
|
+
@message = "Error in template loop: #{e.message}"
|
3386
|
+
exit_loop = true
|
3197
3387
|
end
|
3198
3388
|
end
|
3199
3389
|
|
3200
|
-
#
|
3201
|
-
|
3202
|
-
|
3203
|
-
|
3204
|
-
|
3205
|
-
|
3206
|
-
|
3207
|
-
|
3208
|
-
|
3209
|
-
|
3390
|
+
# Restore original state
|
3391
|
+
@items = original_state[:items]
|
3392
|
+
@current = original_state[:current]
|
3393
|
+
@offset = original_state[:offset]
|
3394
|
+
@filename = original_state[:filename]
|
3395
|
+
@modified = original_state[:modified]
|
3396
|
+
|
3397
|
+
# Flush any remaining input to avoid accidental actions
|
3398
|
+
begin
|
3399
|
+
while IO.select([$stdin], nil, nil, 0)
|
3400
|
+
$stdin.read_nonblock(1024)
|
3401
|
+
end
|
3402
|
+
rescue IO::WaitReadable, EOFError
|
3403
|
+
# No more input to flush
|
3404
|
+
end
|
3405
|
+
|
3406
|
+
# Insert template if one was selected
|
3407
|
+
if selected_template && !selected_template.empty?
|
3408
|
+
begin
|
3409
|
+
template_items = @templates[selected_template]
|
3410
|
+
if template_items && template_items.is_a?(Array) && !template_items.empty?
|
3411
|
+
insert_template(selected_template)
|
3412
|
+
else
|
3413
|
+
@message = "Template '#{selected_template}' not found or empty"
|
3414
|
+
end
|
3415
|
+
rescue => e
|
3416
|
+
@message = "Error inserting template: #{e.message}"
|
3417
|
+
end
|
3210
3418
|
end
|
3211
3419
|
end
|
3212
3420
|
|
@@ -3214,10 +3422,15 @@ class HyperListApp
|
|
3214
3422
|
template_items = @templates[template_key]
|
3215
3423
|
return unless template_items
|
3216
3424
|
|
3217
|
-
|
3425
|
+
# Ensure we have valid items and current position
|
3426
|
+
return if @items.nil? || @items.empty?
|
3427
|
+
return if @current.nil? || @current < 0 || @current >= @items.length
|
3428
|
+
return unless @items[@current]
|
3429
|
+
|
3430
|
+
save_undo_state
|
3218
3431
|
|
3219
3432
|
# Get current item level to adjust template indentation
|
3220
|
-
current_level = @items[@current]["level"]
|
3433
|
+
current_level = @items[@current]["level"] || 0
|
3221
3434
|
|
3222
3435
|
# Insert template items after current position
|
3223
3436
|
insertion_point = @current + 1
|
@@ -3317,13 +3530,29 @@ class HyperListApp
|
|
3317
3530
|
when "V"
|
3318
3531
|
toggle_checkbox_with_date
|
3319
3532
|
when "TAB"
|
3320
|
-
|
3533
|
+
if @split_view && @active_pane == :split
|
3534
|
+
indent_split_right(true) # with children
|
3535
|
+
else
|
3536
|
+
indent_right(true) # with children
|
3537
|
+
end
|
3321
3538
|
when "S-TAB"
|
3322
|
-
|
3539
|
+
if @split_view && @active_pane == :split
|
3540
|
+
indent_split_left(true) # with children
|
3541
|
+
else
|
3542
|
+
indent_left(true) # with children
|
3543
|
+
end
|
3323
3544
|
when "RIGHT"
|
3324
|
-
|
3545
|
+
if @split_view && @active_pane == :split
|
3546
|
+
indent_split_right(false)
|
3547
|
+
else
|
3548
|
+
indent_right(false)
|
3549
|
+
end
|
3325
3550
|
when "LEFT"
|
3326
|
-
|
3551
|
+
if @split_view && @active_pane == :split
|
3552
|
+
indent_split_left(false)
|
3553
|
+
else
|
3554
|
+
indent_left(false)
|
3555
|
+
end
|
3327
3556
|
when " "
|
3328
3557
|
toggle_fold
|
3329
3558
|
when "u"
|
@@ -3340,17 +3569,29 @@ class HyperListApp
|
|
3340
3569
|
def toggle_split_view
|
3341
3570
|
@split_view = !@split_view
|
3342
3571
|
if @split_view
|
3343
|
-
|
3572
|
+
# Use the actual items array for full document split view
|
3573
|
+
# This ensures changes in either pane affect both views
|
3574
|
+
@split_items = @items
|
3344
3575
|
@split_current = @current
|
3345
3576
|
@split_offset = @offset
|
3346
3577
|
@active_pane = :main
|
3347
|
-
set_message("Split view enabled. Use
|
3578
|
+
set_message("Split view enabled. Use Ctrl-w w to switch panes.")
|
3348
3579
|
else
|
3349
3580
|
@split_items = []
|
3350
3581
|
set_message("Split view disabled")
|
3351
3582
|
end
|
3583
|
+
|
3584
|
+
# Clear cached content to force re-render
|
3585
|
+
@last_rendered_content = ""
|
3586
|
+
@processed_cache.clear
|
3587
|
+
|
3588
|
+
# Recreate UI
|
3352
3589
|
setup_ui
|
3353
|
-
|
3590
|
+
|
3591
|
+
# Force complete re-render
|
3592
|
+
render_main
|
3593
|
+
render_split_pane if @split_view
|
3594
|
+
render_footer
|
3354
3595
|
end
|
3355
3596
|
|
3356
3597
|
def copy_section_to_split
|
@@ -3382,7 +3623,8 @@ class HyperListApp
|
|
3382
3623
|
end_idx = idx
|
3383
3624
|
end
|
3384
3625
|
|
3385
|
-
# Copy section to split view
|
3626
|
+
# Copy section to split view - use references, not copies
|
3627
|
+
# The slice creates a new array but contains references to the same item objects
|
3386
3628
|
@split_items = @items[start_idx..end_idx]
|
3387
3629
|
@split_current = 0
|
3388
3630
|
@split_offset = 0
|
@@ -3398,6 +3640,10 @@ class HyperListApp
|
|
3398
3640
|
if @split_view
|
3399
3641
|
@active_pane = (@active_pane == :main) ? :split : :main
|
3400
3642
|
@message = "Switched to #{@active_pane} pane"
|
3643
|
+
# Force re-render both panes to update highlighting
|
3644
|
+
render_main
|
3645
|
+
render_split_pane
|
3646
|
+
render_footer
|
3401
3647
|
end
|
3402
3648
|
when "v"
|
3403
3649
|
# Vertical split
|
@@ -3413,48 +3659,111 @@ class HyperListApp
|
|
3413
3659
|
end
|
3414
3660
|
end
|
3415
3661
|
|
3662
|
+
def get_visible_split_items
|
3663
|
+
return [] unless @split_items
|
3664
|
+
|
3665
|
+
visible = []
|
3666
|
+
skip_until_level = nil
|
3667
|
+
|
3668
|
+
@split_items.each do |item|
|
3669
|
+
# Skip items that are folded
|
3670
|
+
if skip_until_level
|
3671
|
+
if item["level"] > skip_until_level
|
3672
|
+
next
|
3673
|
+
else
|
3674
|
+
skip_until_level = nil
|
3675
|
+
end
|
3676
|
+
end
|
3677
|
+
|
3678
|
+
visible << item
|
3679
|
+
|
3680
|
+
# Check if this item is folded and has children
|
3681
|
+
if item["fold"] && has_children_in_array?(visible.length - 1, @split_items)
|
3682
|
+
skip_until_level = item["level"]
|
3683
|
+
end
|
3684
|
+
end
|
3685
|
+
|
3686
|
+
visible
|
3687
|
+
end
|
3688
|
+
|
3416
3689
|
def render_split_pane
|
3417
3690
|
return unless @split_view && @split_pane
|
3418
3691
|
|
3419
|
-
#
|
3420
|
-
|
3421
|
-
if @split_current < @split_offset
|
3422
|
-
@split_offset = @split_current
|
3423
|
-
elsif @split_current >= @split_offset + view_height
|
3424
|
-
@split_offset = @split_current - view_height + 1
|
3425
|
-
end
|
3692
|
+
# Get visible items respecting fold state
|
3693
|
+
visible_items = get_visible_split_items
|
3426
3694
|
|
3695
|
+
# Build ALL lines for the pane (like we do for main pane)
|
3427
3696
|
lines = []
|
3428
|
-
|
3429
|
-
end_idx = [@split_offset + view_height, @split_items.length].min
|
3430
|
-
|
3431
|
-
(start_idx...end_idx).each do |idx|
|
3432
|
-
item = @split_items[idx]
|
3697
|
+
visible_items.each_with_index do |item, idx|
|
3433
3698
|
next unless item
|
3434
3699
|
|
3435
3700
|
line = " " * item["level"]
|
3436
3701
|
|
3437
|
-
# Add fold indicator
|
3438
|
-
|
3439
|
-
|
3440
|
-
|
3441
|
-
|
3702
|
+
# Add fold indicator with colors
|
3703
|
+
# Find the item's position in the original split_items array
|
3704
|
+
real_idx = @split_items.index(item)
|
3705
|
+
if real_idx && has_children_in_array?(real_idx, @split_items)
|
3706
|
+
if item["fold"]
|
3707
|
+
line += "▶".fg("245") + " "
|
3708
|
+
else
|
3709
|
+
line += "▷".fg("245") + " "
|
3710
|
+
end
|
3442
3711
|
else
|
3443
3712
|
line += " "
|
3444
3713
|
end
|
3445
3714
|
|
3446
|
-
#
|
3447
|
-
|
3715
|
+
# Apply process_text for syntax highlighting
|
3716
|
+
processed = process_text(item["text"], false)
|
3717
|
+
line += processed
|
3448
3718
|
|
3449
|
-
#
|
3719
|
+
# Apply background highlighting for current item in split pane
|
3450
3720
|
if idx == @split_current
|
3451
|
-
|
3721
|
+
# Choose background color based on whether this is the active pane
|
3722
|
+
bg_color = @active_pane == :split ? "237" : "234"
|
3723
|
+
bg_code = "\e[48;5;#{bg_color}m"
|
3724
|
+
reset_bg = "\e[49m"
|
3725
|
+
line = bg_code + line.gsub(/\e\[49m/, '') + reset_bg
|
3452
3726
|
end
|
3453
3727
|
|
3454
3728
|
lines << line
|
3455
3729
|
end
|
3456
3730
|
|
3731
|
+
# Add a blank line at the bottom to show end of document
|
3732
|
+
lines << ""
|
3733
|
+
|
3734
|
+
# Set the full content to the pane and let rcurses handle scrolling
|
3457
3735
|
@split_pane.text = lines.join("\n")
|
3736
|
+
|
3737
|
+
# Calculate how many extra lines are created by wrapping
|
3738
|
+
extra_wrapped_lines = 0
|
3739
|
+
lines[0..-2].each do |line| # Exclude the blank line we added
|
3740
|
+
# Remove ANSI codes for length calculation
|
3741
|
+
clean_line = line.gsub(/\e\[[0-9;]*m/, '')
|
3742
|
+
if clean_line.length > @split_pane.w
|
3743
|
+
# This line will wrap - calculate how many extra lines it creates
|
3744
|
+
extra_lines = (clean_line.length.to_f / @split_pane.w).ceil - 1
|
3745
|
+
extra_wrapped_lines += extra_lines
|
3746
|
+
end
|
3747
|
+
end
|
3748
|
+
|
3749
|
+
# Calculate scroll position exactly like RTFM does, but account for wrapping
|
3750
|
+
scrolloff = 3
|
3751
|
+
total = visible_items.length + 1 # +1 for the blank line
|
3752
|
+
page = @split_pane.h
|
3753
|
+
|
3754
|
+
if total <= page
|
3755
|
+
# If everything fits, always start from the very top
|
3756
|
+
@split_pane.ix = 0
|
3757
|
+
elsif @split_current - @split_pane.ix < scrolloff
|
3758
|
+
# If we're too close to the top of the pane, scroll up
|
3759
|
+
@split_pane.ix = [@split_current - scrolloff, 0].max
|
3760
|
+
elsif (@split_pane.ix + page - 1 - @split_current) < scrolloff
|
3761
|
+
# If we're too close to the bottom of the pane, scroll down
|
3762
|
+
# Account for wrapped lines dynamically
|
3763
|
+
max_off = [total - page + extra_wrapped_lines, 0].max
|
3764
|
+
@split_pane.ix = [@split_current + scrolloff - page + 1, max_off].min
|
3765
|
+
end
|
3766
|
+
|
3458
3767
|
@split_pane.refresh
|
3459
3768
|
end
|
3460
3769
|
|
@@ -3467,11 +3776,22 @@ class HyperListApp
|
|
3467
3776
|
|
3468
3777
|
def move_in_active_pane(direction)
|
3469
3778
|
if @split_view && @active_pane == :split
|
3779
|
+
visible = get_visible_split_items
|
3470
3780
|
case direction
|
3471
3781
|
when :down
|
3472
|
-
|
3782
|
+
if @split_current >= visible.length - 1
|
3783
|
+
# Wrap to top
|
3784
|
+
@split_current = 0
|
3785
|
+
else
|
3786
|
+
@split_current = @split_current + 1
|
3787
|
+
end
|
3473
3788
|
when :up
|
3474
|
-
|
3789
|
+
if @split_current <= 0
|
3790
|
+
# Wrap to bottom
|
3791
|
+
@split_current = visible.length - 1
|
3792
|
+
else
|
3793
|
+
@split_current = @split_current - 1
|
3794
|
+
end
|
3475
3795
|
end
|
3476
3796
|
else
|
3477
3797
|
case direction
|
@@ -3957,28 +4277,57 @@ class HyperListApp
|
|
3957
4277
|
go_to_first_child
|
3958
4278
|
when "LEFT"
|
3959
4279
|
# Unindent only the current item
|
3960
|
-
|
4280
|
+
if @split_view && @active_pane == :split
|
4281
|
+
indent_split_left(false)
|
4282
|
+
else
|
4283
|
+
indent_left(false)
|
4284
|
+
end
|
3961
4285
|
when "RIGHT"
|
3962
4286
|
# Indent only the current item
|
3963
|
-
|
4287
|
+
if @split_view && @active_pane == :split
|
4288
|
+
indent_split_right(false)
|
4289
|
+
else
|
4290
|
+
indent_right(false)
|
4291
|
+
end
|
3964
4292
|
when "PgUP" # Page Up
|
3965
4293
|
page_up
|
3966
4294
|
when "PgDOWN" # Page Down
|
3967
4295
|
page_down
|
3968
4296
|
when "HOME" # Home
|
3969
|
-
@
|
3970
|
-
|
3971
|
-
|
4297
|
+
if @split_view && @active_pane == :split
|
4298
|
+
@split_current = 0
|
4299
|
+
else
|
4300
|
+
@current = 0
|
4301
|
+
@offset = 0
|
4302
|
+
update_presentation_focus if @presentation_mode
|
4303
|
+
end
|
3972
4304
|
when "END" # End
|
3973
|
-
@
|
3974
|
-
|
4305
|
+
if @split_view && @active_pane == :split
|
4306
|
+
visible = get_visible_split_items
|
4307
|
+
@split_current = [visible.length - 1, 0].max
|
4308
|
+
else
|
4309
|
+
visible = get_visible_items
|
4310
|
+
@current = [visible.length - 1, 0].max
|
4311
|
+
update_presentation_focus if @presentation_mode
|
4312
|
+
end
|
3975
4313
|
when "g" # Go to top (was gg)
|
3976
|
-
@
|
3977
|
-
|
3978
|
-
|
4314
|
+
if @split_view && @active_pane == :split
|
4315
|
+
@split_current = 0
|
4316
|
+
else
|
4317
|
+
@current = 0
|
4318
|
+
@offset = 0
|
4319
|
+
update_presentation_focus if @presentation_mode
|
4320
|
+
end
|
3979
4321
|
when "G" # Go to bottom
|
3980
|
-
@
|
3981
|
-
|
4322
|
+
if @split_view && @active_pane == :split
|
4323
|
+
visible = get_visible_split_items
|
4324
|
+
@split_current = [visible.length - 1, 0].max
|
4325
|
+
else
|
4326
|
+
visible = get_visible_items
|
4327
|
+
@current = [visible.length - 1, 0].max
|
4328
|
+
# Let normal offset logic position the last item
|
4329
|
+
update_presentation_focus if @presentation_mode
|
4330
|
+
end
|
3982
4331
|
when "R" # Jump to reference (was gr)
|
3983
4332
|
jump_to_reference
|
3984
4333
|
when "F" # Open file reference (was gf)
|
@@ -4038,6 +4387,8 @@ class HyperListApp
|
|
4038
4387
|
delete_line(false) # D always deletes with children by default
|
4039
4388
|
when "C-D" # Delete line and all descendants explicitly
|
4040
4389
|
delete_line(true)
|
4390
|
+
when "C-E" # Toggle line encryption
|
4391
|
+
toggle_line_encryption
|
4041
4392
|
when "y" # Yank/copy single line
|
4042
4393
|
yank_line(false)
|
4043
4394
|
when "Y" # Yank/copy line with all descendants
|
@@ -4056,10 +4407,18 @@ class HyperListApp
|
|
4056
4407
|
move_item_up(true)
|
4057
4408
|
when "TAB"
|
4058
4409
|
# Indent with all children
|
4059
|
-
|
4410
|
+
if @split_view && @active_pane == :split
|
4411
|
+
indent_split_right(true)
|
4412
|
+
else
|
4413
|
+
indent_right(true)
|
4414
|
+
end
|
4060
4415
|
when "S-TAB" # Shift-Tab
|
4061
4416
|
# Unindent with all children
|
4062
|
-
|
4417
|
+
if @split_view && @active_pane == :split
|
4418
|
+
indent_split_left(true)
|
4419
|
+
else
|
4420
|
+
indent_left(true)
|
4421
|
+
end
|
4063
4422
|
when "u"
|
4064
4423
|
undo
|
4065
4424
|
when "\x12" # Ctrl-R for redo (0x12 is Ctrl-R ASCII code)
|
@@ -4068,8 +4427,6 @@ class HyperListApp
|
|
4068
4427
|
toggle_checkbox
|
4069
4428
|
when "V"
|
4070
4429
|
toggle_checkbox_with_date
|
4071
|
-
when "\x05" # Ctrl-E for encryption toggle
|
4072
|
-
toggle_line_encryption
|
4073
4430
|
when "."
|
4074
4431
|
repeat_last_action
|
4075
4432
|
when "/"
|
@@ -4080,11 +4437,34 @@ class HyperListApp
|
|
4080
4437
|
jump_to_next_template_marker
|
4081
4438
|
when "P"
|
4082
4439
|
toggle_presentation_mode
|
4440
|
+
when "\x15" # Ctrl-U for State/Transition underline toggle
|
4441
|
+
# Cycle through underline modes: 0 (none) -> 1 (states) -> 2 (transitions) -> 0
|
4442
|
+
@st_underline_mode = (@st_underline_mode + 1) % 3
|
4443
|
+
# Clear cache to force re-rendering
|
4444
|
+
@processed_cache.clear
|
4445
|
+
case @st_underline_mode
|
4446
|
+
when 0
|
4447
|
+
@message = "Underline mode: OFF"
|
4448
|
+
when 1
|
4449
|
+
@message = "Underline mode: STATES (S: and |)"
|
4450
|
+
when 2
|
4451
|
+
@message = "Underline mode: TRANSITIONS (T: and /)"
|
4452
|
+
end
|
4083
4453
|
when "\\"
|
4084
4454
|
next_c = getchr
|
4085
4455
|
case next_c
|
4086
4456
|
when "u"
|
4087
|
-
|
4457
|
+
# Alternative for Ctrl-U (backslash-u)
|
4458
|
+
@st_underline_mode = (@st_underline_mode + 1) % 3
|
4459
|
+
@processed_cache.clear
|
4460
|
+
case @st_underline_mode
|
4461
|
+
when 0
|
4462
|
+
@message = "Underline mode: OFF"
|
4463
|
+
when 1
|
4464
|
+
@message = "Underline mode: STATES (S: and |)"
|
4465
|
+
when 2
|
4466
|
+
@message = "Underline mode: TRANSITIONS (T: and /)"
|
4467
|
+
end
|
4088
4468
|
end
|
4089
4469
|
when ":"
|
4090
4470
|
handle_command
|
@@ -4128,15 +4508,16 @@ class HyperListApp
|
|
4128
4508
|
next_c = getchr
|
4129
4509
|
play_macro(next_c) if next_c && next_c =~ /[a-z]/
|
4130
4510
|
when "w"
|
4131
|
-
#
|
4132
|
-
|
4133
|
-
if next_c == "w" && @split_view
|
4134
|
-
# Switch active pane
|
4511
|
+
# Switch active pane if split view is active
|
4512
|
+
if @split_view
|
4135
4513
|
@active_pane = (@active_pane == :main) ? :split : :main
|
4136
4514
|
@message = "Switched to #{@active_pane} pane"
|
4515
|
+
# Force re-render both panes to update highlighting
|
4516
|
+
render_main
|
4517
|
+
render_split_pane
|
4518
|
+
render_footer
|
4137
4519
|
else
|
4138
|
-
|
4139
|
-
@message = "Unknown command: w#{next_c}"
|
4520
|
+
@message = "Split view not active. Use :vs to enable"
|
4140
4521
|
end
|
4141
4522
|
when "Q" # Force quit
|
4142
4523
|
quit
|