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