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.
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.1.7 - Terminal User Interface for HyperList files
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.1.7"
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.1.7"
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
- # Load file if provided
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
- Rcurses.clear_screen
140
- Cursor.hide
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", "#{":template".fg("10")}", "Show templates")
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 ||= @encryption_key || prompt_password("Enter encryption password: ")
3089
- return nil unless password
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 ||= @encryption_key || prompt_password("Enter file encryption password: ")
3156
- return nil unless password
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
- encrypted = encrypt_string(current_text)
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
- template_list.each_with_index do |(key, desc), idx|
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
- Rcurses.clear_screen
4891
+ # Screen clearing disabled for debugging
4892
+ # Rcurses.clear_screen
4554
4893
  end
4555
4894
  end
4556
4895
 
4557
4896
  # Main
4558
- if __FILE__ == $0
4559
- # Normal operation - help/version already handled at top of file
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
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |spec|
2
2
  spec.name = "hyperlist"
3
- spec.version = "1.1.7"
3
+ spec.version = "1.2.2"
4
4
  spec.authors = ["Geir Isene"]
5
5
  spec.email = ["g@isene.com"]
6
6
 
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.1.7
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-14 00:00:00.000000000 Z
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
- - hyperlist_ruby34_wrapper.rb
86
- - rcurses_ruby34_patch.rb
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: