hyperlist 1.2.3 → 1.2.5

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 (5) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +18 -6
  3. data/hyperlist +331 -43
  4. data/hyperlist.gemspec +1 -1
  5. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cf122ea3e53905dda1673a8a1c79f059fd19bbb55765cff5330ea9adac850aeb
4
- data.tar.gz: c96e2559e357a69fa21d8fce8286b17c32460ce6aac7261035d5da92034c8772
3
+ metadata.gz: 1bfa281e52d034a301cc645167ff71600f11bbb718b6220ff97ba1df74feed97
4
+ data.tar.gz: 67ef0888bfad609aea9260b07c538cba47ac219e70aec504f568b7fbea00d6cb
5
5
  SHA512:
6
- metadata.gz: 007ae293391311a7f13ece074f98fe2fb6a99b3d2b4eb73e59267828c46337a24104a1d7cbf7fb59ead5df282f6baad25a15722d054483774915757dc4c6ba22
7
- data.tar.gz: e065656d916620d74cdb60e0bcb29f7b998fa2f08faaeb742165eaf340aa45d5dc4cb940bc7c8822c7d519b876665b33732b21c7a02f115a16464a61872408c5
6
+ metadata.gz: 0f932ad62217a97698953ff73930ce28a54d7abe67510eeadbfb1e84ea2a64421a66d3239e2badde5bd7fd19e920d3c916b378a792e1f87c97549298867c0d17
7
+ data.tar.gz: 8fe1a8f377ec7775e024030e3119121727935225da24517dd77e72079ca17969cef632ff320259fdb80a6c93d4b044c51493571a42f2b0bd7ff45c6a7cb7c3c4
data/README.md CHANGED
@@ -29,18 +29,28 @@ For historical context and the original VIM implementation, see: [hyperlist.vim]
29
29
  ### Help Screen
30
30
  ![HyperList Help](img/screenshot_help.png)
31
31
 
32
- ## What's New in v1.2.0
32
+ ## What's New in v1.2.4
33
33
 
34
- ### 📝 User-Defined Templates
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`
43
+
44
+ ## Previous Updates
45
+
46
+ ### v1.2.0 - User-Defined Templates
35
47
  - **Save as Template**: Save any HyperList document as a reusable template (`:st`)
36
48
  - **Template Manager**: List and delete your custom templates (`:lt`, `:dt`)
37
49
  - **Enhanced Template Browser**: Shows both built-in and user templates
38
50
  - **Template Metadata**: Includes description and creation date
39
51
  - Templates stored in `~/.hyperlist/templates/` for easy backup and sharing
40
52
 
41
- ## Previous Updates (v1.1.0)
42
-
43
- ### 🔐 Encryption Support
53
+ ### v1.1.0 - Encryption Support
44
54
  - **File-level encryption** for sensitive files (dot files like `.passwords.hl`)
45
55
  - **Line-level encryption** for individual items (Ctrl-E to toggle)
46
56
  - Secure AES-256-CBC encryption with PBKDF2 key derivation
@@ -150,6 +160,8 @@ hyperlist file.txt # Open any text file
150
160
  - `o` - Insert line below
151
161
  - `O` - Insert line above
152
162
  - `a` - Insert child item
163
+ - `A` - Insert outdented item (one level less)
164
+ - `I` - Cycle indentation size (2-5 spaces)
153
165
  - `D` - Delete and yank line
154
166
  - `C-D` - Delete and yank item with descendants
155
167
  - `y/Y` - Copy line/tree
@@ -174,7 +186,7 @@ hyperlist file.txt # Open any text file
174
186
  #### File Commands
175
187
  - `:w` - Save
176
188
  - `:q` - Quit
177
- - `:wq` - Save and quit
189
+ - `:wq` or `W` - Save and quit
178
190
  - `:e file` - Open file
179
191
  - `:export html` - Export to HTML
180
192
  - `:export md` - Export to Markdown
data/hyperlist CHANGED
@@ -117,6 +117,11 @@ class HyperListApp
117
117
  @active_pane = :main # :main or :split
118
118
  @message_timeout = nil # For timed message display
119
119
 
120
+ # Global configuration
121
+ @config_file = File.expand_path("~/.hyperlist/config.yml")
122
+ @indent_size = 2 # Default indentation (2-5 spaces)
123
+ load_config
124
+
120
125
  # Terminal setup
121
126
  if IO.console
122
127
  @rows, @cols = IO.console.winsize
@@ -219,6 +224,9 @@ class HyperListApp
219
224
  lines = content.split("\n")
220
225
  end
221
226
 
227
+ # Auto-detect indentation from the file
228
+ detect_indentation(lines)
229
+
222
230
  # Check if file is large
223
231
  large_file = lines.length > 10000
224
232
 
@@ -230,7 +238,17 @@ class HyperListApp
230
238
  # Process lines with optional progress updates for large files
231
239
  lines.each_with_index do |line, idx|
232
240
  next if line.strip.empty?
233
- level = line[/^\t*/].length
241
+
242
+ # Detect level based on leading whitespace
243
+ if line.start_with?("\t")
244
+ # Tab-based indentation
245
+ level = line[/^\t*/].length
246
+ else
247
+ # Space-based indentation
248
+ spaces = line[/^ */].length
249
+ level = @indent_size > 0 ? spaces / @indent_size : 0
250
+ end
251
+
234
252
  text = line.strip
235
253
 
236
254
  # Track encrypted lines
@@ -411,7 +429,7 @@ class HyperListApp
411
429
 
412
430
  # Prepare content
413
431
  content = @items.map do |item|
414
- "\t" * item["level"] + item["text"]
432
+ (' ' * @indent_size) * item["level"] + item["text"]
415
433
  end.join("\n")
416
434
 
417
435
  # Check if this should be an encrypted file
@@ -535,7 +553,7 @@ class HyperListApp
535
553
  lines << "## #{text}" unless text.empty?
536
554
  else
537
555
  # Use spaces for indentation in lists
538
- indent = ' ' * [level - 2, 0].max
556
+ indent = ' ' * (@indent_size * [level - 2, 0].max)
539
557
  if text.start_with?('- [')
540
558
  lines << indent + text
541
559
  else
@@ -585,7 +603,7 @@ class HyperListApp
585
603
  @items.each do |item|
586
604
  original_text = item["text"]
587
605
  level = item["level"]
588
- indent = " " * level
606
+ indent = ' ' * (@indent_size * level)
589
607
 
590
608
  # Process the line
591
609
  # We'll apply formatting BEFORE escaping HTML entities
@@ -691,7 +709,7 @@ class HyperListApp
691
709
  def export_to_text
692
710
  lines = []
693
711
  @items.each do |item|
694
- indent = ' ' * item["level"] # 4 spaces per level
712
+ indent = ' ' * (@indent_size * item["level"])
695
713
  lines << indent + item["text"]
696
714
  end
697
715
  lines.join("\n")
@@ -724,7 +742,7 @@ class HyperListApp
724
742
  visible_items.each_with_index do |item, idx|
725
743
  next unless item
726
744
 
727
- line = " " * item["level"] # 4 spaces per level
745
+ line = ' ' * (@indent_size * item["level"])
728
746
 
729
747
  # Add fold indicator
730
748
  real_idx = get_real_index(item)
@@ -765,13 +783,16 @@ class HyperListApp
765
783
  end
766
784
  end
767
785
 
768
- # Apply current item highlighting
786
+ # Apply current item highlighting (but not in presentation mode for focused items)
769
787
  if idx == @current
770
- bg_color = (!@split_view || @active_pane == :main) ? "237" : "234"
771
- if bg_color
772
- bg_code = "\e[48;5;#{bg_color}m"
773
- reset_bg = "\e[49m"
774
- line = bg_code + line.gsub(/\e\[49m/, '') + reset_bg
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
795
+ end
775
796
  end
776
797
  end
777
798
 
@@ -1336,6 +1357,44 @@ class HyperListApp
1336
1357
  end
1337
1358
  end
1338
1359
 
1360
+ def jump_to_next_sibling
1361
+ visible = get_visible_items
1362
+ return if @current >= visible.length - 1
1363
+
1364
+ current_level = visible[@current]["level"]
1365
+
1366
+ # Search forward for the next item at the same level
1367
+ (@current + 1...visible.length).each do |i|
1368
+ if visible[i]["level"] == current_level
1369
+ @current = i
1370
+ update_presentation_focus if @presentation_mode
1371
+ return
1372
+ elsif visible[i]["level"] < current_level
1373
+ # We've gone up a level, no more siblings
1374
+ return
1375
+ end
1376
+ end
1377
+ end
1378
+
1379
+ def jump_to_prev_sibling
1380
+ visible = get_visible_items
1381
+ return if @current <= 0
1382
+
1383
+ current_level = visible[@current]["level"]
1384
+
1385
+ # Search backward for the previous item at the same level
1386
+ (@current - 1).downto(0) do |i|
1387
+ if visible[i]["level"] == current_level
1388
+ @current = i
1389
+ update_presentation_focus if @presentation_mode
1390
+ return
1391
+ elsif visible[i]["level"] < current_level
1392
+ # We've gone up a level, no more siblings before this
1393
+ return
1394
+ end
1395
+ end
1396
+ end
1397
+
1339
1398
  def expand_to_level(level)
1340
1399
  @items.each do |item|
1341
1400
  item["fold"] = item["level"] >= level
@@ -1426,9 +1485,11 @@ class HyperListApp
1426
1485
  item["presentation_focus"] = false
1427
1486
  end
1428
1487
 
1429
- # Mark current item as in focus and unfold it
1430
- current_item["presentation_focus"] = true
1431
- current_item["fold"] = false
1488
+ # Mark current item as in focus and unfold it in the main items array
1489
+ if current_real_idx && current_real_idx < @items.length
1490
+ @items[current_real_idx]["presentation_focus"] = true
1491
+ @items[current_real_idx]["fold"] = false
1492
+ end
1432
1493
 
1433
1494
  # Unfold all ancestors of current item
1434
1495
  ancestor_indices = []
@@ -1554,6 +1615,8 @@ class HyperListApp
1554
1615
  insert_line_with_text(@last_action)
1555
1616
  when :insert_child
1556
1617
  insert_child_with_text(@last_action)
1618
+ when :insert_outdented
1619
+ insert_outdented_with_text(@last_action)
1557
1620
  when :delete_line
1558
1621
  delete_line
1559
1622
  when :edit_line
@@ -1659,6 +1722,21 @@ class HyperListApp
1659
1722
  @footer.refresh
1660
1723
  end
1661
1724
 
1725
+ def insert_outdented
1726
+ @mode = :insert
1727
+
1728
+ input = @footer.ask("New outdented item: ", "")
1729
+
1730
+ if input && !input.strip.empty?
1731
+ insert_outdented_with_text(input)
1732
+ record_last_action(:insert_outdented, input)
1733
+ end
1734
+
1735
+ @mode = :normal
1736
+ @footer.clear # Clear footer immediately
1737
+ @footer.refresh
1738
+ end
1739
+
1662
1740
  def insert_child_with_text(text)
1663
1741
  return unless text && !text.strip.empty?
1664
1742
 
@@ -1679,6 +1757,35 @@ class HyperListApp
1679
1757
  @current += 1
1680
1758
  end
1681
1759
 
1760
+ def insert_outdented_with_text(text)
1761
+ return unless text && !text.strip.empty?
1762
+
1763
+ save_undo_state # Save state before modification
1764
+ visible = get_visible_items
1765
+ if @current < visible.length
1766
+ current_level = visible[@current]["level"]
1767
+ # Outdent means one level less, but not less than 0
1768
+ level = [current_level - 1, 0].max
1769
+ real_idx = get_real_index(visible[@current])
1770
+
1771
+ # Find where to insert: after current item and all its descendants
1772
+ insert_idx = real_idx + 1
1773
+ while insert_idx < @items.length && @items[insert_idx]["level"] > current_level
1774
+ insert_idx += 1
1775
+ end
1776
+
1777
+ @items.insert(insert_idx, {"text" => text, "level" => level, "fold" => false})
1778
+
1779
+ # Update cursor to point to the new item
1780
+ @current = get_visible_items.index { |item| item.equal?(@items[insert_idx]) } || (@current + 1)
1781
+ else
1782
+ # If at end of list, add at level 0
1783
+ @items << {"text" => text, "level" => 0, "fold" => false}
1784
+ @current = get_visible_items.length - 1
1785
+ end
1786
+ @modified = true
1787
+ end
1788
+
1682
1789
  def edit_line
1683
1790
  visible = get_visible_items
1684
1791
  return if @current >= visible.length
@@ -1756,10 +1863,16 @@ class HyperListApp
1756
1863
  end
1757
1864
 
1758
1865
  # Delete the items
1866
+ # Remember the level of the deleted item for renumbering
1867
+ deleted_level = item["level"]
1868
+
1759
1869
  delete_count.times { @items.delete_at(real_idx) }
1760
1870
 
1761
1871
  @items = [{"text" => "Empty", "level" => 0, "fold" => false}] if @items.empty?
1762
1872
 
1873
+ # Renumber siblings at the deleted item's level
1874
+ renumber_siblings(deleted_level) unless @items.length == 1
1875
+
1763
1876
  @current = [@current, get_visible_items.length - 1].min
1764
1877
  @current = 0 if @current < 0
1765
1878
  @modified = true
@@ -1863,18 +1976,47 @@ class HyperListApp
1863
1976
  end
1864
1977
  end
1865
1978
 
1979
+ # Find the previous sibling at the same level
1980
+ target_level = item["level"]
1981
+ target_idx = nil
1982
+
1983
+ # Search backwards for a sibling at the same level
1984
+ (real_idx - 1).downto(0) do |i|
1985
+ if @items[i]["level"] == target_level
1986
+ target_idx = i
1987
+ break
1988
+ elsif @items[i]["level"] < target_level
1989
+ # Hit a parent, can't move up
1990
+ return
1991
+ end
1992
+ end
1993
+
1994
+ # Can't move if no sibling found
1995
+ return if target_idx.nil?
1996
+
1997
+ # Remember the first item we're moving (to track it)
1998
+ first_moved_item = items_to_move.first
1999
+
1866
2000
  # Remove items from their current position
1867
2001
  items_to_move.length.times { @items.delete_at(real_idx) }
1868
2002
 
1869
- # Insert one position up (before the item that was above us)
1870
- target_idx = real_idx - 1
2003
+ # Insert before the target sibling
1871
2004
  items_to_move.reverse.each do |item_to_move|
1872
2005
  @items.insert(target_idx, item_to_move)
1873
2006
  end
1874
2007
 
1875
- @current -= 1
2008
+ # Update cursor position to follow the moved item
2009
+ # The moved item is now at position target_idx in @items
2010
+ # Find this position in the visible list
2011
+ new_visible = get_visible_items
2012
+ new_item_idx = new_visible.find_index { |v| v["_real_index"] == target_idx }
2013
+ @current = new_item_idx if new_item_idx
2014
+
2015
+ # Renumber siblings at the moved item's level
2016
+ renumber_siblings(first_moved_item["level"])
2017
+
1876
2018
  @modified = true
1877
- @message = "Moved #{items_to_move.length} item(s) up one line"
2019
+ @message = "Moved #{items_to_move.length} item(s) up"
1878
2020
  record_last_action(:move_item_up, with_children)
1879
2021
  end
1880
2022
 
@@ -1904,26 +2046,78 @@ class HyperListApp
1904
2046
  end
1905
2047
  end
1906
2048
 
1907
- # Can't move if we're already at the end
1908
- return if last_idx >= @items.length - 1
2049
+ # Find the next sibling at the same level
2050
+ target_level = item["level"]
2051
+ target_idx = nil
2052
+ next_sibling_end = nil
2053
+
2054
+ # Search forward for a sibling at the same level
2055
+ ((last_idx + 1)...@items.length).each do |i|
2056
+ if @items[i]["level"] == target_level
2057
+ # Found next sibling, now find where it ends (including its children)
2058
+ next_sibling_end = i
2059
+ ((i + 1)...@items.length).each do |j|
2060
+ if @items[j]["level"] > target_level
2061
+ next_sibling_end = j
2062
+ else
2063
+ break
2064
+ end
2065
+ end
2066
+ target_idx = next_sibling_end + 1
2067
+ break
2068
+ elsif @items[i]["level"] < target_level
2069
+ # Hit a parent level, can't move down
2070
+ return
2071
+ end
2072
+ end
2073
+
2074
+ # Can't move if no sibling found
2075
+ return if target_idx.nil?
2076
+
2077
+ # Remember the first item we're moving (to track it)
2078
+ first_moved_item = items_to_move.first
1909
2079
 
1910
2080
  # Remove items from their current position
1911
2081
  items_to_move.length.times { @items.delete_at(real_idx) }
1912
2082
 
1913
- # Insert one position down (after the item that was below us)
1914
- # Since we removed items, the target position is now at real_idx + 1
1915
- target_idx = real_idx + 1
2083
+ # Adjust target index since we removed items
2084
+ target_idx -= items_to_move.length
1916
2085
 
2086
+ # Insert after the target sibling and its children
1917
2087
  items_to_move.each_with_index do |item_to_move, idx|
1918
2088
  @items.insert(target_idx + idx, item_to_move)
1919
2089
  end
1920
2090
 
1921
- @current += 1
2091
+ # Update cursor position to follow the moved item
2092
+ # The moved item is now at position target_idx in @items
2093
+ # Find this position in the visible list
2094
+ new_visible = get_visible_items
2095
+ new_item_idx = new_visible.find_index { |v| v["_real_index"] == target_idx }
2096
+ @current = new_item_idx if new_item_idx
2097
+
2098
+ # Renumber siblings at the moved item's level
2099
+ renumber_siblings(first_moved_item["level"])
2100
+
1922
2101
  @modified = true
1923
- @message = "Moved #{items_to_move.length} item(s) down one line"
2102
+ @message = "Moved #{items_to_move.length} item(s) down"
1924
2103
  record_last_action(:move_item_down, with_children)
1925
2104
  end
1926
2105
 
2106
+ def renumber_siblings(changed_level)
2107
+ # Renumber items at the specified level that start with numbers
2108
+ current_number = 1
2109
+ @items.each do |item|
2110
+ if item["level"] == changed_level
2111
+ # Check if item starts with a number pattern like "1." or "1)"
2112
+ if item["text"] =~ /^\d+[\.\)]\s+(.+)/
2113
+ # Replace the number with the current sequential number
2114
+ item["text"] = "#{current_number}. #{$1}"
2115
+ current_number += 1
2116
+ end
2117
+ end
2118
+ end
2119
+ end
2120
+
1927
2121
  def indent_right(with_children = true)
1928
2122
  visible = get_visible_items
1929
2123
  return if @current >= visible.length
@@ -2163,11 +2357,13 @@ class HyperListApp
2163
2357
  help_lines << help_line("#{"h".fg("10")}", "Go to parent", "#{"l".fg("10")}", "Go to first child")
2164
2358
  help_lines << help_line("#{"PgUp".fg("10")}", "Page up", "#{"PgDn".fg("10")}", "Page down")
2165
2359
  help_lines << help_line("#{"g/Home".fg("10")}", "Go to top", "#{"G/End".fg("10")}", "Go to bottom")
2166
- help_lines << help_line("#{"/".fg("10")}", "Search", "#{"n".fg("10")}", "Next match")
2167
- help_lines << help_line("#{"?".fg("10")}", "This help", "#{"??".fg("10")}", "Full documentation")
2360
+ help_lines << help_line("#{"R".fg("10")}", "Jump to reference", "#{"F".fg("10")}", "Open file/URL")
2168
2361
  help_lines << help_line("#{"ma".fg("10")}", "Set mark 'a'", "#{"'a".fg("10")}", "Jump to mark 'a'")
2169
2362
  help_lines << help_line("#{"''".fg("10")}", "Jump to prev position", "#{"N".fg("10")}", "Next = template marker")
2170
2363
  help_lines << ""
2364
+ help_lines << "#{"SEARCH".fg("14")}"
2365
+ help_lines << help_line("#{"/".fg("10")}", "Search forward", "#{"n".fg("10")}", "Next match")
2366
+ help_lines << ""
2171
2367
  help_lines << "#{"FOLDING".fg("14")}"
2172
2368
  help_lines << help_line("#{"Space".fg("10")}", "Toggle fold", "#{"za".fg("10")}", "Toggle all folds")
2173
2369
  help_lines << help_line("#{"zo".fg("10")}", "Open fold", "#{"zc".fg("10")}", "Close fold")
@@ -2179,6 +2375,8 @@ class HyperListApp
2179
2375
  help_lines << "#{"EDITING".fg("14")}"
2180
2376
  help_lines << help_line("#{"i/Enter".fg("10")}", "Edit line", "#{"o".fg("10")}", "Insert line below")
2181
2377
  help_lines << help_line("#{"O".fg("10")}", "Insert line above", "#{"a".fg("10")}", "Insert child")
2378
+ help_lines << help_line("#{"A".fg("10")}", "Insert outdented", "#{"W".fg("10")}", "Save and quit")
2379
+ help_lines << help_line("#{"I".fg("10")}", "Cycle indent (2-5)", "", "")
2182
2380
  help_lines << help_line("#{"D".fg("10")}", "Delete+yank line", "#{"C-D".fg("10")}", "Delete+yank item&descendants")
2183
2381
  help_lines << help_line("#{"y".fg("10")}" + "/".fg("10") + "#{"Y".fg("10")}", "Copy line/tree", "#{"p".fg("10")}", "Paste")
2184
2382
  help_lines << help_line("#{"u".fg("10")}", "Undo", "#{".".fg("10")}", "Repeat last action")
@@ -2188,14 +2386,12 @@ class HyperListApp
2188
2386
  help_lines << help_line("#{"Tab".fg("10")}", "Indent item+kids", "#{"S-Tab".fg("10")}", "Unindent item+kids")
2189
2387
  help_lines << help_line("#{"→".fg("10")}", "Indent item only", "#{"←".fg("10")}", "Unindent item only")
2190
2388
  help_lines << ""
2191
- help_lines << "#{"FEATURES".fg("14")}"
2389
+ help_lines << "#{"SPECIAL FEATURES".fg("14")}"
2192
2390
  help_lines << help_line("#{"v".fg("10")}", "Toggle checkbox", "#{"V".fg("10")}", "Checkbox with date")
2193
2391
  help_lines << help_line("#{"C-E".fg("10")}", "Encrypt/decrypt line", "#{"C-U".fg("10")}", "Toggle State/Trans underline")
2194
- help_lines << help_line("#{"R".fg("10")}", "Go to reference", "#{"F".fg("10")}", "Open file")
2195
- help_lines << help_line("#{"N".fg("10")}", "Next = marker", "#{"P".fg("10")}", "Presentation mode")
2196
- help_lines << help_line("#{"t".fg("10")}", "Insert template", "#{":st".fg("10")}", "Save as template")
2392
+ help_lines << help_line("#{"P".fg("10")}", "Presentation mode", "#{"Tab/S-Tab".fg("10")}", "Next/prev sibling (in P)")
2197
2393
  help_lines << help_line("#{"Ma".fg("10")}", "Record macro 'a'", "#{"@a".fg("10")}", "Play macro 'a'")
2198
- help_lines << help_line("#{":vsplit".fg("10")}", "Split view vertically", "#{"w".fg("10")}", "Switch panes")
2394
+ help_lines << help_line("#{"w".fg("10")}", "Switch panes (split view)", "", "")
2199
2395
  help_lines << ""
2200
2396
  help_lines << "#{"FILE OPERATIONS".fg("14")}"
2201
2397
  help_lines << help_line("#{":w".fg("10")}", "Save", "#{":q".fg("10")}", "Quit")
@@ -2206,9 +2402,11 @@ class HyperListApp
2206
2402
  help_lines << help_line("#{":as N".fg("10")}", "Set interval (secs)", "#{":as".fg("10")}", "Show autosave status")
2207
2403
  help_lines << ""
2208
2404
  help_lines << "#{"TEMPLATES".fg("14")}"
2209
- help_lines << help_line("#{":st".fg("10")}", "Save as template", "#{":dt".fg("10")}", "Delete template")
2210
- help_lines << help_line("#{":lt".fg("10")}", "List user templates", "#{"t".fg("10")}", "Insert template")
2405
+ help_lines << help_line("#{"t".fg("10")}", "Insert template", "#{":st".fg("10")}", "Save as template")
2406
+ help_lines << help_line("#{":dt".fg("10")}", "Delete template", "#{":lt".fg("10")}", "List user templates")
2211
2407
  help_lines << ""
2408
+ help_lines << "#{"HELP & QUIT".fg("14")}"
2409
+ help_lines << help_line("#{"?".fg("10")}", "This help", "#{"??".fg("10")}", "Full documentation")
2212
2410
  help_lines << help_line("#{"q".fg("10")}", "Quit (asks to save)", "#{"Q".fg("10")}", "Force quit")
2213
2411
  help_lines << ""
2214
2412
  help_lines << "#{"COLOR SCHEME".fg("14")}"
@@ -3282,6 +3480,69 @@ class HyperListApp
3282
3480
  end
3283
3481
  end
3284
3482
 
3483
+ def detect_indentation(lines)
3484
+ # Try to auto-detect indentation size from the file
3485
+ # Find the greatest common divisor of all indentation levels
3486
+ indents = []
3487
+
3488
+ lines.each do |line|
3489
+ next if line.strip.empty?
3490
+
3491
+ # Skip tab-indented lines for space detection
3492
+ next if line.start_with?("\t")
3493
+
3494
+ # Count leading spaces
3495
+ spaces = line[/^ */].length
3496
+ indents << spaces if spaces > 0
3497
+ end
3498
+
3499
+ return if indents.empty?
3500
+
3501
+ # Find the GCD of all indentation levels
3502
+ gcd = indents.reduce(indents.first) { |g, n| n.gcd(g) }
3503
+
3504
+ # Clamp to reasonable range (2-5)
3505
+ if gcd >= 2 && gcd <= 5
3506
+ @indent_size = gcd
3507
+ elsif gcd > 5
3508
+ # If GCD is too large, try to find a divisor between 2-5
3509
+ [5, 4, 3, 2].each do |size|
3510
+ if gcd % size == 0
3511
+ @indent_size = size
3512
+ break
3513
+ end
3514
+ end
3515
+ end
3516
+ # If we can't detect, keep the current setting
3517
+ end
3518
+
3519
+ def load_config
3520
+ # Don't load config, let each file's indentation be detected
3521
+ # Config is only saved when user explicitly changes with 'I'
3522
+ end
3523
+
3524
+ def save_config
3525
+ require 'yaml'
3526
+ config_dir = File.dirname(@config_file)
3527
+ FileUtils.mkdir_p(config_dir) unless Dir.exist?(config_dir)
3528
+
3529
+ config = {
3530
+ 'indent_size' => @indent_size
3531
+ }
3532
+
3533
+ File.write(@config_file, config.to_yaml)
3534
+ rescue => e
3535
+ @message = "Failed to save config: #{e.message}"
3536
+ end
3537
+
3538
+ def cycle_indent_size
3539
+ @indent_size = (@indent_size % 5) + 1 # Cycles through 1,2,3,4,5
3540
+ @indent_size = 2 if @indent_size < 2 # Ensure minimum of 2
3541
+ save_config
3542
+ @message = "Indentation set to #{@indent_size} spaces"
3543
+ @modified = true # Mark as modified since display will change
3544
+ end
3545
+
3285
3546
  def ensure_templates_dir
3286
3547
  FileUtils.mkdir_p(@templates_dir) unless File.exist?(@templates_dir)
3287
3548
  end
@@ -3839,6 +4100,8 @@ class HyperListApp
3839
4100
  insert_line_above
3840
4101
  when "a"
3841
4102
  insert_child
4103
+ when "A"
4104
+ insert_outdented
3842
4105
  when "d"
3843
4106
  # Check if it's dd
3844
4107
  if getchr == "d"
@@ -4728,6 +4991,21 @@ class HyperListApp
4728
4991
  @current += 1
4729
4992
  when "a"
4730
4993
  insert_child
4994
+ when "A"
4995
+ insert_outdented
4996
+ when "W"
4997
+ # Save and quit (shortcut for :wq)
4998
+ if @filename
4999
+ save_file
5000
+ else
5001
+ filename_input = @footer.ask("Save as: ", "")
5002
+ @filename = File.expand_path(filename_input) if filename_input && !filename_input.empty?
5003
+ save_file if @filename && !@filename.empty?
5004
+ end
5005
+ quit if @filename
5006
+ when "I"
5007
+ # Cycle through indentation sizes (2-5 spaces)
5008
+ cycle_indent_size
4731
5009
  when "t"
4732
5010
  show_templates
4733
5011
  when "D" # Delete line (with children)
@@ -4753,18 +5031,28 @@ class HyperListApp
4753
5031
  when "C-K" # Alternative: Move item and descendants up (for terminals that intercept C-UP)
4754
5032
  move_item_up(true)
4755
5033
  when "TAB"
4756
- # Indent with all children
4757
- if @split_view && @active_pane == :split
4758
- indent_split_right(true)
5034
+ if @presentation_mode
5035
+ # In presentation mode, Tab goes to next sibling
5036
+ jump_to_next_sibling
4759
5037
  else
4760
- indent_right(true)
5038
+ # Normal mode: Indent with all children
5039
+ if @split_view && @active_pane == :split
5040
+ indent_split_right(true)
5041
+ else
5042
+ indent_right(true)
5043
+ end
4761
5044
  end
4762
5045
  when "S-TAB" # Shift-Tab
4763
- # Unindent with all children
4764
- if @split_view && @active_pane == :split
4765
- indent_split_left(true)
5046
+ if @presentation_mode
5047
+ # In presentation mode, Shift-Tab goes to previous sibling
5048
+ jump_to_prev_sibling
4766
5049
  else
4767
- indent_left(true)
5050
+ # Normal mode: Unindent with all children
5051
+ if @split_view && @active_pane == :split
5052
+ indent_split_left(true)
5053
+ else
5054
+ indent_left(true)
5055
+ end
4768
5056
  end
4769
5057
  when "u"
4770
5058
  undo
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.3"
3
+ spec.version = "1.2.5"
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.2.3
4
+ version: 1.2.5
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-22 00:00:00.000000000 Z
11
+ date: 2025-08-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rcurses