hyperlist 1.1.7 → 1.2.2
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 +33 -0
- data/README.md +22 -5
- data/hyperlist +371 -31
- data/hyperlist.gemspec +1 -1
- metadata +5 -13
- data/debug_instructions.md +0 -112
- data/diagnose.rb +0 -115
- data/diagnose_ruby34.rb +0 -145
- data/diagnose_ruby34_detailed.rb +0 -156
- data/diagnose_safe.rb +0 -198
- data/fix_terminal.sh +0 -33
- data/hyperlist_ruby34_wrapper.rb +0 -57
- data/rcurses_ruby34_patch.rb +0 -149
- /data/{hyperlist_logo.svg → img/hyperlist_logo.svg} +0 -0
- /data/{screenshot_help.png → img/screenshot_help.png} +0 -0
- /data/{screenshot_sample.png → img/screenshot_sample.png} +0 -0
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.2.2 - 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.2.2"
|
56
56
|
exit 0
|
57
57
|
end
|
58
58
|
|
@@ -64,13 +64,15 @@ require 'cgi'
|
|
64
64
|
require 'openssl'
|
65
65
|
require 'digest'
|
66
66
|
require 'base64'
|
67
|
+
require 'fileutils'
|
68
|
+
require 'json'
|
67
69
|
|
68
70
|
class HyperListApp
|
69
71
|
include Rcurses
|
70
72
|
include Rcurses::Input
|
71
73
|
include Rcurses::Cursor
|
72
74
|
|
73
|
-
VERSION = "1.
|
75
|
+
VERSION = "1.2.2"
|
74
76
|
|
75
77
|
def initialize(filename = nil)
|
76
78
|
@filename = filename ? File.expand_path(filename) : nil
|
@@ -98,6 +100,8 @@ class HyperListApp
|
|
98
100
|
@auto_save_enabled = false
|
99
101
|
@auto_save_interval = 60 # seconds
|
100
102
|
@last_auto_save = Time.now
|
103
|
+
@templates_dir = File.expand_path("~/.hyperlist/templates")
|
104
|
+
ensure_templates_dir
|
101
105
|
@templates = load_templates
|
102
106
|
@macro_recording = false
|
103
107
|
@macro_register = {} # Store macros by key
|
@@ -124,20 +128,24 @@ class HyperListApp
|
|
124
128
|
# Debug: uncomment to see terminal size
|
125
129
|
# puts "Terminal size: #{@rows}x#{@cols}"
|
126
130
|
|
127
|
-
#
|
131
|
+
# Setup UI first (to initialize @footer) before loading file
|
132
|
+
setup_ui
|
133
|
+
|
134
|
+
# Load file if provided (after UI is set up)
|
128
135
|
if @filename && File.exist?(@filename)
|
129
136
|
load_file(@filename)
|
130
137
|
else
|
131
138
|
# Start with empty list
|
132
139
|
@items = [{"text" => "New HyperList", "level" => 0, "fold" => false}]
|
133
140
|
end
|
134
|
-
|
135
|
-
setup_ui
|
136
141
|
end
|
137
142
|
|
138
143
|
def setup_ui
|
139
|
-
|
140
|
-
|
144
|
+
# Only do rcurses operations if initialized
|
145
|
+
if defined?(Rcurses.instance_variable_get) && Rcurses.instance_variable_get(:@initialized)
|
146
|
+
Rcurses.clear_screen
|
147
|
+
Cursor.hide
|
148
|
+
end
|
141
149
|
|
142
150
|
# Ensure we have the latest terminal size
|
143
151
|
if IO.console
|
@@ -190,6 +198,7 @@ class HyperListApp
|
|
190
198
|
@redo_position = []
|
191
199
|
|
192
200
|
# Check if file is encrypted (dot file or encrypted content)
|
201
|
+
is_encrypted = false
|
193
202
|
if is_encrypted_file?(file)
|
194
203
|
if content.start_with?("ENC:")
|
195
204
|
# Whole file is encrypted
|
@@ -200,7 +209,8 @@ class HyperListApp
|
|
200
209
|
return
|
201
210
|
end
|
202
211
|
lines = decrypted_content.split("\n")
|
203
|
-
@message = "File decrypted successfully"
|
212
|
+
@message = "File decrypted successfully (folded for privacy)"
|
213
|
+
is_encrypted = true
|
204
214
|
else
|
205
215
|
# Dot file but not encrypted yet - just load normally
|
206
216
|
lines = content.split("\n")
|
@@ -240,6 +250,17 @@ class HyperListApp
|
|
240
250
|
|
241
251
|
@items = [{"text" => "Empty file", "level" => 0, "fold" => false}] if @items.empty?
|
242
252
|
|
253
|
+
# Auto-fold for privacy/security on encrypted files
|
254
|
+
if is_encrypted
|
255
|
+
@fold_level = 0 # Set fold level to 0 (everything folded)
|
256
|
+
# Fold all items that have children
|
257
|
+
@items.each_with_index do |item, idx|
|
258
|
+
if has_children?(idx, @items)
|
259
|
+
item["fold"] = true
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
243
264
|
# Auto-fold deep levels for large files
|
244
265
|
if large_file
|
245
266
|
auto_fold_deep_levels(3) # Auto-fold everything deeper than level 3
|
@@ -838,6 +859,9 @@ class HyperListApp
|
|
838
859
|
|
839
860
|
# Helper method to safely apply regexes without corrupting ANSI sequences
|
840
861
|
def safe_regex_replace(text, pattern, &block)
|
862
|
+
# Ensure text is UTF-8 encoded
|
863
|
+
text = text.to_s.force_encoding('UTF-8')
|
864
|
+
|
841
865
|
# Find all ANSI sequences and replace with placeholders
|
842
866
|
ansi_sequences = []
|
843
867
|
placeholder_text = text.gsub(/\e\[[0-9;]*m/) do |match|
|
@@ -2169,7 +2193,7 @@ class HyperListApp
|
|
2169
2193
|
help_lines << help_line("#{"C-E".fg("10")}", "Encrypt/decrypt line", "#{"C-U".fg("10")}", "Toggle State/Trans underline")
|
2170
2194
|
help_lines << help_line("#{"R".fg("10")}", "Go to reference", "#{"F".fg("10")}", "Open file")
|
2171
2195
|
help_lines << help_line("#{"N".fg("10")}", "Next = marker", "#{"P".fg("10")}", "Presentation mode")
|
2172
|
-
help_lines << help_line("#{"t".fg("10")}", "Insert template", "#{":
|
2196
|
+
help_lines << help_line("#{"t".fg("10")}", "Insert template", "#{":st".fg("10")}", "Save as template")
|
2173
2197
|
help_lines << help_line("#{"Ma".fg("10")}", "Record macro 'a'", "#{"@a".fg("10")}", "Play macro 'a'")
|
2174
2198
|
help_lines << help_line("#{":vsplit".fg("10")}", "Split view vertically", "#{"w".fg("10")}", "Switch panes")
|
2175
2199
|
help_lines << ""
|
@@ -2180,6 +2204,11 @@ class HyperListApp
|
|
2180
2204
|
help_lines << help_line("#{":graph :g".fg("10")}", "Export to PNG graph", "#{":vsplit :vs".fg("10")}", "Split view")
|
2181
2205
|
help_lines << help_line("#{":as on".fg("10")}", "Enable autosave", "#{":as off".fg("10")}", "Disable autosave")
|
2182
2206
|
help_lines << help_line("#{":as N".fg("10")}", "Set interval (secs)", "#{":as".fg("10")}", "Show autosave status")
|
2207
|
+
help_lines << ""
|
2208
|
+
help_lines << "#{"TEMPLATES".fg("14")}"
|
2209
|
+
help_lines << help_line("#{":st".fg("10")}", "Save as template", "#{":dt".fg("10")}", "Delete template")
|
2210
|
+
help_lines << help_line("#{":lt".fg("10")}", "List user templates", "#{"t".fg("10")}", "Insert template")
|
2211
|
+
help_lines << ""
|
2183
2212
|
help_lines << help_line("#{"q".fg("10")}", "Quit (asks to save)", "#{"Q".fg("10")}", "Force quit")
|
2184
2213
|
help_lines << ""
|
2185
2214
|
help_lines << "#{"COLOR SCHEME".fg("14")}"
|
@@ -2853,8 +2882,18 @@ class HyperListApp
|
|
2853
2882
|
when "autosave", "as"
|
2854
2883
|
status = @auto_save_enabled ? "enabled" : "disabled"
|
2855
2884
|
@message = "Auto-save is #{status} (interval: #{@auto_save_interval}s)"
|
2856
|
-
when "t"
|
2885
|
+
when "t", "template", "templates"
|
2857
2886
|
show_templates
|
2887
|
+
when "save-template", "savetemplate", "st"
|
2888
|
+
save_as_template
|
2889
|
+
when /^save-template\s+(.+)/, /^savetemplate\s+(.+)/, /^st\s+(.+)/
|
2890
|
+
save_as_template($1.strip)
|
2891
|
+
when "delete-template", "deletetemplate", "dt"
|
2892
|
+
delete_template
|
2893
|
+
when /^delete-template\s+(.+)/, /^deletetemplate\s+(.+)/, /^dt\s+(.+)/
|
2894
|
+
delete_template($1.strip)
|
2895
|
+
when "list-templates", "listtemplates", "lt"
|
2896
|
+
show_template_manager(:list)
|
2858
2897
|
when "foldlevel"
|
2859
2898
|
level = @footer.ask("Fold to level (0-9): ", "")
|
2860
2899
|
if level =~ /^[0-9]$/
|
@@ -3085,8 +3124,24 @@ class HyperListApp
|
|
3085
3124
|
end
|
3086
3125
|
|
3087
3126
|
def encrypt_string(text, password = nil)
|
3088
|
-
password
|
3089
|
-
|
3127
|
+
if password.nil? && @encryption_key.nil?
|
3128
|
+
# First time encrypting - ask for password twice
|
3129
|
+
password = prompt_password("Enter encryption password: ")
|
3130
|
+
return nil unless password
|
3131
|
+
|
3132
|
+
confirm = prompt_password("Confirm encryption password: ")
|
3133
|
+
return nil unless confirm
|
3134
|
+
|
3135
|
+
if password != confirm
|
3136
|
+
@message = "Passwords do not match - encryption cancelled"
|
3137
|
+
return nil
|
3138
|
+
end
|
3139
|
+
|
3140
|
+
@encryption_key = password
|
3141
|
+
else
|
3142
|
+
password ||= @encryption_key || prompt_password("Enter encryption password: ")
|
3143
|
+
return nil unless password
|
3144
|
+
end
|
3090
3145
|
|
3091
3146
|
# Store key for session if not already stored
|
3092
3147
|
@encryption_key ||= password
|
@@ -3129,7 +3184,10 @@ class HyperListApp
|
|
3129
3184
|
cipher.key = key
|
3130
3185
|
cipher.iv = iv
|
3131
3186
|
|
3132
|
-
cipher.update(encrypted) + cipher.final
|
3187
|
+
decrypted = cipher.update(encrypted) + cipher.final
|
3188
|
+
# Force UTF-8 encoding on decrypted content
|
3189
|
+
decrypted.force_encoding('UTF-8')
|
3190
|
+
decrypted
|
3133
3191
|
rescue => e
|
3134
3192
|
@message = "Decryption failed. Wrong password?"
|
3135
3193
|
nil
|
@@ -3152,8 +3210,24 @@ class HyperListApp
|
|
3152
3210
|
end
|
3153
3211
|
|
3154
3212
|
def encrypt_file(content, password = nil)
|
3155
|
-
password
|
3156
|
-
|
3213
|
+
if password.nil? && @encryption_key.nil?
|
3214
|
+
# First time encrypting - ask for password twice
|
3215
|
+
password = prompt_password("Enter encryption password: ")
|
3216
|
+
return nil unless password
|
3217
|
+
|
3218
|
+
confirm = prompt_password("Confirm encryption password: ")
|
3219
|
+
return nil unless confirm
|
3220
|
+
|
3221
|
+
if password != confirm
|
3222
|
+
@message = "Passwords do not match - encryption cancelled"
|
3223
|
+
return nil
|
3224
|
+
end
|
3225
|
+
|
3226
|
+
@encryption_key = password
|
3227
|
+
else
|
3228
|
+
password ||= @encryption_key || prompt_password("Enter file encryption password: ")
|
3229
|
+
return nil unless password
|
3230
|
+
end
|
3157
3231
|
|
3158
3232
|
encrypt_string(content, password)
|
3159
3233
|
end
|
@@ -3162,6 +3236,9 @@ class HyperListApp
|
|
3162
3236
|
password ||= @encryption_key || prompt_password("Enter file decryption password: ")
|
3163
3237
|
return nil unless password
|
3164
3238
|
|
3239
|
+
# Store the key for the session so line encryption can use it
|
3240
|
+
@encryption_key = password
|
3241
|
+
|
3165
3242
|
decrypt_string(encrypted_content, password)
|
3166
3243
|
end
|
3167
3244
|
|
@@ -3186,13 +3263,16 @@ class HyperListApp
|
|
3186
3263
|
end
|
3187
3264
|
else
|
3188
3265
|
# Encrypt the line
|
3189
|
-
|
3266
|
+
# Use the existing encryption key if available (from file decryption)
|
3267
|
+
encrypted = encrypt_string(current_text, @encryption_key)
|
3190
3268
|
if encrypted
|
3191
3269
|
save_undo_state
|
3192
3270
|
item["text"] = encrypted
|
3193
3271
|
@encrypted_lines[@current] = true
|
3194
3272
|
@modified = true
|
3195
|
-
@message = "Line encrypted"
|
3273
|
+
@message = "Line encrypted (🔒 indicator shown)"
|
3274
|
+
# Clear the processed cache to force re-rendering
|
3275
|
+
@processed_cache = {}
|
3196
3276
|
else
|
3197
3277
|
@message = "Encryption cancelled"
|
3198
3278
|
end
|
@@ -3202,8 +3282,13 @@ class HyperListApp
|
|
3202
3282
|
end
|
3203
3283
|
end
|
3204
3284
|
|
3285
|
+
def ensure_templates_dir
|
3286
|
+
FileUtils.mkdir_p(@templates_dir) unless File.exist?(@templates_dir)
|
3287
|
+
end
|
3288
|
+
|
3205
3289
|
def load_templates
|
3206
|
-
|
3290
|
+
# Start with built-in templates
|
3291
|
+
templates = {
|
3207
3292
|
"project" => [
|
3208
3293
|
{"text" => "Project: =Project Name=", "level" => 0},
|
3209
3294
|
{"text" => "[_] Define project scope", "level" => 1},
|
@@ -3306,6 +3391,214 @@ class HyperListApp
|
|
3306
3391
|
{"text" => "=Tips, variations, serving suggestions=", "level" => 2}
|
3307
3392
|
]
|
3308
3393
|
}
|
3394
|
+
|
3395
|
+
# Load user templates from templates directory
|
3396
|
+
if File.directory?(@templates_dir)
|
3397
|
+
Dir.glob(File.join(@templates_dir, "*.hlt")).each do |template_file|
|
3398
|
+
template_name = File.basename(template_file, ".hlt")
|
3399
|
+
begin
|
3400
|
+
template_data = JSON.parse(File.read(template_file))
|
3401
|
+
# Convert template data to the expected format
|
3402
|
+
if template_data.is_a?(Hash) && template_data["items"]
|
3403
|
+
templates[template_name] = template_data["items"].map do |item|
|
3404
|
+
{"text" => item["text"], "level" => item["level"]}
|
3405
|
+
end
|
3406
|
+
end
|
3407
|
+
rescue => e
|
3408
|
+
# Skip invalid template files
|
3409
|
+
end
|
3410
|
+
end
|
3411
|
+
end
|
3412
|
+
|
3413
|
+
templates
|
3414
|
+
end
|
3415
|
+
|
3416
|
+
def save_as_template(name = nil)
|
3417
|
+
# Ask for template name if not provided
|
3418
|
+
if name.nil? || name.empty?
|
3419
|
+
name = @footer.ask("Template name: ", "")
|
3420
|
+
return if name.nil? || name.empty?
|
3421
|
+
end
|
3422
|
+
|
3423
|
+
# Sanitize template name
|
3424
|
+
name = name.gsub(/[^a-zA-Z0-9_-]/, '_')
|
3425
|
+
|
3426
|
+
# Ask for description
|
3427
|
+
description = @footer.ask("Template description (optional): ", "")
|
3428
|
+
|
3429
|
+
# Prepare template data
|
3430
|
+
template_data = {
|
3431
|
+
"name" => name,
|
3432
|
+
"description" => description,
|
3433
|
+
"created" => Time.now.strftime("%Y-%m-%dT%H:%M:%S"),
|
3434
|
+
"items" => @items.map do |item|
|
3435
|
+
{
|
3436
|
+
"text" => item["text"],
|
3437
|
+
"level" => item["level"]
|
3438
|
+
}
|
3439
|
+
end
|
3440
|
+
}
|
3441
|
+
|
3442
|
+
# Save template file
|
3443
|
+
template_path = File.join(@templates_dir, "#{name}.hlt")
|
3444
|
+
|
3445
|
+
# Check if template already exists
|
3446
|
+
if File.exist?(template_path)
|
3447
|
+
response = @footer.ask("Template '#{name}' already exists. Overwrite? (y/n): ", "")
|
3448
|
+
return unless response.downcase == 'y'
|
3449
|
+
end
|
3450
|
+
|
3451
|
+
File.write(template_path, JSON.pretty_generate(template_data))
|
3452
|
+
@message = "Template '#{name}' saved successfully"
|
3453
|
+
end
|
3454
|
+
|
3455
|
+
def delete_template(name = nil)
|
3456
|
+
# Get list of user templates
|
3457
|
+
user_templates = get_user_templates
|
3458
|
+
|
3459
|
+
if user_templates.empty?
|
3460
|
+
@message = "No user templates found"
|
3461
|
+
return
|
3462
|
+
end
|
3463
|
+
|
3464
|
+
# Show template list if name not provided
|
3465
|
+
if name.nil? || name.empty?
|
3466
|
+
show_template_manager(:delete)
|
3467
|
+
return
|
3468
|
+
end
|
3469
|
+
|
3470
|
+
template_path = File.join(@templates_dir, "#{name}.hlt")
|
3471
|
+
|
3472
|
+
if File.exist?(template_path)
|
3473
|
+
response = @footer.ask("Delete template '#{name}'? (y/n): ", "")
|
3474
|
+
if response.downcase == 'y'
|
3475
|
+
File.delete(template_path)
|
3476
|
+
@message = "Template '#{name}' deleted"
|
3477
|
+
# Reload templates
|
3478
|
+
@templates = load_templates
|
3479
|
+
end
|
3480
|
+
else
|
3481
|
+
@message = "Template '#{name}' not found"
|
3482
|
+
end
|
3483
|
+
end
|
3484
|
+
|
3485
|
+
def get_user_templates
|
3486
|
+
templates = []
|
3487
|
+
if File.directory?(@templates_dir)
|
3488
|
+
Dir.glob(File.join(@templates_dir, "*.hlt")).each do |template_file|
|
3489
|
+
template_name = File.basename(template_file, ".hlt")
|
3490
|
+
begin
|
3491
|
+
template_data = JSON.parse(File.read(template_file))
|
3492
|
+
templates << {
|
3493
|
+
"name" => template_name,
|
3494
|
+
"description" => template_data["description"] || "",
|
3495
|
+
"created" => template_data["created"] || "",
|
3496
|
+
"path" => template_file
|
3497
|
+
}
|
3498
|
+
rescue => e
|
3499
|
+
# Skip invalid template files
|
3500
|
+
end
|
3501
|
+
end
|
3502
|
+
end
|
3503
|
+
templates.sort_by { |t| t["name"] }
|
3504
|
+
end
|
3505
|
+
|
3506
|
+
def show_template_manager(action = :list)
|
3507
|
+
# Store original state
|
3508
|
+
original_state = {
|
3509
|
+
items: @items.dup,
|
3510
|
+
current: @current,
|
3511
|
+
offset: @offset,
|
3512
|
+
filename: @filename,
|
3513
|
+
modified: @modified
|
3514
|
+
}
|
3515
|
+
|
3516
|
+
user_templates = get_user_templates
|
3517
|
+
|
3518
|
+
# Build template manager view
|
3519
|
+
@items = []
|
3520
|
+
case action
|
3521
|
+
when :delete
|
3522
|
+
@items << {"text" => "DELETE TEMPLATE (press Enter to delete, q to cancel)", "level" => 0, "fold" => false, "raw" => true}
|
3523
|
+
else
|
3524
|
+
@items << {"text" => "USER TEMPLATES (press Enter to select, q to cancel)", "level" => 0, "fold" => false, "raw" => true}
|
3525
|
+
end
|
3526
|
+
@items << {"text" => "="*50, "level" => 0, "fold" => false, "raw" => true}
|
3527
|
+
|
3528
|
+
if user_templates.empty?
|
3529
|
+
@items << {"text" => "No user templates found", "level" => 0, "fold" => false, "raw" => true}
|
3530
|
+
else
|
3531
|
+
user_templates.each_with_index do |template, idx|
|
3532
|
+
desc = template["description"].empty? ? "No description" : template["description"]
|
3533
|
+
created = template["created"].empty? ? "" : " (#{Time.parse(template["created"]).strftime('%Y-%m-%d')})"
|
3534
|
+
@items << {
|
3535
|
+
"text" => "#{idx+1}. #{template["name"]}: #{desc}#{created}",
|
3536
|
+
"level" => 0,
|
3537
|
+
"fold" => false,
|
3538
|
+
"raw" => true,
|
3539
|
+
"template_name" => template["name"],
|
3540
|
+
"template_action" => action
|
3541
|
+
}
|
3542
|
+
end
|
3543
|
+
end
|
3544
|
+
|
3545
|
+
@current = 2 # Start at first template
|
3546
|
+
@offset = 0
|
3547
|
+
|
3548
|
+
selected_template = nil
|
3549
|
+
exit_loop = false
|
3550
|
+
|
3551
|
+
# Template manager loop
|
3552
|
+
while !exit_loop
|
3553
|
+
begin
|
3554
|
+
render_main
|
3555
|
+
footer_text = case action
|
3556
|
+
when :delete
|
3557
|
+
"Delete Template | Enter: delete | q: cancel | j/k: navigate"
|
3558
|
+
else
|
3559
|
+
"User Templates | Enter: select | q: cancel | j/k: navigate"
|
3560
|
+
end
|
3561
|
+
@footer.text = footer_text
|
3562
|
+
@footer.refresh
|
3563
|
+
|
3564
|
+
c = getchr
|
3565
|
+
next if c.nil?
|
3566
|
+
|
3567
|
+
case c
|
3568
|
+
when "q", "ESC", "C-c", "Q"
|
3569
|
+
exit_loop = true
|
3570
|
+
when "j", "DOWN"
|
3571
|
+
@current = [@current + 1, @items.length - 1].min
|
3572
|
+
when "k", "UP"
|
3573
|
+
@current = [@current - 1, 2].max
|
3574
|
+
when "ENTER", "RETURN", "\n", "\r"
|
3575
|
+
if @current >= 2 && @current < @items.length
|
3576
|
+
item = @items[@current]
|
3577
|
+
if item && item.is_a?(Hash) && item["template_name"]
|
3578
|
+
selected_template = item["template_name"]
|
3579
|
+
exit_loop = true
|
3580
|
+
end
|
3581
|
+
end
|
3582
|
+
end
|
3583
|
+
rescue => e
|
3584
|
+
@message = "Error in template manager: #{e.message}"
|
3585
|
+
exit_loop = true
|
3586
|
+
end
|
3587
|
+
end
|
3588
|
+
|
3589
|
+
# Restore original state
|
3590
|
+
@items = original_state[:items]
|
3591
|
+
@current = original_state[:current]
|
3592
|
+
@offset = original_state[:offset]
|
3593
|
+
@filename = original_state[:filename]
|
3594
|
+
@modified = original_state[:modified]
|
3595
|
+
|
3596
|
+
# Process selected template
|
3597
|
+
if selected_template && action == :delete
|
3598
|
+
delete_template(selected_template)
|
3599
|
+
end
|
3600
|
+
|
3601
|
+
selected_template
|
3309
3602
|
end
|
3310
3603
|
|
3311
3604
|
def show_templates
|
@@ -3318,24 +3611,33 @@ class HyperListApp
|
|
3318
3611
|
modified: @modified
|
3319
3612
|
}
|
3320
3613
|
|
3321
|
-
# Create template selection view
|
3614
|
+
# Create template selection view combining built-in and user templates
|
3322
3615
|
template_list = [
|
3323
|
-
["project", "Project Plan - Complete project management template"],
|
3324
|
-
["meeting", "Meeting Agenda - Structure for meeting notes"],
|
3325
|
-
["daily", "Daily Planner - Daily task and schedule template"],
|
3326
|
-
["checklist", "Simple Checklist - Basic checkbox list"],
|
3327
|
-
["brainstorm", "Brainstorming Session - Idea generation template"],
|
3328
|
-
["recipe", "Recipe - Cooking recipe structure"]
|
3616
|
+
["project", "Project Plan - Complete project management template", "built-in"],
|
3617
|
+
["meeting", "Meeting Agenda - Structure for meeting notes", "built-in"],
|
3618
|
+
["daily", "Daily Planner - Daily task and schedule template", "built-in"],
|
3619
|
+
["checklist", "Simple Checklist - Basic checkbox list", "built-in"],
|
3620
|
+
["brainstorm", "Brainstorming Session - Idea generation template", "built-in"],
|
3621
|
+
["recipe", "Recipe - Cooking recipe structure", "built-in"]
|
3329
3622
|
]
|
3330
3623
|
|
3624
|
+
# Add user templates to the list
|
3625
|
+
user_templates = get_user_templates
|
3626
|
+
user_templates.each do |template|
|
3627
|
+
desc = template["description"].empty? ? "User template" : template["description"]
|
3628
|
+
template_list << [template["name"], desc, "user"]
|
3629
|
+
end
|
3630
|
+
|
3331
3631
|
# Build template selection items
|
3332
3632
|
@items = []
|
3333
3633
|
@items << {"text" => "TEMPLATES (press Enter to insert, q to cancel)", "level" => 0, "fold" => false, "raw" => true}
|
3334
3634
|
@items << {"text" => "="*50, "level" => 0, "fold" => false, "raw" => true}
|
3335
3635
|
|
3336
|
-
|
3636
|
+
# Add built-in templates section
|
3637
|
+
@items << {"text" => "BUILT-IN TEMPLATES:", "level" => 0, "fold" => false, "raw" => true}
|
3638
|
+
template_list.select { |t| t[2] == "built-in" }.each_with_index do |(key, desc, type), idx|
|
3337
3639
|
@items << {
|
3338
|
-
"text" => "#{idx+1}. #{key.capitalize}: #{desc}",
|
3640
|
+
"text" => " #{idx+1}. #{key.capitalize}: #{desc}",
|
3339
3641
|
"level" => 0,
|
3340
3642
|
"fold" => false,
|
3341
3643
|
"raw" => true,
|
@@ -3343,6 +3645,21 @@ class HyperListApp
|
|
3343
3645
|
}
|
3344
3646
|
end
|
3345
3647
|
|
3648
|
+
# Add user templates section if any exist
|
3649
|
+
if user_templates.any?
|
3650
|
+
@items << {"text" => "", "level" => 0, "fold" => false, "raw" => true}
|
3651
|
+
@items << {"text" => "USER TEMPLATES:", "level" => 0, "fold" => false, "raw" => true}
|
3652
|
+
template_list.select { |t| t[2] == "user" }.each_with_index do |(key, desc, type), idx|
|
3653
|
+
@items << {
|
3654
|
+
"text" => " #{key}: #{desc}",
|
3655
|
+
"level" => 0,
|
3656
|
+
"fold" => false,
|
3657
|
+
"raw" => true,
|
3658
|
+
"template_key" => key
|
3659
|
+
}
|
3660
|
+
end
|
3661
|
+
end
|
3662
|
+
|
3346
3663
|
@current = 2 # Start at first template
|
3347
3664
|
@offset = 0
|
3348
3665
|
|
@@ -4252,6 +4569,19 @@ class HyperListApp
|
|
4252
4569
|
end
|
4253
4570
|
|
4254
4571
|
def run
|
4572
|
+
# Initialize rcurses explicitly (no longer auto-initialized)
|
4573
|
+
Rcurses.init!
|
4574
|
+
|
4575
|
+
# Flush any buffered input (like the Enter key from running the command)
|
4576
|
+
while IO.select([$stdin], nil, nil, 0)
|
4577
|
+
$stdin.read_nonblock(1024) rescue break
|
4578
|
+
end
|
4579
|
+
|
4580
|
+
# Now that rcurses is initialized, set up the UI properly
|
4581
|
+
Rcurses.clear_screen
|
4582
|
+
Cursor.hide
|
4583
|
+
setup_ui
|
4584
|
+
|
4255
4585
|
render
|
4256
4586
|
|
4257
4587
|
loop do
|
@@ -4260,6 +4590,14 @@ class HyperListApp
|
|
4260
4590
|
|
4261
4591
|
c = getchr
|
4262
4592
|
|
4593
|
+
# Skip nil input (shouldn't happen normally)
|
4594
|
+
next if c.nil?
|
4595
|
+
|
4596
|
+
# Skip newline at the very start (buffered from command execution)
|
4597
|
+
if c == "\n" && @last_key.nil?
|
4598
|
+
next
|
4599
|
+
end
|
4600
|
+
|
4263
4601
|
# Track last key for double-key combinations
|
4264
4602
|
prev_key = @last_key
|
4265
4603
|
@last_key = c
|
@@ -4550,13 +4888,15 @@ class HyperListApp
|
|
4550
4888
|
quit
|
4551
4889
|
ensure
|
4552
4890
|
Cursor.show
|
4553
|
-
|
4891
|
+
# Screen clearing disabled for debugging
|
4892
|
+
# Rcurses.clear_screen
|
4554
4893
|
end
|
4555
4894
|
end
|
4556
4895
|
|
4557
4896
|
# Main
|
4558
|
-
|
4559
|
-
|
4897
|
+
# When installed as a gem executable, this file is loaded (not executed directly)
|
4898
|
+
# so we can't use __FILE__ == $0. Just run unless we already handled help/version.
|
4899
|
+
unless ARGV[0] == '-h' || ARGV[0] == '--help' || ARGV[0] == '-v' || ARGV[0] == '--version'
|
4560
4900
|
app = HyperListApp.new(ARGV[0])
|
4561
4901
|
app.run
|
4562
4902
|
end
|
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.2.2
|
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-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rcurses
|
@@ -73,20 +73,12 @@ files:
|
|
73
73
|
- CHANGELOG.md
|
74
74
|
- LICENSE
|
75
75
|
- README.md
|
76
|
-
- debug_instructions.md
|
77
|
-
- diagnose.rb
|
78
|
-
- diagnose_ruby34.rb
|
79
|
-
- diagnose_ruby34_detailed.rb
|
80
|
-
- diagnose_safe.rb
|
81
|
-
- fix_terminal.sh
|
82
76
|
- hyperlist
|
83
77
|
- hyperlist.gemspec
|
84
|
-
- hyperlist_logo.svg
|
85
|
-
-
|
86
|
-
-
|
78
|
+
- img/hyperlist_logo.svg
|
79
|
+
- img/screenshot_help.png
|
80
|
+
- img/screenshot_sample.png
|
87
81
|
- sample.hl
|
88
|
-
- screenshot_help.png
|
89
|
-
- screenshot_sample.png
|
90
82
|
- test.hl
|
91
83
|
homepage: https://github.com/isene/HyperList
|
92
84
|
licenses:
|