heathrow 0.7.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: af875b0f7d9ee70cf0f340207d21b5d367e6a8c98464361560c8b7649b2b6195
4
- data.tar.gz: 30cc45fe89911cfe787a32a1e36e6d5d7a41a8e407779a2fb73cb0428b1b2e75
3
+ metadata.gz: 9c40dfa3681941910b0e8ac358021003220ee9e88b9861b1c20b735bb0444623
4
+ data.tar.gz: f2acadd2ea11c58fa522db1abf94ebdb31a20fd0fc346eac94aefac38a9fe847
5
5
  SHA512:
6
- metadata.gz: b2d822a3c2a09d17e13b8f579f4a91fe896145c303be3c51620dc059888ca710c733c302d705eaa7ecd0c709b79bb4524c831bc7f66aff937f39779b3c886c8a
7
- data.tar.gz: 7ff0e102006c3bb8c33e527612890832120b67f9e3300840d20419bb04e67c0b50afd9b1896e951025c2abd913fe119b959ddf19554b1eb3a6947209a07341f8
6
+ metadata.gz: '084222a43672e22966f1716b62a59d7d5961a5148c8a47f858c08cb3a33d863fb1933c74649792ae4a8945fb0a36bbbb6fcbfbe46bdb1e9ed045874708530f95'
7
+ data.tar.gz: 31b2b552ea905f4cdb463f347cc7855d7f6d7b0b9125e4ba903927b20002e9a87b38754a1b2cedf9c519eb37a9692e805dd0ef66b9c25fc85958e0994f214e81
data/.gitignore CHANGED
@@ -18,17 +18,26 @@
18
18
  /lib/bundler/man/
19
19
  .bundle/
20
20
 
21
- # Chitt specific
22
- ~/.chitt/
21
+ # Heathrow data
22
+ ~/.heathrow/
23
23
  *.db
24
24
  *.db-shm
25
25
  *.db-wal
26
26
  /logs/
27
27
  /attachments/
28
28
 
29
- # Environment
29
+ # Secrets and credentials
30
30
  .env
31
31
  .env.local
32
+ .env.*
33
+ *.pem
34
+ *.key
35
+ *.p12
36
+ *.pfx
37
+ credentials.json
38
+ token.json
39
+ *_secret*
40
+ *_token*
32
41
 
33
42
  # Editor directories
34
43
  .vscode/
@@ -27,7 +27,8 @@ module Heathrow
27
27
  # Compose a forward of a message
28
28
  def compose_forward
29
29
  template = build_forward_template
30
- content = edit_in_editor(template)
30
+ # Cursor on "To: " line (line 2)
31
+ content = edit_in_editor(template, cursor_line: 2)
31
32
  return nil if content.nil? || content.strip.empty?
32
33
  parse_composed_message(content)
33
34
  end
@@ -3702,6 +3702,7 @@ module Heathrow
3702
3702
  def show_folder_browser
3703
3703
  @in_folder_browser = true
3704
3704
  @folder_browser_index = 0
3705
+ @panes[:top].bg = @topcolor
3705
3706
  @folder_collapsed ||= {}
3706
3707
  @folder_count_cache = {} # Fresh counts each time
3707
3708
  @browser_favorites = nil # Fresh favorites read
@@ -3969,6 +3970,7 @@ module Heathrow
3969
3970
  favorites = get_favorite_folders
3970
3971
  @in_folder_browser = true
3971
3972
  @folder_browser_index = 0
3973
+ @panes[:top].bg = @topcolor
3972
3974
  @folder_count_cache = {} # Fresh counts each time
3973
3975
 
3974
3976
  # Build display from favorites only (no DB queries — counts fetched on select)
@@ -4250,6 +4252,13 @@ module Heathrow
4250
4252
  msg['folder'] = dest
4251
4253
  msg['metadata'] = metadata
4252
4254
  msg['labels'] = labels
4255
+
4256
+ # Mark as read (we've seen it if we're filing it)
4257
+ if msg['is_read'].to_i == 0
4258
+ @db.mark_as_read(msg['id'])
4259
+ msg['is_read'] = 1
4260
+ sync_maildir_flag(msg, 'S', true)
4261
+ end
4253
4262
  end
4254
4263
 
4255
4264
  # ── Save by browsing folders (sB) ──
@@ -4517,7 +4526,7 @@ module Heathrow
4517
4526
  def ai_assistant
4518
4527
  msg = current_message
4519
4528
  return unless msg && !msg['is_header'] && !msg['is_channel_header'] && !msg['is_thread_header']
4520
- ensure_full_message(msg)
4529
+ msg = ensure_full_message(msg)
4521
4530
 
4522
4531
  # Build message context
4523
4532
  context = ai_message_context(msg)
@@ -4963,9 +4972,19 @@ module Heathrow
4963
4972
  end
4964
4973
 
4965
4974
  def ensure_full_message(msg)
4966
- if msg && msg['id'] && !msg.key?('content') && !msg['is_header']
4975
+ if msg && msg['id'] && !msg['_full_loaded'] && !msg['is_header']
4967
4976
  full = @db.get_message(msg['id'])
4968
- msg.merge!(full) if full
4977
+ if full
4978
+ if msg.frozen?
4979
+ # Replace frozen hash in filtered_messages with mutable copy
4980
+ idx = @filtered_messages.index { |m| m.equal?(msg) }
4981
+ msg = full.merge('_full_loaded' => true)
4982
+ @filtered_messages[idx] = msg if idx
4983
+ else
4984
+ msg.merge!(full)
4985
+ msg['_full_loaded'] = true
4986
+ end
4987
+ end
4969
4988
  end
4970
4989
  msg
4971
4990
  end
@@ -4975,7 +4994,7 @@ module Heathrow
4975
4994
  def reply_to_message(force_editor: false)
4976
4995
  msg = current_message
4977
4996
  return unless msg
4978
- ensure_full_message(msg)
4997
+ msg = ensure_full_message(msg)
4979
4998
 
4980
4999
  # Don't allow replying to header messages
4981
5000
  if msg['is_header'] || msg['is_channel_header'] || msg['is_thread_header'] || msg['is_dm_header']
@@ -5004,15 +5023,15 @@ module Heathrow
5004
5023
  @panes[:bottom].refresh
5005
5024
 
5006
5025
  composed = composer.compose_reply(false)
5026
+ setup_display
5027
+ create_panes
5028
+ render_all
5007
5029
  if composed
5008
5030
  finalize_compose(source, composed, "Reply cancelled")
5009
5031
  else
5010
5032
  set_feedback("Reply cancelled", 245, 1)
5011
5033
  end
5012
5034
  end
5013
-
5014
- render_message_list
5015
- render_bottom_bar
5016
5035
  end
5017
5036
 
5018
5037
  def chat_reply_context(msg, source_type)
@@ -5088,7 +5107,7 @@ module Heathrow
5088
5107
  def reply_all_to_message
5089
5108
  msg = current_message
5090
5109
  return unless msg
5091
- ensure_full_message(msg)
5110
+ msg = ensure_full_message(msg)
5092
5111
  source_id = msg['source_id']
5093
5112
 
5094
5113
  # Check if source supports replying
@@ -5106,20 +5125,21 @@ module Heathrow
5106
5125
  # Compose the reply
5107
5126
  composed = composer.compose_reply(true)
5108
5127
 
5128
+ setup_display
5129
+ create_panes
5130
+ render_all
5109
5131
  if composed
5110
5132
  finalize_compose(source, composed, "Reply-all cancelled")
5111
5133
  else
5112
5134
  set_feedback("Reply-all cancelled", 245, 1)
5113
5135
  end
5114
-
5115
- render_bottom_bar
5116
5136
  end
5117
5137
 
5118
5138
  def edit_message_content
5119
5139
  msg = current_message
5120
5140
  return unless msg
5121
5141
  return if msg['is_header'] || msg['is_channel_header'] || msg['is_thread_header']
5122
- ensure_full_message(msg)
5142
+ msg = ensure_full_message(msg)
5123
5143
 
5124
5144
  source_id = msg['source_id']
5125
5145
  source = @db.get_source(source_id)
@@ -5147,7 +5167,7 @@ module Heathrow
5147
5167
  def forward_message
5148
5168
  msg = current_message
5149
5169
  return unless msg
5150
- ensure_full_message(msg)
5170
+ msg = ensure_full_message(msg)
5151
5171
  source_id = msg['source_id']
5152
5172
 
5153
5173
  # Check if source supports sending
@@ -5162,18 +5182,59 @@ module Heathrow
5162
5182
  @panes[:bottom].text = " Opening editor to forward message...".fg(226)
5163
5183
  @panes[:bottom].refresh
5164
5184
 
5185
+ # Extract attachments BEFORE editor
5186
+ orig_attachments = extract_original_attachments(msg)
5187
+
5165
5188
  # Compose the forward
5166
5189
  composed = composer.compose_forward
5167
5190
 
5191
+ # Rebuild panes after editor (vim clears the screen)
5192
+ setup_display
5193
+ create_panes
5194
+ render_all
5195
+
5168
5196
  if composed
5197
+ # Include original attachments
5198
+ composed[:attachments] = orig_attachments if orig_attachments && !orig_attachments.empty?
5169
5199
  finalize_compose(source, composed, "Forward cancelled")
5170
5200
  else
5171
5201
  set_feedback("Forward cancelled", 245, 1)
5172
5202
  end
5173
-
5174
- render_bottom_bar
5175
5203
  end
5176
5204
 
5205
+ # Extract original attachments from a message for forwarding
5206
+ def extract_original_attachments(msg)
5207
+ metadata = msg['metadata']
5208
+ metadata = JSON.parse(metadata) if metadata.is_a?(String) rescue nil
5209
+ return nil unless metadata.is_a?(Hash)
5210
+
5211
+ file_path = metadata['maildir_file']
5212
+ return nil unless file_path && File.exist?(file_path)
5213
+
5214
+ require 'mail'
5215
+ require 'tmpdir'
5216
+ mail = Mail.read(file_path)
5217
+ return nil if mail.attachments.empty?
5218
+
5219
+ tmp_dir = File.join(Dir.tmpdir, "heathrow-fwd-#{Process.pid}")
5220
+ FileUtils.mkdir_p(tmp_dir)
5221
+
5222
+ paths = []
5223
+ mail.attachments.each do |att|
5224
+ next unless att.filename
5225
+ out = File.join(tmp_dir, att.filename)
5226
+ File.write(out, att.decoded)
5227
+ paths << out
5228
+ end
5229
+ paths.empty? ? nil : paths
5230
+ rescue => e
5231
+ File.open('/tmp/heathrow-crash.log', 'a') { |f|
5232
+ f.puts "#{Time.now.strftime('%Y-%m-%d %H:%M:%S')} extract_attachments: #{e.class}: #{e.message}"
5233
+ f.puts " #{e.backtrace&.first(3)&.join("\n ")}"
5234
+ }
5235
+ nil
5236
+ end
5237
+
5177
5238
  def compose_new_message
5178
5239
  # Build list of sendable channels from all DB sources
5179
5240
  channels = []
@@ -5250,6 +5311,9 @@ module Heathrow
5250
5311
 
5251
5312
  composed = composer.compose_new
5252
5313
  end
5314
+ setup_display
5315
+ create_panes
5316
+ render_all
5253
5317
  if composed
5254
5318
  finalize_compose(source, composed, "Message cancelled")
5255
5319
  else
@@ -5402,8 +5466,10 @@ module Heathrow
5402
5466
  setup_display
5403
5467
  create_panes
5404
5468
  render_all
5469
+ pending_attachments = Array(composed[:attachments]).dup
5405
5470
  loop do
5406
- attachments = prompt_attachments(composed: composed)
5471
+ attachments = prompt_attachments(pending_attachments, composed: composed)
5472
+ pending_attachments = [] # Only seed on first iteration
5407
5473
  case attachments
5408
5474
  when :postpone
5409
5475
  postpone_message(source, composed)
@@ -7719,7 +7785,7 @@ Required: URL, optional CSS selector
7719
7785
 
7720
7786
  msg = current_message
7721
7787
  return unless msg
7722
- ensure_full_message(msg)
7788
+ msg = ensure_full_message(msg)
7723
7789
 
7724
7790
  http_urls = []
7725
7791
 
@@ -1,3 +1,3 @@
1
1
  module Heathrow
2
- VERSION = '0.7.0'
2
+ VERSION = '0.7.1'
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: heathrow
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Geir Isene
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2026-03-16 00:00:00.000000000 Z
12
+ date: 2026-03-17 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rcurses