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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 134e2668f610e62a6235ba3ca1a629237321a62b175454cf7b26d1f365e2a2e5
4
- data.tar.gz: 1c82ce134d1ce3a91ced0cd0e041c7285721530cd7bd7c802d59b3a850b9a1d2
3
+ metadata.gz: f04bfa860db3f25e2c0327b2a5b81e35ac1903f87a5516b077789dc9a98a96cd
4
+ data.tar.gz: 6c62b2f854dede524a162c34fa37c53e941173e1e35f2516b1e809e7092db7ae
5
5
  SHA512:
6
- metadata.gz: 60af0235c69750163718233ced6d74436fa3fbf5716ca2cfcc38f8c80bea242193131fe266b1aebaf47c9c95b517df881b0da443cc0a8f7d93c7af74f85cafa7
7
- data.tar.gz: dd9f54b1f74513e947f5d9b89a72ff2abe1ccab6af3d4f1c5b607b6e366806b6fd4aca7b3c612b8f469d0bd57c023f1a7b53769f0befa8453575b46f70d84042
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 Ruby TUI
1
+ # HyperList TUI
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/hyperlist.svg)](https://badge.fury.io/rb/hyperlist)
4
+ [![License](https://img.shields.io/badge/License-Public%20Domain-brightgreen.svg)](https://unlicense.org/)
5
+ [![Ruby](https://img.shields.io/badge/Ruby-3.0%2B-red.svg)](https://www.ruby-lang.org/)
6
+ [![GitHub stars](https://img.shields.io/github/stars/isene/HyperList.svg)](https://github.com/isene/HyperList/stargazers)
7
+ [![Stay Amazing](https://img.shields.io/badge/Stay-Amazing-blue.svg)](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
+ ![HyperList Main View](screenshot_sample.png)
27
+
28
+ ### Help Screen
29
+ ![HyperList Help](screenshot_help.png)
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 and ancestors only
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.1 - Terminal User Interface for HyperList files
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.1"
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.1"
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
- lines = File.readlines(file) rescue []
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
- File.open(@filename, 'w') do |f|
350
- @items.each do |item|
351
- f.puts("\t" * item["level"] + item["text"])
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
- line += "▶".fg("245") + " " # Gray triangle for collapsed (has hidden children)
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
- line += "▷".fg("245") + " " # Gray triangle for expanded (has visible children)
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
- processed_text_key = has_match ? "search_#{item['text']}_#{@search}" : "text_#{item['text']}"
703
- if @processed_cache[processed_text_key] && !has_match
704
- line += @processed_cache[processed_text_key]
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
- processed = process_text(item["text"], has_match)
707
- @processed_cache[processed_text_key] = processed if @processed_cache.size < 1000 && !has_match
708
- line += processed
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
- @processed_cache[cache_key] = line if idx != @current
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.r # Always use reverse (white background) for current 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("226").fg("0") } # Yellow background, black text
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 all items
1212
+ # Exit presentation mode - restore normal view
1120
1213
  @presentation_mode = false
1121
- @items.each { |item| item["fold"] = false }
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 - show only current item and ancestors
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
- show_only_current_and_ancestors
1127
- @message = "Presentation mode enabled"
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 show_only_current_and_ancestors
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.each { |item| item["fold"] = true }
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
- # Unfold current item
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
- ancestor_level = current_level - 1
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 && ancestor_level >= 0
1150
- if @items[idx]["level"] == ancestor_level
1274
+ while idx >= 0 && search_level >= 0
1275
+ if @items[idx]["level"] == search_level
1151
1276
  @items[idx]["fold"] = false
1152
- ancestor_level -= 1
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
- # Unfold immediate children of current item
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]["fold"] = false
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("#{"R".fg("10")}", "Go to reference", "#{"F".fg("10")}", "Open file")
1887
- help_lines << help_line("#{"N".fg("10")}", "Next = marker", "#{"P".fg("10")}", "Presentation mode")
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.r # Always use reverse (white background) for current 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.0.0"
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"
@@ -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.0.0
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-12 00:00:00.000000000 Z
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