hyperlist 1.5.2 → 1.7.0

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/CHANGELOG.md +31 -1
  3. data/hyperlist +323 -158
  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: ec123fe286a3c475f76868ed8d0cdd1a512f7323c6da8d9446d2ea7e3913ce61
4
- data.tar.gz: 637a405887c8725cd229ba0dbd8bfa0d0f75cffa787f336a59477a32a70b1669
3
+ metadata.gz: 014e81876165c457903e5f20e8f848f2c59fce0d2672e700ff909873c03c260a
4
+ data.tar.gz: c60b73c642984f70ba2bfdcceb7ca6a9bc501c46071cb362d9e9343778cd8f40
5
5
  SHA512:
6
- metadata.gz: c1ec3ebc57c928ab5c583da3ab2663cbaf73da09e36818f14e0973f3392583ebb5dc4ad1ee543f8263b386709677b70b73227b71c8145738f5afeee14e7a6af4
7
- data.tar.gz: 8d2ff0119d45decfba5fbef08d96d19ebf92f63d0ce33fe35350a3a53cd92fbb94cc845fe85a951d746c374b87b2932c50f5bd54623b953edab162e8eee901c8
6
+ metadata.gz: 37ba0da349a30af46424cc374ece24e315c32a22ee48bf81f4893c43e20cdf18f42ec71c3b3fe123688f9b97597a29e8aa583884cceb361d44583645546aaf25
7
+ data.tar.gz: 5a8737705ad4d231e6ce1a8a1b8f440cf8b0f3c2ffa7586052dc64be5a55d8bc4491ed721b07adb192e6674f0560a9ac35a6a067ed27831b200066b786456081
data/CHANGELOG.md CHANGED
@@ -2,11 +2,41 @@
2
2
 
3
3
  All notable changes to the HyperList Ruby TUI will be documented in this file.
4
4
 
5
+ ## [1.7.0] - 2025-09-18
6
+
7
+ ### Added
8
+ - **Checkbox removal feature**
9
+ - **C-X key**: Remove checkboxes from items
10
+ - Removes all checkbox patterns: `[ ]`, `[X]`, `[x]`, `[O]`, `[-]`, `[_]`
11
+ - Also removes associated timestamps (YYYY-MM-DD HH.MM: format)
12
+ - Provides feedback messages for successful removal or no checkbox found
13
+ - Added to help documentation under SPECIAL FEATURES
14
+ - Complements existing v/V checkbox toggle functionality
15
+
16
+ ### Enhanced
17
+ - **Cross-parent movement improvements**
18
+ - C-UP/C-DOWN now respect visibility principle (move only item if uncollapsed, item+children if collapsed)
19
+ - Added orphaned children reunification logic
20
+ - Items moved into collapsed areas automatically unfold destination
21
+ - Moved items are guaranteed to remain visible (safety net protection)
22
+ - Single-step movement: C-DOWN moves exactly one position down, not to end of descendants
23
+
24
+ ## [1.6.0] - 2025-09-18
25
+
26
+ ### BREAKING CHANGES
27
+ - **Complete reassignment of arrow key functionality**
28
+ - **RIGHT key**: Now uncollapses items (if collapsed)
29
+ - **LEFT key**: Now collapses items (if they have children)
30
+ - **TAB key**: Takes over old RIGHT key functionality (indent right)
31
+ - **S-TAB key**: Takes over old LEFT key functionality (indent left)
32
+ - This provides more intuitive navigation where arrow keys control visibility and TAB keys control indentation
33
+ - Updated help documentation to reflect new key mappings
34
+
5
35
  ## [1.5.2] - 2025-09-18
6
36
 
7
37
  ### Enhanced
8
38
  - **Smart movement behavior for collapsed items**
9
- - LEFT/RIGHT arrow keys now move collapsed trees as a unit when item is folded
39
+ - LEFT/RIGHT arrow keys move collapsed trees as a unit when item is folded
10
40
  - Uncollapsed items move individually (only the current item)
11
41
  - Maintains "move what's visible" principle for intuitive navigation
12
42
  - Works in both normal and split-view modes
data/hyperlist CHANGED
@@ -72,7 +72,7 @@ class HyperListApp
72
72
  include Rcurses::Input
73
73
  include Rcurses::Cursor
74
74
 
75
- VERSION = "1.5.1"
75
+ VERSION = "1.7.0"
76
76
 
77
77
  def initialize(filename = nil)
78
78
  @filename = filename ? File.expand_path(filename) : nil
@@ -1733,7 +1733,61 @@ class HyperListApp
1733
1733
  end
1734
1734
  end
1735
1735
  end
1736
-
1736
+
1737
+ def collapse_item
1738
+ if @split_view && @active_pane == :split
1739
+ # Handle collapse in split pane
1740
+ visible_items = get_visible_split_items
1741
+ return if @split_current >= visible_items.length
1742
+
1743
+ item = visible_items[@split_current]
1744
+ real_idx = @split_items.index(item)
1745
+
1746
+ if real_idx && has_children_in_array?(real_idx, @split_items) && !item["fold"]
1747
+ item["fold"] = true
1748
+ end
1749
+ else
1750
+ # Handle collapse in main pane
1751
+ visible = get_visible_items
1752
+ return if @current >= visible.length
1753
+
1754
+ item = visible[@current]
1755
+ real_idx = get_real_index(item)
1756
+
1757
+ if real_idx && has_children?(real_idx, @items) && !@items[real_idx]["fold"]
1758
+ @items[real_idx]["fold"] = true
1759
+ record_last_action(:toggle_fold, nil)
1760
+ end
1761
+ end
1762
+ end
1763
+
1764
+ def uncollapse_item
1765
+ if @split_view && @active_pane == :split
1766
+ # Handle uncollapse in split pane
1767
+ visible_items = get_visible_split_items
1768
+ return if @split_current >= visible_items.length
1769
+
1770
+ item = visible_items[@split_current]
1771
+ real_idx = @split_items.index(item)
1772
+
1773
+ if real_idx && has_children_in_array?(real_idx, @split_items) && item["fold"]
1774
+ item["fold"] = false
1775
+ end
1776
+ else
1777
+ # Handle uncollapse in main pane
1778
+ visible = get_visible_items
1779
+ return if @current >= visible.length
1780
+
1781
+ item = visible[@current]
1782
+ real_idx = get_real_index(item)
1783
+
1784
+ if real_idx && has_children?(real_idx, @items) && @items[real_idx]["fold"]
1785
+ @items[real_idx]["fold"] = false
1786
+ record_last_action(:toggle_fold, nil)
1787
+ end
1788
+ end
1789
+ end
1790
+
1737
1791
  def move_up
1738
1792
  if @presentation_mode
1739
1793
  # In presentation mode, we need to handle navigation differently
@@ -2578,26 +2632,132 @@ class HyperListApp
2578
2632
  @message = "Pasted #{@clipboard.length} item(s)"
2579
2633
  record_last_action(:paste, nil)
2580
2634
  end
2581
-
2635
+
2636
+ def calculate_level_for_position(target_real_idx)
2637
+ # Calculate appropriate level for an item at target_real_idx position
2638
+ # based on surrounding items in the @items array
2639
+
2640
+ # If at the beginning, level 0
2641
+ return 0 if target_real_idx == 0
2642
+
2643
+ # Look at the previous item
2644
+ prev_item = @items[target_real_idx - 1]
2645
+ prev_level = prev_item["level"]
2646
+
2647
+ # Look at the next item if it exists
2648
+ if target_real_idx < @items.length
2649
+ next_item = @items[target_real_idx]
2650
+ next_level = next_item["level"]
2651
+
2652
+ # If next item is at same or lower level than previous,
2653
+ # we can be at the same level as previous (sibling)
2654
+ if next_level <= prev_level
2655
+ return prev_level
2656
+ else
2657
+ # Next item is deeper, so we could be parent (prev_level)
2658
+ # or child (prev_level + 1), choose to be sibling of previous
2659
+ return prev_level
2660
+ end
2661
+ else
2662
+ # At the end, can be sibling of previous item
2663
+ return prev_level
2664
+ end
2665
+ end
2666
+
2667
+ def find_orphaned_children(item_text, target_real_idx)
2668
+ # Look for children that might belong to the item being moved
2669
+ orphaned_children = []
2670
+
2671
+ # Search from target position onwards for potential children
2672
+ # Look for items at level 2 or higher that could be children
2673
+ (target_real_idx...@items.length).each do |i|
2674
+ item_level = @items[i]["level"]
2675
+
2676
+ # Stop if we hit an item at level 0 or 1 (new parent/sibling)
2677
+ break if item_level <= 1
2678
+
2679
+ # If we find items at level 2+, they could be orphaned children
2680
+ # (In the example: Subtask A1.1, A1.2 are level 2)
2681
+ if item_level >= 2
2682
+ orphaned_children << i
2683
+ else
2684
+ break # Hit a sibling, stop looking
2685
+ end
2686
+ end
2687
+
2688
+ orphaned_children
2689
+ end
2690
+
2691
+ def ensure_destination_visible(target_real_idx, item_level)
2692
+ # Unfold any collapsed areas that would hide an item at target_real_idx with item_level
2693
+ return if target_real_idx >= @items.length || target_real_idx < 0
2694
+
2695
+ # Walk backwards from the target position to find potential ancestor items
2696
+ (target_real_idx - 1).downto(0) do |i|
2697
+ item = @items[i]
2698
+
2699
+ # If this item is at a higher level (lower number) than our item will be, it's a potential ancestor
2700
+ if item["level"] < item_level
2701
+ # If this ancestor is folded, unfold it to make the destination visible
2702
+ if item["fold"] && has_children?(i, @items)
2703
+ item["fold"] = false
2704
+ end
2705
+
2706
+ # Update item_level to continue searching for higher ancestors
2707
+ item_level = item["level"]
2708
+
2709
+ # If we reach level 0, we're done
2710
+ break if item_level == 0
2711
+ end
2712
+ end
2713
+ end
2714
+
2715
+ def force_item_visible(target_real_idx)
2716
+ # Absolutely ensure the item at target_real_idx is visible - last resort safety net
2717
+ return if target_real_idx >= @items.length || target_real_idx < 0
2718
+
2719
+ target_item = @items[target_real_idx]
2720
+ target_level = target_item["level"]
2721
+
2722
+ # Walk backwards and unfold ALL ancestors, no matter what
2723
+ (target_real_idx - 1).downto(0) do |i|
2724
+ item = @items[i]
2725
+
2726
+ # If this item could be an ancestor (higher in hierarchy)
2727
+ if item["level"] < target_level
2728
+ # Force unfold it, regardless of whether it has children
2729
+ item["fold"] = false if item.has_key?("fold")
2730
+
2731
+ # Update target_level to continue searching
2732
+ target_level = item["level"]
2733
+
2734
+ # Continue until we reach level 0
2735
+ break if target_level == 0
2736
+ end
2737
+ end
2738
+
2739
+ # Force a cache clear to ensure visibility updates
2740
+ clear_cache if respond_to?(:clear_cache)
2741
+ end
2742
+
2582
2743
  def move_item_up(with_children = false)
2583
2744
  visible = get_visible_items
2584
2745
  return if @current >= visible.length || @current == 0
2585
-
2746
+
2586
2747
  save_undo_state # Save state before modification
2587
-
2588
- item = visible[@current]
2589
- real_idx = get_real_index(item)
2590
-
2591
- # Can't move if already at the beginning
2592
- return if real_idx == 0
2593
-
2594
- # Collect item(s) to move
2595
- items_to_move = [item]
2596
-
2748
+
2749
+ current_item = visible[@current]
2750
+ current_real_idx = get_real_index(current_item)
2751
+
2752
+ # Get the previous visible item
2753
+ prev_visible_item = visible[@current - 1]
2754
+ prev_real_idx = get_real_index(prev_visible_item)
2755
+
2756
+ # Collect items to move (current item + children if requested)
2757
+ items_to_move = [current_item]
2597
2758
  if with_children
2598
- # Collect all children
2599
- level = item["level"]
2600
- ((real_idx + 1)...@items.length).each do |i|
2759
+ level = current_item["level"]
2760
+ ((current_real_idx + 1)...@items.length).each do |i|
2601
2761
  if @items[i]["level"] > level
2602
2762
  items_to_move << @items[i]
2603
2763
  else
@@ -2605,46 +2765,42 @@ class HyperListApp
2605
2765
  end
2606
2766
  end
2607
2767
  end
2608
-
2609
- # Find the previous sibling at the same level
2610
- target_level = item["level"]
2611
- target_idx = nil
2612
-
2613
- # Search backwards for a sibling at the same level
2614
- (real_idx - 1).downto(0) do |i|
2615
- if @items[i]["level"] == target_level
2616
- target_idx = i
2617
- break
2618
- elsif @items[i]["level"] < target_level
2619
- # Hit a parent, can't move up
2620
- return
2621
- end
2768
+
2769
+ # Calculate what level the moved item should have at the target position
2770
+ new_level = calculate_level_for_position(prev_real_idx)
2771
+ level_diff = new_level - items_to_move.first["level"]
2772
+
2773
+ # Unfold destination area BEFORE moving if needed
2774
+ ensure_destination_visible(prev_real_idx, new_level)
2775
+
2776
+ # Remove the items to move
2777
+ items_to_move.length.times { @items.delete_at(current_real_idx) }
2778
+
2779
+ # Adjust target position since we removed items after it
2780
+ adjusted_target = prev_real_idx
2781
+ if prev_real_idx > current_real_idx
2782
+ adjusted_target -= items_to_move.length
2622
2783
  end
2623
-
2624
- # Can't move if no sibling found
2625
- return if target_idx.nil?
2626
-
2627
- # Remember the first item we're moving (to track it)
2628
- first_moved_item = items_to_move.first
2629
-
2630
- # Remove items from their current position
2631
- items_to_move.length.times { @items.delete_at(real_idx) }
2632
-
2633
- # Insert before the target sibling
2634
- items_to_move.reverse.each do |item_to_move|
2635
- @items.insert(target_idx, item_to_move)
2784
+
2785
+ # Adjust levels of moved items
2786
+ items_to_move.each do |moved_item|
2787
+ moved_item["level"] += level_diff
2788
+ moved_item["level"] = [moved_item["level"], 0].max # Ensure level doesn't go negative
2636
2789
  end
2637
-
2638
- # Update cursor position to follow the moved item
2639
- # The moved item is now at position target_idx in @items
2640
- # Find this position in the visible list
2790
+
2791
+ # Insert items at target position
2792
+ items_to_move.each_with_index do |item_to_move, idx|
2793
+ @items.insert(adjusted_target + idx, item_to_move)
2794
+ end
2795
+
2796
+ # FORCE the moved item to be visible (safety net)
2797
+ force_item_visible(adjusted_target)
2798
+
2799
+ # Update cursor to follow the moved item
2641
2800
  new_visible = get_visible_items
2642
- new_item_idx = new_visible.find_index { |v| v["_real_index"] == target_idx }
2801
+ new_item_idx = new_visible.find_index { |v| get_real_index(v) == adjusted_target }
2643
2802
  @current = new_item_idx if new_item_idx
2644
-
2645
- # Renumber siblings at the moved item's level
2646
- renumber_siblings(first_moved_item["level"])
2647
-
2803
+
2648
2804
  @modified = true
2649
2805
  @message = "Moved #{items_to_move.length} item(s) up"
2650
2806
  record_last_action(:move_item_up, with_children)
@@ -2653,81 +2809,71 @@ class HyperListApp
2653
2809
  def move_item_down(with_children = false)
2654
2810
  visible = get_visible_items
2655
2811
  return if @current >= visible.length - 1
2656
-
2812
+
2657
2813
  save_undo_state # Save state before modification
2658
-
2659
- item = visible[@current]
2660
- real_idx = get_real_index(item)
2661
-
2662
- # Collect item(s) to move
2663
- items_to_move = [item]
2664
- last_idx = real_idx
2665
-
2814
+
2815
+ current_item = visible[@current]
2816
+ current_real_idx = get_real_index(current_item)
2817
+
2818
+ # Get the next visible item
2819
+ next_visible_item = visible[@current + 1]
2820
+ next_real_idx = get_real_index(next_visible_item)
2821
+
2822
+ # Collect items to move (current item + children if requested)
2823
+ items_to_move = [current_item]
2666
2824
  if with_children
2667
- # Collect all children
2668
- level = item["level"]
2669
- ((real_idx + 1)...@items.length).each do |i|
2825
+ level = current_item["level"]
2826
+ ((current_real_idx + 1)...@items.length).each do |i|
2670
2827
  if @items[i]["level"] > level
2671
2828
  items_to_move << @items[i]
2672
- last_idx = i
2673
2829
  else
2674
2830
  break
2675
2831
  end
2676
2832
  end
2677
2833
  end
2678
-
2679
- # Find the next sibling at the same level
2680
- target_level = item["level"]
2681
- target_idx = nil
2682
- next_sibling_end = nil
2683
-
2684
- # Search forward for a sibling at the same level
2685
- ((last_idx + 1)...@items.length).each do |i|
2686
- if @items[i]["level"] == target_level
2687
- # Found next sibling, now find where it ends (including its children)
2688
- next_sibling_end = i
2689
- ((i + 1)...@items.length).each do |j|
2690
- if @items[j]["level"] > target_level
2691
- next_sibling_end = j
2692
- else
2693
- break
2694
- end
2695
- end
2696
- target_idx = next_sibling_end + 1
2697
- break
2698
- elsif @items[i]["level"] < target_level
2699
- # Hit a parent level, can't move down
2700
- return
2701
- end
2834
+
2835
+ # Target is immediately after the next visible item (not after its children)
2836
+ target_real_idx = next_real_idx + 1
2837
+
2838
+ # Check if there are orphaned children at the target position
2839
+ # If so, position before them to reunite
2840
+ orphaned_children = find_orphaned_children(current_item["text"], target_real_idx)
2841
+ if !orphaned_children.empty?
2842
+ target_real_idx = orphaned_children.first
2702
2843
  end
2703
-
2704
- # Can't move if no sibling found
2705
- return if target_idx.nil?
2706
-
2707
- # Remember the first item we're moving (to track it)
2708
- first_moved_item = items_to_move.first
2709
-
2710
- # Remove items from their current position
2711
- items_to_move.length.times { @items.delete_at(real_idx) }
2712
-
2713
- # Adjust target index since we removed items
2714
- target_idx -= items_to_move.length
2715
-
2716
- # Insert after the target sibling and its children
2844
+
2845
+ # Calculate what level the moved item should have at the target position
2846
+ new_level = calculate_level_for_position(target_real_idx)
2847
+ level_diff = new_level - items_to_move.first["level"]
2848
+
2849
+ # Unfold destination area BEFORE moving if needed
2850
+ ensure_destination_visible(target_real_idx, new_level)
2851
+
2852
+ # Remove the items to move
2853
+ items_to_move.length.times { @items.delete_at(current_real_idx) }
2854
+
2855
+ # Adjust target position since we removed items before it
2856
+ adjusted_target = target_real_idx - items_to_move.length
2857
+
2858
+ # Adjust levels of moved items
2859
+ items_to_move.each do |moved_item|
2860
+ moved_item["level"] += level_diff
2861
+ moved_item["level"] = [moved_item["level"], 0].max # Ensure level doesn't go negative
2862
+ end
2863
+
2864
+ # Insert items at target position
2717
2865
  items_to_move.each_with_index do |item_to_move, idx|
2718
- @items.insert(target_idx + idx, item_to_move)
2866
+ @items.insert(adjusted_target + idx, item_to_move)
2719
2867
  end
2720
-
2721
- # Update cursor position to follow the moved item
2722
- # The moved item is now at position target_idx in @items
2723
- # Find this position in the visible list
2868
+
2869
+ # FORCE the moved item to be visible (safety net)
2870
+ force_item_visible(adjusted_target)
2871
+
2872
+ # Update cursor to follow the moved item
2724
2873
  new_visible = get_visible_items
2725
- new_item_idx = new_visible.find_index { |v| v["_real_index"] == target_idx }
2874
+ new_item_idx = new_visible.find_index { |v| get_real_index(v) == adjusted_target }
2726
2875
  @current = new_item_idx if new_item_idx
2727
-
2728
- # Renumber siblings at the moved item's level
2729
- renumber_siblings(first_moved_item["level"])
2730
-
2876
+
2731
2877
  @modified = true
2732
2878
  @message = "Moved #{items_to_move.length} item(s) down"
2733
2879
  record_last_action(:move_item_down, with_children)
@@ -2876,7 +3022,35 @@ class HyperListApp
2876
3022
  @modified = true
2877
3023
  record_last_action(:toggle_checkbox_with_date, nil)
2878
3024
  end
2879
-
3025
+
3026
+ def remove_checkbox
3027
+ visible = get_visible_items
3028
+ return if @current >= visible.length
3029
+
3030
+ save_undo_state # Save state before modification
3031
+
3032
+ item = visible[@current]
3033
+ real_idx = get_real_index(item)
3034
+ text = @items[real_idx]["text"]
3035
+
3036
+ # Remove checkbox patterns: [ ], [X], [x], [O], [-], [_]
3037
+ if text =~ /^\[[ X_Ox-]\]/
3038
+ # Remove the checkbox and any following space
3039
+ text = text.sub(/^\[[ X_Ox-]\]\s*/, '')
3040
+
3041
+ # Also remove any timestamp that might be associated with checkbox
3042
+ # Format: YYYY-MM-DD HH.MM: or similar
3043
+ text = text.sub(/^\d{4}-\d{2}-\d{2} \d{2}\.\d{2}:\s*/, '')
3044
+
3045
+ @items[real_idx]["text"] = text
3046
+ @modified = true
3047
+ @message = "Removed checkbox"
3048
+ record_last_action(:remove_checkbox, nil)
3049
+ else
3050
+ @message = "No checkbox to remove"
3051
+ end
3052
+ end
3053
+
2880
3054
  def search_forward
2881
3055
  @mode = :search
2882
3056
  @search = @footer.ask("Search: ", @search || "")
@@ -3013,12 +3187,13 @@ class HyperListApp
3013
3187
  help_lines << help_line("#{"u".fg("10")}", "Undo", "#{".".fg("10")}", "Repeat last action")
3014
3188
  help_lines << help_line("#{"r".fg("10")}" + ", ".fg("10") + "#{"C-R".fg("10")}", "Redo")
3015
3189
  help_lines << help_line("#{"S-UP".fg("10")}", "Move item up", "#{"S-DOWN".fg("10")}", "Move item down")
3016
- help_lines << help_line("#{"C-UP".fg("10")}", "Move item&descendants up", "#{"C-DOWN".fg("10")}", "Move item&descendants down")
3190
+ help_lines << help_line("#{"C-UP".fg("10")}", "Move up in visible list", "#{"C-DOWN".fg("10")}", "Move down in visible list")
3191
+ help_lines << help_line("#{"→".fg("10")}", "Uncollapse item", "#{"←".fg("10")}", "Collapse item")
3017
3192
  help_lines << help_line("#{"Tab".fg("10")}", "Indent item+kids", "#{"S-Tab".fg("10")}", "Unindent item+kids")
3018
- help_lines << help_line("#{"→".fg("10")}", "Indent item only", "#{"←".fg("10")}", "Unindent item only")
3019
3193
  help_lines << ""
3020
3194
  help_lines << "#{"SPECIAL FEATURES".fg("14")}"
3021
3195
  help_lines << help_line("#{"v".fg("10")}", "Toggle checkbox", "#{"V".fg("10")}", "Checkbox with date")
3196
+ help_lines << help_line("#{"C-X".fg("10")}", "Remove checkbox", "", "")
3022
3197
  help_lines << help_line("#{"C-E".fg("10")}", "Encrypt/decrypt line", "#{"C-U".fg("10")}", "Toggle State/Trans underline")
3023
3198
  help_lines << help_line("#{"P".fg("10")}", "Presentation mode", "#{"Tab/S-Tab".fg("10")}", "Next/prev sibling (in P)")
3024
3199
  help_lines << help_line("#{"Ma".fg("10")}", "Record macro 'a'", "#{"@a".fg("10")}", "Play macro 'a'")
@@ -4894,34 +5069,30 @@ class HyperListApp
4894
5069
  toggle_checkbox
4895
5070
  when "V"
4896
5071
  toggle_checkbox_with_date
5072
+ when "C-X" # Ctrl-Shift-X for remove checkbox
5073
+ remove_checkbox
4897
5074
  when "TAB"
4898
- if @split_view && @active_pane == :split
4899
- indent_split_right(true) # with children
4900
- else
4901
- indent_right(true) # with children
4902
- end
4903
- when "S-TAB"
4904
- if @split_view && @active_pane == :split
4905
- indent_split_left(true) # with children
4906
- else
4907
- indent_left(true) # with children
4908
- end
4909
- when "RIGHT"
4910
- # Move collapsed trees as a unit, but only the item if uncollapsed
5075
+ # Indent with move conditions (moved from RIGHT key)
4911
5076
  should_move_children = should_move_with_children?
4912
5077
  if @split_view && @active_pane == :split
4913
5078
  indent_split_right(should_move_children)
4914
5079
  else
4915
5080
  indent_right(should_move_children)
4916
5081
  end
4917
- when "LEFT"
4918
- # Move collapsed trees as a unit, but only the item if uncollapsed
5082
+ when "S-TAB"
5083
+ # Unindent with move conditions (moved from LEFT key)
4919
5084
  should_move_children = should_move_with_children?
4920
5085
  if @split_view && @active_pane == :split
4921
5086
  indent_split_left(should_move_children)
4922
5087
  else
4923
5088
  indent_left(should_move_children)
4924
5089
  end
5090
+ when "RIGHT"
5091
+ # Uncollapse item if it is collapsed
5092
+ uncollapse_item
5093
+ when "LEFT"
5094
+ # Collapse item if it has children
5095
+ collapse_item
4925
5096
  when " "
4926
5097
  toggle_fold
4927
5098
  when "u"
@@ -5709,21 +5880,11 @@ class HyperListApp
5709
5880
  when "l"
5710
5881
  go_to_first_child
5711
5882
  when "LEFT"
5712
- # Move collapsed trees as a unit, but only the item if uncollapsed
5713
- should_move_children = should_move_with_children?
5714
- if @split_view && @active_pane == :split
5715
- indent_split_left(should_move_children)
5716
- else
5717
- indent_left(should_move_children)
5718
- end
5883
+ # Collapse item if it has children
5884
+ collapse_item
5719
5885
  when "RIGHT"
5720
- # Move collapsed trees as a unit, but only the item if uncollapsed
5721
- should_move_children = should_move_with_children?
5722
- if @split_view && @active_pane == :split
5723
- indent_split_right(should_move_children)
5724
- else
5725
- indent_right(should_move_children)
5726
- end
5886
+ # Uncollapse item if it is collapsed
5887
+ uncollapse_item
5727
5888
  when "PgUP" # Page Up
5728
5889
  page_up
5729
5890
  when "PgDOWN" # Page Down
@@ -5857,22 +6018,23 @@ class HyperListApp
5857
6018
  move_item_up(false)
5858
6019
  when "S-DOWN" # Move item down
5859
6020
  move_item_down(false)
5860
- when "C-UP" # Move item and descendants up
5861
- move_item_up(true)
5862
- when "C-DOWN" # Move item and descendants down
5863
- move_item_down(true)
5864
- when "C-K" # Alternative: Move item and descendants up (for terminals that intercept C-UP)
5865
- move_item_up(true)
6021
+ when "C-UP" # Move up in visible list (with children only if collapsed)
6022
+ move_item_up(should_move_with_children?)
6023
+ when "C-DOWN" # Move down in visible list (with children only if collapsed)
6024
+ move_item_down(should_move_with_children?)
6025
+ when "C-K" # Alternative: Move up in visible list (for terminals that intercept C-UP)
6026
+ move_item_up(should_move_with_children?)
5866
6027
  when "TAB"
5867
6028
  if @presentation_mode
5868
6029
  # In presentation mode, Tab goes to next sibling
5869
6030
  jump_to_next_sibling
5870
6031
  else
5871
- # Normal mode: Indent with all children
6032
+ # Normal mode: Indent with move conditions (moved from RIGHT key)
6033
+ should_move_children = should_move_with_children?
5872
6034
  if @split_view && @active_pane == :split
5873
- indent_split_right(true)
6035
+ indent_split_right(should_move_children)
5874
6036
  else
5875
- indent_right(true)
6037
+ indent_right(should_move_children)
5876
6038
  end
5877
6039
  end
5878
6040
  when "S-TAB" # Shift-Tab
@@ -5880,11 +6042,12 @@ class HyperListApp
5880
6042
  # In presentation mode, Shift-Tab goes to previous sibling
5881
6043
  jump_to_prev_sibling
5882
6044
  else
5883
- # Normal mode: Unindent with all children
6045
+ # Normal mode: Unindent with move conditions (moved from LEFT key)
6046
+ should_move_children = should_move_with_children?
5884
6047
  if @split_view && @active_pane == :split
5885
- indent_split_left(true)
6048
+ indent_split_left(should_move_children)
5886
6049
  else
5887
- indent_left(true)
6050
+ indent_left(should_move_children)
5888
6051
  end
5889
6052
  end
5890
6053
  when "u"
@@ -5895,6 +6058,8 @@ class HyperListApp
5895
6058
  toggle_checkbox
5896
6059
  when "V"
5897
6060
  toggle_checkbox_with_date
6061
+ when "C-X" # Ctrl-Shift-X for remove checkbox
6062
+ remove_checkbox
5898
6063
  when "."
5899
6064
  repeat_last_action
5900
6065
  when "/"
data/hyperlist.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |spec|
2
2
  spec.name = "hyperlist"
3
- spec.version = "1.5.2"
3
+ spec.version = "1.7.0"
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.5.2
4
+ version: 1.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Geir Isene
8
8
  autorequire:
9
9
  bindir: "."
10
10
  cert_chain: []
11
- date: 2025-09-18 00:00:00.000000000 Z
11
+ date: 2025-09-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rcurses