hyperlist 1.8.1 → 1.9.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.
- checksums.yaml +4 -4
- data/README.md +43 -4
- data/hyperlist +424 -122
- data/hyperlist.gemspec +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1f638bfa971829555f7f078ed1c2e5a498a3e0d4026dea67fd547bc881fdec61
|
|
4
|
+
data.tar.gz: 3cbb6c08d664e5ed7111384ef275293c9616c8c213f3cc189beaf1e3054fbfac
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a71a5cfa04c8e52652f990d970f6fc7a1e65a5a89a9869b558937a016ae7c8e2aa0017910a23941158abbbfb6c8d92a2f0b8d50075cb3cb303d06c04f62c876d
|
|
7
|
+
data.tar.gz: cccf1614ea64a35296d0de28b35d3ca3098fc31d68296cb271ef93ca5e085ffc2801fac04b98df96c72340b00adb39003fbcb55651f5561fe7e3a36ecf63a9e6
|
data/README.md
CHANGED
|
@@ -29,7 +29,36 @@ For historical context and the original VIM implementation, see: [hyperlist.vim]
|
|
|
29
29
|
### Help Screen
|
|
30
30
|

|
|
31
31
|
|
|
32
|
-
## What's New in v1.
|
|
32
|
+
## What's New in v1.9.0
|
|
33
|
+
|
|
34
|
+
### 🏷️ Item Tagging & Batch Operations
|
|
35
|
+
- **Tag items**: Press 't' to tag/untag items for batch operations
|
|
36
|
+
- **Auto-advance**: Cursor automatically moves to next item after tagging for fast consecutive tagging
|
|
37
|
+
- **Visual feedback**: Tagged items show dark blue background, lighter blue when selected
|
|
38
|
+
- **Status indicator**: Shows `[T:N]` in status line with count of tagged items
|
|
39
|
+
- **Clear tags**: Press 'u' to clear all tags
|
|
40
|
+
- **Regex tagging**: Press 'C-T' to tag all items matching a regex pattern
|
|
41
|
+
- **Batch operations**: Delete (D/C-D), yank (y/Y), and indent (Tab/S-Tab) work on all tagged items
|
|
42
|
+
- Tag consecutive or non-consecutive items, then perform operations on the entire set
|
|
43
|
+
|
|
44
|
+
### ✏️ External Editor Support
|
|
45
|
+
- **Edit in $EDITOR**: Press 'E' to spawn your preferred editor (vim, nano, emacs, etc.)
|
|
46
|
+
- **Seamless workflow**: File saved automatically, editor launched, changes reloaded on exit
|
|
47
|
+
- **Terminal management**: Terminal state properly saved and restored
|
|
48
|
+
- Uses `$EDITOR` environment variable (defaults to vi if not set)
|
|
49
|
+
|
|
50
|
+
### 📋 Enhanced Paste & Navigation
|
|
51
|
+
- **Paste above**: Press 'P' to paste above current item (vim-style)
|
|
52
|
+
- **Paste below**: Press 'p' to paste below current item (existing)
|
|
53
|
+
- **Presentation mode**: Moved to 'C-P' (was 'P') for consistency
|
|
54
|
+
- **Templates**: Moved to 'T' key (was 't')
|
|
55
|
+
- **Undo**: Moved to 'U' key (was 'u') - consistent with RTFM
|
|
56
|
+
|
|
57
|
+
### 🎯 Smart Modified Flag
|
|
58
|
+
- **Intelligent tracking**: `[+]` indicator automatically removed when undoing back to original file state
|
|
59
|
+
- **Clean status**: No false "modified" indicator after complete undo to original
|
|
60
|
+
|
|
61
|
+
## Previous Release: v1.8.0
|
|
33
62
|
|
|
34
63
|
### 📋 Multi-Line Paste Support
|
|
35
64
|
- **Paste multiple lines**: When pasting multi-line content into item insertion prompts ('o', 'O', 'a', 'A'), each line becomes a separate item
|
|
@@ -224,7 +253,17 @@ hyperlist file.txt # Open any text file
|
|
|
224
253
|
- `D` - Delete and yank line
|
|
225
254
|
- `C-D` - Delete and yank item with descendants
|
|
226
255
|
- `y/Y` - Copy line/tree
|
|
227
|
-
- `p` - Paste
|
|
256
|
+
- `p` - Paste below
|
|
257
|
+
- `P` - Paste above
|
|
258
|
+
- `U` - Undo
|
|
259
|
+
- `r` or `C-R` - Redo
|
|
260
|
+
- `E` - Edit in $EDITOR
|
|
261
|
+
|
|
262
|
+
#### Tagging & Batch Operations
|
|
263
|
+
- `t` - Tag/untag current item
|
|
264
|
+
- `u` - Clear all tags
|
|
265
|
+
- `C-T` - Tag items matching regex pattern
|
|
266
|
+
- Operations (D/C-D, y/Y, Tab/S-Tab) work on all tagged items when tags exist
|
|
228
267
|
|
|
229
268
|
#### Folding
|
|
230
269
|
- `Space` - Toggle fold
|
|
@@ -238,8 +277,8 @@ hyperlist file.txt # Open any text file
|
|
|
238
277
|
- `C-E` - Encrypt/decrypt current line
|
|
239
278
|
- `R` - Go to reference
|
|
240
279
|
- `F` - Open file reference
|
|
241
|
-
- `P` - Presentation mode (with auto-collapse)
|
|
242
|
-
- `
|
|
280
|
+
- `C-P` - Presentation mode (with auto-collapse)
|
|
281
|
+
- `T` - Insert template (built-in or custom)
|
|
243
282
|
- `?` - Help screen
|
|
244
283
|
|
|
245
284
|
#### File Commands
|
data/hyperlist
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
# Check for help/version BEFORE loading any libraries
|
|
8
8
|
if ARGV[0] == '-h' || ARGV[0] == '--help'
|
|
9
9
|
puts <<~HELP
|
|
10
|
-
HyperList v1.
|
|
10
|
+
HyperList v1.9.0 - 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.
|
|
55
|
+
puts "HyperList v1.9.0"
|
|
56
56
|
exit 0
|
|
57
57
|
end
|
|
58
58
|
|
|
@@ -66,13 +66,14 @@ require 'digest'
|
|
|
66
66
|
require 'base64'
|
|
67
67
|
require 'fileutils'
|
|
68
68
|
require 'json'
|
|
69
|
+
require 'shellwords'
|
|
69
70
|
|
|
70
71
|
class HyperListApp
|
|
71
72
|
include Rcurses
|
|
72
73
|
include Rcurses::Input
|
|
73
74
|
include Rcurses::Cursor
|
|
74
75
|
|
|
75
|
-
VERSION = "1.
|
|
76
|
+
VERSION = "1.9.0"
|
|
76
77
|
|
|
77
78
|
def initialize(filename = nil)
|
|
78
79
|
@filename = filename ? File.expand_path(filename) : nil
|
|
@@ -91,6 +92,8 @@ class HyperListApp
|
|
|
91
92
|
@show_numbers = false # Line numbers disabled by default
|
|
92
93
|
@command_history = load_command_history # Command history for : commands
|
|
93
94
|
@clipboard = nil
|
|
95
|
+
@tagged_items = [] # Items tagged for batch operations
|
|
96
|
+
@original_items = nil # Track original file state for smart modified flag
|
|
94
97
|
@undo_stack = []
|
|
95
98
|
@undo_position = [] # Stack of cursor positions for undo
|
|
96
99
|
@redo_stack = []
|
|
@@ -223,7 +226,8 @@ class HyperListApp
|
|
|
223
226
|
@items = []
|
|
224
227
|
@encrypted_lines = {}
|
|
225
228
|
@config_line = nil # Reset config line before loading
|
|
226
|
-
|
|
229
|
+
@tagged_items = [] # Clear tags when loading new file
|
|
230
|
+
|
|
227
231
|
# Read file content
|
|
228
232
|
content = File.read(file) rescue ""
|
|
229
233
|
|
|
@@ -354,6 +358,9 @@ class HyperListApp
|
|
|
354
358
|
|
|
355
359
|
# Update recent files list
|
|
356
360
|
add_to_recent_files(File.expand_path(file)) if file
|
|
361
|
+
|
|
362
|
+
# Save original state for smart modified flag
|
|
363
|
+
@original_items = @items.map { |item| item.dup }
|
|
357
364
|
end
|
|
358
365
|
|
|
359
366
|
def auto_fold_deep_levels(max_level)
|
|
@@ -703,6 +710,7 @@ class HyperListApp
|
|
|
703
710
|
end
|
|
704
711
|
|
|
705
712
|
@modified = false
|
|
713
|
+
@original_items = @items.map { |item| item.dup } # Save state after save
|
|
706
714
|
@last_auto_save = Time.now if @auto_save_enabled
|
|
707
715
|
end
|
|
708
716
|
|
|
@@ -1297,20 +1305,31 @@ class HyperListApp
|
|
|
1297
1305
|
end
|
|
1298
1306
|
end
|
|
1299
1307
|
|
|
1300
|
-
# Apply
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1308
|
+
# Apply background highlighting based on tagged/selected state
|
|
1309
|
+
real_idx = get_real_index(item)
|
|
1310
|
+
is_tagged = real_idx && @tagged_items.include?(real_idx)
|
|
1311
|
+
is_selected = idx == @current
|
|
1312
|
+
|
|
1313
|
+
bg_color = nil
|
|
1314
|
+
if is_tagged && is_selected
|
|
1315
|
+
# Tagged AND selected - lighter blue
|
|
1316
|
+
bg_color = "21" unless @presentation_mode && is_item_in_presentation_focus?(item)
|
|
1317
|
+
elsif is_tagged
|
|
1318
|
+
# Only tagged - dark blue
|
|
1319
|
+
bg_color = "17"
|
|
1320
|
+
elsif is_selected
|
|
1321
|
+
# Only selected - normal gray
|
|
1322
|
+
unless @presentation_mode && is_item_in_presentation_focus?(item)
|
|
1304
1323
|
bg_color = (!@split_view || @active_pane == :main) ? "237" : "234"
|
|
1305
|
-
if bg_color
|
|
1306
|
-
# Pad line to full width and apply background
|
|
1307
|
-
padded_line = line + " " * [@cols - line.pure.length, 0].max
|
|
1308
|
-
bg_code = "\e[48;5;#{bg_color}m"
|
|
1309
|
-
reset_bg = "\e[49m"
|
|
1310
|
-
line = bg_code + padded_line.gsub(/\e\[49m/, '') + reset_bg
|
|
1311
|
-
end
|
|
1312
1324
|
end
|
|
1313
1325
|
end
|
|
1326
|
+
|
|
1327
|
+
if bg_color
|
|
1328
|
+
padded_line = line + " " * [@cols - line.pure.length, 0].max
|
|
1329
|
+
bg_code = "\e[48;5;#{bg_color}m"
|
|
1330
|
+
reset_bg = "\e[49m"
|
|
1331
|
+
line = bg_code + padded_line.gsub(/\e\[49m/, '') + reset_bg
|
|
1332
|
+
end
|
|
1314
1333
|
|
|
1315
1334
|
lines << line
|
|
1316
1335
|
end
|
|
@@ -1679,16 +1698,19 @@ class HyperListApp
|
|
|
1679
1698
|
|
|
1680
1699
|
# Auto-save indicator
|
|
1681
1700
|
auto_save_indicator = @auto_save_enabled ? "[A]" : ""
|
|
1682
|
-
|
|
1683
|
-
# Split view indicator
|
|
1701
|
+
|
|
1702
|
+
# Split view indicator
|
|
1684
1703
|
split_indicator = @split_view ? "[#{@active_pane.upcase}]" : ""
|
|
1685
|
-
|
|
1704
|
+
|
|
1705
|
+
# Tagged items indicator
|
|
1706
|
+
tagged_indicator = @tagged_items.any? ? "[T:#{@tagged_items.length}]" : ""
|
|
1707
|
+
|
|
1686
1708
|
# Build status line components
|
|
1687
1709
|
# Use full path with ~ for home directory
|
|
1688
1710
|
full_path = @filename ? @filename.gsub(ENV['HOME'], '~') : "New HyperList"
|
|
1689
1711
|
file_part = "#{full_path}#{modified_indicator}"
|
|
1690
1712
|
stats_part = "L#{pos} W:#{word_count}"
|
|
1691
|
-
indicators = "#{auto_save_indicator}#{split_indicator}"
|
|
1713
|
+
indicators = "#{auto_save_indicator}#{split_indicator}#{tagged_indicator}"
|
|
1692
1714
|
right_side = "? help #{version_text}"
|
|
1693
1715
|
|
|
1694
1716
|
# Combine left elements
|
|
@@ -2426,9 +2448,24 @@ class HyperListApp
|
|
|
2426
2448
|
|
|
2427
2449
|
# Clear cache since items changed
|
|
2428
2450
|
clear_cache
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
@
|
|
2451
|
+
|
|
2452
|
+
# Check if we've returned to original state
|
|
2453
|
+
if @original_items && items_match?(@items, @original_items)
|
|
2454
|
+
@modified = false
|
|
2455
|
+
@message = "Undone to original state (#{@undo_stack.length} undo levels, #{@redo_stack.length} redo available)"
|
|
2456
|
+
else
|
|
2457
|
+
@modified = true
|
|
2458
|
+
@message = "Undone (#{@undo_stack.length} undo levels, #{@redo_stack.length} redo available)"
|
|
2459
|
+
end
|
|
2460
|
+
end
|
|
2461
|
+
|
|
2462
|
+
def items_match?(items1, items2)
|
|
2463
|
+
return false if items1.length != items2.length
|
|
2464
|
+
items1.each_with_index do |item, idx|
|
|
2465
|
+
return false if item["text"] != items2[idx]["text"] ||
|
|
2466
|
+
item["level"] != items2[idx]["level"]
|
|
2467
|
+
end
|
|
2468
|
+
true
|
|
2432
2469
|
end
|
|
2433
2470
|
|
|
2434
2471
|
def record_last_action(type, data = nil)
|
|
@@ -2762,55 +2799,86 @@ class HyperListApp
|
|
|
2762
2799
|
visible = get_visible_items
|
|
2763
2800
|
return if visible.empty?
|
|
2764
2801
|
return if @current >= visible.length
|
|
2765
|
-
|
|
2802
|
+
|
|
2766
2803
|
save_undo_state # Save state before modification
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2804
|
+
|
|
2805
|
+
# Handle tagged items
|
|
2806
|
+
if @tagged_items.any?
|
|
2807
|
+
@clipboard = []
|
|
2808
|
+
@clipboard_is_tree = false
|
|
2809
|
+
|
|
2810
|
+
# Sort tagged indices in reverse to delete from end to start
|
|
2811
|
+
@tagged_items.sort.reverse.each do |real_idx|
|
|
2812
|
+
next if real_idx >= @items.length
|
|
2813
|
+
@clipboard.unshift(@items[real_idx].dup)
|
|
2814
|
+
if with_children
|
|
2815
|
+
# Delete item and children
|
|
2816
|
+
level = @items[real_idx]["level"]
|
|
2817
|
+
delete_count = 1
|
|
2818
|
+
((real_idx + 1)...@items.length).each do |i|
|
|
2819
|
+
if @items[i]["level"] > level
|
|
2820
|
+
@clipboard << @items[i].dup
|
|
2821
|
+
delete_count += 1
|
|
2822
|
+
else
|
|
2823
|
+
break
|
|
2824
|
+
end
|
|
2825
|
+
end
|
|
2826
|
+
delete_count.times { @items.delete_at(real_idx) }
|
|
2786
2827
|
else
|
|
2787
|
-
|
|
2828
|
+
@items.delete_at(real_idx)
|
|
2788
2829
|
end
|
|
2789
2830
|
end
|
|
2831
|
+
|
|
2832
|
+
@message = "Deleted and yanked #{@tagged_items.length} tagged item(s)"
|
|
2833
|
+
@tagged_items = [] # Clear tags after operation
|
|
2790
2834
|
else
|
|
2791
|
-
#
|
|
2792
|
-
|
|
2793
|
-
|
|
2835
|
+
# Normal single item delete
|
|
2836
|
+
item = visible[@current]
|
|
2837
|
+
real_idx = get_real_index(item)
|
|
2838
|
+
|
|
2839
|
+
# First, yank the item(s) to clipboard
|
|
2840
|
+
@clipboard = []
|
|
2841
|
+
@clipboard << item.dup
|
|
2842
|
+
|
|
2843
|
+
# Determine what to delete
|
|
2844
|
+
level = item["level"]
|
|
2845
|
+
delete_count = 1
|
|
2846
|
+
|
|
2847
|
+
if with_children
|
|
2848
|
+
# Delete item and its children (C-D was used)
|
|
2849
|
+
@clipboard_is_tree = true # Mark as tree for paste behavior
|
|
2850
|
+
((real_idx + 1)...@items.length).each do |i|
|
|
2851
|
+
if @items[i]["level"] > level
|
|
2852
|
+
@clipboard << @items[i].dup # Also add children to clipboard
|
|
2853
|
+
delete_count += 1
|
|
2854
|
+
else
|
|
2855
|
+
break
|
|
2856
|
+
end
|
|
2857
|
+
end
|
|
2858
|
+
else
|
|
2859
|
+
# For single line delete (D key - delete only the current line)
|
|
2860
|
+
@clipboard_is_tree = false # Mark as single/adaptive for paste behavior
|
|
2861
|
+
# Don't include children - delete_count stays at 1
|
|
2862
|
+
end
|
|
2863
|
+
|
|
2864
|
+
# Delete the items
|
|
2865
|
+
# Remember the level of the deleted item for renumbering
|
|
2866
|
+
deleted_level = item["level"]
|
|
2867
|
+
|
|
2868
|
+
delete_count.times { @items.delete_at(real_idx) }
|
|
2869
|
+
|
|
2870
|
+
# Renumber siblings at the deleted item's level
|
|
2871
|
+
renumber_siblings(deleted_level) unless @items.length == 1
|
|
2872
|
+
|
|
2873
|
+
@message = "Deleted and yanked #{@clipboard.length} item(s)"
|
|
2794
2874
|
end
|
|
2795
|
-
|
|
2796
|
-
# Delete the items
|
|
2797
|
-
# Remember the level of the deleted item for renumbering
|
|
2798
|
-
deleted_level = item["level"]
|
|
2799
|
-
|
|
2800
|
-
delete_count.times { @items.delete_at(real_idx) }
|
|
2801
|
-
|
|
2875
|
+
|
|
2802
2876
|
@items = [{"text" => "Empty", "level" => 0, "fold" => false}] if @items.empty?
|
|
2803
|
-
|
|
2804
|
-
# Renumber siblings at the deleted item's level
|
|
2805
|
-
renumber_siblings(deleted_level) unless @items.length == 1
|
|
2806
|
-
|
|
2877
|
+
|
|
2807
2878
|
@current = [@current, get_visible_items.length - 1].min
|
|
2808
2879
|
@current = 0 if @current < 0
|
|
2809
2880
|
@modified = true
|
|
2810
|
-
|
|
2811
|
-
# Show message
|
|
2812
|
-
@message = "Deleted and yanked #{@clipboard.length} item(s)"
|
|
2813
|
-
|
|
2881
|
+
|
|
2814
2882
|
record_last_action(:delete_line, with_children)
|
|
2815
2883
|
end
|
|
2816
2884
|
|
|
@@ -2818,23 +2886,49 @@ class HyperListApp
|
|
|
2818
2886
|
visible = get_visible_items
|
|
2819
2887
|
return if @current >= visible.length
|
|
2820
2888
|
|
|
2821
|
-
item = visible[@current]
|
|
2822
|
-
real_idx = get_real_index(item)
|
|
2823
|
-
|
|
2824
2889
|
@clipboard = []
|
|
2825
|
-
@
|
|
2826
|
-
|
|
2890
|
+
@clipboard_is_tree = with_children
|
|
2891
|
+
|
|
2892
|
+
# Handle tagged items
|
|
2893
|
+
if @tagged_items.any?
|
|
2894
|
+
@tagged_items.sort.each do |real_idx|
|
|
2895
|
+
next if real_idx >= @items.length
|
|
2896
|
+
@clipboard << @items[real_idx].dup
|
|
2897
|
+
if with_children
|
|
2898
|
+
# Copy children too
|
|
2899
|
+
level = @items[real_idx]["level"]
|
|
2900
|
+
((real_idx + 1)...@items.length).each do |i|
|
|
2901
|
+
if @items[i]["level"] > level
|
|
2902
|
+
@clipboard << @items[i].dup
|
|
2903
|
+
else
|
|
2904
|
+
break
|
|
2905
|
+
end
|
|
2906
|
+
end
|
|
2907
|
+
end
|
|
2908
|
+
end
|
|
2827
2909
|
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2910
|
+
@message = "Yanked #{@tagged_items.length} tagged item(s)"
|
|
2911
|
+
@tagged_items = [] # Clear tags after operation
|
|
2912
|
+
else
|
|
2913
|
+
# Normal single item yank
|
|
2914
|
+
item = visible[@current]
|
|
2915
|
+
real_idx = get_real_index(item)
|
|
2916
|
+
|
|
2917
|
+
@clipboard << item.dup
|
|
2918
|
+
|
|
2919
|
+
# Copy children if requested
|
|
2920
|
+
if with_children
|
|
2921
|
+
level = item["level"]
|
|
2922
|
+
((real_idx + 1)...@items.length).each do |i|
|
|
2923
|
+
if @items[i]["level"] > level
|
|
2924
|
+
@clipboard << @items[i].dup
|
|
2925
|
+
else
|
|
2926
|
+
break
|
|
2927
|
+
end
|
|
2836
2928
|
end
|
|
2837
2929
|
end
|
|
2930
|
+
|
|
2931
|
+
@message = "Yanked #{@clipboard.length} item(s)"
|
|
2838
2932
|
end
|
|
2839
2933
|
|
|
2840
2934
|
# Copy to system clipboard for middle-click paste
|
|
@@ -2845,7 +2939,6 @@ class HyperListApp
|
|
|
2845
2939
|
# Silently fail if clipboard gem not available
|
|
2846
2940
|
end
|
|
2847
2941
|
|
|
2848
|
-
@message = "Yanked #{@clipboard.length} item(s)"
|
|
2849
2942
|
record_last_action(:yank_line, with_children)
|
|
2850
2943
|
end
|
|
2851
2944
|
|
|
@@ -2888,6 +2981,137 @@ class HyperListApp
|
|
|
2888
2981
|
record_last_action(:paste, nil)
|
|
2889
2982
|
end
|
|
2890
2983
|
|
|
2984
|
+
def paste_above
|
|
2985
|
+
return unless @clipboard && !@clipboard.empty?
|
|
2986
|
+
|
|
2987
|
+
save_undo_state # Save state before modification
|
|
2988
|
+
|
|
2989
|
+
visible = get_visible_items
|
|
2990
|
+
if @current < visible.length
|
|
2991
|
+
real_idx = get_real_index(visible[@current])
|
|
2992
|
+
|
|
2993
|
+
if @clipboard_is_tree
|
|
2994
|
+
# For tree paste (C-D or Y), maintain original indentation structure
|
|
2995
|
+
@clipboard.reverse.each do |item|
|
|
2996
|
+
new_item = item.dup
|
|
2997
|
+
@items.insert(real_idx, new_item)
|
|
2998
|
+
end
|
|
2999
|
+
else
|
|
3000
|
+
# For single/adaptive paste (D or y), adjust to match context
|
|
3001
|
+
base_level = visible[@current]["level"]
|
|
3002
|
+
level_diff = base_level - @clipboard[0]["level"]
|
|
3003
|
+
|
|
3004
|
+
@clipboard.reverse.each do |item|
|
|
3005
|
+
new_item = item.dup
|
|
3006
|
+
new_item["level"] = item["level"] + level_diff
|
|
3007
|
+
@items.insert(real_idx, new_item)
|
|
3008
|
+
end
|
|
3009
|
+
end
|
|
3010
|
+
else
|
|
3011
|
+
# Pasting at end of list - maintain original levels
|
|
3012
|
+
@clipboard.each do |item|
|
|
3013
|
+
@items << item.dup
|
|
3014
|
+
end
|
|
3015
|
+
end
|
|
3016
|
+
|
|
3017
|
+
@modified = true
|
|
3018
|
+
@message = "Pasted #{@clipboard.length} item(s) above"
|
|
3019
|
+
record_last_action(:paste_above, nil)
|
|
3020
|
+
end
|
|
3021
|
+
|
|
3022
|
+
def toggle_tag
|
|
3023
|
+
visible = get_visible_items
|
|
3024
|
+
return if @current >= visible.length
|
|
3025
|
+
|
|
3026
|
+
real_idx = get_real_index(visible[@current])
|
|
3027
|
+
|
|
3028
|
+
if @tagged_items.include?(real_idx)
|
|
3029
|
+
@tagged_items.delete(real_idx)
|
|
3030
|
+
@message = "Untagged item (#{@tagged_items.length} tagged)"
|
|
3031
|
+
else
|
|
3032
|
+
@tagged_items << real_idx
|
|
3033
|
+
@message = "Tagged item (#{@tagged_items.length} tagged)"
|
|
3034
|
+
# Auto-advance cursor for easy consecutive tagging
|
|
3035
|
+
@current += 1 if @current < visible.length - 1
|
|
3036
|
+
end
|
|
3037
|
+
end
|
|
3038
|
+
|
|
3039
|
+
def clear_tags
|
|
3040
|
+
count = @tagged_items.length
|
|
3041
|
+
@tagged_items = []
|
|
3042
|
+
@message = "Cleared #{count} tagged item(s)"
|
|
3043
|
+
end
|
|
3044
|
+
|
|
3045
|
+
def tag_by_regex
|
|
3046
|
+
pattern = @footer.ask("Tag pattern (regex): ", "")
|
|
3047
|
+
return if pattern.nil? || pattern.strip.empty?
|
|
3048
|
+
|
|
3049
|
+
begin
|
|
3050
|
+
regex = Regexp.new(pattern, Regexp::IGNORECASE)
|
|
3051
|
+
count = 0
|
|
3052
|
+
|
|
3053
|
+
@items.each_with_index do |item, idx|
|
|
3054
|
+
if item["text"] =~ regex
|
|
3055
|
+
@tagged_items << idx unless @tagged_items.include?(idx)
|
|
3056
|
+
count += 1
|
|
3057
|
+
end
|
|
3058
|
+
end
|
|
3059
|
+
|
|
3060
|
+
@tagged_items.uniq!
|
|
3061
|
+
@message = "Tagged #{count} item(s) matching /#{pattern}/"
|
|
3062
|
+
rescue RegexpError => e
|
|
3063
|
+
@message = "Invalid regex: #{e.message}"
|
|
3064
|
+
end
|
|
3065
|
+
end
|
|
3066
|
+
|
|
3067
|
+
def spawn_editor
|
|
3068
|
+
return unless @filename
|
|
3069
|
+
|
|
3070
|
+
# Save file before editing
|
|
3071
|
+
save_file
|
|
3072
|
+
|
|
3073
|
+
# Save terminal state
|
|
3074
|
+
system("stty -g < /dev/tty > /tmp/hyperlist_stty_$$")
|
|
3075
|
+
|
|
3076
|
+
# Reset terminal to cooked mode for editor
|
|
3077
|
+
system('stty sane < /dev/tty')
|
|
3078
|
+
system('clear < /dev/tty > /dev/tty')
|
|
3079
|
+
Rcurses::Cursor.show
|
|
3080
|
+
|
|
3081
|
+
# Launch editor
|
|
3082
|
+
editor = ENV.fetch('EDITOR', 'vi')
|
|
3083
|
+
system("#{editor} #{Shellwords.escape(@filename)}")
|
|
3084
|
+
|
|
3085
|
+
# Restore terminal state
|
|
3086
|
+
system("stty $(cat /tmp/hyperlist_stty_$$) < /dev/tty")
|
|
3087
|
+
system("rm -f /tmp/hyperlist_stty_$$")
|
|
3088
|
+
|
|
3089
|
+
# Flush input and reset stdin
|
|
3090
|
+
$stdin.iflush if $stdin.respond_to?(:iflush)
|
|
3091
|
+
system('stty raw -echo isig < /dev/tty')
|
|
3092
|
+
$stdin.raw!
|
|
3093
|
+
$stdin.echo = false
|
|
3094
|
+
|
|
3095
|
+
# Reinitialize rcurses
|
|
3096
|
+
Rcurses.init!
|
|
3097
|
+
Rcurses::Cursor.hide
|
|
3098
|
+
Rcurses.clear_screen
|
|
3099
|
+
|
|
3100
|
+
# Reload the file
|
|
3101
|
+
load_file(@filename)
|
|
3102
|
+
@current = 0
|
|
3103
|
+
@offset = 0
|
|
3104
|
+
|
|
3105
|
+
@message = "File reloaded from $EDITOR"
|
|
3106
|
+
|
|
3107
|
+
# Force complete screen refresh
|
|
3108
|
+
clear_cache
|
|
3109
|
+
@main.full_refresh if @main
|
|
3110
|
+
@footer.full_refresh if @footer
|
|
3111
|
+
@split_pane.full_refresh if @split_pane && @split_view
|
|
3112
|
+
render
|
|
3113
|
+
end
|
|
3114
|
+
|
|
2891
3115
|
def calculate_level_for_position(target_real_idx)
|
|
2892
3116
|
# Calculate appropriate level for an item at target_real_idx position
|
|
2893
3117
|
# based on surrounding items in the @items array
|
|
@@ -3152,64 +3376,122 @@ class HyperListApp
|
|
|
3152
3376
|
def indent_right(with_children = true)
|
|
3153
3377
|
visible = get_visible_items
|
|
3154
3378
|
return if @current >= visible.length
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3379
|
+
|
|
3380
|
+
save_undo_state # Save state before modification
|
|
3381
|
+
|
|
3382
|
+
# Handle tagged items
|
|
3383
|
+
if @tagged_items.any?
|
|
3384
|
+
@tagged_items.sort.each do |real_idx|
|
|
3385
|
+
next if real_idx >= @items.length
|
|
3386
|
+
next if real_idx == 0 # Can't indent first item
|
|
3387
|
+
|
|
3388
|
+
original_level = @items[real_idx]["level"]
|
|
3389
|
+
@items[real_idx]["level"] += 1
|
|
3390
|
+
|
|
3391
|
+
# Also indent children if requested
|
|
3392
|
+
if with_children
|
|
3393
|
+
((real_idx + 1)...@items.length).each do |i|
|
|
3394
|
+
if @items[i]["level"] > original_level
|
|
3395
|
+
@items[i]["level"] += 1
|
|
3396
|
+
else
|
|
3397
|
+
break
|
|
3398
|
+
end
|
|
3172
3399
|
end
|
|
3173
3400
|
end
|
|
3174
3401
|
end
|
|
3175
|
-
|
|
3176
|
-
# Ensure the moved item remains visible
|
|
3177
|
-
force_item_visible(real_idx)
|
|
3178
3402
|
|
|
3403
|
+
@message = "Indented #{@tagged_items.length} tagged item(s)"
|
|
3404
|
+
@tagged_items = [] # Clear tags after operation
|
|
3179
3405
|
@modified = true
|
|
3180
|
-
|
|
3406
|
+
else
|
|
3407
|
+
# Normal single item indent
|
|
3408
|
+
item = visible[@current]
|
|
3409
|
+
real_idx = get_real_index(item)
|
|
3410
|
+
|
|
3411
|
+
# Can only indent if there's a previous item at same or higher level
|
|
3412
|
+
if real_idx > 0
|
|
3413
|
+
original_level = @items[real_idx]["level"]
|
|
3414
|
+
@items[real_idx]["level"] += 1
|
|
3415
|
+
|
|
3416
|
+
# Also indent children if requested
|
|
3417
|
+
if with_children
|
|
3418
|
+
((real_idx + 1)...@items.length).each do |i|
|
|
3419
|
+
if @items[i]["level"] > original_level
|
|
3420
|
+
@items[i]["level"] += 1
|
|
3421
|
+
else
|
|
3422
|
+
break
|
|
3423
|
+
end
|
|
3424
|
+
end
|
|
3425
|
+
end
|
|
3426
|
+
|
|
3427
|
+
# Ensure the moved item remains visible
|
|
3428
|
+
force_item_visible(real_idx)
|
|
3429
|
+
|
|
3430
|
+
@modified = true
|
|
3431
|
+
end
|
|
3181
3432
|
end
|
|
3433
|
+
|
|
3434
|
+
record_last_action(:indent_right, with_children)
|
|
3182
3435
|
end
|
|
3183
3436
|
|
|
3184
3437
|
def indent_left(with_children = true)
|
|
3185
3438
|
visible = get_visible_items
|
|
3186
3439
|
return if @current >= visible.length
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
if @
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3440
|
+
|
|
3441
|
+
save_undo_state # Save state before modification
|
|
3442
|
+
|
|
3443
|
+
# Handle tagged items
|
|
3444
|
+
if @tagged_items.any?
|
|
3445
|
+
@tagged_items.sort.each do |real_idx|
|
|
3446
|
+
next if real_idx >= @items.length
|
|
3447
|
+
next if @items[real_idx]["level"] == 0 # Already at leftmost
|
|
3448
|
+
|
|
3449
|
+
original_level = @items[real_idx]["level"]
|
|
3450
|
+
@items[real_idx]["level"] -= 1
|
|
3451
|
+
|
|
3452
|
+
# Also unindent children if requested
|
|
3453
|
+
if with_children
|
|
3454
|
+
((real_idx + 1)...@items.length).each do |i|
|
|
3455
|
+
if @items[i]["level"] > original_level
|
|
3456
|
+
@items[i]["level"] -= 1
|
|
3457
|
+
else
|
|
3458
|
+
break
|
|
3459
|
+
end
|
|
3203
3460
|
end
|
|
3204
3461
|
end
|
|
3205
3462
|
end
|
|
3206
|
-
|
|
3207
|
-
# Ensure the moved item remains visible
|
|
3208
|
-
force_item_visible(real_idx)
|
|
3209
3463
|
|
|
3464
|
+
@message = "Unindented #{@tagged_items.length} tagged item(s)"
|
|
3465
|
+
@tagged_items = [] # Clear tags after operation
|
|
3210
3466
|
@modified = true
|
|
3211
|
-
|
|
3467
|
+
else
|
|
3468
|
+
# Normal single item unindent
|
|
3469
|
+
item = visible[@current]
|
|
3470
|
+
real_idx = get_real_index(item)
|
|
3471
|
+
|
|
3472
|
+
if @items[real_idx]["level"] > 0
|
|
3473
|
+
original_level = @items[real_idx]["level"]
|
|
3474
|
+
@items[real_idx]["level"] -= 1
|
|
3475
|
+
|
|
3476
|
+
# Also unindent children if requested
|
|
3477
|
+
if with_children
|
|
3478
|
+
((real_idx + 1)...@items.length).each do |i|
|
|
3479
|
+
if @items[i]["level"] > original_level
|
|
3480
|
+
@items[i]["level"] -= 1
|
|
3481
|
+
else
|
|
3482
|
+
break
|
|
3483
|
+
end
|
|
3484
|
+
end
|
|
3485
|
+
end
|
|
3486
|
+
|
|
3487
|
+
# Ensure the moved item remains visible
|
|
3488
|
+
force_item_visible(real_idx)
|
|
3489
|
+
|
|
3490
|
+
@modified = true
|
|
3491
|
+
end
|
|
3212
3492
|
end
|
|
3493
|
+
|
|
3494
|
+
record_last_action(:indent_left, with_children)
|
|
3213
3495
|
end
|
|
3214
3496
|
|
|
3215
3497
|
def toggle_checkbox
|
|
@@ -3444,8 +3726,9 @@ class HyperListApp
|
|
|
3444
3726
|
help_lines << help_line("#{"gU".fg("10")}", "Uppercase line", "#{"gu".fg("10")}", "Lowercase line")
|
|
3445
3727
|
help_lines << help_line("#{"I".fg("10")}", "Cycle indent (2-5)")
|
|
3446
3728
|
help_lines << help_line("#{"D".fg("10")}", "Delete+yank line", "#{"C-D".fg("10")}", "Delete+yank item&descendants")
|
|
3447
|
-
help_lines << help_line("#{"y".fg("10")}" + "/".fg("10") + "#{"Y".fg("10")}", "Copy line/tree", "#{"p".fg("10")}", "Paste")
|
|
3448
|
-
help_lines << help_line("#{"
|
|
3729
|
+
help_lines << help_line("#{"y".fg("10")}" + "/".fg("10") + "#{"Y".fg("10")}", "Copy line/tree", "#{"p".fg("10")}", "Paste below")
|
|
3730
|
+
help_lines << help_line("#{"P".fg("10")}", "Paste above", "#{"E".fg("10")}", "Edit in $EDITOR")
|
|
3731
|
+
help_lines << help_line("#{"U".fg("10")}", "Undo", "#{".".fg("10")}", "Repeat last action")
|
|
3449
3732
|
help_lines << help_line("#{"r".fg("10")}" + ", ".fg("10") + "#{"C-R".fg("10")}", "Redo")
|
|
3450
3733
|
help_lines << help_line("#{"S-UP".fg("10")}", "Move item up", "#{"S-DOWN".fg("10")}", "Move item down")
|
|
3451
3734
|
help_lines << help_line("#{"C-UP".fg("10")}", "Move up in visible list", "#{"C-DOWN".fg("10")}", "Move down in visible list")
|
|
@@ -3456,7 +3739,7 @@ class HyperListApp
|
|
|
3456
3739
|
help_lines << help_line("#{"v".fg("10")}", "Toggle checkbox", "#{"V".fg("10")}", "Checkbox with date")
|
|
3457
3740
|
help_lines << help_line("#{"C-X".fg("10")}", "Remove checkbox", "", "")
|
|
3458
3741
|
help_lines << help_line("#{"C-E".fg("10")}", "Encrypt/decrypt line", "#{"C-U".fg("10")}", "Toggle State/Trans underline")
|
|
3459
|
-
help_lines << help_line("#{"P".fg("10")}", "Presentation mode", "#{"Tab/S-Tab".fg("10")}", "Next/prev sibling (in P)")
|
|
3742
|
+
help_lines << help_line("#{"C-P".fg("10")}", "Presentation mode", "#{"Tab/S-Tab".fg("10")}", "Next/prev sibling (in P)")
|
|
3460
3743
|
help_lines << help_line("#{"Ma".fg("10")}", "Record macro 'a'", "#{"@a".fg("10")}", "Play macro 'a'")
|
|
3461
3744
|
help_lines << help_line("#{"w".fg("10")}", "Switch panes (split view)")
|
|
3462
3745
|
help_lines << ""
|
|
@@ -3468,8 +3751,15 @@ class HyperListApp
|
|
|
3468
3751
|
help_lines << help_line("#{":as on".fg("10")}", "Enable autosave", "#{":as off".fg("10")}", "Disable autosave")
|
|
3469
3752
|
help_lines << help_line("#{":as N".fg("10")}", "Set interval (secs)", "#{":as".fg("10")}", "Show autosave status")
|
|
3470
3753
|
help_lines << ""
|
|
3754
|
+
help_lines << "#{"TAGGING & BATCH OPS".fg("14")}"
|
|
3755
|
+
help_lines << help_line("#{"t".fg("10")}", "Tag/untag item", "#{"u".fg("10")}", "Clear all tags")
|
|
3756
|
+
help_lines << help_line("#{"C-T".fg("10")}", "Tag by regex pattern", "", "")
|
|
3757
|
+
help_lines << help_line("#{"[T:N]".fg("245")}", "Status shows N tagged", "#{"Blue bg".fg("245")}", "Tagged items")
|
|
3758
|
+
help_lines << help_line("#{"D/y/Tab".fg("245")}", "Ops work on tagged items")
|
|
3759
|
+
help_lines << ""
|
|
3760
|
+
|
|
3471
3761
|
help_lines << "#{"TEMPLATES".fg("14")}"
|
|
3472
|
-
help_lines << help_line("#{"
|
|
3762
|
+
help_lines << help_line("#{"T".fg("10")}", "Insert template", "#{":st".fg("10")}", "Save as template")
|
|
3473
3763
|
help_lines << help_line("#{":dt".fg("10")}", "Delete template", "#{":lt".fg("10")}", "List user templates")
|
|
3474
3764
|
help_lines << ""
|
|
3475
3765
|
|
|
@@ -5112,7 +5402,7 @@ class HyperListApp
|
|
|
5112
5402
|
@items << {"text" => "="*50, "level" => 0, "fold" => false, "raw" => true}
|
|
5113
5403
|
|
|
5114
5404
|
# Add built-in templates section
|
|
5115
|
-
@items << {"text" => "BUILT-IN TEMPLATES:", "level" => 0, "fold" => false, "raw" => true}
|
|
5405
|
+
@items << {"text" => "BUILT-IN TEMPLATES:".fg("39"), "level" => 0, "fold" => false, "raw" => true}
|
|
5116
5406
|
template_list.select { |t| t[2] == "built-in" }.each_with_index do |(key, desc, type), idx|
|
|
5117
5407
|
@items << {
|
|
5118
5408
|
"text" => " #{idx+1}. #{key.capitalize}: #{desc}",
|
|
@@ -5126,7 +5416,7 @@ class HyperListApp
|
|
|
5126
5416
|
# Add user templates section if any exist
|
|
5127
5417
|
if user_templates.any?
|
|
5128
5418
|
@items << {"text" => "", "level" => 0, "fold" => false, "raw" => true}
|
|
5129
|
-
@items << {"text" => "USER TEMPLATES:", "level" => 0, "fold" => false, "raw" => true}
|
|
5419
|
+
@items << {"text" => "USER TEMPLATES:".fg("39"), "level" => 0, "fold" => false, "raw" => true}
|
|
5130
5420
|
template_list.select { |t| t[2] == "user" }.each_with_index do |(key, desc, type), idx|
|
|
5131
5421
|
@items << {
|
|
5132
5422
|
"text" => " #{key}: #{desc}",
|
|
@@ -5357,6 +5647,8 @@ class HyperListApp
|
|
|
5357
5647
|
when " "
|
|
5358
5648
|
toggle_fold
|
|
5359
5649
|
when "u"
|
|
5650
|
+
clear_tags
|
|
5651
|
+
when "U"
|
|
5360
5652
|
undo
|
|
5361
5653
|
when "/"
|
|
5362
5654
|
# Skip search in macros for now
|
|
@@ -6262,11 +6554,17 @@ class HyperListApp
|
|
|
6262
6554
|
# Cycle through indentation sizes (2-5 spaces)
|
|
6263
6555
|
cycle_indent_size
|
|
6264
6556
|
when "t"
|
|
6557
|
+
toggle_tag
|
|
6558
|
+
when "T"
|
|
6265
6559
|
show_templates
|
|
6560
|
+
when "C-T"
|
|
6561
|
+
tag_by_regex
|
|
6266
6562
|
when "D" # Delete line only (without children)
|
|
6267
6563
|
delete_line(false) # Delete current line only
|
|
6268
6564
|
when "C-D" # Delete line and all descendants explicitly
|
|
6269
6565
|
delete_line(true)
|
|
6566
|
+
when "E"
|
|
6567
|
+
spawn_editor
|
|
6270
6568
|
when "C-E" # Toggle line encryption
|
|
6271
6569
|
toggle_line_encryption
|
|
6272
6570
|
when "y" # Yank/copy single line
|
|
@@ -6312,6 +6610,8 @@ class HyperListApp
|
|
|
6312
6610
|
end
|
|
6313
6611
|
end
|
|
6314
6612
|
when "u"
|
|
6613
|
+
clear_tags
|
|
6614
|
+
when "U"
|
|
6315
6615
|
undo
|
|
6316
6616
|
when "\x12" # Ctrl-R for redo (0x12 is Ctrl-R ASCII code)
|
|
6317
6617
|
redo_change
|
|
@@ -6330,6 +6630,8 @@ class HyperListApp
|
|
|
6330
6630
|
when "N"
|
|
6331
6631
|
jump_to_next_template_marker
|
|
6332
6632
|
when "P"
|
|
6633
|
+
paste_above
|
|
6634
|
+
when "C-P"
|
|
6333
6635
|
toggle_presentation_mode
|
|
6334
6636
|
when "\x15" # Ctrl-U for State/Transition underline toggle
|
|
6335
6637
|
# Cycle through underline modes: 0 (none) -> 1 (states) -> 2 (transitions) -> 0
|
data/hyperlist.gemspec
CHANGED
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.
|
|
4
|
+
version: 1.9.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-10-
|
|
11
|
+
date: 2025-10-24 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rcurses
|