hyperlist 1.8.1 → 1.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +43 -0
- data/README.md +62 -11
- data/hyperlist +485 -132
- 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: 485f51c3f998ca45514979e831f928a2ca907674483ba1ceef644ce6db22423e
|
|
4
|
+
data.tar.gz: ca21fe8db31f2b1ac1a1e8059c92f94dbc17a8d42790247238f8073c77ef973d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d40e130fbff4cb2564f8a9c541257cd084072f1917067d529a86427c63b0e3716b7a6704f584cf64dc3e204fd0c63a71a20443f3bcef99bc5377bb43c657b7f3
|
|
7
|
+
data.tar.gz: 5e6a7cdf9f8b2fb7e93382e41ba8d6e2e39d0403a0e64480d763d8385f5e4d7320684ea4b1fd7503d5a4ed08fe515d5176a04d8ab8723f9e43b7ab9d04a9e2b8
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,49 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the HyperList Ruby TUI will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.9.1] - 2025-12-02
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **External file change detection**
|
|
9
|
+
- Automatically detects when the file is modified by other processes (vim, another Claude Code session, etc.)
|
|
10
|
+
- Prompts user to reload with "File changed externally. Reload? (y/n)"
|
|
11
|
+
- Preserves cursor position after reload
|
|
12
|
+
- Option to ignore external changes (updates internal timestamp to avoid repeated prompts)
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- **Quote coloring inside parentheses (comments)**
|
|
16
|
+
- Fixed issue where quotes inside parentheses like `(this is a "comment" in here)` would break the cyan coloring
|
|
17
|
+
- The closing quote would terminate coloring, leaving the rest of the comment white
|
|
18
|
+
- Now the entire parentheses content stays cyan as intended
|
|
19
|
+
|
|
20
|
+
## [1.9.0] - 2025-10-24
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- **Item Tagging & Batch Operations**
|
|
24
|
+
- Tag items with 't' key for batch operations (auto-advance cursor)
|
|
25
|
+
- Visual feedback: dark blue background for tagged items, lighter blue for tagged+selected
|
|
26
|
+
- Status bar shows [T:N] count of tagged items
|
|
27
|
+
- Clear all tags with 'u' key
|
|
28
|
+
- Regex tagging with 'C-T' - tag all items matching a pattern
|
|
29
|
+
- Batch operations: D/C-D (delete), y/Y (yank), Tab/S-Tab (indent) work on all tagged items
|
|
30
|
+
|
|
31
|
+
- **External Editor Support**
|
|
32
|
+
- Press 'E' to spawn $EDITOR (vim, nano, etc.)
|
|
33
|
+
- Automatic save/reload workflow
|
|
34
|
+
- Proper terminal state management
|
|
35
|
+
- Full screen refresh on return
|
|
36
|
+
|
|
37
|
+
- **Enhanced Paste & Navigation**
|
|
38
|
+
- 'P' to paste above current item (vim-style)
|
|
39
|
+
- Presentation mode moved to 'C-P'
|
|
40
|
+
- Templates moved to 'T' key
|
|
41
|
+
- Undo moved to 'U' key (RTFM-consistent)
|
|
42
|
+
|
|
43
|
+
### Enhanced
|
|
44
|
+
- **Smart Modified Flag**
|
|
45
|
+
- [+] indicator removed when undoing back to original file state
|
|
46
|
+
- Tracks original state to detect true modifications
|
|
47
|
+
|
|
5
48
|
## [1.8.0] - 2025-09-22
|
|
6
49
|
|
|
7
50
|
### Enhanced
|
data/README.md
CHANGED
|
@@ -29,30 +29,71 @@ For historical context and the original VIM implementation, see: [hyperlist.vim]
|
|
|
29
29
|
### Help Screen
|
|
30
30
|

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