hyperlist 1.0.0 → 1.1.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/CHANGELOG.md +28 -0
- data/README.md +62 -3
- data/hyperlist +339 -41
- data/hyperlist.gemspec +2 -2
- data/hyperlist_logo.svg +77 -0
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f04bfa860db3f25e2c0327b2a5b81e35ac1903f87a5516b077789dc9a98a96cd
|
4
|
+
data.tar.gz: 6c62b2f854dede524a162c34fa37c53e941173e1e35f2516b1e809e7092db7ae
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 024502aaf0a6c23539d5303aef4eebba9a6dcf91204923b0f7f8d8217d210f00194bddf93c1a9cdefa7b68ea82915ee2b7c543d46c9b4347f2ddbc9593d4048a
|
7
|
+
data.tar.gz: f69dfaca4c44e986614cc264af2574f648bf8144cc2da104ebe0055d014ebbf97bc8e46e36157449e1ef2ce40ab5a89c29d3d2cb4896ae51d9f7bd34f7d9e904
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,34 @@
|
|
2
2
|
|
3
3
|
All notable changes to the HyperList Ruby TUI will be documented in this file.
|
4
4
|
|
5
|
+
## [1.1.0] - 2025-08-13
|
6
|
+
|
7
|
+
### Added
|
8
|
+
- **Encryption Support**
|
9
|
+
- File-level encryption for sensitive files (automatic for dot files)
|
10
|
+
- Line-level encryption for individual items (Ctrl-E to toggle)
|
11
|
+
- Secure AES-256-CBC encryption with PBKDF2 key derivation
|
12
|
+
- Password caching for the session
|
13
|
+
- Visual indicators for encrypted content (lock icon)
|
14
|
+
|
15
|
+
- **Enhanced Presentation Mode**
|
16
|
+
- Auto-collapse everything outside the current context
|
17
|
+
- Smart focus showing only current item, ancestors, and immediate children
|
18
|
+
- Visual hierarchy with focused items in full color, others greyed out
|
19
|
+
- Proper cursor tracking when folding changes
|
20
|
+
|
21
|
+
### Changed
|
22
|
+
- **Improved Visual Experience**
|
23
|
+
- Current line highlighting now uses dark gray background instead of reverse video
|
24
|
+
- Preserves all syntax colors when items are selected
|
25
|
+
- Search highlighting no longer overrides text colors
|
26
|
+
- More subtle and professional appearance
|
27
|
+
|
28
|
+
### Fixed
|
29
|
+
- Cursor position tracking in presentation mode when folds change
|
30
|
+
- Cache invalidation issues in presentation mode
|
31
|
+
- Navigation responsiveness improvements
|
32
|
+
|
5
33
|
## [1.0.0] - 2025-08-12
|
6
34
|
|
7
35
|
### Initial Release
|
data/README.md
CHANGED
@@ -1,7 +1,17 @@
|
|
1
|
-
# HyperList
|
1
|
+
# HyperList TUI
|
2
|
+
|
3
|
+
[](https://badge.fury.io/rb/hyperlist)
|
4
|
+
[](https://unlicense.org/)
|
5
|
+
[](https://www.ruby-lang.org/)
|
6
|
+
[](https://github.com/isene/HyperList/stargazers)
|
7
|
+
[](https://isene.org)
|
8
|
+
|
9
|
+
<img src="hyperlist_logo.svg" align="left" width="150" height="150">
|
2
10
|
|
3
11
|
A powerful Terminal User Interface (TUI) application for creating, editing, and managing HyperLists - a methodology for describing anything in a hierarchical, structured format.
|
4
12
|
|
13
|
+
<br clear="left"/>
|
14
|
+
|
5
15
|
## What is HyperList?
|
6
16
|
|
7
17
|
HyperList is a universal methodology for describing anything - any state, item, pattern, action, process, transition, program, instruction set, etc. It can be used as an outliner, a todo list handler, a process design tool, a data modeler, or any other way you want to describe something.
|
@@ -10,6 +20,33 @@ Learn more about the HyperList methodology at: [https://isene.org/hyperlist/](ht
|
|
10
20
|
|
11
21
|
For historical context and the original VIM implementation, see: [hyperlist.vim](https://github.com/isene/hyperlist.vim)
|
12
22
|
|
23
|
+
## Screenshots
|
24
|
+
|
25
|
+
### Main Interface
|
26
|
+

|
27
|
+
|
28
|
+
### Help Screen
|
29
|
+

|
30
|
+
|
31
|
+
## What's New in v1.1.0
|
32
|
+
|
33
|
+
### 🔐 Encryption Support
|
34
|
+
- **File-level encryption** for sensitive files (dot files like `.passwords.hl`)
|
35
|
+
- **Line-level encryption** for individual items (Ctrl-E to toggle)
|
36
|
+
- Secure AES-256-CBC encryption with PBKDF2 key derivation
|
37
|
+
- Password caching for the session
|
38
|
+
|
39
|
+
### 🎯 Enhanced Presentation Mode
|
40
|
+
- **Auto-collapse** everything outside the current context
|
41
|
+
- **Smart focus**: Shows only current item, ancestors, and immediate children
|
42
|
+
- **Visual hierarchy**: Focused items in full color, others greyed out
|
43
|
+
- Improved navigation with proper cursor tracking
|
44
|
+
|
45
|
+
### 🎨 Better Visual Experience
|
46
|
+
- **Improved highlighting**: Dark gray background preserves syntax colors
|
47
|
+
- **Subtle selection**: No more harsh reverse video
|
48
|
+
- **Preserved colors**: All HyperList elements maintain their colors when selected
|
49
|
+
|
13
50
|
## Features
|
14
51
|
|
15
52
|
### Core Functionality
|
@@ -26,7 +63,13 @@ For historical context and the original VIM implementation, see: [hyperlist.vim]
|
|
26
63
|
- **Full Editing Capabilities**: Create, edit, delete, move, and reorganize items
|
27
64
|
- **Checkbox Support**: Multiple checkbox types with completion tracking
|
28
65
|
- **Template System**: Jump to and fill in template markers
|
29
|
-
- **Presentation Mode**: Focus on current item
|
66
|
+
- **Presentation Mode**: Focus on current item with auto-collapse
|
67
|
+
|
68
|
+
### Security Features
|
69
|
+
- **Encryption**: Protect sensitive data with AES-256 encryption
|
70
|
+
- **Automatic detection**: Dot files automatically prompt for encryption
|
71
|
+
- **Line-level security**: Encrypt individual sensitive items
|
72
|
+
- **Visual indicators**: Encrypted lines show lock icon
|
30
73
|
|
31
74
|
### Text Formatting
|
32
75
|
- **Bold**: `*text*`
|
@@ -47,6 +90,7 @@ For historical context and the original VIM implementation, see: [hyperlist.vim]
|
|
47
90
|
- Multiple file support with recent files list
|
48
91
|
- Autosave functionality with configurable intervals
|
49
92
|
- Split view for working with multiple lists
|
93
|
+
- Encryption support for sensitive files
|
50
94
|
|
51
95
|
## Installation
|
52
96
|
|
@@ -74,6 +118,7 @@ chmod +x hyperlist
|
|
74
118
|
```bash
|
75
119
|
hyperlist # Start with empty document
|
76
120
|
hyperlist file.hl # Open existing HyperList file
|
121
|
+
hyperlist .passwords.hl # Open encrypted file (will prompt for password)
|
77
122
|
hyperlist file.txt # Open any text file
|
78
123
|
```
|
79
124
|
|
@@ -109,9 +154,10 @@ hyperlist file.txt # Open any text file
|
|
109
154
|
#### Features
|
110
155
|
- `v` - Toggle checkbox
|
111
156
|
- `V` - Toggle checkbox with timestamp
|
157
|
+
- `C-E` - Encrypt/decrypt current line
|
112
158
|
- `R` - Go to reference
|
113
159
|
- `F` - Open file reference
|
114
|
-
- `P` - Presentation mode
|
160
|
+
- `P` - Presentation mode (with auto-collapse)
|
115
161
|
- `?` - Help screen
|
116
162
|
|
117
163
|
#### File Commands
|
@@ -138,6 +184,19 @@ Daily Tasks
|
|
138
184
|
[ ] Afternoon tasks
|
139
185
|
```
|
140
186
|
|
187
|
+
### Encrypted Password Manager
|
188
|
+
Save as `.passwords.hl` for automatic encryption:
|
189
|
+
```
|
190
|
+
Online Accounts
|
191
|
+
GitHub
|
192
|
+
Username: myuser
|
193
|
+
Password: [will be encrypted]
|
194
|
+
2FA: enabled
|
195
|
+
Banking
|
196
|
+
Account: 12345678
|
197
|
+
PIN: [will be encrypted]
|
198
|
+
```
|
199
|
+
|
141
200
|
### Project Structure
|
142
201
|
```
|
143
202
|
MyProject #project
|
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.0
|
10
|
+
HyperList v1.1.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.0
|
55
|
+
puts "HyperList v1.1.0"
|
56
56
|
exit 0
|
57
57
|
end
|
58
58
|
|
@@ -61,13 +61,16 @@ require 'io/console'
|
|
61
61
|
require 'date'
|
62
62
|
require 'rcurses'
|
63
63
|
require 'cgi'
|
64
|
+
require 'openssl'
|
65
|
+
require 'digest'
|
66
|
+
require 'base64'
|
64
67
|
|
65
68
|
class HyperListApp
|
66
69
|
include Rcurses
|
67
70
|
include Rcurses::Input
|
68
71
|
include Rcurses::Cursor
|
69
72
|
|
70
|
-
VERSION = "1.0
|
73
|
+
VERSION = "1.1.0"
|
71
74
|
|
72
75
|
def initialize(filename = nil)
|
73
76
|
@filename = filename ? File.expand_path(filename) : nil
|
@@ -100,6 +103,8 @@ class HyperListApp
|
|
100
103
|
@macro_register = {} # Store macros by key
|
101
104
|
@macro_buffer = [] # Current macro being recorded
|
102
105
|
@macro_key = nil # Key for current macro
|
106
|
+
@encryption_key = nil # Store derived encryption key
|
107
|
+
@encrypted_lines = {} # Track which lines are encrypted
|
103
108
|
@split_view = false
|
104
109
|
@split_items = [] # Second view items
|
105
110
|
@split_current = 0 # Second view cursor
|
@@ -165,7 +170,10 @@ class HyperListApp
|
|
165
170
|
|
166
171
|
def load_file(file)
|
167
172
|
@items = []
|
168
|
-
|
173
|
+
@encrypted_lines = {}
|
174
|
+
|
175
|
+
# Read file content
|
176
|
+
content = File.read(file) rescue ""
|
169
177
|
|
170
178
|
# Clear undo/redo stacks when loading a new file
|
171
179
|
@undo_stack = []
|
@@ -173,6 +181,26 @@ class HyperListApp
|
|
173
181
|
@redo_stack = []
|
174
182
|
@redo_position = []
|
175
183
|
|
184
|
+
# Check if file is encrypted (dot file or encrypted content)
|
185
|
+
if is_encrypted_file?(file)
|
186
|
+
if content.start_with?("ENC:")
|
187
|
+
# Whole file is encrypted
|
188
|
+
decrypted_content = decrypt_file(content)
|
189
|
+
if decrypted_content.nil?
|
190
|
+
@message = "Failed to decrypt file"
|
191
|
+
@items = [{"text" => "Failed to decrypt file", "level" => 0, "fold" => false}]
|
192
|
+
return
|
193
|
+
end
|
194
|
+
lines = decrypted_content.split("\n")
|
195
|
+
@message = "File decrypted successfully"
|
196
|
+
else
|
197
|
+
# Dot file but not encrypted yet - just load normally
|
198
|
+
lines = content.split("\n")
|
199
|
+
end
|
200
|
+
else
|
201
|
+
lines = content.split("\n")
|
202
|
+
end
|
203
|
+
|
176
204
|
# Check if file is large
|
177
205
|
large_file = lines.length > 10000
|
178
206
|
|
@@ -186,6 +214,12 @@ class HyperListApp
|
|
186
214
|
next if line.strip.empty?
|
187
215
|
level = line[/^\t*/].length
|
188
216
|
text = line.strip
|
217
|
+
|
218
|
+
# Track encrypted lines
|
219
|
+
if text.start_with?("ENC:")
|
220
|
+
@encrypted_lines[@items.length] = true
|
221
|
+
end
|
222
|
+
|
189
223
|
@items << {"text" => text, "level" => level, "fold" => false}
|
190
224
|
|
191
225
|
# Update progress for large files
|
@@ -346,13 +380,48 @@ class HyperListApp
|
|
346
380
|
def save_file
|
347
381
|
return unless @filename
|
348
382
|
|
349
|
-
|
350
|
-
|
351
|
-
|
383
|
+
# Prepare content
|
384
|
+
content = @items.map do |item|
|
385
|
+
"\t" * item["level"] + item["text"]
|
386
|
+
end.join("\n")
|
387
|
+
|
388
|
+
# Check if this should be an encrypted file
|
389
|
+
if is_encrypted_file?(@filename) && !content.empty?
|
390
|
+
# Check if any lines are already encrypted
|
391
|
+
has_encrypted_lines = @items.any? { |item| item["text"].start_with?("ENC:") }
|
392
|
+
|
393
|
+
if !has_encrypted_lines
|
394
|
+
# Ask if user wants to encrypt the whole file
|
395
|
+
@footer.text = "Encrypt entire file? (y/n): "
|
396
|
+
@footer.refresh
|
397
|
+
response = getchr
|
398
|
+
|
399
|
+
if response.downcase == 'y'
|
400
|
+
encrypted_content = encrypt_file(content)
|
401
|
+
if encrypted_content
|
402
|
+
File.write(@filename, encrypted_content)
|
403
|
+
@message = "File saved (encrypted)"
|
404
|
+
else
|
405
|
+
@message = "Encryption cancelled - file not saved"
|
406
|
+
return
|
407
|
+
end
|
408
|
+
else
|
409
|
+
# Save as plain text even though it's a dot file
|
410
|
+
File.write(@filename, content)
|
411
|
+
@message = "Saved to #{@filename} (unencrypted)"
|
412
|
+
end
|
413
|
+
else
|
414
|
+
# Has encrypted lines, save as is
|
415
|
+
File.write(@filename, content)
|
416
|
+
@message = "Saved to #{@filename}"
|
352
417
|
end
|
418
|
+
else
|
419
|
+
# Regular file, save normally
|
420
|
+
File.write(@filename, content)
|
421
|
+
@message = "Saved to #{@filename}"
|
353
422
|
end
|
423
|
+
|
354
424
|
@modified = false
|
355
|
-
@message = "Saved to #{@filename}"
|
356
425
|
@last_auto_save = Time.now if @auto_save_enabled
|
357
426
|
end
|
358
427
|
|
@@ -651,8 +720,8 @@ class HyperListApp
|
|
651
720
|
# Create cache key for this item
|
652
721
|
cache_key = "#{item['text']}_#{item['level']}_#{item['fold']}"
|
653
722
|
|
654
|
-
# Check cache first
|
655
|
-
if @processed_cache[cache_key] && idx != @current
|
723
|
+
# Check cache first (skip cache in presentation mode to ensure proper rendering)
|
724
|
+
if @processed_cache[cache_key] && idx != @current && !@presentation_mode
|
656
725
|
lines << @processed_cache[cache_key]
|
657
726
|
else
|
658
727
|
line = " " * item["level"] # 4 spaces per level
|
@@ -660,9 +729,13 @@ class HyperListApp
|
|
660
729
|
# Add fold indicator
|
661
730
|
real_idx = @items.index(item) # Get the real index in the full array
|
662
731
|
if real_idx && has_children?(real_idx, @items) && item["fold"]
|
663
|
-
|
732
|
+
# Use darker grey for unfocused items in presentation mode
|
733
|
+
color = (@presentation_mode && !is_item_in_presentation_focus?(item)) ? "240" : "245"
|
734
|
+
line += "▶".fg(color) + " " # Triangle for collapsed (has hidden children)
|
664
735
|
elsif real_idx && has_children?(real_idx, @items)
|
665
|
-
|
736
|
+
# Use darker grey for unfocused items in presentation mode
|
737
|
+
color = (@presentation_mode && !is_item_in_presentation_focus?(item)) ? "240" : "245"
|
738
|
+
line += "▷".fg(color) + " " # Triangle for expanded (has visible children)
|
666
739
|
else
|
667
740
|
line += " "
|
668
741
|
end
|
@@ -699,23 +772,31 @@ class HyperListApp
|
|
699
772
|
if item["raw"]
|
700
773
|
line += item["text"]
|
701
774
|
else
|
702
|
-
|
703
|
-
if @
|
704
|
-
|
775
|
+
# Check if we should grey out this item in presentation mode
|
776
|
+
if @presentation_mode && !is_item_in_presentation_focus?(item)
|
777
|
+
# Grey out items not in focus
|
778
|
+
line += item["text"].fg("240") # Dark grey for unfocused items
|
705
779
|
else
|
706
|
-
|
707
|
-
@processed_cache[processed_text_key]
|
708
|
-
|
780
|
+
processed_text_key = has_match ? "search_#{item['text']}_#{@search}" : "text_#{item['text']}"
|
781
|
+
if @processed_cache[processed_text_key] && !has_match && !@presentation_mode
|
782
|
+
line += @processed_cache[processed_text_key]
|
783
|
+
else
|
784
|
+
processed = process_text(item["text"], has_match)
|
785
|
+
@processed_cache[processed_text_key] = processed if @processed_cache.size < 1000 && !has_match && !@presentation_mode
|
786
|
+
line += processed
|
787
|
+
end
|
709
788
|
end
|
710
789
|
end
|
711
790
|
end
|
712
791
|
|
713
|
-
# Cache the line (without highlight)
|
714
|
-
|
792
|
+
# Cache the line (without highlight) - but not in presentation mode or for focused items
|
793
|
+
if idx != @current && !@presentation_mode && !item["presentation_focus"]
|
794
|
+
@processed_cache[cache_key] = line
|
795
|
+
end
|
715
796
|
|
716
|
-
# Highlight current line
|
797
|
+
# Highlight current line with dark gray background
|
717
798
|
if idx == @current
|
718
|
-
line = line.
|
799
|
+
line = line.bg("237") # Dark gray background to preserve foreground colors
|
719
800
|
end
|
720
801
|
|
721
802
|
lines << line
|
@@ -741,6 +822,12 @@ class HyperListApp
|
|
741
822
|
return result
|
742
823
|
end
|
743
824
|
|
825
|
+
# Check if this is an encrypted line
|
826
|
+
if result.start_with?("ENC:")
|
827
|
+
# Show encrypted indicator instead of the encrypted data
|
828
|
+
return "🔒 [ENCRYPTED LINE - Press Ctrl-E to decrypt]".fg("196") # Bright red
|
829
|
+
end
|
830
|
+
|
744
831
|
# Check if this is a literal block marker (single backslash)
|
745
832
|
if result.strip == "\\"
|
746
833
|
return result.fg("3") # Yellow for literal block markers
|
@@ -750,7 +837,7 @@ class HyperListApp
|
|
750
837
|
if highlight_search && @search && !@search.empty?
|
751
838
|
# Find all occurrences of the search term (case insensitive)
|
752
839
|
search_regex = Regexp.new(Regexp.escape(@search), Regexp::IGNORECASE)
|
753
|
-
result.gsub!(search_regex) { |match| match.bg("
|
840
|
+
result.gsub!(search_regex) { |match| match.bg("220") } # Yellow background, preserve text color
|
754
841
|
end
|
755
842
|
|
756
843
|
# Handle identifiers at the beginning (like "1.1.1.1" or "1A1A")
|
@@ -1036,21 +1123,25 @@ class HyperListApp
|
|
1036
1123
|
|
1037
1124
|
def move_up
|
1038
1125
|
@current = [@current - 1, 0].max
|
1126
|
+
update_presentation_focus if @presentation_mode
|
1039
1127
|
end
|
1040
1128
|
|
1041
1129
|
def move_down
|
1042
1130
|
max = get_visible_items.length - 1
|
1043
1131
|
@current = [@current + 1, max].min
|
1132
|
+
update_presentation_focus if @presentation_mode
|
1044
1133
|
end
|
1045
1134
|
|
1046
1135
|
def page_up
|
1047
1136
|
@current = [@current - (@main.h - 1), 0].max
|
1048
1137
|
@offset = [@offset - (@main.h - 1), 0].max
|
1138
|
+
update_presentation_focus if @presentation_mode
|
1049
1139
|
end
|
1050
1140
|
|
1051
1141
|
def page_down
|
1052
1142
|
max = get_visible_items.length - 1
|
1053
1143
|
@current = [@current + (@main.h - 1), max].min
|
1144
|
+
update_presentation_focus if @presentation_mode
|
1054
1145
|
end
|
1055
1146
|
|
1056
1147
|
def go_to_parent
|
@@ -1064,6 +1155,7 @@ class HyperListApp
|
|
1064
1155
|
(@current - 1).downto(0) do |i|
|
1065
1156
|
if visible[i]["level"] < current_level
|
1066
1157
|
@current = i
|
1158
|
+
update_presentation_focus if @presentation_mode
|
1067
1159
|
break
|
1068
1160
|
end
|
1069
1161
|
end
|
@@ -1076,6 +1168,7 @@ class HyperListApp
|
|
1076
1168
|
current_level = visible[@current]["level"]
|
1077
1169
|
if visible[@current + 1]["level"] > current_level
|
1078
1170
|
@current += 1
|
1171
|
+
update_presentation_focus if @presentation_mode
|
1079
1172
|
end
|
1080
1173
|
end
|
1081
1174
|
|
@@ -1116,52 +1209,102 @@ class HyperListApp
|
|
1116
1209
|
|
1117
1210
|
def toggle_presentation_mode
|
1118
1211
|
if @presentation_mode
|
1119
|
-
# Exit presentation mode - restore
|
1212
|
+
# Exit presentation mode - restore normal view
|
1120
1213
|
@presentation_mode = false
|
1121
|
-
|
1214
|
+
# Clear all presentation focus flags and unfold everything
|
1215
|
+
@items.each do |item|
|
1216
|
+
item["presentation_focus"] = false
|
1217
|
+
item["fold"] = false # Unfold everything when exiting presentation mode
|
1218
|
+
end
|
1219
|
+
# Clear cache to force re-rendering
|
1220
|
+
@processed_cache.clear
|
1122
1221
|
@message = "Presentation mode disabled"
|
1123
1222
|
else
|
1124
|
-
# Enter presentation mode
|
1223
|
+
# Enter presentation mode
|
1224
|
+
# Remember which item we're on before any folding changes
|
1225
|
+
visible_items = get_visible_items
|
1226
|
+
if @current < visible_items.length
|
1227
|
+
target_item = visible_items[@current]
|
1228
|
+
end
|
1229
|
+
|
1125
1230
|
@presentation_mode = true
|
1126
|
-
|
1127
|
-
@
|
1231
|
+
# Clear cache to force re-rendering
|
1232
|
+
@processed_cache.clear
|
1233
|
+
update_presentation_focus
|
1234
|
+
|
1235
|
+
# Make sure cursor is still on the same item after initial folding
|
1236
|
+
if target_item
|
1237
|
+
new_visible = get_visible_items
|
1238
|
+
new_position = new_visible.index(target_item)
|
1239
|
+
@current = new_position if new_position
|
1240
|
+
end
|
1241
|
+
|
1242
|
+
@message = "Presentation mode enabled - focus on current item"
|
1128
1243
|
end
|
1129
1244
|
end
|
1130
1245
|
|
1131
|
-
def
|
1246
|
+
def update_presentation_focus
|
1247
|
+
# This method updates the focus in presentation mode
|
1248
|
+
# It will be called whenever the cursor moves
|
1249
|
+
return unless @presentation_mode
|
1250
|
+
|
1132
1251
|
visible_items = get_visible_items
|
1133
1252
|
return if visible_items.empty? || @current >= visible_items.length
|
1134
1253
|
|
1254
|
+
# Remember which item we're focused on
|
1135
1255
|
current_item = visible_items[@current]
|
1136
1256
|
current_level = current_item["level"]
|
1137
1257
|
current_real_idx = @items.index(current_item)
|
1138
1258
|
|
1139
1259
|
# First, fold everything
|
1140
|
-
@items.
|
1260
|
+
@items.each_with_index do |item, idx|
|
1261
|
+
item["fold"] = has_children?(idx, @items)
|
1262
|
+
item["presentation_focus"] = false
|
1263
|
+
end
|
1141
1264
|
|
1142
|
-
#
|
1265
|
+
# Mark current item as in focus and unfold it
|
1266
|
+
current_item["presentation_focus"] = true
|
1143
1267
|
current_item["fold"] = false
|
1144
1268
|
|
1145
|
-
# Unfold all ancestors
|
1146
|
-
|
1269
|
+
# Unfold all ancestors of current item
|
1270
|
+
ancestor_indices = []
|
1271
|
+
search_level = current_level - 1
|
1147
1272
|
idx = current_real_idx - 1
|
1148
1273
|
|
1149
|
-
while idx >= 0 &&
|
1150
|
-
if @items[idx]["level"] ==
|
1274
|
+
while idx >= 0 && search_level >= 0
|
1275
|
+
if @items[idx]["level"] == search_level
|
1151
1276
|
@items[idx]["fold"] = false
|
1152
|
-
|
1277
|
+
@items[idx]["presentation_focus"] = false # Ancestors visible but not focused
|
1278
|
+
ancestor_indices << idx
|
1279
|
+
search_level -= 1
|
1153
1280
|
end
|
1154
1281
|
idx -= 1
|
1155
1282
|
end
|
1156
1283
|
|
1157
|
-
#
|
1284
|
+
# Mark immediate children as in focus (only one level down)
|
1158
1285
|
idx = current_real_idx + 1
|
1159
1286
|
while idx < @items.length && @items[idx]["level"] > current_level
|
1160
1287
|
if @items[idx]["level"] == current_level + 1
|
1161
|
-
@items[idx]["
|
1288
|
+
@items[idx]["presentation_focus"] = true
|
1289
|
+
# Don't unfold children - let them stay folded unless user explicitly unfolds
|
1162
1290
|
end
|
1163
1291
|
idx += 1
|
1164
1292
|
end
|
1293
|
+
|
1294
|
+
# Now recalculate the cursor position to point to the same item
|
1295
|
+
new_visible_items = get_visible_items
|
1296
|
+
new_position = new_visible_items.index(current_item)
|
1297
|
+
if new_position
|
1298
|
+
@current = new_position
|
1299
|
+
end
|
1300
|
+
|
1301
|
+
# Clear cache to force re-rendering with new focus
|
1302
|
+
@processed_cache.clear
|
1303
|
+
end
|
1304
|
+
|
1305
|
+
def is_item_in_presentation_focus?(item)
|
1306
|
+
return true unless @presentation_mode
|
1307
|
+
item["presentation_focus"] == true
|
1165
1308
|
end
|
1166
1309
|
|
1167
1310
|
def save_undo_state
|
@@ -1883,8 +2026,9 @@ class HyperListApp
|
|
1883
2026
|
help_lines << ""
|
1884
2027
|
help_lines << "#{"FEATURES".fg("14")}"
|
1885
2028
|
help_lines << help_line("#{"v".fg("10")}", "Toggle checkbox", "#{"V".fg("10")}", "Checkbox with date")
|
1886
|
-
help_lines << help_line("#{"
|
1887
|
-
help_lines << help_line("#{"
|
2029
|
+
help_lines << help_line("#{"C-E".fg("10")}", "Encrypt/decrypt line", "#{"R".fg("10")}", "Go to reference")
|
2030
|
+
help_lines << help_line("#{"F".fg("10")}", "Open file", "#{"N".fg("10")}", "Next = marker")
|
2031
|
+
help_lines << help_line("#{"P".fg("10")}", "Presentation mode")
|
1888
2032
|
help_lines << help_line("#{"t".fg("10")}", "Insert template", "#{":template".fg("10")}", "Show templates")
|
1889
2033
|
help_lines << help_line("#{"Ma".fg("10")}", "Record macro 'a'", "#{"@a".fg("10")}", "Play macro 'a'")
|
1890
2034
|
help_lines << help_line("#{":vsplit".fg("10")}", "Split view vertically", "#{"ww".fg("10")}", "Switch panes")
|
@@ -2731,6 +2875,154 @@ class HyperListApp
|
|
2731
2875
|
@message = "File not found: #{filepath}"
|
2732
2876
|
end
|
2733
2877
|
end
|
2878
|
+
|
2879
|
+
# Encryption methods
|
2880
|
+
def prompt_password(prompt = "Password: ")
|
2881
|
+
@footer.text = prompt
|
2882
|
+
@footer.refresh
|
2883
|
+
|
2884
|
+
password = ""
|
2885
|
+
loop do
|
2886
|
+
c = getchr
|
2887
|
+
case c
|
2888
|
+
when "ENTER"
|
2889
|
+
break
|
2890
|
+
when "BACKSPACE", "C-h"
|
2891
|
+
password.chop! unless password.empty?
|
2892
|
+
when "ESC", "C-c"
|
2893
|
+
return nil
|
2894
|
+
else
|
2895
|
+
if c.length == 1 && c.ord >= 32 && c.ord <= 126
|
2896
|
+
password += c
|
2897
|
+
end
|
2898
|
+
end
|
2899
|
+
@footer.text = prompt + "*" * password.length
|
2900
|
+
@footer.refresh
|
2901
|
+
end
|
2902
|
+
password
|
2903
|
+
end
|
2904
|
+
|
2905
|
+
def derive_key(password, salt = nil)
|
2906
|
+
salt ||= OpenSSL::Random.random_bytes(16)
|
2907
|
+
iterations = 10000
|
2908
|
+
key_len = 32 # 256-bit key
|
2909
|
+
key = OpenSSL::PKCS5.pbkdf2_hmac(password, salt, iterations, key_len, OpenSSL::Digest::SHA256.new)
|
2910
|
+
[key, salt]
|
2911
|
+
end
|
2912
|
+
|
2913
|
+
def encrypt_string(text, password = nil)
|
2914
|
+
password ||= @encryption_key || prompt_password("Enter encryption password: ")
|
2915
|
+
return nil unless password
|
2916
|
+
|
2917
|
+
# Store key for session if not already stored
|
2918
|
+
@encryption_key ||= password
|
2919
|
+
|
2920
|
+
cipher = OpenSSL::Cipher.new('AES-256-CBC')
|
2921
|
+
cipher.encrypt
|
2922
|
+
|
2923
|
+
salt = OpenSSL::Random.random_bytes(16)
|
2924
|
+
key, _ = derive_key(password, salt)
|
2925
|
+
cipher.key = key
|
2926
|
+
|
2927
|
+
iv = cipher.random_iv
|
2928
|
+
encrypted = cipher.update(text) + cipher.final
|
2929
|
+
|
2930
|
+
# Combine salt, iv, and encrypted data, then base64 encode
|
2931
|
+
combined = salt + iv + encrypted
|
2932
|
+
"ENC:" + Base64.strict_encode64(combined)
|
2933
|
+
end
|
2934
|
+
|
2935
|
+
def decrypt_string(encrypted_text, password = nil)
|
2936
|
+
return encrypted_text unless encrypted_text.start_with?("ENC:")
|
2937
|
+
|
2938
|
+
password ||= @encryption_key || prompt_password("Enter decryption password: ")
|
2939
|
+
return nil unless password
|
2940
|
+
|
2941
|
+
# Store key for session if not already stored
|
2942
|
+
@encryption_key ||= password
|
2943
|
+
|
2944
|
+
begin
|
2945
|
+
data = Base64.strict_decode64(encrypted_text[4..-1])
|
2946
|
+
|
2947
|
+
salt = data[0..15]
|
2948
|
+
iv = data[16..31]
|
2949
|
+
encrypted = data[32..-1]
|
2950
|
+
|
2951
|
+
cipher = OpenSSL::Cipher.new('AES-256-CBC')
|
2952
|
+
cipher.decrypt
|
2953
|
+
|
2954
|
+
key, _ = derive_key(password, salt)
|
2955
|
+
cipher.key = key
|
2956
|
+
cipher.iv = iv
|
2957
|
+
|
2958
|
+
cipher.update(encrypted) + cipher.final
|
2959
|
+
rescue => e
|
2960
|
+
@message = "Decryption failed. Wrong password?"
|
2961
|
+
nil
|
2962
|
+
end
|
2963
|
+
end
|
2964
|
+
|
2965
|
+
def is_encrypted_file?(filename)
|
2966
|
+
# Check if it's a dot file or has .enc extension
|
2967
|
+
basename = File.basename(filename)
|
2968
|
+
return true if basename.start_with?(".")
|
2969
|
+
return true if filename.end_with?(".enc")
|
2970
|
+
|
2971
|
+
# Check first line of file for encryption marker
|
2972
|
+
begin
|
2973
|
+
first_line = File.open(filename, &:readline).strip
|
2974
|
+
first_line.start_with?("ENC:")
|
2975
|
+
rescue
|
2976
|
+
false
|
2977
|
+
end
|
2978
|
+
end
|
2979
|
+
|
2980
|
+
def encrypt_file(content, password = nil)
|
2981
|
+
password ||= @encryption_key || prompt_password("Enter file encryption password: ")
|
2982
|
+
return nil unless password
|
2983
|
+
|
2984
|
+
encrypt_string(content, password)
|
2985
|
+
end
|
2986
|
+
|
2987
|
+
def decrypt_file(encrypted_content, password = nil)
|
2988
|
+
password ||= @encryption_key || prompt_password("Enter file decryption password: ")
|
2989
|
+
return nil unless password
|
2990
|
+
|
2991
|
+
decrypt_string(encrypted_content, password)
|
2992
|
+
end
|
2993
|
+
|
2994
|
+
def toggle_line_encryption
|
2995
|
+
return if @items.empty?
|
2996
|
+
|
2997
|
+
item = @items[@current]
|
2998
|
+
current_text = item["text"]
|
2999
|
+
|
3000
|
+
if current_text.start_with?("ENC:")
|
3001
|
+
# Decrypt the line
|
3002
|
+
decrypted = decrypt_string(current_text)
|
3003
|
+
if decrypted
|
3004
|
+
save_state
|
3005
|
+
item["text"] = decrypted
|
3006
|
+
@encrypted_lines.delete(@current)
|
3007
|
+
@modified = true
|
3008
|
+
@message = "Line decrypted"
|
3009
|
+
else
|
3010
|
+
@message = "Decryption failed"
|
3011
|
+
end
|
3012
|
+
else
|
3013
|
+
# Encrypt the line
|
3014
|
+
encrypted = encrypt_string(current_text)
|
3015
|
+
if encrypted
|
3016
|
+
save_state
|
3017
|
+
item["text"] = encrypted
|
3018
|
+
@encrypted_lines[@current] = true
|
3019
|
+
@modified = true
|
3020
|
+
@message = "Line encrypted"
|
3021
|
+
else
|
3022
|
+
@message = "Encryption cancelled"
|
3023
|
+
end
|
3024
|
+
end
|
3025
|
+
end
|
2734
3026
|
|
2735
3027
|
def load_templates
|
2736
3028
|
{
|
@@ -3156,7 +3448,7 @@ class HyperListApp
|
|
3156
3448
|
|
3157
3449
|
# Highlight current line in split pane
|
3158
3450
|
if idx == @split_current
|
3159
|
-
line = line.
|
3451
|
+
line = line.bg("237") # Dark gray background to preserve foreground colors
|
3160
3452
|
end
|
3161
3453
|
|
3162
3454
|
lines << line
|
@@ -3676,13 +3968,17 @@ class HyperListApp
|
|
3676
3968
|
when "HOME" # Home
|
3677
3969
|
@current = 0
|
3678
3970
|
@offset = 0
|
3971
|
+
update_presentation_focus if @presentation_mode
|
3679
3972
|
when "END" # End
|
3680
3973
|
@current = get_visible_items.length - 1
|
3974
|
+
update_presentation_focus if @presentation_mode
|
3681
3975
|
when "g" # Go to top (was gg)
|
3682
3976
|
@current = 0
|
3683
3977
|
@offset = 0
|
3978
|
+
update_presentation_focus if @presentation_mode
|
3684
3979
|
when "G" # Go to bottom
|
3685
3980
|
@current = get_visible_items.length - 1
|
3981
|
+
update_presentation_focus if @presentation_mode
|
3686
3982
|
when "R" # Jump to reference (was gr)
|
3687
3983
|
jump_to_reference
|
3688
3984
|
when "F" # Open file reference (was gf)
|
@@ -3772,6 +4068,8 @@ class HyperListApp
|
|
3772
4068
|
toggle_checkbox
|
3773
4069
|
when "V"
|
3774
4070
|
toggle_checkbox_with_date
|
4071
|
+
when "\x05" # Ctrl-E for encryption toggle
|
4072
|
+
toggle_line_encryption
|
3775
4073
|
when "."
|
3776
4074
|
repeat_last_action
|
3777
4075
|
when "/"
|
data/hyperlist.gemspec
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
Gem::Specification.new do |spec|
|
2
2
|
spec.name = "hyperlist"
|
3
|
-
spec.version = "1.
|
3
|
+
spec.version = "1.1.0"
|
4
4
|
spec.authors = ["Geir Isene"]
|
5
5
|
spec.email = ["g@isene.com"]
|
6
6
|
|
7
7
|
spec.summary = "A powerful Terminal User Interface for HyperList management"
|
8
|
-
spec.description = "HyperList is a Ruby TUI application for creating, editing, and managing hierarchical lists using the HyperList methodology. It features rich syntax highlighting, advanced folding, powerful navigation, full editing capabilities, checkbox support, and multiple export formats."
|
8
|
+
spec.description = "HyperList is a Ruby TUI application for creating, editing, and managing hierarchical lists using the HyperList methodology. It features rich syntax highlighting, advanced folding, powerful navigation, full editing capabilities, checkbox support, encryption for sensitive data, enhanced presentation mode, and multiple export formats."
|
9
9
|
spec.homepage = "https://github.com/isene/HyperList"
|
10
10
|
spec.license = "Unlicense"
|
11
11
|
spec.required_ruby_version = ">= 3.0.0"
|
data/hyperlist_logo.svg
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
|
3
|
+
<defs>
|
4
|
+
<!-- Terminal glow effect -->
|
5
|
+
<filter id="glow">
|
6
|
+
<feGaussianBlur stdDeviation="3" result="coloredBlur"/>
|
7
|
+
<feMerge>
|
8
|
+
<feMergeNode in="coloredBlur"/>
|
9
|
+
<feMergeNode in="SourceGraphic"/>
|
10
|
+
</feMerge>
|
11
|
+
</filter>
|
12
|
+
<!-- Gradient for depth -->
|
13
|
+
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
|
14
|
+
<stop offset="0%" style="stop-color:#00ff88;stop-opacity:1" />
|
15
|
+
<stop offset="100%" style="stop-color:#00cc66;stop-opacity:1" />
|
16
|
+
</linearGradient>
|
17
|
+
</defs>
|
18
|
+
|
19
|
+
<!-- Dark terminal background -->
|
20
|
+
<rect width="512" height="512" fill="#0d1117"/>
|
21
|
+
|
22
|
+
<!-- Terminal frame with rounded corners -->
|
23
|
+
<rect x="32" y="32" width="448" height="448" rx="16" ry="16"
|
24
|
+
fill="none" stroke="#00ff88" stroke-width="3" opacity="0.8"/>
|
25
|
+
|
26
|
+
<!-- Terminal header bar -->
|
27
|
+
<rect x="32" y="32" width="448" height="40" rx="16" ry="16" fill="#161b22"/>
|
28
|
+
<circle cx="56" cy="52" r="6" fill="#ff5f56"/>
|
29
|
+
<circle cx="76" cy="52" r="6" fill="#ffbd2e"/>
|
30
|
+
<circle cx="96" cy="52" r="6" fill="#27c93f"/>
|
31
|
+
|
32
|
+
<!-- Main HyperList logo design -->
|
33
|
+
<g filter="url(#glow)">
|
34
|
+
<!-- Large H that forms into list structure -->
|
35
|
+
<!-- Left vertical of H -->
|
36
|
+
<rect x="96" y="120" width="24" height="200" fill="url(#grad)"/>
|
37
|
+
<!-- Right vertical of H -->
|
38
|
+
<rect x="200" y="120" width="24" height="200" fill="url(#grad)"/>
|
39
|
+
<!-- Horizontal crossbar of H -->
|
40
|
+
<rect x="96" y="208" width="128" height="24" fill="url(#grad)"/>
|
41
|
+
|
42
|
+
<!-- List items branching from H -->
|
43
|
+
<!-- Main branch -->
|
44
|
+
<rect x="224" y="208" width="160" height="4" fill="#00ff88" opacity="0.8"/>
|
45
|
+
|
46
|
+
<!-- List item 1 -->
|
47
|
+
<rect x="260" y="160" width="8" height="8" fill="#00ff88"/>
|
48
|
+
<rect x="276" y="162" width="100" height="4" fill="#00ff88" opacity="0.6"/>
|
49
|
+
|
50
|
+
<!-- List item 2 with checkbox -->
|
51
|
+
<rect x="260" y="190" width="12" height="12" fill="none" stroke="#00ff88" stroke-width="2"/>
|
52
|
+
<polyline points="263,196 266,199 270,193" stroke="#00ff88" stroke-width="2" fill="none"/>
|
53
|
+
<rect x="280" y="194" width="96" height="4" fill="#00ff88" opacity="0.6"/>
|
54
|
+
|
55
|
+
<!-- List item 3 -->
|
56
|
+
<rect x="260" y="220" width="8" height="8" fill="#00ff88"/>
|
57
|
+
<rect x="276" y="222" width="100" height="4" fill="#00ff88" opacity="0.6"/>
|
58
|
+
|
59
|
+
<!-- Nested item -->
|
60
|
+
<rect x="288" y="250" width="6" height="6" fill="#00cc66"/>
|
61
|
+
<rect x="300" y="251" width="76" height="3" fill="#00cc66" opacity="0.5"/>
|
62
|
+
|
63
|
+
<!-- List item 4 -->
|
64
|
+
<rect x="260" y="280" width="8" height="8" fill="#00ff88"/>
|
65
|
+
<rect x="276" y="282" width="100" height="4" fill="#00ff88" opacity="0.6"/>
|
66
|
+
|
67
|
+
<!-- Terminal cursor blink -->
|
68
|
+
<rect x="384" y="280" width="16" height="24" fill="#00ff88">
|
69
|
+
<animate attributeName="opacity" values="1;0;1" dur="1s" repeatCount="indefinite"/>
|
70
|
+
</rect>
|
71
|
+
</g>
|
72
|
+
|
73
|
+
<!-- Subtle terminal prompt at bottom -->
|
74
|
+
<text x="96" y="380" font-family="monospace" font-size="14" fill="#00ff88" opacity="0.4">
|
75
|
+
hyperlist v1.0.0 $
|
76
|
+
</text>
|
77
|
+
</svg>
|
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.1.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-08-
|
11
|
+
date: 2025-08-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rcurses
|
@@ -55,7 +55,7 @@ dependencies:
|
|
55
55
|
description: HyperList is a Ruby TUI application for creating, editing, and managing
|
56
56
|
hierarchical lists using the HyperList methodology. It features rich syntax highlighting,
|
57
57
|
advanced folding, powerful navigation, full editing capabilities, checkbox support,
|
58
|
-
and multiple export formats.
|
58
|
+
encryption for sensitive data, enhanced presentation mode, and multiple export formats.
|
59
59
|
email:
|
60
60
|
- g@isene.com
|
61
61
|
executables:
|
@@ -69,6 +69,7 @@ files:
|
|
69
69
|
- README.md
|
70
70
|
- hyperlist
|
71
71
|
- hyperlist.gemspec
|
72
|
+
- hyperlist_logo.svg
|
72
73
|
- sample.hl
|
73
74
|
- screenshot_help.png
|
74
75
|
- screenshot_sample.png
|