hyperlist 1.2.7 → 1.4.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 (7) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +95 -0
  3. data/README.md +71 -11
  4. data/hyperlist +546 -79
  5. data/hyperlist.gemspec +1 -1
  6. data/sample.hl +84 -82
  7. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cb7b134a799cd11a97a50f9163852f9c3afd2935826141b9cbb108bbcf5a3afd
4
- data.tar.gz: f6a621767eeac7faf998a8ad29d843f343a2d2a07a8f7ed58d9184c52eab87b8
3
+ metadata.gz: 1e6315c6d2ea2f89a99a74e0d1b9e06b0f0aee972f53f8b9b098e0bbe1d38545
4
+ data.tar.gz: 21e2b64e3c54fc9969114c6382b3bbe50526022aab77edc13ea1d8b9ebdb562f
5
5
  SHA512:
6
- metadata.gz: e4864cd12fb10a0d9051342af0895a43a813b7d4683623c865a1e013ed5617fe5350c219f28e053af8af249805ae21ce387fb4c2849b08142b5b454451301d81
7
- data.tar.gz: 9eac427645abd7c2b571259d9e0daf97911508f05115d2417e45124e35101cd36540214ec66ac056d1ad459087b745c3c979f5b2a069642fc81348724e59e651
6
+ metadata.gz: cc3ffc31138c89a0bf68ea1636297c7f261884f4e6b734e0dd96b176b85fd53d361fed3f7889d850ace5d7dde70d040e5d6eecb4b339d596b6a7017085c6ee02
7
+ data.tar.gz: d5a210e0c40b9f0eccae343c698ef694d418e2feda327b5e341bad60df3d1e24f4e0917b5b3e4920a5db047f98d271596f26a36130f8c273bbc483da9462639e
data/CHANGELOG.md CHANGED
@@ -2,6 +2,101 @@
2
2
 
3
3
  All notable changes to the HyperList Ruby TUI will be documented in this file.
4
4
 
5
+ ## [1.4.2] - 2025-08-29
6
+
7
+ ### Fixed
8
+ - **Help screen improvements**
9
+ - Fixed help screen crash issue with CONFIGURATION section
10
+ - Improved alignment of :set commands in help
11
+ - Added working CONFIGURATION section with proper formatting
12
+
13
+ ### Changed
14
+ - **Code cleanup**
15
+ - Removed debug logging code
16
+ - Cleaned up unused variables
17
+ - Improved code consistency
18
+
19
+ ## [1.4.1] - 2025-08-29
20
+
21
+ ### Fixed
22
+ - **Configuration system improvements**
23
+ - Config lines are now completely invisible (not shown in gray)
24
+ - Config management only through `:set` commands
25
+ - Fixed `:set` commands to properly update config lines when saving
26
+ - Fixed fold_level persistence issue when using `:set fold_level=N`
27
+ - Fixed help screen crash when showing configuration section
28
+ - Added error logging for help screen debugging
29
+
30
+ ### Changed
31
+ - **Line numbering improvements**
32
+ - Line numbers now show actual line position in file (including collapsed lines)
33
+ - Both main pane and split pane show true line numbers
34
+ - Wrapped lines show line number only on first line
35
+
36
+ ### Added
37
+ - **Help system documentation**
38
+ - Added CONFIGURATION section to help screen (?)
39
+ - Documents all `:set` commands and available options
40
+ - Explains config line format and behavior
41
+
42
+ ## [1.4.0] - 2025-08-28
43
+
44
+ ### Added
45
+ - **Configuration Lines & Theming**
46
+ - New configuration line format: `((option=value, option2=value))`
47
+ - Configuration lines displayed in gray when shown
48
+ - Three color themes: `light` (bright), `normal` (standard), `dark` (for light terminals)
49
+ - Line wrapping with `+` prefix per HyperList specification
50
+ - Line number display option
51
+ - Manual configuration via `:set` commands
52
+ - `:set option=value` to change settings
53
+ - `:set option` to view a setting
54
+ - `:set` to view all settings
55
+ - **Command history** with UP/DOWN arrow navigation
56
+ - History persistence between sessions (~/.hyperlist_command_history)
57
+ - Configuration options:
58
+ - `theme` - Color theme (light/normal/dark)
59
+ - `wrap` - Line wrapping (yes/no)
60
+ - `show_numbers` - Display line numbers (yes/no)
61
+ - `fold_level` - Default fold level (0-99)
62
+ - `auto_save` - Auto-save enable (yes/no)
63
+ - `auto_save_interval` - Auto-save frequency (seconds)
64
+ - `tab_width` - Indentation width (2-8)
65
+
66
+ ## [1.3.0] - 2025-08-28
67
+
68
+ ### Added
69
+ - **Initial Configuration Support**
70
+ - Basic config line parsing (old format, now deprecated)
71
+
72
+ ## [1.2.7] - 2025-08-27
73
+
74
+ ### Fixed
75
+ - Fixed 'D' key to delete only the current line, not from cursor to end of file
76
+
77
+ ## [1.2.6] - 2025-08-26
78
+
79
+ ### Fixed
80
+ - Fixed presentation mode navigation issues
81
+ - Corrected presentation mode footer display
82
+
83
+ ## [1.2.5] - 2025-08-25
84
+
85
+ ### Enhanced
86
+ - Improved presentation mode with better focus
87
+ - Reorganized help page for better readability
88
+
89
+ ## [1.2.4] - 2025-08-24
90
+
91
+ ### Enhanced
92
+ - Enhanced item movement and indentation
93
+ - Improved editing capabilities
94
+
95
+ ## [1.2.3] - 2025-08-23
96
+
97
+ ### Fixed
98
+ - Various bug fixes and improvements
99
+
5
100
  ## [1.2.2] - 2025-08-20
6
101
 
7
102
  ### Fixed
data/README.md CHANGED
@@ -29,17 +29,53 @@ 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.2.4
33
-
34
- ### 🎯 Enhanced Item Movement & Editing
35
- - **Smart Item Movement**: `C-UP`/`C-DOWN` now move items past siblings at the same level
36
- - **Cursor Tracking**: Cursor follows moved items to their new position
37
- - **Auto-renumbering**: Numbered lists automatically renumber when items are moved or deleted
38
- - **Outdented Insert**: Press `A` to insert a new item one level less indented
39
- - **Quick Save & Quit**: Press `W` as a shortcut for `:wq`
40
- - **Configurable Indentation**: Press `I` to cycle between 2-5 spaces per indent level
41
- - **Auto-detect Indentation**: Automatically detects indentation from loaded files
42
- - **Global Settings**: Indentation preference saved in `~/.hyperlist/config.yml`
32
+ ## What's New in v1.4.0
33
+
34
+ ### 🎨 Configuration Lines & Theming
35
+ - **Configuration Lines**: Add settings at the bottom of HyperList files using `((option=value, option2=value))`
36
+ - **Theme Support**: Three color themes - `light` (bright colors), `normal` (standard), `dark` (for light terminals)
37
+ - **Line Wrapping**: Enable with `wrap=yes` - wrapped lines use `+` prefix per HyperList spec
38
+ - **Line Numbers**: Enable with `show_numbers=yes`
39
+ - **Manual Configuration**: Use `:set option=value` to change settings on the fly
40
+ - **View Settings**: Use `:set` to see all settings, `:set option` to see one setting
41
+ - **Auto-apply**: Settings from config lines are applied when files are loaded
42
+ - **Invisible Config Lines**: Config lines are stored but never displayed in the editor
43
+ - **Persistent Settings**: All `:set` commands automatically update the config line in the file
44
+
45
+ ### Configuration Options
46
+ - `theme` - Color theme: light, normal, or dark
47
+ - `wrap` - Line wrapping: yes or no
48
+ - `show_numbers` - Show line numbers: yes or no
49
+ - `fold_level` - Default fold level: 0-99 (0=all folded, 99=all open)
50
+ - `auto_save` - Enable auto-save: yes or no
51
+ - `auto_save_interval` - Auto-save frequency in seconds
52
+ - `tab_width` - Indentation width: 2-8 spaces
53
+
54
+ Example config line: `((theme=dark, wrap=yes, fold_level=2))`
55
+
56
+ ### Using Configuration
57
+
58
+ #### Per-File Configuration
59
+ Add a configuration line at the bottom of any HyperList file:
60
+ ```
61
+ My HyperList
62
+ Item 1
63
+ Item 2
64
+
65
+ ((fold_level=2, theme=light))
66
+ ```
67
+
68
+ #### Runtime Configuration
69
+ Use `:set` commands while editing:
70
+ ```
71
+ :set # Show all current settings
72
+ :set fold_level # Show current fold level
73
+ :set fold_level=3 # Set fold level to 3
74
+ :set theme=dark # Switch to dark theme
75
+ :set wrap=yes # Enable line wrapping
76
+ ```
77
+
78
+ All `:set` commands automatically update the file's configuration line.
43
79
 
44
80
  ## Previous Updates
45
81
 
@@ -266,9 +302,33 @@ Team Meeting 2025-08-12 14:00
266
302
 
267
303
  ## Configuration
268
304
 
305
+ ### Application Configuration
269
306
  The application stores configuration in `~/.hyperlist/`:
270
307
  - `recent_files.txt` - List of recently opened files
271
308
  - `marks.yml` - Saved marks across sessions
309
+ - `command_history` - Command history for `:` commands
310
+ - `templates/` - User-defined templates
311
+
312
+ ### Per-File Configuration
313
+ Each HyperList file can have its own configuration line at the bottom:
314
+ ```
315
+ ((option=value, option2=value))
316
+ ```
317
+
318
+ This configuration line is:
319
+ - Automatically applied when the file is loaded
320
+ - Updated when you use `:set` commands
321
+ - Invisible in the editor (not displayed as content)
322
+ - Preserved when saving the file
323
+
324
+ Available options:
325
+ - `theme` - light/normal/dark
326
+ - `wrap` - yes/no
327
+ - `show_numbers` - yes/no
328
+ - `fold_level` - 0-99
329
+ - `auto_save` - yes/no
330
+ - `auto_save_interval` - seconds
331
+ - `tab_width` - 2-8
272
332
 
273
333
  ## Testing
274
334
 
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.2.3 - Terminal User Interface for HyperList files
10
+ HyperList v1.4.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.2.3"
55
+ puts "HyperList v1.4.0"
56
56
  exit 0
57
57
  end
58
58
 
@@ -72,7 +72,7 @@ class HyperListApp
72
72
  include Rcurses::Input
73
73
  include Rcurses::Cursor
74
74
 
75
- VERSION = "1.2.3"
75
+ VERSION = "1.4.2"
76
76
 
77
77
  def initialize(filename = nil)
78
78
  @filename = filename ? File.expand_path(filename) : nil
@@ -85,6 +85,11 @@ class HyperListApp
85
85
  @search = ""
86
86
  @search_matches = [] # Track search match positions
87
87
  @fold_level = 99
88
+ @config_line = nil # Store config line from file
89
+ @theme = "normal" # Default theme
90
+ @wrap = false # Line wrapping disabled by default
91
+ @show_numbers = false # Line numbers disabled by default
92
+ @command_history = load_command_history # Command history for : commands
88
93
  @clipboard = nil
89
94
  @undo_stack = []
90
95
  @undo_position = [] # Stack of cursor positions for undo
@@ -192,6 +197,7 @@ class HyperListApp
192
197
  def load_file(file)
193
198
  @items = []
194
199
  @encrypted_lines = {}
200
+ @config_line = nil # Reset config line before loading
195
201
 
196
202
  # Read file content
197
203
  content = File.read(file) rescue ""
@@ -202,6 +208,9 @@ class HyperListApp
202
208
  @redo_stack = []
203
209
  @redo_position = []
204
210
 
211
+ # Parse config line if present (before processing content)
212
+ parse_config_line(content)
213
+
205
214
  # Check if file is encrypted (dot file or encrypted content)
206
215
  is_encrypted = false
207
216
  if is_encrypted_file?(file)
@@ -239,6 +248,11 @@ class HyperListApp
239
248
  lines.each_with_index do |line, idx|
240
249
  next if line.strip.empty?
241
250
 
251
+ # Skip config lines (don't add them to items)
252
+ if line.strip =~ /^\(\(.+\)\)$/
253
+ next
254
+ end
255
+
242
256
  # Detect level based on leading whitespace
243
257
  if line.start_with?("\t")
244
258
  # Tab-based indentation
@@ -285,6 +299,12 @@ class HyperListApp
285
299
  @message = "Large file loaded. Deep levels auto-folded for performance."
286
300
  end
287
301
 
302
+ # Apply configured fold level if set (after items are loaded)
303
+ if @fold_level != 99 && !is_encrypted # Don't override encrypted file folding
304
+ apply_fold_level(@fold_level)
305
+ @message = "Applied fold level: #{@fold_level}" if @message.nil? || @message.empty?
306
+ end
307
+
288
308
  # Update recent files list
289
309
  add_to_recent_files(File.expand_path(file)) if file
290
310
  end
@@ -297,6 +317,128 @@ class HyperListApp
297
317
  end
298
318
  end
299
319
 
320
+ def apply_fold_level(level)
321
+ # Apply fold level: 0 = all folded, 99 = all open
322
+ # Show items up to and including the specified level
323
+ # Fold items at levels greater than specified level
324
+ @items.each_with_index do |item, idx|
325
+ if has_children?(idx, @items)
326
+ # Fold if item level is greater than or equal to the fold level
327
+ # This means: fold_level=1 shows level 0 expanded, level 1 and deeper folded
328
+ # fold_level=2 shows levels 0-1 expanded, level 2 and deeper folded
329
+ item["fold"] = item["level"] >= level
330
+ end
331
+ end
332
+
333
+ # Special cases
334
+ if level == 0
335
+ # Fold everything that has children
336
+ @items.each_with_index do |item, idx|
337
+ item["fold"] = true if has_children?(idx, @items)
338
+ end
339
+ elsif level >= 99
340
+ # Unfold everything
341
+ @items.each { |item| item["fold"] = false }
342
+ end
343
+ end
344
+
345
+ def parse_config_line(content)
346
+ # Look for config line at the bottom of the file
347
+ # Format: ((option1=value, option2=value))
348
+ lines = content.split("\n")
349
+
350
+ # Check last 10 lines for config (in case there are empty lines or other content)
351
+ config_line = nil
352
+ lines.last(10).each do |line|
353
+ # Allow indented config lines
354
+ if line.strip =~ /^\(\((.+)\)\)$/
355
+ @config_line = line.strip # Store the full line to preserve when saving
356
+ config_line = $1
357
+ break
358
+ end
359
+ end
360
+
361
+ return unless config_line
362
+
363
+ # Parse options (comma-separated)
364
+ config_options = {}
365
+ config_line.split(',').each do |option|
366
+ option = option.strip
367
+ if option =~ /(\w+)=(.+)/
368
+ key = $1.strip
369
+ value = $2.strip
370
+ # Convert values to appropriate types
371
+ case key
372
+ when "fold_level", "auto_save_interval", "tab_width"
373
+ config_options[key] = value.to_i
374
+ when "auto_save", "wrap", "show_numbers", "highlight_current", "checkbox_date", "backup", "encrypt"
375
+ config_options[key] = value.downcase == "true" || value.downcase == "yes"
376
+ else
377
+ config_options[key] = value
378
+ end
379
+ end
380
+ end
381
+
382
+ # Apply config options
383
+ apply_config(config_options)
384
+ end
385
+
386
+ def apply_config(options)
387
+ # Apply configuration options from config line
388
+ options.each do |key, value|
389
+ case key
390
+ when "fold_level"
391
+ @fold_level = value if value >= 0 && value <= 99
392
+ when "auto_save"
393
+ @auto_save_enabled = value
394
+ when "auto_save_interval"
395
+ @auto_save_interval = value if value > 0
396
+ when "tab_width", "indent_size"
397
+ @indent_size = value if value >= 2 && value <= 8
398
+ when "presentation_mode", "presentation"
399
+ @presentation_mode = value
400
+ setup_ui if value # Refresh UI if entering presentation mode
401
+ when "default_view"
402
+ case value
403
+ when "split"
404
+ @split_view = true
405
+ setup_ui
406
+ when "presentation"
407
+ @presentation_mode = true
408
+ setup_ui
409
+ end
410
+ when "theme"
411
+ # Apply theme setting
412
+ @theme = value if ["light", "normal", "dark"].include?(value)
413
+ when "wrap"
414
+ @wrap = value
415
+ when "show_numbers"
416
+ @show_numbers = value
417
+ when "search_case"
418
+ @search_case = value # sensitive/insensitive/smart
419
+ when "backup"
420
+ @backup_enabled = value
421
+ when "encrypt"
422
+ # Handle encryption enabling (would need password prompt)
423
+ @encrypt_enabled = value
424
+ end
425
+ end
426
+
427
+ # Show message about applied config
428
+ if options.any?
429
+ # Build message showing what was applied
430
+ msg_parts = []
431
+ msg_parts << "fold_level=#{@fold_level}" if @fold_level != 99
432
+ msg_parts << "theme=#{@theme}" if options["theme"]
433
+ msg_parts << "wrap=#{@wrap ? 'yes' : 'no'}" if options.key?("wrap")
434
+ msg_parts << "auto_save=#{@auto_save_enabled ? 'yes' : 'no'}" if options.key?("auto_save")
435
+
436
+ @message = "Applied config: #{msg_parts.join(', ')}" if msg_parts.any?
437
+ # Set a longer timeout for config message
438
+ @message_timeout = Time.now + 5.0
439
+ end
440
+ end
441
+
300
442
  def add_to_recent_files(filepath)
301
443
  return unless filepath && File.exist?(filepath)
302
444
 
@@ -328,6 +470,24 @@ class HyperListApp
328
470
  []
329
471
  end
330
472
 
473
+ def load_command_history
474
+ history_file = File.expand_path("~/.hyperlist_command_history")
475
+ return [] unless File.exist?(history_file)
476
+
477
+ File.readlines(history_file).map(&:strip).reject(&:empty?).last(100)
478
+ rescue
479
+ []
480
+ end
481
+
482
+ def save_command_history
483
+ history_file = File.expand_path("~/.hyperlist_command_history")
484
+ File.open(history_file, 'w') do |f|
485
+ @command_history.last(100).each { |cmd| f.puts(cmd) }
486
+ end
487
+ rescue
488
+ # Silently fail if can't write history
489
+ end
490
+
331
491
  def show_recent_files
332
492
  recent = load_recent_files
333
493
 
@@ -432,6 +592,23 @@ class HyperListApp
432
592
  (' ' * @indent_size) * item["level"] + item["text"]
433
593
  end.join("\n")
434
594
 
595
+ # Append config line if present
596
+ if @config_line && !@config_line.empty?
597
+ # Ensure there's a blank line before config
598
+ content += "\n" unless content.end_with?("\n")
599
+ content += "\n" + @config_line
600
+ else
601
+ # Debug: Check why config line is missing
602
+ if @fold_level != 99
603
+ # Rebuild config line if we have config settings but no line
604
+ update_config_line
605
+ if @config_line && !@config_line.empty?
606
+ content += "\n" unless content.end_with?("\n")
607
+ content += "\n" + @config_line
608
+ end
609
+ end
610
+ end
611
+
435
612
  # Check if this should be an encrypted file
436
613
  if is_encrypted_file?(@filename) && !content.empty?
437
614
  # Check if any lines are already encrypted
@@ -731,6 +908,61 @@ class HyperListApp
731
908
  end
732
909
 
733
910
 
911
+ def wrap_line(text, width, indent_level)
912
+ # Wrap a line per HyperList spec:
913
+ # Multi-line items start with '+' on first line
914
+ # Continuation lines have just a space prefix
915
+ return [text] unless @wrap
916
+
917
+ # Calculate effective width (account for indent and fold indicators)
918
+ indent_width = @indent_size * indent_level + 2 # +2 for fold indicator
919
+ effective_width = width - indent_width - 5 # Extra margin for readability
920
+
921
+ return [text] if text.length <= effective_width || effective_width <= 10
922
+
923
+ wrapped = []
924
+ remaining = text.dup
925
+ first_line = true
926
+
927
+ # Check if line already starts with + (multi-line indicator)
928
+ has_plus = text.strip.start_with?('+')
929
+ if has_plus
930
+ # Remove the + for processing, we'll add it back
931
+ remaining = remaining.sub(/^\s*\+\s*/, '')
932
+ end
933
+
934
+ while remaining && !remaining.empty?
935
+ if first_line
936
+ # First line gets the multi-line indicator if needed
937
+ if remaining.length <= effective_width
938
+ wrapped << (has_plus || wrapped.any? ? "+ #{remaining}" : remaining)
939
+ break
940
+ else
941
+ # Find a good break point (prefer spaces)
942
+ break_point = remaining[0...effective_width].rindex(' ') || effective_width
943
+ line_text = remaining[0...break_point].rstrip
944
+ # Add + to first line of multi-line items
945
+ wrapped << "+ #{line_text}"
946
+ remaining = remaining[break_point..-1].lstrip
947
+ first_line = false
948
+ end
949
+ else
950
+ # Continuation lines get just a space prefix per HyperList spec
951
+ cont_width = effective_width - 1 # Account for space prefix
952
+ if remaining.length <= cont_width
953
+ wrapped << " #{remaining}"
954
+ break
955
+ else
956
+ break_point = remaining[0...cont_width].rindex(' ') || cont_width
957
+ wrapped << " #{remaining[0...break_point].rstrip}"
958
+ remaining = remaining[break_point..-1].lstrip
959
+ end
960
+ end
961
+ end
962
+
963
+ wrapped
964
+ end
965
+
734
966
  def render_main
735
967
  visible_items = get_visible_items
736
968
 
@@ -742,62 +974,92 @@ class HyperListApp
742
974
  visible_items.each_with_index do |item, idx|
743
975
  next unless item
744
976
 
745
- line = ' ' * (@indent_size * item["level"])
746
-
747
- # Add fold indicator
748
- real_idx = get_real_index(item)
749
- if real_idx && has_children?(real_idx, @items) && item["fold"]
750
- color = (@presentation_mode && !is_item_in_presentation_focus?(item)) ? "240" : "245"
751
- line += "▶".fg(color) + " "
752
- elsif real_idx && has_children?(real_idx, @items)
753
- color = (@presentation_mode && !is_item_in_presentation_focus?(item)) ? "240" : "245"
754
- line += "▷".fg(color) + " "
977
+ # Handle line wrapping if enabled
978
+ if @wrap
979
+ text_lines = wrap_line(item["text"], @cols, item["level"])
755
980
  else
756
- line += " "
981
+ text_lines = [item["text"]]
757
982
  end
758
983
 
759
- # Handle literal blocks and syntax highlighting
760
- if item["text"].strip == "\\"
761
- if !in_literal_block
762
- in_literal_block = true
763
- literal_start_level = item["level"]
764
- spaces = item["text"].match(/^(\s*)/)[1]
765
- line += spaces + "\\".fg("3")
766
- elsif item["level"] == literal_start_level
767
- in_literal_block = false
768
- literal_start_level = -1
769
- spaces = item["text"].match(/^(\s*)/)[1]
770
- line += spaces + "\\".fg("3")
984
+ text_lines.each_with_index do |text_line, line_idx|
985
+ # Get the actual line number from the real index in @items
986
+ real_idx = get_real_index(item)
987
+ actual_line_number = real_idx ? real_idx + 1 : 0 # +1 for 1-based line numbers
988
+
989
+ # Add line number if enabled (only on first line of wrapped text)
990
+ line = ""
991
+ if @show_numbers
992
+ if line_idx == 0
993
+ line = "#{actual_line_number.to_s.rjust(4)} "
994
+ else
995
+ line = " " # Empty space for continuation lines
996
+ end
997
+ end
998
+
999
+ line += ' ' * (@indent_size * item["level"])
1000
+
1001
+ # Add fold indicator only on first line
1002
+ if line_idx == 0
1003
+ real_idx = get_real_index(item)
1004
+ if real_idx && has_children?(real_idx, @items) && item["fold"]
1005
+ color = (@presentation_mode && !is_item_in_presentation_focus?(item)) ? "240" : "245"
1006
+ line += "▶".fg(color) + " "
1007
+ elsif real_idx && has_children?(real_idx, @items)
1008
+ color = (@presentation_mode && !is_item_in_presentation_focus?(item)) ? "240" : "245"
1009
+ line += "▷".fg(color) + " "
1010
+ else
1011
+ line += " "
1012
+ end
771
1013
  else
772
- line += item["text"]
1014
+ # Continuation lines already have their space prefix from wrap_line
1015
+ line += " " # Just add fold indicator spacing
773
1016
  end
774
- elsif in_literal_block
775
- line += item["text"]
776
- else
777
- # Normal syntax highlighting
778
- has_match = @search_matches.include?(idx) && @search && !@search.empty?
779
- if @presentation_mode && !is_item_in_presentation_focus?(item)
780
- line += item["text"].fg("240")
1017
+
1018
+ # Handle literal blocks and syntax highlighting
1019
+ if item["text"].strip == "\\"
1020
+ if !in_literal_block
1021
+ in_literal_block = true
1022
+ literal_start_level = item["level"]
1023
+ spaces = text_line.match(/^(\s*)/)[1] || ""
1024
+ line += spaces + "\\".fg("3")
1025
+ elsif item["level"] == literal_start_level
1026
+ in_literal_block = false
1027
+ literal_start_level = -1
1028
+ spaces = text_line.match(/^(\s*)/)[1] || ""
1029
+ line += spaces + "\\".fg("3")
1030
+ else
1031
+ line += text_line
1032
+ end
1033
+ elsif in_literal_block
1034
+ line += text_line
781
1035
  else
782
- line += process_text(item["text"], has_match)
1036
+ # Normal syntax highlighting
1037
+ has_match = @search_matches.include?(idx) && @search && !@search.empty?
1038
+ if @presentation_mode && !is_item_in_presentation_focus?(item)
1039
+ line += text_line.fg("240")
1040
+ else
1041
+ line += process_text(text_line, has_match)
1042
+ end
783
1043
  end
784
- end
785
-
786
- # Apply current item highlighting (but not in presentation mode for focused items)
787
- if idx == @current
788
- # Skip background highlighting in presentation mode for items in focus
789
- if !(@presentation_mode && is_item_in_presentation_focus?(item))
790
- bg_color = (!@split_view || @active_pane == :main) ? "237" : "234"
791
- if bg_color
792
- bg_code = "\e[48;5;#{bg_color}m"
793
- reset_bg = "\e[49m"
794
- line = bg_code + line.gsub(/\e\[49m/, '') + reset_bg
1044
+
1045
+ # Apply current item highlighting (all lines of wrapped text get bg)
1046
+ if idx == @current
1047
+ # Skip background highlighting in presentation mode for items in focus
1048
+ if !(@presentation_mode && is_item_in_presentation_focus?(item))
1049
+ bg_color = (!@split_view || @active_pane == :main) ? "237" : "234"
1050
+ if bg_color
1051
+ # Pad line to full width and apply background
1052
+ padded_line = line + " " * [@cols - line.pure.length, 0].max
1053
+ bg_code = "\e[48;5;#{bg_color}m"
1054
+ reset_bg = "\e[49m"
1055
+ line = bg_code + padded_line.gsub(/\e\[49m/, '') + reset_bg
1056
+ end
795
1057
  end
796
1058
  end
1059
+
1060
+ lines << line
797
1061
  end
798
1062
 
799
- lines << line
800
-
801
1063
  # Check if exiting literal block
802
1064
  if in_literal_block && item["level"] <= literal_start_level && !item["text"].strip == "\\"
803
1065
  in_literal_block = false
@@ -847,10 +1109,63 @@ class HyperListApp
847
1109
  @main.refresh
848
1110
  end
849
1111
 
1112
+ def get_theme_colors
1113
+ # Theme definitions based on HyperList spec colors
1114
+ # Using hex RGB colors (RRGGBB format) for rcurses
1115
+ # Per HyperList spec from hyperlist.tex:
1116
+ # red (properties/dates), green (qualifiers/states), blue (operators)
1117
+ # magenta/violet (references), cyan (parentheses/quotes), yellow (literals)
1118
+ # orange (tags)
1119
+ case @theme
1120
+ when "light"
1121
+ # Brighter, more saturated colors for dark terminals
1122
+ {
1123
+ "red" => "FF5050", # Bright red for properties
1124
+ "green" => "50FF50", # Bright green for qualifiers
1125
+ "blue" => "6496FF", # Bright blue for operators
1126
+ "magenta" => "C864FF", # Light purple/violet for references
1127
+ "cyan" => "50FFFF", # Bright cyan for parentheses
1128
+ "yellow" => "FFFF64", # Bright yellow for literals
1129
+ "orange" => "FFB450", # Bright orange for tags
1130
+ "gray" => "C8C8C8" # Light gray
1131
+ }
1132
+ when "dark"
1133
+ # Darker, less saturated colors for light background terminals
1134
+ {
1135
+ "red" => "B40000", # Dark red for properties
1136
+ "green" => "008C00", # Dark green for qualifiers
1137
+ "blue" => "0000B4", # Dark blue for operators
1138
+ "magenta" => "8C008C", # Dark purple for references
1139
+ "cyan" => "008C8C", # Dark cyan for parentheses
1140
+ "yellow" => "8C8C00", # Dark yellow for literals
1141
+ "orange" => "B46400", # Dark orange for tags
1142
+ "gray" => "646464" # Dark gray
1143
+ }
1144
+ else # normal - using 256 color codes for compatibility
1145
+ {
1146
+ "red" => "196", # Red for properties/dates (standard red)
1147
+ "green" => "46", # Green for qualifiers/checkboxes
1148
+ "blue" => "21", # Blue for operators (AND/OR/IF/THEN)
1149
+ "magenta" => "165", # Purple/violet for references
1150
+ "cyan" => "51", # Cyan for parentheses/quotes
1151
+ "yellow" => "226", # Yellow for literals/substitutions
1152
+ "orange" => "208", # Orange for tags
1153
+ "gray" => "245" # Gray
1154
+ }
1155
+ end
1156
+ end
1157
+
850
1158
  def process_text(text, highlight_search = false)
851
1159
  # Work with a clean copy
852
1160
  result = text.dup
853
1161
  processed_checkbox = false
1162
+ colors = get_theme_colors
1163
+
1164
+ # Config lines should never be displayed (they're filtered out)
1165
+ # But if somehow one gets through, don't process it
1166
+ if result =~ /^\(\(.+\)\)$/
1167
+ return ""
1168
+ end
854
1169
 
855
1170
  # If text already contains ANSI codes, return as-is to avoid double-processing
856
1171
  if result.include?("\e[")
@@ -909,7 +1224,7 @@ class HyperListApp
909
1224
 
910
1225
  # Check if this is a literal block marker (single backslash)
911
1226
  if result.strip == "\\"
912
- return result.fg("3") # Yellow for literal block markers
1227
+ return result.fg(colors["yellow"]) # Yellow for literal block markers
913
1228
  end
914
1229
 
915
1230
  # Apply search highlighting if we have an active search
@@ -923,47 +1238,47 @@ class HyperListApp
923
1238
  # Based on hyperlist.vim: '^\(\t\|\*\)*[0-9.]* '
924
1239
  if result =~ /^([0-9][0-9A-Z.]*\s)/
925
1240
  identifier = $1
926
- result = result.sub(/^[0-9][0-9A-Z.]*\s/, identifier.fg("5")) # Magenta for identifiers
1241
+ result = result.sub(/^[0-9][0-9A-Z.]*\s/, identifier.fg(colors["magenta"])) # Magenta for identifiers
927
1242
  end
928
1243
 
929
1244
  # Handle multi-line indicator at the beginning (+ with space)
930
1245
  # Based on hyperlist.vim: '^\(\t\|\*\)*+ '
931
1246
  if result =~ /^\+\s/
932
- result = result.sub(/^(\+\s)/, "+".fg("1") + " ") # Red for multi-line indicator
1247
+ result = result.sub(/^(\+\s)/, "+".fg(colors["red"]) + " ") # Red for multi-line indicator
933
1248
  end
934
1249
 
935
1250
  # Handle continuation markers (+ at start of indented lines in References section)
936
1251
  if result =~ /^\s*\+\s/
937
1252
  spaces = $1 || ""
938
1253
  marker = $2 || "+ "
939
- result = result.sub(/^(\s*)(\+\s)/, spaces + marker.fg("1")) # Red for continuation marker
1254
+ result = result.sub(/^(\s*)(\+\s)/, spaces + marker.fg(colors["red"])) # Red for continuation marker
940
1255
  end
941
1256
 
942
1257
  # Process checkboxes anywhere in the line (can have leading spaces)
943
1258
  if result =~ /^(\s*)(\[X\]|\[x\])/
944
1259
  spaces = $1
945
- colored = "[X]".fg("10")
1260
+ colored = "[X]".fg(colors["green"])
946
1261
  result = result.sub(/^(\s*)(\[X\]|\[x\])/, "#{spaces}#{colored}") # Bright green for completed
947
1262
  processed_checkbox = true
948
1263
  elsif result =~ /^(\s*)(\[O\])/
949
1264
  spaces = $1
950
- colored = "[O]".fg("10").b
1265
+ colored = "[O]".fg(colors["green"]).b
951
1266
  result = result.sub(/^(\s*)(\[O\])/, "#{spaces}#{colored}") # Bold bright green for in-progress
952
1267
  processed_checkbox = true
953
1268
  elsif result =~ /^(\s*)(\[-\])/
954
1269
  spaces = $1
955
- colored = "[-]".fg("2")
1270
+ colored = "[-]".fg(colors["green"])
956
1271
  result = result.sub(/^(\s*)(\[-\])/, "#{spaces}#{colored}") # Green for partial
957
1272
  processed_checkbox = true
958
1273
  elsif result =~ /^(\s*)(\[ \]|\[_\])/
959
1274
  spaces = $1
960
- colored = "[ ]".fg("22")
1275
+ colored = "[ ]".fg(colors["green"])
961
1276
  result = result.sub(/^(\s*)(\[ \]|\[_\])/, "#{spaces}#{colored}") # Dark green for unchecked
962
1277
  processed_checkbox = true
963
1278
  elsif !processed_checkbox
964
1279
  # Only handle other qualifiers if we didn't process a checkbox
965
1280
  # Based on hyperlist.vim: '\[.\{-}\]'
966
- result.gsub!(/\[([^\]]*)\]/) { "[#{$1}]".fg("2") } # Green for all qualifiers
1281
+ result.gsub!(/\[([^\]]*)\]/) { "[#{$1}]".fg(colors["green"]) } # Green for all qualifiers
967
1282
  end
968
1283
 
969
1284
  # We'll handle parentheses AFTER operators/properties to avoid conflicts
@@ -971,7 +1286,7 @@ class HyperListApp
971
1286
  # Handle date timestamps as properties (for checkbox dates)
972
1287
  # Format: YYYY-MM-DD HH.MM:
973
1288
  result.gsub!(/(\d{4}-\d{2}-\d{2} \d{2}\.\d{2}):/) do
974
- "#{$1}:".fg("1") # Red for timestamp properties
1289
+ "#{$1}:".fg(colors["red"]) # Red for timestamp properties
975
1290
  end
976
1291
 
977
1292
  # Handle operators and properties with colon pattern
@@ -985,10 +1300,10 @@ class HyperListApp
985
1300
 
986
1301
  # Check if it's an operator (ALL-CAPS with optional _, -, (), /, =, spaces)
987
1302
  if text_part =~ /^[A-Z][A-Z_\-() \/=]*$/
988
- prefix_space + text_part.fg("4") + colon_space.fg("4") # Blue for operators (including S: and T:)
1303
+ prefix_space + text_part.fg(colors["blue"]) + colon_space.fg(colors["blue"]) # Blue for operators (including S: and T:)
989
1304
  elsif text_part.length >= 2 && space_after.include?(" ")
990
1305
  # It's a property (mixed case, at least 2 chars, has space after colon)
991
- prefix_space + text_part.fg("1") + colon_space.fg("1") # Red for properties
1306
+ prefix_space + text_part.fg(colors["red"]) + colon_space.fg(colors["red"]) # Red for properties
992
1307
  else
993
1308
  # Leave as is
994
1309
  prefix_space + text_part + colon_space
@@ -997,57 +1312,57 @@ class HyperListApp
997
1312
 
998
1313
 
999
1314
  # Color special state/transition markers (| and /) green
1000
- result.gsub!(/^(\s*)\|\s+/) { $1 + "| ".fg("2") } # Green for pipe (state marker)
1001
- result.gsub!(/^(\s*)\/\s+/) { $1 + "/ ".fg("2") } # Green for slash (transition marker)
1315
+ result.gsub!(/^(\s*)\|\s+/) { $1 + "| ".fg(colors["green"]) } # Green for pipe (state marker)
1316
+ result.gsub!(/^(\s*)\/\s+/) { $1 + "/ ".fg(colors["green"]) } # Green for slash (transition marker)
1002
1317
 
1003
1318
  # Handle OR: at the beginning of a line (with optional spaces)
1004
- result.sub!(/^(\s*)(OR):/) { $1 + "OR:".fg("4") } # Blue for OR: at line start
1319
+ result.sub!(/^(\s*)(OR):/) { $1 + "OR:".fg(colors["blue"]) } # Blue for OR: at line start
1005
1320
 
1006
1321
  # Handle parentheses content (moved here to avoid conflicts with properties)
1007
1322
  # Based on hyperlist.vim: '(.\{-})'
1008
1323
  result = safe_regex_replace(result, /\(([^)]*)\)/) do |match|
1009
1324
  content = match[1..-2] # Extract content between parentheses
1010
- "(".fg("6") + content.fg("6") + ")".fg("6")
1325
+ "(".fg(colors["cyan"]) + content.fg(colors["cyan"]) + ")".fg(colors["cyan"])
1011
1326
  end
1012
1327
 
1013
1328
  # Handle semicolons as separators (they separate items on the same line)
1014
1329
  # Semicolons are green like qualifiers
1015
- result = safe_regex_replace(result, /;/) { ";".fg("2") }
1330
+ result = safe_regex_replace(result, /;/) { ";".fg(colors["green"]) }
1016
1331
 
1017
1332
  # Handle references - color entire reference including brackets
1018
1333
  # Based on hyperlist.vim: '<\{1,2}[...]\+>\{1,2}'
1019
- result.gsub!(/<{1,2}([^>]+)>{1,2}/) { |match| match.fg("5") } # Magenta for references
1334
+ result.gsub!(/<{1,2}([^>]+)>{1,2}/) { |match| match.fg(colors["magenta"]) } # Magenta for references
1020
1335
 
1021
1336
  # Handle special keywords SKIP and END
1022
- result.gsub!(/\b(SKIP|END)\b/) { $1.fg("5") } # Magenta for special keywords (like references)
1337
+ result.gsub!(/\b(SKIP|END)\b/) { $1.fg(colors["magenta"]) } # Magenta for special keywords (like references)
1023
1338
 
1024
1339
  # Handle quoted strings (only double quotes are special in HyperList)
1025
1340
  # Based on hyperlist.vim: '".\{-}"'
1026
1341
  result.gsub!(/"([^"]*)"/) do
1027
1342
  content = $1
1028
1343
  # Color any ## sequences inside the quotes as red
1029
- content.gsub!(/(##[<>-]+)/) { $1.fg("1") }
1030
- '"'.fg("6") + content.fg("6") + '"'.fg("6") # Cyan for quoted strings
1344
+ content.gsub!(/(##[<>-]+)/) { $1.fg(colors["red"]) }
1345
+ '"'.fg(colors["cyan"]) + content.fg(colors["cyan"]) + '"'.fg(colors["cyan"]) # Cyan for quoted strings
1031
1346
  end
1032
1347
 
1033
1348
  # Handle change markup - all double-hashes should be red
1034
1349
  # First handle ##><Reference>##-> style (with reference in the middle)
1035
1350
  result.gsub!(/(##[<>-]+)(<[^>]+>)(##[<>-]+)/) do
1036
- $1.fg("1") + $2.fg("5") + $3.fg("1") # Red markers, magenta reference
1351
+ $1.fg(colors["red"]) + $2.fg(colors["magenta"]) + $3.fg(colors["red"]) # Red markers, magenta reference
1037
1352
  end
1038
1353
 
1039
1354
  # Handle ##Text## change info (text between double hashes)
1040
- result.gsub!(/(##)([^#]+)(##)/) { $1.fg("1") + $2 + $3.fg("1") } # Red for change info markers
1355
+ result.gsub!(/(##)([^#]+)(##)/) { $1.fg(colors["red"]) + $2 + $3.fg(colors["red"]) } # Red for change info markers
1041
1356
 
1042
1357
  # Then color any remaining ## sequences red
1043
- result.gsub!(/(##[<>-]*)/) { $1.fg("1") } # Red for all ## markers
1358
+ result.gsub!(/(##[<>-]*)/) { $1.fg(colors["red"]) } # Red for all ## markers
1044
1359
 
1045
1360
  # Handle substitutions {variable}
1046
- result.gsub!(/\{([^}]+)\}/) { "{".fg("3") + $1.fg("3") + "}".fg("3") } # Yellow for substitutions
1361
+ result.gsub!(/\{([^}]+)\}/) { "{".fg(colors["yellow"]) + $1.fg(colors["yellow"]) + "}".fg(colors["yellow"]) } # Yellow for substitutions
1047
1362
 
1048
1363
  # Handle hash tags
1049
1364
  # Based on hyperlist.vim: '#[a-zA-Z0-9.:/_&?%=+\-\*]\+'
1050
- result.gsub!(/#([a-zA-Z0-9.:_\/&?%=+\-*]+)/) { "##{$1}".fg("184") } # Yellow/gold for tags
1365
+ result.gsub!(/#([a-zA-Z0-9.:_\/&?%=+\-*]+)/) { "##{$1}".fg(colors["orange"]) } # Orange for tags
1051
1366
 
1052
1367
  # Handle text formatting (bold, italic, underline)
1053
1368
  # Based on hyperlist.vim patterns with tab/space boundaries
@@ -2509,7 +2824,7 @@ class HyperListApp
2509
2824
  help_lines << help_line("#{"i/Enter".fg("10")}", "Edit line", "#{"o".fg("10")}", "Insert line below")
2510
2825
  help_lines << help_line("#{"O".fg("10")}", "Insert line above", "#{"a".fg("10")}", "Insert child")
2511
2826
  help_lines << help_line("#{"A".fg("10")}", "Insert outdented", "#{"W".fg("10")}", "Save and quit")
2512
- help_lines << help_line("#{"I".fg("10")}", "Cycle indent (2-5)", "", "")
2827
+ help_lines << help_line("#{"I".fg("10")}", "Cycle indent (2-5)")
2513
2828
  help_lines << help_line("#{"D".fg("10")}", "Delete+yank line", "#{"C-D".fg("10")}", "Delete+yank item&descendants")
2514
2829
  help_lines << help_line("#{"y".fg("10")}" + "/".fg("10") + "#{"Y".fg("10")}", "Copy line/tree", "#{"p".fg("10")}", "Paste")
2515
2830
  help_lines << help_line("#{"u".fg("10")}", "Undo", "#{".".fg("10")}", "Repeat last action")
@@ -2524,7 +2839,7 @@ class HyperListApp
2524
2839
  help_lines << help_line("#{"C-E".fg("10")}", "Encrypt/decrypt line", "#{"C-U".fg("10")}", "Toggle State/Trans underline")
2525
2840
  help_lines << help_line("#{"P".fg("10")}", "Presentation mode", "#{"Tab/S-Tab".fg("10")}", "Next/prev sibling (in P)")
2526
2841
  help_lines << help_line("#{"Ma".fg("10")}", "Record macro 'a'", "#{"@a".fg("10")}", "Play macro 'a'")
2527
- help_lines << help_line("#{"w".fg("10")}", "Switch panes (split view)", "", "")
2842
+ help_lines << help_line("#{"w".fg("10")}", "Switch panes (split view)")
2528
2843
  help_lines << ""
2529
2844
  help_lines << "#{"FILE OPERATIONS".fg("14")}"
2530
2845
  help_lines << help_line("#{":w".fg("10")}", "Save", "#{":q".fg("10")}", "Quit")
@@ -2538,6 +2853,15 @@ class HyperListApp
2538
2853
  help_lines << help_line("#{"t".fg("10")}", "Insert template", "#{":st".fg("10")}", "Save as template")
2539
2854
  help_lines << help_line("#{":dt".fg("10")}", "Delete template", "#{":lt".fg("10")}", "List user templates")
2540
2855
  help_lines << ""
2856
+
2857
+ help_lines << "#{"CONFIGURATION".fg("14")}"
2858
+ help_lines << help_line("#{":set".fg("10")}", "Show all settings")
2859
+ help_lines << help_line("#{":set o".fg("10")}", "Show option value")
2860
+ help_lines << help_line("#{":set o=val".fg("10")}", "Set option value")
2861
+ help_lines << help_line("#{"Options:".fg("245")}", "theme, wrap, fold_level")
2862
+ help_lines << help_line("#{"".fg("245")}", "show_numbers, tab_width")
2863
+ help_lines << ""
2864
+
2541
2865
  help_lines << "#{"HELP & QUIT".fg("14")}"
2542
2866
  help_lines << help_line("#{"?".fg("10")}", "This help", "#{"??".fg("10")}", "Full documentation")
2543
2867
  help_lines << help_line("#{"q".fg("10")}", "Quit (asks to save)", "#{"Q".fg("10")}", "Force quit")
@@ -3142,6 +3466,9 @@ class HyperListApp
3142
3466
 
3143
3467
  def handle_command
3144
3468
  @mode = :command
3469
+ # Set command history (reversed for proper UP arrow navigation)
3470
+ @footer.history = @command_history.reverse
3471
+ @footer.record = true
3145
3472
  @command = @footer.ask(":", "")
3146
3473
  @mode = :normal
3147
3474
  @footer.clear # Clear footer immediately
@@ -3149,6 +3476,14 @@ class HyperListApp
3149
3476
 
3150
3477
  return unless @command
3151
3478
 
3479
+ # Add to history if not empty and not duplicate of last entry
3480
+ if !@command.empty? && (@command_history.empty? || @command_history.last != @command)
3481
+ @command_history << @command
3482
+ # Keep only last 100 commands
3483
+ @command_history = @command_history.last(100)
3484
+ save_command_history
3485
+ end
3486
+
3152
3487
  case @command
3153
3488
  when "w", "write"
3154
3489
  if @filename
@@ -3240,11 +3575,134 @@ class HyperListApp
3240
3575
  when "split"
3241
3576
  # Copy current section to split view
3242
3577
  copy_section_to_split
3578
+ when /^set\s+(\w+)=(.+)$/
3579
+ # Handle :set option=value
3580
+ option = $1
3581
+ value = $2.strip
3582
+ set_config_option(option, value)
3583
+ when /^set\s+(\w+)$/
3584
+ # Handle :set option (show current value)
3585
+ option = $1
3586
+ show_config_option(option)
3587
+ when "set"
3588
+ # Show all current settings
3589
+ show_all_config_options
3243
3590
  else
3244
3591
  @message = "Unknown command: #{@command}"
3245
3592
  end
3246
3593
  end
3247
3594
 
3595
+ def set_config_option(option, value)
3596
+ # Convert value to appropriate type
3597
+ case option
3598
+ when "theme"
3599
+ if ["light", "normal", "dark"].include?(value)
3600
+ @theme = value
3601
+ @message = "Theme set to: #{value}"
3602
+ else
3603
+ @message = "Invalid theme. Use: light, normal, or dark"
3604
+ end
3605
+ when "wrap"
3606
+ @wrap = value == "yes" || value == "true"
3607
+ @message = "Line wrapping #{@wrap ? 'enabled' : 'disabled'}"
3608
+ when "show_numbers"
3609
+ @show_numbers = value == "yes" || value == "true"
3610
+ @message = "Line numbers #{@show_numbers ? 'enabled' : 'disabled'}"
3611
+ when "fold_level"
3612
+ level = value.to_i
3613
+ if level >= 0 && level <= 99
3614
+ @fold_level = level
3615
+ apply_fold_level(level)
3616
+ @message = "Fold level set to: #{level}"
3617
+ else
3618
+ @message = "Invalid fold level. Use 0-99"
3619
+ end
3620
+ when "auto_save"
3621
+ @auto_save_enabled = value == "yes" || value == "true"
3622
+ @message = "Auto-save #{@auto_save_enabled ? 'enabled' : 'disabled'}"
3623
+ when "auto_save_interval"
3624
+ interval = value.to_i
3625
+ if interval > 0
3626
+ @auto_save_interval = interval
3627
+ @message = "Auto-save interval set to: #{interval} seconds"
3628
+ else
3629
+ @message = "Invalid interval. Must be > 0"
3630
+ end
3631
+ when "tab_width", "indent_size"
3632
+ width = value.to_i
3633
+ if width >= 2 && width <= 8
3634
+ @indent_size = width
3635
+ @message = "Tab width set to: #{width}"
3636
+ else
3637
+ @message = "Invalid tab width. Use 2-8"
3638
+ end
3639
+ else
3640
+ @message = "Unknown option: #{option}"
3641
+ end
3642
+
3643
+ # Update config line if it exists
3644
+ update_config_line
3645
+ end
3646
+
3647
+ def show_config_option(option)
3648
+ case option
3649
+ when "theme"
3650
+ @message = "theme=#{@theme}"
3651
+ when "wrap"
3652
+ @message = "wrap=#{@wrap ? 'yes' : 'no'}"
3653
+ when "show_numbers"
3654
+ @message = "show_numbers=#{@show_numbers ? 'yes' : 'no'}"
3655
+ when "fold_level"
3656
+ @message = "fold_level=#{@fold_level}"
3657
+ when "auto_save"
3658
+ @message = "auto_save=#{@auto_save_enabled ? 'yes' : 'no'}"
3659
+ when "auto_save_interval"
3660
+ @message = "auto_save_interval=#{@auto_save_interval}"
3661
+ when "tab_width", "indent_size"
3662
+ @message = "tab_width=#{@indent_size}"
3663
+ else
3664
+ @message = "Unknown option: #{option}"
3665
+ end
3666
+ end
3667
+
3668
+ def show_all_config_options
3669
+ options = []
3670
+ options << "theme=#{@theme}"
3671
+ options << "wrap=#{@wrap ? 'yes' : 'no'}"
3672
+ options << "show_numbers=#{@show_numbers ? 'yes' : 'no'}"
3673
+ options << "fold_level=#{@fold_level}"
3674
+ options << "auto_save=#{@auto_save_enabled ? 'yes' : 'no'}"
3675
+ options << "auto_save_interval=#{@auto_save_interval}"
3676
+ options << "tab_width=#{@indent_size}"
3677
+ @message = "Settings: #{options.join(', ')}"
3678
+ end
3679
+
3680
+ def update_config_line
3681
+ # Build new config line based on current settings
3682
+ # This should preserve the config line and update it with current values
3683
+ options = []
3684
+
3685
+ # Include fold_level if it's not the default
3686
+ if @fold_level != 99
3687
+ options << "fold_level=#{@fold_level}"
3688
+ end
3689
+
3690
+ options << "theme=#{@theme}" if @theme != "normal"
3691
+ options << "wrap=yes" if @wrap
3692
+ options << "show_numbers=yes" if @show_numbers
3693
+ options << "auto_save=yes" if @auto_save_enabled
3694
+ options << "auto_save_interval=#{@auto_save_interval}" if @auto_save_interval != 60
3695
+ options << "tab_width=#{@indent_size}" if @indent_size != 2
3696
+
3697
+ if options.any?
3698
+ @config_line = "((#{options.join(', ')}))"
3699
+ else
3700
+ @config_line = nil
3701
+ end
3702
+
3703
+ @modified = true
3704
+ end
3705
+
3248
3706
  def jump_to_reference
3249
3707
  visible = get_visible_items
3250
3708
  return if @current >= visible.length
@@ -4419,11 +4877,19 @@ class HyperListApp
4419
4877
  visible_items.each_with_index do |item, idx|
4420
4878
  next unless item
4421
4879
 
4422
- line = " " * item["level"]
4423
-
4424
- # Add fold indicator with colors
4425
4880
  # Find the item's position in the original split_items array
4426
4881
  real_idx = @split_items.index(item)
4882
+
4883
+ # Add line number if enabled
4884
+ line = ""
4885
+ if @show_numbers
4886
+ actual_line_number = real_idx ? real_idx + 1 : 0 # +1 for 1-based line numbers
4887
+ line = "#{actual_line_number.to_s.rjust(4)} "
4888
+ end
4889
+
4890
+ line += " " * item["level"]
4891
+
4892
+ # Add fold indicator with colors
4427
4893
  if real_idx && has_children_in_array?(real_idx, @split_items)
4428
4894
  if item["fold"]
4429
4895
  line += "▶".fg("245") + " "
@@ -4959,6 +5425,7 @@ class HyperListApp
4959
5425
  end
4960
5426
 
4961
5427
  def quit
5428
+ save_command_history
4962
5429
  Cursor.show
4963
5430
  Rcurses.clear_screen
4964
5431
  exit
data/hyperlist.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |spec|
2
2
  spec.name = "hyperlist"
3
- spec.version = "1.2.7"
3
+ spec.version = "1.4.2"
4
4
  spec.authors = ["Geir Isene"]
5
5
  spec.email = ["g@isene.com"]
6
6
 
data/sample.hl CHANGED
@@ -1,83 +1,85 @@
1
1
  HyperList Sample Document
2
- Project Management #project
3
- [ ] Planning Phase
4
- [X] Define objectives
5
- [X] Identify stakeholders
6
- [O] Create timeline
7
- [ ] Budget estimation
8
- Research costs
9
- Get quotes from vendors
10
- [3] Review with finance team
11
- Implementation
12
- Development Tasks
13
- [ ] Backend API
14
- Authentication module
15
- Database schema
16
- REST endpoints
17
- [ ] Frontend UI
18
- Login page
19
- Dashboard
20
- Reports section
21
- Testing
22
- Unit tests
23
- Integration tests
24
- User acceptance testing
25
- Documentation
26
- Technical documentation
27
- User manual
28
- Training materials
29
- Personal Tasks #personal
30
- [X] Morning routine
31
- [X] Exercise
32
- [X] Breakfast
33
- [X] Review calendar
34
- [x] 2025-08-11 13.30: Review meeting notes
35
- [ ] Shopping list
36
- [5 liters] Milk
37
- [2 packages] Butter
38
- Vegetables
39
- Tomatoes
40
- Lettuce
41
- Carrots
42
- Learning goals
43
- *Master* Ruby programming
44
- Study /design patterns/
45
- Practice _algorithms_
46
- References and Links
47
- <file:~/Documents/specs.pdf>
48
- <https://example.com/documentation>
49
- See also: <Project Management>
50
- Previous version: <-10>
51
- Complex Examples
52
- 2025-08-11 11.51: Meeting notes
53
- State: System initialized
54
- Transition: User login -> Dashboard
55
- [2..5] Retry attempts allowed
56
- [?] Optional configuration
57
- ; This is a comment
58
- Setting A = value1
59
- Setting B = value2
60
- Examples with parentheses and quotes
61
- Meeting (scheduled for 2pm)
62
- Task "Review documentation" (high priority)
63
- Note: 'This is important' (see reference)
64
- Conditional examples with operators
65
- AND: Complete all tests AND: Deploy to production
66
- OR: Use Docker OR: Use Kubernetes
67
- IF: Tests pass THEN: Deploy automatically
68
- NOT: Include debug logs
69
- WHEN: User logged in THEN: Show dashboard
70
- UNLESS: Admin privileges THEN: Deny access
71
- Multi-level nesting example
72
- Level 1
73
- Level 2
74
- Level 3
75
- Level 4
76
- Level 5
77
- Deep nesting test
78
- Markdown-style formatting
79
- *Bold text example*
80
- /Italic text example/
81
- _Underlined text example_
82
- Combined: *bold and /italic/ together*
83
- Hash tags: #important #urgent #review
2
+ Project Management #project
3
+ [ ] Planning Phase
4
+ [X] Define objectives
5
+ [X] Identify stakeholders
6
+ [O] Create timeline
7
+ [ ] Budget estimation
8
+ Research costs
9
+ Get quotes from vendors
10
+ [3] Review with finance team
11
+ Implementation
12
+ Development Tasks
13
+ [ ] Backend API
14
+ Authentication module
15
+ Database schema
16
+ REST endpoints
17
+ [ ] Frontend UI
18
+ Login page
19
+ Dashboard
20
+ Reports section
21
+ Testing
22
+ Unit tests
23
+ Integration tests
24
+ User acceptance testing
25
+ Documentation
26
+ Technical documentation
27
+ User manual
28
+ Training materials
29
+ Personal Tasks #personal
30
+ [X] Morning routine
31
+ [X] Exercise
32
+ [X] Breakfast
33
+ [X] Review calendar
34
+ [x] 2025-08-11 13.30: Review meeting notes
35
+ [ ] Shopping list
36
+ [5 liters] Milk
37
+ [2 packages] Butter
38
+ Vegetables
39
+ Tomatoes
40
+ Lettuce
41
+ Carrots
42
+ Learning goals
43
+ *Master* Ruby programming
44
+ Study /design patterns/
45
+ Practice _algorithms_
46
+ References and Links
47
+ <file:~/Documents/specs.pdf>
48
+ <https://example.com/documentation>
49
+ See also: <Project Management>
50
+ Previous version: <-10>
51
+ Complex Examples
52
+ 2025-08-11 11.51: Meeting notes
53
+ State: System initialized
54
+ Transition: User login -> Dashboard
55
+ [2..5] Retry attempts allowed
56
+ [?] Optional configuration
57
+ ; This is a comment
58
+ Setting A = value1
59
+ Setting B = value2
60
+ Examples with parentheses and quotes
61
+ Meeting (scheduled for 2pm)
62
+ Task "Review documentation" (high priority)
63
+ Note: 'This is important' (see reference)
64
+ Conditional examples with operators
65
+ AND: Complete all tests AND: Deploy to production
66
+ OR: Use Docker OR: Use Kubernetes
67
+ IF: Tests pass THEN: Deploy automatically
68
+ NOT: Include debug logs
69
+ WHEN: User logged in THEN: Show dashboard
70
+ UNLESS: Admin privileges THEN: Deny access
71
+ Multi-level nesting example
72
+ Level 1
73
+ Level 2
74
+ Level 3
75
+ Level 4
76
+ Level 5
77
+ Deep nesting test
78
+ Markdown-style formatting
79
+ *Bold text example*
80
+ /Italic text example/
81
+ _Underlined text example_
82
+ Combined: *bold and /italic/ together*
83
+ Hash tags: #important #urgent #review
84
+
85
+ ((fold_level=3))
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.2.7
4
+ version: 1.4.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-08-26 00:00:00.000000000 Z
11
+ date: 2025-08-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rcurses