hyperlist 1.8.1 → 1.9.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.
Files changed (6) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +43 -0
  3. data/README.md +62 -11
  4. data/hyperlist +485 -132
  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: 3ab71a37a8c51bc72f5a944172d2fae808da9ed42e4a21e13f937d2a4410596a
4
- data.tar.gz: 346c2edd59e6a6e4a116d05a506ded75b313996baa06f938def3fb0947c05850
3
+ metadata.gz: 485f51c3f998ca45514979e831f928a2ca907674483ba1ceef644ce6db22423e
4
+ data.tar.gz: ca21fe8db31f2b1ac1a1e8059c92f94dbc17a8d42790247238f8073c77ef973d
5
5
  SHA512:
6
- metadata.gz: b66f71b652663dbf1fcbcee37fa7478b718813a7dd234081aad77e9110d1e01c472f2ce3d2a325ad6cd1e922b2efacb398c531fab79cd594999c5e2db20ad039
7
- data.tar.gz: 3f2a1573bb24704f3f3df797e8cd3766e78d265aa27c68ca41fc7326976a23ec361c9fc93c814b9a8afc7b9003921b7e0c3111e6bfbbc9df0ebf8efddee3401e
6
+ metadata.gz: d40e130fbff4cb2564f8a9c541257cd084072f1917067d529a86427c63b0e3716b7a6704f584cf64dc3e204fd0c63a71a20443f3bcef99bc5377bb43c657b7f3
7
+ data.tar.gz: 5e6a7cdf9f8b2fb7e93382e41ba8d6e2e39d0403a0e64480d763d8385f5e4d7320684ea4b1fd7503d5a4ed08fe515d5176a04d8ab8723f9e43b7ab9d04a9e2b8
data/CHANGELOG.md CHANGED
@@ -2,6 +2,49 @@
2
2
 
3
3
  All notable changes to the HyperList Ruby TUI will be documented in this file.
4
4
 
5
+ ## [1.9.1] - 2025-12-02
6
+
7
+ ### Added
8
+ - **External file change detection**
9
+ - Automatically detects when the file is modified by other processes (vim, another Claude Code session, etc.)
10
+ - Prompts user to reload with "File changed externally. Reload? (y/n)"
11
+ - Preserves cursor position after reload
12
+ - Option to ignore external changes (updates internal timestamp to avoid repeated prompts)
13
+
14
+ ### Fixed
15
+ - **Quote coloring inside parentheses (comments)**
16
+ - Fixed issue where quotes inside parentheses like `(this is a "comment" in here)` would break the cyan coloring
17
+ - The closing quote would terminate coloring, leaving the rest of the comment white
18
+ - Now the entire parentheses content stays cyan as intended
19
+
20
+ ## [1.9.0] - 2025-10-24
21
+
22
+ ### Added
23
+ - **Item Tagging & Batch Operations**
24
+ - Tag items with 't' key for batch operations (auto-advance cursor)
25
+ - Visual feedback: dark blue background for tagged items, lighter blue for tagged+selected
26
+ - Status bar shows [T:N] count of tagged items
27
+ - Clear all tags with 'u' key
28
+ - Regex tagging with 'C-T' - tag all items matching a pattern
29
+ - Batch operations: D/C-D (delete), y/Y (yank), Tab/S-Tab (indent) work on all tagged items
30
+
31
+ - **External Editor Support**
32
+ - Press 'E' to spawn $EDITOR (vim, nano, etc.)
33
+ - Automatic save/reload workflow
34
+ - Proper terminal state management
35
+ - Full screen refresh on return
36
+
37
+ - **Enhanced Paste & Navigation**
38
+ - 'P' to paste above current item (vim-style)
39
+ - Presentation mode moved to 'C-P'
40
+ - Templates moved to 'T' key
41
+ - Undo moved to 'U' key (RTFM-consistent)
42
+
43
+ ### Enhanced
44
+ - **Smart Modified Flag**
45
+ - [+] indicator removed when undoing back to original file state
46
+ - Tracks original state to detect true modifications
47
+
5
48
  ## [1.8.0] - 2025-09-22
6
49
 
7
50
  ### Enhanced
data/README.md CHANGED
@@ -29,30 +29,71 @@ 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.8.0
33
-
34
- ### 📋 Multi-Line Paste Support
32
+ ## What's New in v1.9.1
33
+
34
+ ### External File Change Detection
35
+ - **Auto-detect external changes**: HyperList now monitors when the file is modified by other processes (vim, another Claude Code session, etc.)
36
+ - **Reload prompt**: Shows "File changed externally. Reload? (y/n)" when changes are detected
37
+ - **Cursor preservation**: Your cursor position is preserved after reload
38
+ - **Skip option**: Press 'n' to ignore external changes and continue editing
39
+
40
+ ### Bug Fix: Quote Coloring in Comments
41
+ - Fixed an issue where quotes inside parentheses (comments) like `(this is a "comment" in here)` would break the cyan coloring
42
+ - The entire parentheses content now stays cyan as intended
43
+
44
+ ## Previous Release: v1.9.0
45
+
46
+ ### Item Tagging & Batch Operations
47
+ - **Tag items**: Press 't' to tag/untag items for batch operations
48
+ - **Auto-advance**: Cursor automatically moves to next item after tagging for fast consecutive tagging
49
+ - **Visual feedback**: Tagged items show dark blue background, lighter blue when selected
50
+ - **Status indicator**: Shows `[T:N]` in status line with count of tagged items
51
+ - **Clear tags**: Press 'u' to clear all tags
52
+ - **Regex tagging**: Press 'C-T' to tag all items matching a regex pattern
53
+ - **Batch operations**: Delete (D/C-D), yank (y/Y), and indent (Tab/S-Tab) work on all tagged items
54
+ - Tag consecutive or non-consecutive items, then perform operations on the entire set
55
+
56
+ ### External Editor Support
57
+ - **Edit in $EDITOR**: Press 'E' to spawn your preferred editor (vim, nano, emacs, etc.)
58
+ - **Seamless workflow**: File saved automatically, editor launched, changes reloaded on exit
59
+ - **Terminal management**: Terminal state properly saved and restored
60
+ - Uses `$EDITOR` environment variable (defaults to vi if not set)
61
+
62
+ ### Enhanced Paste & Navigation
63
+ - **Paste above**: Press 'P' to paste above current item (vim-style)
64
+ - **Paste below**: Press 'p' to paste below current item (existing)
65
+ - **Presentation mode**: Moved to 'C-P' (was 'P') for consistency
66
+ - **Templates**: Moved to 'T' key (was 't')
67
+ - **Undo**: Moved to 'U' key (was 'u') - consistent with RTFM
68
+
69
+ ### Smart Modified Flag
70
+ - **Intelligent tracking**: `[+]` indicator automatically removed when undoing back to original file state
71
+ - **Clean status**: No false "modified" indicator after complete undo to original
72
+
73
+ ## Previous Release: v1.8.0
74
+
75
+ ### Multi-Line Paste Support
35
76
  - **Paste multiple lines**: When pasting multi-line content into item insertion prompts ('o', 'O', 'a', 'A'), each line becomes a separate item
36
77
  - **Visual feedback**: Shows `[+N lines]` indicator during multi-line paste
37
78
  - **Smart insertion**: All pasted lines inserted as siblings at the same level
38
79
  - Great for importing bullet lists from PDFs, emails, or other documents
39
80
  - Requires rcurses 6.1.5+
40
81
 
41
- ### 📄 PDF/LaTeX Export
82
+ ### PDF/LaTeX Export
42
83
  - **Export to PDF**: `:export pdf filename.pdf` - Full LaTeX-based PDF generation
43
84
  - **Export to LaTeX**: `:export latex filename.tex` - Get the LaTeX source
44
85
  - **Professional output**: Color-coded elements, table of contents, headers
45
86
  - **Complete HyperList support**: All syntax elements rendered beautifully
46
87
  - Requires: texlive-latex-base and texlive-latex-extra packages
47
88
 
48
- ### 📋 System Clipboard Integration
89
+ ### System Clipboard Integration
49
90
  - **Yank to clipboard**: 'y' and 'Y' now copy to system clipboard
50
91
  - **Middle-click paste**: Yanked items can be pasted into other terminals
51
92
  - **Preserves indentation**: Copied text maintains proper structure
52
93
 
53
94
  ## Previous Version Features (v1.4.0)
54
95
 
55
- ### 🎨 Configuration Lines & Theming
96
+ ### Configuration Lines & Theming
56
97
  - **Configuration Lines**: Add settings at the bottom of HyperList files using `((option=value, option2=value))`
57
98
  - **Theme Support**: Three color themes - `light` (bright colors), `normal` (standard), `dark` (for light terminals)
58
99
  - **Line Wrapping**: Enable with `wrap=yes` - wrapped lines use `+` prefix per HyperList spec
@@ -113,13 +154,13 @@ All `:set` commands automatically update the file's configuration line.
113
154
  - Secure AES-256-CBC encryption with PBKDF2 key derivation
114
155
  - Password caching for the session
115
156
 
116
- ### 🎯 Enhanced Presentation Mode
157
+ ### Enhanced Presentation Mode
117
158
  - **Auto-collapse** everything outside the current context
118
159
  - **Smart focus**: Shows only current item, ancestors, and immediate children
119
160
  - **Visual hierarchy**: Focused items in full color, others greyed out
120
161
  - Improved navigation with proper cursor tracking
121
162
 
122
- ### 🎨 Better Visual Experience
163
+ ### Better Visual Experience
123
164
  - **Improved highlighting**: Dark gray background preserves syntax colors
124
165
  - **Subtle selection**: No more harsh reverse video
125
166
  - **Preserved colors**: All HyperList elements maintain their colors when selected
@@ -224,7 +265,17 @@ hyperlist file.txt # Open any text file
224
265
  - `D` - Delete and yank line
225
266
  - `C-D` - Delete and yank item with descendants
226
267
  - `y/Y` - Copy line/tree
227
- - `p` - Paste
268
+ - `p` - Paste below
269
+ - `P` - Paste above
270
+ - `U` - Undo
271
+ - `r` or `C-R` - Redo
272
+ - `E` - Edit in $EDITOR
273
+
274
+ #### Tagging & Batch Operations
275
+ - `t` - Tag/untag current item
276
+ - `u` - Clear all tags
277
+ - `C-T` - Tag items matching regex pattern
278
+ - Operations (D/C-D, y/Y, Tab/S-Tab) work on all tagged items when tags exist
228
279
 
229
280
  #### Folding
230
281
  - `Space` - Toggle fold
@@ -238,8 +289,8 @@ hyperlist file.txt # Open any text file
238
289
  - `C-E` - Encrypt/decrypt current line
239
290
  - `R` - Go to reference
240
291
  - `F` - Open file reference
241
- - `P` - Presentation mode (with auto-collapse)
242
- - `t` - Insert template (built-in or custom)
292
+ - `C-P` - Presentation mode (with auto-collapse)
293
+ - `T` - Insert template (built-in or custom)
243
294
  - `?` - Help screen
244
295
 
245
296
  #### File Commands
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.8.0 - Terminal User Interface for HyperList files
10
+ HyperList v1.9.1 - 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.8.0"
55
+ puts "HyperList v1.9.1"
56
56
  exit 0
57
57
  end
58
58
 
@@ -66,13 +66,14 @@ require 'digest'
66
66
  require 'base64'
67
67
  require 'fileutils'
68
68
  require 'json'
69
+ require 'shellwords'
69
70
 
70
71
  class HyperListApp
71
72
  include Rcurses
72
73
  include Rcurses::Input
73
74
  include Rcurses::Cursor
74
75
 
75
- VERSION = "1.8.0"
76
+ VERSION = "1.9.1"
76
77
 
77
78
  def initialize(filename = nil)
78
79
  @filename = filename ? File.expand_path(filename) : nil
@@ -91,6 +92,8 @@ class HyperListApp
91
92
  @show_numbers = false # Line numbers disabled by default
92
93
  @command_history = load_command_history # Command history for : commands
93
94
  @clipboard = nil
95
+ @tagged_items = [] # Items tagged for batch operations
96
+ @original_items = nil # Track original file state for smart modified flag
94
97
  @undo_stack = []
95
98
  @undo_position = [] # Stack of cursor positions for undo
96
99
  @redo_stack = []
@@ -121,7 +124,8 @@ class HyperListApp
121
124
  @split_offset = 0 # Second view scroll offset
122
125
  @active_pane = :main # :main or :split
123
126
  @message_timeout = nil # For timed message display
124
-
127
+ @file_mtime = nil # Track file modification time for external change detection
128
+
125
129
  # Global configuration
126
130
  @config_file = File.expand_path("~/.hyperlist/config.yml")
127
131
  @indent_size = 2 # Default indentation (2-5 spaces)
@@ -223,7 +227,11 @@ class HyperListApp
223
227
  @items = []
224
228
  @encrypted_lines = {}
225
229
  @config_line = nil # Reset config line before loading
226
-
230
+ @tagged_items = [] # Clear tags when loading new file
231
+
232
+ # Track file modification time for external change detection
233
+ @file_mtime = File.mtime(file) rescue nil
234
+
227
235
  # Read file content
228
236
  content = File.read(file) rescue ""
229
237
 
@@ -354,6 +362,9 @@ class HyperListApp
354
362
 
355
363
  # Update recent files list
356
364
  add_to_recent_files(File.expand_path(file)) if file
365
+
366
+ # Save original state for smart modified flag
367
+ @original_items = @items.map { |item| item.dup }
357
368
  end
358
369
 
359
370
  def auto_fold_deep_levels(max_level)
@@ -520,12 +531,48 @@ class HyperListApp
520
531
  def load_command_history
521
532
  history_file = File.expand_path("~/.hyperlist_command_history")
522
533
  return [] unless File.exist?(history_file)
523
-
534
+
524
535
  File.readlines(history_file).map(&:strip).reject(&:empty?).last(100)
525
536
  rescue
526
537
  []
527
538
  end
528
-
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
+
529
576
  def save_command_history
530
577
  history_file = File.expand_path("~/.hyperlist_command_history")
531
578
  File.open(history_file, 'w') do |f|
@@ -703,7 +750,9 @@ class HyperListApp
703
750
  end
704
751
 
705
752
  @modified = false
753
+ @original_items = @items.map { |item| item.dup } # Save state after save
706
754
  @last_auto_save = Time.now if @auto_save_enabled
755
+ @file_mtime = File.mtime(@filename) rescue nil # Update mtime after save
707
756
  end
708
757
 
709
758
  def check_auto_save
@@ -1297,20 +1346,31 @@ class HyperListApp
1297
1346
  end
1298
1347
  end
1299
1348
 
1300
- # Apply current item highlighting (all lines of wrapped text get bg)
1301
- if idx == @current
1302
- # Skip background highlighting in presentation mode for items in focus
1303
- if !(@presentation_mode && is_item_in_presentation_focus?(item))
1349
+ # Apply background highlighting based on tagged/selected state
1350
+ real_idx = get_real_index(item)
1351
+ is_tagged = real_idx && @tagged_items.include?(real_idx)
1352
+ is_selected = idx == @current
1353
+
1354
+ bg_color = nil
1355
+ if is_tagged && is_selected
1356
+ # Tagged AND selected - lighter blue
1357
+ bg_color = "21" unless @presentation_mode && is_item_in_presentation_focus?(item)
1358
+ elsif is_tagged
1359
+ # Only tagged - dark blue
1360
+ bg_color = "17"
1361
+ elsif is_selected
1362
+ # Only selected - normal gray
1363
+ unless @presentation_mode && is_item_in_presentation_focus?(item)
1304
1364
  bg_color = (!@split_view || @active_pane == :main) ? "237" : "234"
1305
- if bg_color
1306
- # Pad line to full width and apply background
1307
- padded_line = line + " " * [@cols - line.pure.length, 0].max
1308
- bg_code = "\e[48;5;#{bg_color}m"
1309
- reset_bg = "\e[49m"
1310
- line = bg_code + padded_line.gsub(/\e\[49m/, '') + reset_bg
1311
- end
1312
1365
  end
1313
1366
  end
1367
+
1368
+ if bg_color
1369
+ padded_line = line + " " * [@cols - line.pure.length, 0].max
1370
+ bg_code = "\e[48;5;#{bg_color}m"
1371
+ reset_bg = "\e[49m"
1372
+ line = bg_code + padded_line.gsub(/\e\[49m/, '') + reset_bg
1373
+ end
1314
1374
 
1315
1375
  lines << line
1316
1376
  end
@@ -1587,9 +1647,10 @@ class HyperListApp
1587
1647
 
1588
1648
  # Handle parentheses content (moved here to avoid conflicts with properties)
1589
1649
  # Based on hyperlist.vim: '(.\{-})'
1650
+ # Color entire parentheses content as cyan - quotes inside are also cyan
1590
1651
  result = safe_regex_replace(result, /\(([^)]*)\)/) do |match|
1591
- content = match[1..-2] # Extract content between parentheses
1592
- "(".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"])
1593
1654
  end
1594
1655
 
1595
1656
  # Handle semicolons as separators (they separate items on the same line)
@@ -1605,11 +1666,17 @@ class HyperListApp
1605
1666
 
1606
1667
  # Handle quoted strings (only double quotes are special in HyperList)
1607
1668
  # Based on hyperlist.vim: '".\{-}"'
1608
- result.gsub!(/"([^"]*)"/) do
1609
- 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
1610
1672
  # Color any ## sequences inside the quotes as red
1611
- content.gsub!(/(##[<>-]+)/) { $1.fg(colors["red"]) }
1612
- '"'.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
1613
1680
  end
1614
1681
 
1615
1682
  # Handle change markup - all double-hashes should be red
@@ -1679,16 +1746,19 @@ class HyperListApp
1679
1746
 
1680
1747
  # Auto-save indicator
1681
1748
  auto_save_indicator = @auto_save_enabled ? "[A]" : ""
1682
-
1683
- # Split view indicator
1749
+
1750
+ # Split view indicator
1684
1751
  split_indicator = @split_view ? "[#{@active_pane.upcase}]" : ""
1685
-
1752
+
1753
+ # Tagged items indicator
1754
+ tagged_indicator = @tagged_items.any? ? "[T:#{@tagged_items.length}]" : ""
1755
+
1686
1756
  # Build status line components
1687
1757
  # Use full path with ~ for home directory
1688
1758
  full_path = @filename ? @filename.gsub(ENV['HOME'], '~') : "New HyperList"
1689
1759
  file_part = "#{full_path}#{modified_indicator}"
1690
1760
  stats_part = "L#{pos} W:#{word_count}"
1691
- indicators = "#{auto_save_indicator}#{split_indicator}"
1761
+ indicators = "#{auto_save_indicator}#{split_indicator}#{tagged_indicator}"
1692
1762
  right_side = "? help #{version_text}"
1693
1763
 
1694
1764
  # Combine left elements
@@ -2426,9 +2496,24 @@ class HyperListApp
2426
2496
 
2427
2497
  # Clear cache since items changed
2428
2498
  clear_cache
2429
-
2430
- @modified = true
2431
- @message = "Undone (#{@undo_stack.length} undo levels, #{@redo_stack.length} redo available)"
2499
+
2500
+ # Check if we've returned to original state
2501
+ if @original_items && items_match?(@items, @original_items)
2502
+ @modified = false
2503
+ @message = "Undone to original state (#{@undo_stack.length} undo levels, #{@redo_stack.length} redo available)"
2504
+ else
2505
+ @modified = true
2506
+ @message = "Undone (#{@undo_stack.length} undo levels, #{@redo_stack.length} redo available)"
2507
+ end
2508
+ end
2509
+
2510
+ def items_match?(items1, items2)
2511
+ return false if items1.length != items2.length
2512
+ items1.each_with_index do |item, idx|
2513
+ return false if item["text"] != items2[idx]["text"] ||
2514
+ item["level"] != items2[idx]["level"]
2515
+ end
2516
+ true
2432
2517
  end
2433
2518
 
2434
2519
  def record_last_action(type, data = nil)
@@ -2762,55 +2847,86 @@ class HyperListApp
2762
2847
  visible = get_visible_items
2763
2848
  return if visible.empty?
2764
2849
  return if @current >= visible.length
2765
-
2850
+
2766
2851
  save_undo_state # Save state before modification
2767
-
2768
- item = visible[@current]
2769
- real_idx = get_real_index(item)
2770
-
2771
- # First, yank the item(s) to clipboard
2772
- @clipboard = []
2773
- @clipboard << item.dup
2774
-
2775
- # Determine what to delete
2776
- level = item["level"]
2777
- delete_count = 1
2778
-
2779
- if with_children
2780
- # Delete item and its children (C-D was used)
2781
- @clipboard_is_tree = true # Mark as tree for paste behavior
2782
- ((real_idx + 1)...@items.length).each do |i|
2783
- if @items[i]["level"] > level
2784
- @clipboard << @items[i].dup # Also add children to clipboard
2785
- delete_count += 1
2852
+
2853
+ # Handle tagged items
2854
+ if @tagged_items.any?
2855
+ @clipboard = []
2856
+ @clipboard_is_tree = false
2857
+
2858
+ # Sort tagged indices in reverse to delete from end to start
2859
+ @tagged_items.sort.reverse.each do |real_idx|
2860
+ next if real_idx >= @items.length
2861
+ @clipboard.unshift(@items[real_idx].dup)
2862
+ if with_children
2863
+ # Delete item and children
2864
+ level = @items[real_idx]["level"]
2865
+ delete_count = 1
2866
+ ((real_idx + 1)...@items.length).each do |i|
2867
+ if @items[i]["level"] > level
2868
+ @clipboard << @items[i].dup
2869
+ delete_count += 1
2870
+ else
2871
+ break
2872
+ end
2873
+ end
2874
+ delete_count.times { @items.delete_at(real_idx) }
2786
2875
  else
2787
- break
2876
+ @items.delete_at(real_idx)
2788
2877
  end
2789
2878
  end
2879
+
2880
+ @message = "Deleted and yanked #{@tagged_items.length} tagged item(s)"
2881
+ @tagged_items = [] # Clear tags after operation
2790
2882
  else
2791
- # For single line delete (D key - delete only the current line)
2792
- @clipboard_is_tree = false # Mark as single/adaptive for paste behavior
2793
- # Don't include children - delete_count stays at 1
2883
+ # Normal single item delete
2884
+ item = visible[@current]
2885
+ real_idx = get_real_index(item)
2886
+
2887
+ # First, yank the item(s) to clipboard
2888
+ @clipboard = []
2889
+ @clipboard << item.dup
2890
+
2891
+ # Determine what to delete
2892
+ level = item["level"]
2893
+ delete_count = 1
2894
+
2895
+ if with_children
2896
+ # Delete item and its children (C-D was used)
2897
+ @clipboard_is_tree = true # Mark as tree for paste behavior
2898
+ ((real_idx + 1)...@items.length).each do |i|
2899
+ if @items[i]["level"] > level
2900
+ @clipboard << @items[i].dup # Also add children to clipboard
2901
+ delete_count += 1
2902
+ else
2903
+ break
2904
+ end
2905
+ end
2906
+ else
2907
+ # For single line delete (D key - delete only the current line)
2908
+ @clipboard_is_tree = false # Mark as single/adaptive for paste behavior
2909
+ # Don't include children - delete_count stays at 1
2910
+ end
2911
+
2912
+ # Delete the items
2913
+ # Remember the level of the deleted item for renumbering
2914
+ deleted_level = item["level"]
2915
+
2916
+ delete_count.times { @items.delete_at(real_idx) }
2917
+
2918
+ # Renumber siblings at the deleted item's level
2919
+ renumber_siblings(deleted_level) unless @items.length == 1
2920
+
2921
+ @message = "Deleted and yanked #{@clipboard.length} item(s)"
2794
2922
  end
2795
-
2796
- # Delete the items
2797
- # Remember the level of the deleted item for renumbering
2798
- deleted_level = item["level"]
2799
-
2800
- delete_count.times { @items.delete_at(real_idx) }
2801
-
2923
+
2802
2924
  @items = [{"text" => "Empty", "level" => 0, "fold" => false}] if @items.empty?
2803
-
2804
- # Renumber siblings at the deleted item's level
2805
- renumber_siblings(deleted_level) unless @items.length == 1
2806
-
2925
+
2807
2926
  @current = [@current, get_visible_items.length - 1].min
2808
2927
  @current = 0 if @current < 0
2809
2928
  @modified = true
2810
-
2811
- # Show message
2812
- @message = "Deleted and yanked #{@clipboard.length} item(s)"
2813
-
2929
+
2814
2930
  record_last_action(:delete_line, with_children)
2815
2931
  end
2816
2932
 
@@ -2818,23 +2934,49 @@ class HyperListApp
2818
2934
  visible = get_visible_items
2819
2935
  return if @current >= visible.length
2820
2936
 
2821
- item = visible[@current]
2822
- real_idx = get_real_index(item)
2823
-
2824
2937
  @clipboard = []
2825
- @clipboard << item.dup
2826
- @clipboard_is_tree = with_children # Remember if this is a tree copy (Y) or single (y)
2938
+ @clipboard_is_tree = with_children
2939
+
2940
+ # Handle tagged items
2941
+ if @tagged_items.any?
2942
+ @tagged_items.sort.each do |real_idx|
2943
+ next if real_idx >= @items.length
2944
+ @clipboard << @items[real_idx].dup
2945
+ if with_children
2946
+ # Copy children too
2947
+ level = @items[real_idx]["level"]
2948
+ ((real_idx + 1)...@items.length).each do |i|
2949
+ if @items[i]["level"] > level
2950
+ @clipboard << @items[i].dup
2951
+ else
2952
+ break
2953
+ end
2954
+ end
2955
+ end
2956
+ end
2827
2957
 
2828
- # Copy children if requested
2829
- if with_children
2830
- level = item["level"]
2831
- ((real_idx + 1)...@items.length).each do |i|
2832
- if @items[i]["level"] > level
2833
- @clipboard << @items[i].dup
2834
- else
2835
- break
2958
+ @message = "Yanked #{@tagged_items.length} tagged item(s)"
2959
+ @tagged_items = [] # Clear tags after operation
2960
+ else
2961
+ # Normal single item yank
2962
+ item = visible[@current]
2963
+ real_idx = get_real_index(item)
2964
+
2965
+ @clipboard << item.dup
2966
+
2967
+ # Copy children if requested
2968
+ if with_children
2969
+ level = item["level"]
2970
+ ((real_idx + 1)...@items.length).each do |i|
2971
+ if @items[i]["level"] > level
2972
+ @clipboard << @items[i].dup
2973
+ else
2974
+ break
2975
+ end
2836
2976
  end
2837
2977
  end
2978
+
2979
+ @message = "Yanked #{@clipboard.length} item(s)"
2838
2980
  end
2839
2981
 
2840
2982
  # Copy to system clipboard for middle-click paste
@@ -2845,7 +2987,6 @@ class HyperListApp
2845
2987
  # Silently fail if clipboard gem not available
2846
2988
  end
2847
2989
 
2848
- @message = "Yanked #{@clipboard.length} item(s)"
2849
2990
  record_last_action(:yank_line, with_children)
2850
2991
  end
2851
2992
 
@@ -2888,6 +3029,137 @@ class HyperListApp
2888
3029
  record_last_action(:paste, nil)
2889
3030
  end
2890
3031
 
3032
+ def paste_above
3033
+ return unless @clipboard && !@clipboard.empty?
3034
+
3035
+ save_undo_state # Save state before modification
3036
+
3037
+ visible = get_visible_items
3038
+ if @current < visible.length
3039
+ real_idx = get_real_index(visible[@current])
3040
+
3041
+ if @clipboard_is_tree
3042
+ # For tree paste (C-D or Y), maintain original indentation structure
3043
+ @clipboard.reverse.each do |item|
3044
+ new_item = item.dup
3045
+ @items.insert(real_idx, new_item)
3046
+ end
3047
+ else
3048
+ # For single/adaptive paste (D or y), adjust to match context
3049
+ base_level = visible[@current]["level"]
3050
+ level_diff = base_level - @clipboard[0]["level"]
3051
+
3052
+ @clipboard.reverse.each do |item|
3053
+ new_item = item.dup
3054
+ new_item["level"] = item["level"] + level_diff
3055
+ @items.insert(real_idx, new_item)
3056
+ end
3057
+ end
3058
+ else
3059
+ # Pasting at end of list - maintain original levels
3060
+ @clipboard.each do |item|
3061
+ @items << item.dup
3062
+ end
3063
+ end
3064
+
3065
+ @modified = true
3066
+ @message = "Pasted #{@clipboard.length} item(s) above"
3067
+ record_last_action(:paste_above, nil)
3068
+ end
3069
+
3070
+ def toggle_tag
3071
+ visible = get_visible_items
3072
+ return if @current >= visible.length
3073
+
3074
+ real_idx = get_real_index(visible[@current])
3075
+
3076
+ if @tagged_items.include?(real_idx)
3077
+ @tagged_items.delete(real_idx)
3078
+ @message = "Untagged item (#{@tagged_items.length} tagged)"
3079
+ else
3080
+ @tagged_items << real_idx
3081
+ @message = "Tagged item (#{@tagged_items.length} tagged)"
3082
+ # Auto-advance cursor for easy consecutive tagging
3083
+ @current += 1 if @current < visible.length - 1
3084
+ end
3085
+ end
3086
+
3087
+ def clear_tags
3088
+ count = @tagged_items.length
3089
+ @tagged_items = []
3090
+ @message = "Cleared #{count} tagged item(s)"
3091
+ end
3092
+
3093
+ def tag_by_regex
3094
+ pattern = @footer.ask("Tag pattern (regex): ", "")
3095
+ return if pattern.nil? || pattern.strip.empty?
3096
+
3097
+ begin
3098
+ regex = Regexp.new(pattern, Regexp::IGNORECASE)
3099
+ count = 0
3100
+
3101
+ @items.each_with_index do |item, idx|
3102
+ if item["text"] =~ regex
3103
+ @tagged_items << idx unless @tagged_items.include?(idx)
3104
+ count += 1
3105
+ end
3106
+ end
3107
+
3108
+ @tagged_items.uniq!
3109
+ @message = "Tagged #{count} item(s) matching /#{pattern}/"
3110
+ rescue RegexpError => e
3111
+ @message = "Invalid regex: #{e.message}"
3112
+ end
3113
+ end
3114
+
3115
+ def spawn_editor
3116
+ return unless @filename
3117
+
3118
+ # Save file before editing
3119
+ save_file
3120
+
3121
+ # Save terminal state
3122
+ system("stty -g < /dev/tty > /tmp/hyperlist_stty_$$")
3123
+
3124
+ # Reset terminal to cooked mode for editor
3125
+ system('stty sane < /dev/tty')
3126
+ system('clear < /dev/tty > /dev/tty')
3127
+ Rcurses::Cursor.show
3128
+
3129
+ # Launch editor
3130
+ editor = ENV.fetch('EDITOR', 'vi')
3131
+ system("#{editor} #{Shellwords.escape(@filename)}")
3132
+
3133
+ # Restore terminal state
3134
+ system("stty $(cat /tmp/hyperlist_stty_$$) < /dev/tty")
3135
+ system("rm -f /tmp/hyperlist_stty_$$")
3136
+
3137
+ # Flush input and reset stdin
3138
+ $stdin.iflush if $stdin.respond_to?(:iflush)
3139
+ system('stty raw -echo isig < /dev/tty')
3140
+ $stdin.raw!
3141
+ $stdin.echo = false
3142
+
3143
+ # Reinitialize rcurses
3144
+ Rcurses.init!
3145
+ Rcurses::Cursor.hide
3146
+ Rcurses.clear_screen
3147
+
3148
+ # Reload the file
3149
+ load_file(@filename)
3150
+ @current = 0
3151
+ @offset = 0
3152
+
3153
+ @message = "File reloaded from $EDITOR"
3154
+
3155
+ # Force complete screen refresh
3156
+ clear_cache
3157
+ @main.full_refresh if @main
3158
+ @footer.full_refresh if @footer
3159
+ @split_pane.full_refresh if @split_pane && @split_view
3160
+ render
3161
+ end
3162
+
2891
3163
  def calculate_level_for_position(target_real_idx)
2892
3164
  # Calculate appropriate level for an item at target_real_idx position
2893
3165
  # based on surrounding items in the @items array
@@ -3152,64 +3424,122 @@ class HyperListApp
3152
3424
  def indent_right(with_children = true)
3153
3425
  visible = get_visible_items
3154
3426
  return if @current >= visible.length
3155
-
3156
- item = visible[@current]
3157
- real_idx = get_real_index(item)
3158
-
3159
- # Can only indent if there's a previous item at same or higher level
3160
- if real_idx > 0
3161
- save_undo_state # Save state before modification
3162
- original_level = @items[real_idx]["level"]
3163
- @items[real_idx]["level"] += 1
3164
-
3165
- # Also indent children if requested
3166
- if with_children
3167
- ((real_idx + 1)...@items.length).each do |i|
3168
- if @items[i]["level"] > original_level
3169
- @items[i]["level"] += 1
3170
- else
3171
- break
3427
+
3428
+ save_undo_state # Save state before modification
3429
+
3430
+ # Handle tagged items
3431
+ if @tagged_items.any?
3432
+ @tagged_items.sort.each do |real_idx|
3433
+ next if real_idx >= @items.length
3434
+ next if real_idx == 0 # Can't indent first item
3435
+
3436
+ original_level = @items[real_idx]["level"]
3437
+ @items[real_idx]["level"] += 1
3438
+
3439
+ # Also indent children if requested
3440
+ if with_children
3441
+ ((real_idx + 1)...@items.length).each do |i|
3442
+ if @items[i]["level"] > original_level
3443
+ @items[i]["level"] += 1
3444
+ else
3445
+ break
3446
+ end
3172
3447
  end
3173
3448
  end
3174
3449
  end
3175
-
3176
- # Ensure the moved item remains visible
3177
- force_item_visible(real_idx)
3178
3450
 
3451
+ @message = "Indented #{@tagged_items.length} tagged item(s)"
3452
+ @tagged_items = [] # Clear tags after operation
3179
3453
  @modified = true
3180
- record_last_action(:indent_right, with_children)
3454
+ else
3455
+ # Normal single item indent
3456
+ item = visible[@current]
3457
+ real_idx = get_real_index(item)
3458
+
3459
+ # Can only indent if there's a previous item at same or higher level
3460
+ if real_idx > 0
3461
+ original_level = @items[real_idx]["level"]
3462
+ @items[real_idx]["level"] += 1
3463
+
3464
+ # Also indent children if requested
3465
+ if with_children
3466
+ ((real_idx + 1)...@items.length).each do |i|
3467
+ if @items[i]["level"] > original_level
3468
+ @items[i]["level"] += 1
3469
+ else
3470
+ break
3471
+ end
3472
+ end
3473
+ end
3474
+
3475
+ # Ensure the moved item remains visible
3476
+ force_item_visible(real_idx)
3477
+
3478
+ @modified = true
3479
+ end
3181
3480
  end
3481
+
3482
+ record_last_action(:indent_right, with_children)
3182
3483
  end
3183
3484
 
3184
3485
  def indent_left(with_children = true)
3185
3486
  visible = get_visible_items
3186
3487
  return if @current >= visible.length
3187
-
3188
- item = visible[@current]
3189
- real_idx = get_real_index(item)
3190
-
3191
- if @items[real_idx]["level"] > 0
3192
- save_undo_state # Save state before modification
3193
- original_level = @items[real_idx]["level"]
3194
- @items[real_idx]["level"] -= 1
3195
-
3196
- # Also unindent children if requested
3197
- if with_children
3198
- ((real_idx + 1)...@items.length).each do |i|
3199
- if @items[i]["level"] > original_level
3200
- @items[i]["level"] -= 1
3201
- else
3202
- break
3488
+
3489
+ save_undo_state # Save state before modification
3490
+
3491
+ # Handle tagged items
3492
+ if @tagged_items.any?
3493
+ @tagged_items.sort.each do |real_idx|
3494
+ next if real_idx >= @items.length
3495
+ next if @items[real_idx]["level"] == 0 # Already at leftmost
3496
+
3497
+ original_level = @items[real_idx]["level"]
3498
+ @items[real_idx]["level"] -= 1
3499
+
3500
+ # Also unindent children if requested
3501
+ if with_children
3502
+ ((real_idx + 1)...@items.length).each do |i|
3503
+ if @items[i]["level"] > original_level
3504
+ @items[i]["level"] -= 1
3505
+ else
3506
+ break
3507
+ end
3203
3508
  end
3204
3509
  end
3205
3510
  end
3206
-
3207
- # Ensure the moved item remains visible
3208
- force_item_visible(real_idx)
3209
3511
 
3512
+ @message = "Unindented #{@tagged_items.length} tagged item(s)"
3513
+ @tagged_items = [] # Clear tags after operation
3210
3514
  @modified = true
3211
- record_last_action(:indent_left, with_children)
3515
+ else
3516
+ # Normal single item unindent
3517
+ item = visible[@current]
3518
+ real_idx = get_real_index(item)
3519
+
3520
+ if @items[real_idx]["level"] > 0
3521
+ original_level = @items[real_idx]["level"]
3522
+ @items[real_idx]["level"] -= 1
3523
+
3524
+ # Also unindent children if requested
3525
+ if with_children
3526
+ ((real_idx + 1)...@items.length).each do |i|
3527
+ if @items[i]["level"] > original_level
3528
+ @items[i]["level"] -= 1
3529
+ else
3530
+ break
3531
+ end
3532
+ end
3533
+ end
3534
+
3535
+ # Ensure the moved item remains visible
3536
+ force_item_visible(real_idx)
3537
+
3538
+ @modified = true
3539
+ end
3212
3540
  end
3541
+
3542
+ record_last_action(:indent_left, with_children)
3213
3543
  end
3214
3544
 
3215
3545
  def toggle_checkbox
@@ -3444,8 +3774,9 @@ class HyperListApp
3444
3774
  help_lines << help_line("#{"gU".fg("10")}", "Uppercase line", "#{"gu".fg("10")}", "Lowercase line")
3445
3775
  help_lines << help_line("#{"I".fg("10")}", "Cycle indent (2-5)")
3446
3776
  help_lines << help_line("#{"D".fg("10")}", "Delete+yank line", "#{"C-D".fg("10")}", "Delete+yank item&descendants")
3447
- help_lines << help_line("#{"y".fg("10")}" + "/".fg("10") + "#{"Y".fg("10")}", "Copy line/tree", "#{"p".fg("10")}", "Paste")
3448
- help_lines << help_line("#{"u".fg("10")}", "Undo", "#{".".fg("10")}", "Repeat last action")
3777
+ help_lines << help_line("#{"y".fg("10")}" + "/".fg("10") + "#{"Y".fg("10")}", "Copy line/tree", "#{"p".fg("10")}", "Paste below")
3778
+ help_lines << help_line("#{"P".fg("10")}", "Paste above", "#{"E".fg("10")}", "Edit in $EDITOR")
3779
+ help_lines << help_line("#{"U".fg("10")}", "Undo", "#{".".fg("10")}", "Repeat last action")
3449
3780
  help_lines << help_line("#{"r".fg("10")}" + ", ".fg("10") + "#{"C-R".fg("10")}", "Redo")
3450
3781
  help_lines << help_line("#{"S-UP".fg("10")}", "Move item up", "#{"S-DOWN".fg("10")}", "Move item down")
3451
3782
  help_lines << help_line("#{"C-UP".fg("10")}", "Move up in visible list", "#{"C-DOWN".fg("10")}", "Move down in visible list")
@@ -3456,7 +3787,7 @@ class HyperListApp
3456
3787
  help_lines << help_line("#{"v".fg("10")}", "Toggle checkbox", "#{"V".fg("10")}", "Checkbox with date")
3457
3788
  help_lines << help_line("#{"C-X".fg("10")}", "Remove checkbox", "", "")
3458
3789
  help_lines << help_line("#{"C-E".fg("10")}", "Encrypt/decrypt line", "#{"C-U".fg("10")}", "Toggle State/Trans underline")
3459
- help_lines << help_line("#{"P".fg("10")}", "Presentation mode", "#{"Tab/S-Tab".fg("10")}", "Next/prev sibling (in P)")
3790
+ help_lines << help_line("#{"C-P".fg("10")}", "Presentation mode", "#{"Tab/S-Tab".fg("10")}", "Next/prev sibling (in P)")
3460
3791
  help_lines << help_line("#{"Ma".fg("10")}", "Record macro 'a'", "#{"@a".fg("10")}", "Play macro 'a'")
3461
3792
  help_lines << help_line("#{"w".fg("10")}", "Switch panes (split view)")
3462
3793
  help_lines << ""
@@ -3468,8 +3799,15 @@ class HyperListApp
3468
3799
  help_lines << help_line("#{":as on".fg("10")}", "Enable autosave", "#{":as off".fg("10")}", "Disable autosave")
3469
3800
  help_lines << help_line("#{":as N".fg("10")}", "Set interval (secs)", "#{":as".fg("10")}", "Show autosave status")
3470
3801
  help_lines << ""
3802
+ help_lines << "#{"TAGGING & BATCH OPS".fg("14")}"
3803
+ help_lines << help_line("#{"t".fg("10")}", "Tag/untag item", "#{"u".fg("10")}", "Clear all tags")
3804
+ help_lines << help_line("#{"C-T".fg("10")}", "Tag by regex pattern", "", "")
3805
+ help_lines << help_line("#{"[T:N]".fg("245")}", "Status shows N tagged", "#{"Blue bg".fg("245")}", "Tagged items")
3806
+ help_lines << help_line("#{"D/y/Tab".fg("245")}", "Ops work on tagged items")
3807
+ help_lines << ""
3808
+
3471
3809
  help_lines << "#{"TEMPLATES".fg("14")}"
3472
- help_lines << help_line("#{"t".fg("10")}", "Insert template", "#{":st".fg("10")}", "Save as template")
3810
+ help_lines << help_line("#{"T".fg("10")}", "Insert template", "#{":st".fg("10")}", "Save as template")
3473
3811
  help_lines << help_line("#{":dt".fg("10")}", "Delete template", "#{":lt".fg("10")}", "List user templates")
3474
3812
  help_lines << ""
3475
3813
 
@@ -5112,7 +5450,7 @@ class HyperListApp
5112
5450
  @items << {"text" => "="*50, "level" => 0, "fold" => false, "raw" => true}
5113
5451
 
5114
5452
  # Add built-in templates section
5115
- @items << {"text" => "BUILT-IN TEMPLATES:", "level" => 0, "fold" => false, "raw" => true}
5453
+ @items << {"text" => "BUILT-IN TEMPLATES:".fg("39"), "level" => 0, "fold" => false, "raw" => true}
5116
5454
  template_list.select { |t| t[2] == "built-in" }.each_with_index do |(key, desc, type), idx|
5117
5455
  @items << {
5118
5456
  "text" => " #{idx+1}. #{key.capitalize}: #{desc}",
@@ -5126,7 +5464,7 @@ class HyperListApp
5126
5464
  # Add user templates section if any exist
5127
5465
  if user_templates.any?
5128
5466
  @items << {"text" => "", "level" => 0, "fold" => false, "raw" => true}
5129
- @items << {"text" => "USER TEMPLATES:", "level" => 0, "fold" => false, "raw" => true}
5467
+ @items << {"text" => "USER TEMPLATES:".fg("39"), "level" => 0, "fold" => false, "raw" => true}
5130
5468
  template_list.select { |t| t[2] == "user" }.each_with_index do |(key, desc, type), idx|
5131
5469
  @items << {
5132
5470
  "text" => " #{key}: #{desc}",
@@ -5357,6 +5695,8 @@ class HyperListApp
5357
5695
  when " "
5358
5696
  toggle_fold
5359
5697
  when "u"
5698
+ clear_tags
5699
+ when "U"
5360
5700
  undo
5361
5701
  when "/"
5362
5702
  # Skip search in macros for now
@@ -6105,7 +6445,10 @@ class HyperListApp
6105
6445
  loop do
6106
6446
  # Check for auto-save
6107
6447
  check_auto_save if @auto_save_enabled
6108
-
6448
+
6449
+ # Check for external file changes
6450
+ check_file_changed
6451
+
6109
6452
  c = getchr
6110
6453
 
6111
6454
  # Skip nil input (shouldn't happen normally)
@@ -6262,11 +6605,17 @@ class HyperListApp
6262
6605
  # Cycle through indentation sizes (2-5 spaces)
6263
6606
  cycle_indent_size
6264
6607
  when "t"
6608
+ toggle_tag
6609
+ when "T"
6265
6610
  show_templates
6611
+ when "C-T"
6612
+ tag_by_regex
6266
6613
  when "D" # Delete line only (without children)
6267
6614
  delete_line(false) # Delete current line only
6268
6615
  when "C-D" # Delete line and all descendants explicitly
6269
6616
  delete_line(true)
6617
+ when "E"
6618
+ spawn_editor
6270
6619
  when "C-E" # Toggle line encryption
6271
6620
  toggle_line_encryption
6272
6621
  when "y" # Yank/copy single line
@@ -6312,6 +6661,8 @@ class HyperListApp
6312
6661
  end
6313
6662
  end
6314
6663
  when "u"
6664
+ clear_tags
6665
+ when "U"
6315
6666
  undo
6316
6667
  when "\x12" # Ctrl-R for redo (0x12 is Ctrl-R ASCII code)
6317
6668
  redo_change
@@ -6330,6 +6681,8 @@ class HyperListApp
6330
6681
  when "N"
6331
6682
  jump_to_next_template_marker
6332
6683
  when "P"
6684
+ paste_above
6685
+ when "C-P"
6333
6686
  toggle_presentation_mode
6334
6687
  when "\x15" # Ctrl-U for State/Transition underline toggle
6335
6688
  # Cycle through underline modes: 0 (none) -> 1 (states) -> 2 (transitions) -> 0
data/hyperlist.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |spec|
2
2
  spec.name = "hyperlist"
3
- spec.version = "1.8.1"
3
+ spec.version = "1.9.1"
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.8.1
4
+ version: 1.9.1
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-13 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