hyperlist 1.9.0 → 1.9.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.
Files changed (6) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +52 -0
  3. data/README.md +30 -11
  4. data/hyperlist +140 -95
  5. data/hyperlist.gemspec +1 -1
  6. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1f638bfa971829555f7f078ed1c2e5a498a3e0d4026dea67fd547bc881fdec61
4
- data.tar.gz: 3cbb6c08d664e5ed7111384ef275293c9616c8c213f3cc189beaf1e3054fbfac
3
+ metadata.gz: bb81e544d2363e852a1e29bb1eef3d2fd891e766b2ade2a3e5c56fdc3073302e
4
+ data.tar.gz: 14f24e55bfb6f3241ef9120cadcc680971617bfa5378893c3523e4eb1cb1105b
5
5
  SHA512:
6
- metadata.gz: a71a5cfa04c8e52652f990d970f6fc7a1e65a5a89a9869b558937a016ae7c8e2aa0017910a23941158abbbfb6c8d92a2f0b8d50075cb3cb303d06c04f62c876d
7
- data.tar.gz: cccf1614ea64a35296d0de28b35d3ca3098fc31d68296cb271ef93ca5e085ffc2801fac04b98df96c72340b00adb39003fbcb55651f5561fe7e3a36ecf63a9e6
6
+ metadata.gz: 067b8ba96f97768aa5461e2458b7215cf045926875765ac631b450c9a15ed2313ded67f7986163c6fef8c5408dddf0160a10fa7933ffedbe23722822078e9060
7
+ data.tar.gz: c10a7a1e147fadd212445b9d5ee7baadada737f53ccc55aa89db86f95f2cb208edf1d56a373020c4ac51c5a898e5c2198cb1458cccbfea36526dff104cfe61a7
data/CHANGELOG.md CHANGED
@@ -2,6 +2,58 @@
2
2
 
3
3
  All notable changes to the HyperList Ruby TUI will be documented in this file.
4
4
 
5
+ ## [1.9.2] - 2025-12-02
6
+
7
+ ### Fixed
8
+ - **Presentation mode completely rewritten**
9
+ - Now dynamically folds/unfolds as you navigate (like hyperlist.vim)
10
+ - Entering presentation mode (C-P) now stays on the current item instead of jumping to the first line
11
+ - Moving up/down properly reveals the path to the current item while folding everything else
12
+ - Uses proper item tracking via real indices to handle fold state changes correctly
13
+
14
+ ## [1.9.1] - 2025-12-02
15
+
16
+ ### Added
17
+ - **External file change detection**
18
+ - Automatically detects when the file is modified by other processes (vim, another Claude Code session, etc.)
19
+ - Prompts user to reload with "File changed externally. Reload? (y/n)"
20
+ - Preserves cursor position after reload
21
+ - Option to ignore external changes (updates internal timestamp to avoid repeated prompts)
22
+
23
+ ### Fixed
24
+ - **Quote coloring inside parentheses (comments)**
25
+ - Fixed issue where quotes inside parentheses like `(this is a "comment" in here)` would break the cyan coloring
26
+ - The closing quote would terminate coloring, leaving the rest of the comment white
27
+ - Now the entire parentheses content stays cyan as intended
28
+
29
+ ## [1.9.0] - 2025-10-24
30
+
31
+ ### Added
32
+ - **Item Tagging & Batch Operations**
33
+ - Tag items with 't' key for batch operations (auto-advance cursor)
34
+ - Visual feedback: dark blue background for tagged items, lighter blue for tagged+selected
35
+ - Status bar shows [T:N] count of tagged items
36
+ - Clear all tags with 'u' key
37
+ - Regex tagging with 'C-T' - tag all items matching a pattern
38
+ - Batch operations: D/C-D (delete), y/Y (yank), Tab/S-Tab (indent) work on all tagged items
39
+
40
+ - **External Editor Support**
41
+ - Press 'E' to spawn $EDITOR (vim, nano, etc.)
42
+ - Automatic save/reload workflow
43
+ - Proper terminal state management
44
+ - Full screen refresh on return
45
+
46
+ - **Enhanced Paste & Navigation**
47
+ - 'P' to paste above current item (vim-style)
48
+ - Presentation mode moved to 'C-P'
49
+ - Templates moved to 'T' key
50
+ - Undo moved to 'U' key (RTFM-consistent)
51
+
52
+ ### Enhanced
53
+ - **Smart Modified Flag**
54
+ - [+] indicator removed when undoing back to original file state
55
+ - Tracks original state to detect true modifications
56
+
5
57
  ## [1.8.0] - 2025-09-22
6
58
 
7
59
  ### Enhanced
data/README.md CHANGED
@@ -29,9 +29,28 @@ For historical context and the original VIM implementation, see: [hyperlist.vim]
29
29
  ### Help Screen
30
30
  ![HyperList Help](img/screenshot_help.png)
31
31
 
32
- ## What's New in v1.9.0
32
+ ## What's New in v1.9.2
33
33
 
34
- ### 🏷️ Item Tagging & Batch Operations
34
+ ### Presentation Mode Fixed
35
+ - **Dynamic folding**: Presentation mode now works like hyperlist.vim - folds/unfolds dynamically as you navigate
36
+ - **Cursor preservation**: Entering presentation mode (C-P) stays on the current item instead of jumping to the first line
37
+ - **Proper navigation**: Moving up/down reveals the path to the current item while folding everything else
38
+
39
+ ## Previous Release: v1.9.1
40
+
41
+ ### External File Change Detection
42
+ - **Auto-detect external changes**: HyperList now monitors when the file is modified by other processes (vim, another Claude Code session, etc.)
43
+ - **Reload prompt**: Shows "File changed externally. Reload? (y/n)" when changes are detected
44
+ - **Cursor preservation**: Your cursor position is preserved after reload
45
+ - **Skip option**: Press 'n' to ignore external changes and continue editing
46
+
47
+ ### Bug Fix: Quote Coloring in Comments
48
+ - Fixed an issue where quotes inside parentheses (comments) like `(this is a "comment" in here)` would break the cyan coloring
49
+ - The entire parentheses content now stays cyan as intended
50
+
51
+ ## Previous Release: v1.9.0
52
+
53
+ ### Item Tagging & Batch Operations
35
54
  - **Tag items**: Press 't' to tag/untag items for batch operations
36
55
  - **Auto-advance**: Cursor automatically moves to next item after tagging for fast consecutive tagging
37
56
  - **Visual feedback**: Tagged items show dark blue background, lighter blue when selected
@@ -41,47 +60,47 @@ For historical context and the original VIM implementation, see: [hyperlist.vim]
41
60
  - **Batch operations**: Delete (D/C-D), yank (y/Y), and indent (Tab/S-Tab) work on all tagged items
42
61
  - Tag consecutive or non-consecutive items, then perform operations on the entire set
43
62
 
44
- ### ✏️ External Editor Support
63
+ ### External Editor Support
45
64
  - **Edit in $EDITOR**: Press 'E' to spawn your preferred editor (vim, nano, emacs, etc.)
46
65
  - **Seamless workflow**: File saved automatically, editor launched, changes reloaded on exit
47
66
  - **Terminal management**: Terminal state properly saved and restored
48
67
  - Uses `$EDITOR` environment variable (defaults to vi if not set)
49
68
 
50
- ### 📋 Enhanced Paste & Navigation
69
+ ### Enhanced Paste & Navigation
51
70
  - **Paste above**: Press 'P' to paste above current item (vim-style)
52
71
  - **Paste below**: Press 'p' to paste below current item (existing)
53
72
  - **Presentation mode**: Moved to 'C-P' (was 'P') for consistency
54
73
  - **Templates**: Moved to 'T' key (was 't')
55
74
  - **Undo**: Moved to 'U' key (was 'u') - consistent with RTFM
56
75
 
57
- ### 🎯 Smart Modified Flag
76
+ ### Smart Modified Flag
58
77
  - **Intelligent tracking**: `[+]` indicator automatically removed when undoing back to original file state
59
78
  - **Clean status**: No false "modified" indicator after complete undo to original
60
79
 
61
80
  ## Previous Release: v1.8.0
62
81
 
63
- ### 📋 Multi-Line Paste Support
82
+ ### Multi-Line Paste Support
64
83
  - **Paste multiple lines**: When pasting multi-line content into item insertion prompts ('o', 'O', 'a', 'A'), each line becomes a separate item
65
84
  - **Visual feedback**: Shows `[+N lines]` indicator during multi-line paste
66
85
  - **Smart insertion**: All pasted lines inserted as siblings at the same level
67
86
  - Great for importing bullet lists from PDFs, emails, or other documents
68
87
  - Requires rcurses 6.1.5+
69
88
 
70
- ### 📄 PDF/LaTeX Export
89
+ ### PDF/LaTeX Export
71
90
  - **Export to PDF**: `:export pdf filename.pdf` - Full LaTeX-based PDF generation
72
91
  - **Export to LaTeX**: `:export latex filename.tex` - Get the LaTeX source
73
92
  - **Professional output**: Color-coded elements, table of contents, headers
74
93
  - **Complete HyperList support**: All syntax elements rendered beautifully
75
94
  - Requires: texlive-latex-base and texlive-latex-extra packages
76
95
 
77
- ### 📋 System Clipboard Integration
96
+ ### System Clipboard Integration
78
97
  - **Yank to clipboard**: 'y' and 'Y' now copy to system clipboard
79
98
  - **Middle-click paste**: Yanked items can be pasted into other terminals
80
99
  - **Preserves indentation**: Copied text maintains proper structure
81
100
 
82
101
  ## Previous Version Features (v1.4.0)
83
102
 
84
- ### 🎨 Configuration Lines & Theming
103
+ ### Configuration Lines & Theming
85
104
  - **Configuration Lines**: Add settings at the bottom of HyperList files using `((option=value, option2=value))`
86
105
  - **Theme Support**: Three color themes - `light` (bright colors), `normal` (standard), `dark` (for light terminals)
87
106
  - **Line Wrapping**: Enable with `wrap=yes` - wrapped lines use `+` prefix per HyperList spec
@@ -142,13 +161,13 @@ All `:set` commands automatically update the file's configuration line.
142
161
  - Secure AES-256-CBC encryption with PBKDF2 key derivation
143
162
  - Password caching for the session
144
163
 
145
- ### 🎯 Enhanced Presentation Mode
164
+ ### Enhanced Presentation Mode
146
165
  - **Auto-collapse** everything outside the current context
147
166
  - **Smart focus**: Shows only current item, ancestors, and immediate children
148
167
  - **Visual hierarchy**: Focused items in full color, others greyed out
149
168
  - Improved navigation with proper cursor tracking
150
169
 
151
- ### 🎨 Better Visual Experience
170
+ ### Better Visual Experience
152
171
  - **Improved highlighting**: Dark gray background preserves syntax colors
153
172
  - **Subtle selection**: No more harsh reverse video
154
173
  - **Preserved colors**: All HyperList elements maintain their colors when selected
data/hyperlist CHANGED
@@ -7,7 +7,7 @@
7
7
  # Check for help/version BEFORE loading any libraries
8
8
  if ARGV[0] == '-h' || ARGV[0] == '--help'
9
9
  puts <<~HELP
10
- HyperList v1.9.0 - Terminal User Interface for HyperList files
10
+ HyperList v1.9.2 - Terminal User Interface for HyperList files
11
11
 
12
12
  USAGE
13
13
  hyperlist [OPTIONS] [FILE]
@@ -52,7 +52,7 @@ if ARGV[0] == '-h' || ARGV[0] == '--help'
52
52
  HELP
53
53
  exit 0
54
54
  elsif ARGV[0] == '-v' || ARGV[0] == '--version'
55
- puts "HyperList v1.9.0"
55
+ puts "HyperList v1.9.2"
56
56
  exit 0
57
57
  end
58
58
 
@@ -73,7 +73,7 @@ class HyperListApp
73
73
  include Rcurses::Input
74
74
  include Rcurses::Cursor
75
75
 
76
- VERSION = "1.9.0"
76
+ VERSION = "1.9.2"
77
77
 
78
78
  def initialize(filename = nil)
79
79
  @filename = filename ? File.expand_path(filename) : nil
@@ -124,7 +124,8 @@ class HyperListApp
124
124
  @split_offset = 0 # Second view scroll offset
125
125
  @active_pane = :main # :main or :split
126
126
  @message_timeout = nil # For timed message display
127
-
127
+ @file_mtime = nil # Track file modification time for external change detection
128
+
128
129
  # Global configuration
129
130
  @config_file = File.expand_path("~/.hyperlist/config.yml")
130
131
  @indent_size = 2 # Default indentation (2-5 spaces)
@@ -228,6 +229,9 @@ class HyperListApp
228
229
  @config_line = nil # Reset config line before loading
229
230
  @tagged_items = [] # Clear tags when loading new file
230
231
 
232
+ # Track file modification time for external change detection
233
+ @file_mtime = File.mtime(file) rescue nil
234
+
231
235
  # Read file content
232
236
  content = File.read(file) rescue ""
233
237
 
@@ -527,12 +531,48 @@ class HyperListApp
527
531
  def load_command_history
528
532
  history_file = File.expand_path("~/.hyperlist_command_history")
529
533
  return [] unless File.exist?(history_file)
530
-
534
+
531
535
  File.readlines(history_file).map(&:strip).reject(&:empty?).last(100)
532
536
  rescue
533
537
  []
534
538
  end
535
-
539
+
540
+ # Check if file was modified externally and prompt for reload
541
+ def check_file_changed
542
+ return unless @filename && File.exist?(@filename) && @file_mtime
543
+
544
+ current_mtime = File.mtime(@filename) rescue nil
545
+ return unless current_mtime && current_mtime > @file_mtime
546
+
547
+ # File changed externally - prompt user
548
+ @footer.text = "File changed externally. Reload? (y/n): "
549
+ @footer.refresh
550
+
551
+ response = getchr
552
+ if response&.downcase == 'y'
553
+ # Save cursor position
554
+ saved_current = @current
555
+ saved_offset = @offset
556
+
557
+ load_file(@filename)
558
+
559
+ # Restore cursor position (clamped to new file size)
560
+ visible = get_visible_items
561
+ @current = [saved_current, visible.length - 1, 0].max
562
+ @current = [@current, visible.length - 1].min if visible.length > 0
563
+ @offset = saved_offset
564
+
565
+ @message = "File reloaded"
566
+ @modified = false
567
+ clear_cache
568
+ render
569
+ else
570
+ # Update mtime to avoid repeated prompts
571
+ @file_mtime = current_mtime
572
+ @message = "External changes ignored"
573
+ end
574
+ end
575
+
536
576
  def save_command_history
537
577
  history_file = File.expand_path("~/.hyperlist_command_history")
538
578
  File.open(history_file, 'w') do |f|
@@ -712,6 +752,7 @@ class HyperListApp
712
752
  @modified = false
713
753
  @original_items = @items.map { |item| item.dup } # Save state after save
714
754
  @last_auto_save = Time.now if @auto_save_enabled
755
+ @file_mtime = File.mtime(@filename) rescue nil # Update mtime after save
715
756
  end
716
757
 
717
758
  def check_auto_save
@@ -1606,9 +1647,10 @@ class HyperListApp
1606
1647
 
1607
1648
  # Handle parentheses content (moved here to avoid conflicts with properties)
1608
1649
  # Based on hyperlist.vim: '(.\{-})'
1650
+ # Color entire parentheses content as cyan - quotes inside are also cyan
1609
1651
  result = safe_regex_replace(result, /\(([^)]*)\)/) do |match|
1610
- content = match[1..-2] # Extract content between parentheses
1611
- "(".fg(colors["cyan"]) + content.fg(colors["cyan"]) + ")".fg(colors["cyan"])
1652
+ # Just color the whole thing cyan - no special handling for quotes inside
1653
+ match.fg(colors["cyan"])
1612
1654
  end
1613
1655
 
1614
1656
  # Handle semicolons as separators (they separate items on the same line)
@@ -1624,11 +1666,17 @@ class HyperListApp
1624
1666
 
1625
1667
  # Handle quoted strings (only double quotes are special in HyperList)
1626
1668
  # Based on hyperlist.vim: '".\{-}"'
1627
- result.gsub!(/"([^"]*)"/) do
1628
- content = $1
1669
+ # Use safe_regex_replace to avoid processing quotes inside already-colored parentheses
1670
+ result = safe_regex_replace(result, /"([^"]*)"/) do |match|
1671
+ content = match[1..-2] # Extract content between quotes
1629
1672
  # Color any ## sequences inside the quotes as red
1630
- content.gsub!(/(##[<>-]+)/) { $1.fg(colors["red"]) }
1631
- '"'.fg(colors["cyan"]) + content.fg(colors["cyan"]) + '"'.fg(colors["cyan"]) # Cyan for quoted strings
1673
+ colored_content = content.gsub(/(##[<>-]+)/) { $1.fg(colors["red"]) }
1674
+ # If no ## sequences were found, just color the whole thing cyan
1675
+ if colored_content == content
1676
+ match.fg(colors["cyan"]) # Color entire quoted string cyan
1677
+ else
1678
+ '"'.fg(colors["cyan"]) + colored_content + '"'.fg(colors["cyan"])
1679
+ end
1632
1680
  end
1633
1681
 
1634
1682
  # Handle change markup - all double-hashes should be red
@@ -1995,28 +2043,28 @@ class HyperListApp
1995
2043
 
1996
2044
  def move_up
1997
2045
  if @presentation_mode
1998
- # In presentation mode, we need to handle navigation differently
1999
- visible_before = get_visible_items
2000
-
2046
+ # In presentation mode: move to target, then update folds
2047
+ visible = get_visible_items
2048
+ return if visible.empty?
2049
+
2050
+ # Calculate target position
2001
2051
  if @current == 0
2002
- # Wrap around to last item
2003
- target_index = visible_before.length - 1
2052
+ target_index = visible.length - 1
2004
2053
  else
2005
2054
  target_index = @current - 1
2006
2055
  end
2007
-
2008
- # Get the actual item we want to move to
2009
- target_item = visible_before[target_index]
2010
- target_real_idx = get_real_index(target_item)
2011
-
2012
- # Update presentation focus for the target item
2056
+
2057
+ # Get the real index of the target item
2058
+ target_real_idx = get_real_index(visible[target_index])
2059
+
2060
+ # Temporarily set current to target so update_presentation_focus works on it
2013
2061
  @current = target_index
2014
2062
  update_presentation_focus
2015
-
2016
- # Now find where the target item ended up after reorganization
2017
- visible_after = get_visible_items
2018
- visible_after.each_with_index do |item, idx|
2019
- if get_real_index(item) == target_real_idx
2063
+
2064
+ # After fold changes, find where the target item is now
2065
+ new_visible = get_visible_items
2066
+ new_visible.each_with_index do |item, idx|
2067
+ if item["_real_index"] == target_real_idx
2020
2068
  @current = idx
2021
2069
  break
2022
2070
  end
@@ -2024,9 +2072,8 @@ class HyperListApp
2024
2072
  else
2025
2073
  # Normal mode navigation
2026
2074
  max_items = get_visible_items.length - 1
2027
-
2075
+
2028
2076
  if @current == 0
2029
- # Wrap around to last item
2030
2077
  @current = max_items
2031
2078
  else
2032
2079
  @current = [@current - 1, 0].max
@@ -2036,29 +2083,29 @@ class HyperListApp
2036
2083
 
2037
2084
  def move_down
2038
2085
  if @presentation_mode
2039
- # In presentation mode, we need to handle navigation differently
2040
- visible_before = get_visible_items
2041
- max = visible_before.length - 1
2042
-
2043
- if @current == max
2044
- # Wrap around to first item
2086
+ # In presentation mode: move to target, then update folds
2087
+ visible = get_visible_items
2088
+ return if visible.empty?
2089
+ max = visible.length - 1
2090
+
2091
+ # Calculate target position
2092
+ if @current >= max
2045
2093
  target_index = 0
2046
2094
  else
2047
2095
  target_index = @current + 1
2048
2096
  end
2049
-
2050
- # Get the actual item we want to move to
2051
- target_item = visible_before[target_index]
2052
- target_real_idx = get_real_index(target_item)
2053
-
2054
- # Update presentation focus for the target item
2097
+
2098
+ # Get the real index of the target item
2099
+ target_real_idx = get_real_index(visible[target_index])
2100
+
2101
+ # Temporarily set current to target so update_presentation_focus works on it
2055
2102
  @current = target_index
2056
2103
  update_presentation_focus
2057
-
2058
- # Now find where the target item ended up after reorganization
2059
- visible_after = get_visible_items
2060
- visible_after.each_with_index do |item, idx|
2061
- if get_real_index(item) == target_real_idx
2104
+
2105
+ # After fold changes, find where the target item is now
2106
+ new_visible = get_visible_items
2107
+ new_visible.each_with_index do |item, idx|
2108
+ if item["_real_index"] == target_real_idx
2062
2109
  @current = idx
2063
2110
  break
2064
2111
  end
@@ -2066,9 +2113,8 @@ class HyperListApp
2066
2113
  else
2067
2114
  # Normal mode navigation
2068
2115
  max = get_visible_items.length - 1
2069
-
2070
- if @current == max
2071
- # Wrap around to first item
2116
+
2117
+ if @current >= max
2072
2118
  @current = 0
2073
2119
  else
2074
2120
  @current = [@current + 1, max].min
@@ -2297,84 +2343,80 @@ class HyperListApp
2297
2343
  # Enter presentation mode
2298
2344
  # Remember which item we're on before any folding changes
2299
2345
  visible_items = get_visible_items
2300
- if @current < visible_items.length
2301
- target_item = visible_items[@current]
2302
- end
2303
-
2346
+ return if visible_items.empty? || @current >= visible_items.length
2347
+
2348
+ # Get the real index of the current item
2349
+ target_real_idx = visible_items[@current]["_real_index"]
2350
+
2304
2351
  @presentation_mode = true
2305
- # Clear cache to force re-rendering
2352
+ # Clear cache to force re-rendering
2306
2353
  @processed_cache.clear
2307
2354
  update_presentation_focus
2308
-
2309
- # Make sure cursor is still on the same item after initial folding
2310
- if target_item
2311
- new_visible = get_visible_items
2312
- new_position = new_visible.index(target_item)
2313
- @current = new_position if new_position
2355
+
2356
+ # Find where the target item is after fold changes
2357
+ new_visible = get_visible_items
2358
+ new_visible.each_with_index do |item, idx|
2359
+ if item["_real_index"] == target_real_idx
2360
+ @current = idx
2361
+ break
2362
+ end
2314
2363
  end
2315
-
2316
- @message = "Presentation mode enabled - focus on current item"
2364
+
2365
+ @message = "Presentation mode enabled"
2317
2366
  end
2318
2367
  end
2319
2368
 
2320
2369
  def update_presentation_focus
2321
- # This method updates the focus in presentation mode
2322
- # It will be called whenever the cursor moves
2370
+ # Presentation mode: fold everything, then reveal path to current item
2371
+ # Like vim's: fold to level 0, then zv (reveal current line)
2323
2372
  return unless @presentation_mode
2324
-
2373
+
2325
2374
  visible_items = get_visible_items
2326
2375
  return if visible_items.empty? || @current >= visible_items.length
2327
-
2328
- # Remember which item we're focused on
2376
+
2377
+ # Get the current item before we change anything
2329
2378
  current_item = visible_items[@current]
2330
- current_level = current_item["level"]
2331
2379
  current_real_idx = get_real_index(current_item)
2332
-
2333
- # First, fold everything
2380
+ return unless current_real_idx
2381
+
2382
+ current_level = current_item["level"]
2383
+
2384
+ # Step 1: Fold everything that has children
2334
2385
  @items.each_with_index do |item, idx|
2335
2386
  item["fold"] = has_children?(idx, @items)
2336
2387
  item["presentation_focus"] = false
2337
2388
  end
2338
-
2339
- # Mark current item as in focus and unfold it in the main items array
2340
- if current_real_idx && current_real_idx < @items.length
2341
- @items[current_real_idx]["presentation_focus"] = true
2342
- @items[current_real_idx]["fold"] = false
2343
- end
2344
-
2345
- # Unfold all ancestors of current item
2346
- ancestor_indices = []
2389
+
2390
+ # Step 2: Unfold all ancestors (path from root to current item)
2347
2391
  search_level = current_level - 1
2348
2392
  idx = current_real_idx - 1
2349
-
2350
2393
  while idx >= 0 && search_level >= 0
2351
2394
  if @items[idx]["level"] == search_level
2352
- @items[idx]["fold"] = false
2353
- @items[idx]["presentation_focus"] = false # Ancestors visible but not focused
2354
- ancestor_indices << idx
2395
+ @items[idx]["fold"] = false # Unfold this ancestor
2355
2396
  search_level -= 1
2356
2397
  end
2357
2398
  idx -= 1
2358
2399
  end
2359
-
2360
- # Mark immediate children as in focus (only one level down)
2400
+
2401
+ # Step 3: Unfold the current item to show its direct children
2402
+ @items[current_real_idx]["fold"] = false
2403
+ @items[current_real_idx]["presentation_focus"] = true
2404
+
2405
+ # Step 4: Mark direct children as in focus (for visual highlighting)
2361
2406
  idx = current_real_idx + 1
2362
2407
  while idx < @items.length && @items[idx]["level"] > current_level
2363
2408
  if @items[idx]["level"] == current_level + 1
2364
2409
  @items[idx]["presentation_focus"] = true
2365
- # Don't unfold children - let them stay folded unless user explicitly unfolds
2366
2410
  end
2367
2411
  idx += 1
2368
2412
  end
2369
-
2370
- # Now recalculate the cursor position to point to the same item
2413
+
2414
+ # Step 5: Recalculate cursor position after fold changes
2371
2415
  new_visible_items = get_visible_items
2372
2416
  new_position = new_visible_items.index(current_item)
2373
- if new_position
2374
- @current = new_position
2375
- end
2376
-
2377
- # Clear cache to force re-rendering with new focus
2417
+ @current = new_position if new_position
2418
+
2419
+ # Clear cache to force re-rendering
2378
2420
  @processed_cache.clear
2379
2421
  end
2380
2422
 
@@ -6397,7 +6439,10 @@ class HyperListApp
6397
6439
  loop do
6398
6440
  # Check for auto-save
6399
6441
  check_auto_save if @auto_save_enabled
6400
-
6442
+
6443
+ # Check for external file changes
6444
+ check_file_changed
6445
+
6401
6446
  c = getchr
6402
6447
 
6403
6448
  # Skip nil input (shouldn't happen normally)
data/hyperlist.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |spec|
2
2
  spec.name = "hyperlist"
3
- spec.version = "1.9.0"
3
+ spec.version = "1.9.2"
4
4
  spec.authors = ["Geir Isene"]
5
5
  spec.email = ["g@isene.com"]
6
6
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hyperlist
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.0
4
+ version: 1.9.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Geir Isene
8
8
  autorequire:
9
9
  bindir: "."
10
10
  cert_chain: []
11
- date: 2025-10-24 00:00:00.000000000 Z
11
+ date: 2025-12-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rcurses