heathrow 0.7.8 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/heathrow.gemspec +1 -1
- data/lib/heathrow/message_composer.rb +1 -1
- data/lib/heathrow/sources/messenger.rb +87 -5
- data/lib/heathrow/sources/messenger_fetch_marionette.py +2 -2
- data/lib/heathrow/sources/messenger_fetch_thread.py +131 -0
- data/lib/heathrow/ui/application.rb +299 -93
- data/lib/heathrow/ui/source_wizard.rb +18 -18
- data/lib/heathrow/ui/threaded_view.rb +8 -8
- data/lib/heathrow/version.rb +1 -1
- metadata +7 -6
|
@@ -736,6 +736,8 @@ module Heathrow
|
|
|
736
736
|
jump_to_date
|
|
737
737
|
when 'x'
|
|
738
738
|
open_message_external
|
|
739
|
+
when 'X'
|
|
740
|
+
open_in_brrowser
|
|
739
741
|
when 'HOME'
|
|
740
742
|
go_first
|
|
741
743
|
when 'END'
|
|
@@ -1035,10 +1037,10 @@ module Heathrow
|
|
|
1035
1037
|
idx = 0
|
|
1036
1038
|
|
|
1037
1039
|
render_pick = -> {
|
|
1038
|
-
lines = [title.
|
|
1040
|
+
lines = [title.bd.fg(226), ""]
|
|
1039
1041
|
names.each_with_index do |name, i|
|
|
1040
1042
|
if i == idx
|
|
1041
|
-
lines << "→ #{name}".
|
|
1043
|
+
lines << "→ #{name}".bd.fg(226)
|
|
1042
1044
|
else
|
|
1043
1045
|
lines << " #{name}".fg(252)
|
|
1044
1046
|
end
|
|
@@ -1390,7 +1392,7 @@ module Heathrow
|
|
|
1390
1392
|
|
|
1391
1393
|
# Build colored components
|
|
1392
1394
|
title_part = " Heathrow - ".fg(248)
|
|
1393
|
-
view_part = view_name.
|
|
1395
|
+
view_part = view_name.bd.fg(255)
|
|
1394
1396
|
|
|
1395
1397
|
# Add sort order info (only for message views, not Sources)
|
|
1396
1398
|
sort_part = ""
|
|
@@ -1501,9 +1503,9 @@ module Heathrow
|
|
|
1501
1503
|
end
|
|
1502
1504
|
|
|
1503
1505
|
@panes[:left].text = new_text
|
|
1504
|
-
@panes[:left].
|
|
1506
|
+
@panes[:left].full_refresh
|
|
1505
1507
|
end
|
|
1506
|
-
|
|
1508
|
+
|
|
1507
1509
|
def format_message_line(msg, selected)
|
|
1508
1510
|
# Extract message details
|
|
1509
1511
|
timestamp = (parse_timestamp(msg['timestamp']) || "").ljust(6)
|
|
@@ -1595,7 +1597,7 @@ module Heathrow
|
|
|
1595
1597
|
|
|
1596
1598
|
if selected
|
|
1597
1599
|
content = "#{name} #{poll_col} #{count_col}"
|
|
1598
|
-
health + " " + content.
|
|
1600
|
+
health + " " + content.bd.ul.fg(src_color) + padding
|
|
1599
1601
|
elsif msg['enabled'].to_i == 0
|
|
1600
1602
|
health + " " + line.fg(240)
|
|
1601
1603
|
else
|
|
@@ -1742,7 +1744,7 @@ module Heathrow
|
|
|
1742
1744
|
|
|
1743
1745
|
# Show edit options in right pane
|
|
1744
1746
|
options = []
|
|
1745
|
-
options << "EDIT SOURCE: #{source['name']}".
|
|
1747
|
+
options << "EDIT SOURCE: #{source['name']}".bd.fg(226)
|
|
1746
1748
|
options << "=" * 40
|
|
1747
1749
|
options << ""
|
|
1748
1750
|
options << "What would you like to edit?"
|
|
@@ -1945,6 +1947,9 @@ module Heathrow
|
|
|
1945
1947
|
return
|
|
1946
1948
|
end
|
|
1947
1949
|
|
|
1950
|
+
# Auto-mark as read when content is rendered in the right pane
|
|
1951
|
+
mark_current_message_as_read
|
|
1952
|
+
|
|
1948
1953
|
msg = current_msg
|
|
1949
1954
|
|
|
1950
1955
|
# Lazily load full content if this was a light query result
|
|
@@ -1969,7 +1974,7 @@ module Heathrow
|
|
|
1969
1974
|
|
|
1970
1975
|
# Special handling for RSS/HN messages
|
|
1971
1976
|
if msg['source_type'] == 'rss' || msg['source_type'] == 'hacker_news'
|
|
1972
|
-
header << "📰 #{msg['subject']}".
|
|
1977
|
+
header << "📰 #{msg['subject']}".bd.fg(226) if msg['subject']
|
|
1973
1978
|
|
|
1974
1979
|
# Extract metadata from raw_data if available
|
|
1975
1980
|
if msg['raw_data']
|
|
@@ -2015,9 +2020,9 @@ module Heathrow
|
|
|
2015
2020
|
# For weechat, show channel name from metadata instead of content preview
|
|
2016
2021
|
meta = msg['metadata']
|
|
2017
2022
|
if meta.is_a?(Hash) && meta['channel_name']
|
|
2018
|
-
header << "Subject: #{meta['channel_name']}".
|
|
2023
|
+
header << "Subject: #{meta['channel_name']}".bd.fg(1)
|
|
2019
2024
|
elsif msg['subject']
|
|
2020
|
-
header << "Subject: #{msg['subject']}".
|
|
2025
|
+
header << "Subject: #{msg['subject']}".bd.fg(1)
|
|
2021
2026
|
end
|
|
2022
2027
|
end
|
|
2023
2028
|
|
|
@@ -2063,7 +2068,7 @@ module Heathrow
|
|
|
2063
2068
|
# For RSS/HN, add extra formatting and info
|
|
2064
2069
|
if msg['source_type'] == 'rss' || msg['source_type'] == 'hacker_news'
|
|
2065
2070
|
content_parts = []
|
|
2066
|
-
content_parts << "📄 Article Summary:".
|
|
2071
|
+
content_parts << "📄 Article Summary:".bd.fg(226)
|
|
2067
2072
|
content_parts << ""
|
|
2068
2073
|
|
|
2069
2074
|
# Word wrap the content for better readability
|
|
@@ -2102,7 +2107,7 @@ module Heathrow
|
|
|
2102
2107
|
# Add helpful instructions
|
|
2103
2108
|
content_parts << ""
|
|
2104
2109
|
content_parts << "─" * 40
|
|
2105
|
-
content_parts << "💡 Keyboard Shortcuts:".
|
|
2110
|
+
content_parts << "💡 Keyboard Shortcuts:".bd.fg(156)
|
|
2106
2111
|
content_parts << ""
|
|
2107
2112
|
content_parts << " x - Open full article in browser".fg(250)
|
|
2108
2113
|
content_parts << " SPACE - Toggle read/unread status".fg(250)
|
|
@@ -2172,7 +2177,7 @@ module Heathrow
|
|
|
2172
2177
|
|
|
2173
2178
|
# Title
|
|
2174
2179
|
title = header_msg['subject'] || header_msg['channel_name'] || 'Group'
|
|
2175
|
-
lines << title.
|
|
2180
|
+
lines << title.bd.fg(226)
|
|
2176
2181
|
lines << ""
|
|
2177
2182
|
|
|
2178
2183
|
# Message count
|
|
@@ -2253,8 +2258,14 @@ module Heathrow
|
|
|
2253
2258
|
end
|
|
2254
2259
|
|
|
2255
2260
|
def render_bottom_bar
|
|
2256
|
-
# Check if there's an active feedback message
|
|
2257
|
-
if @
|
|
2261
|
+
# Check if there's an active feedback message (timed or sticky)
|
|
2262
|
+
if @feedback_sticky && @feedback_message
|
|
2263
|
+
if @panes[:bottom]
|
|
2264
|
+
@panes[:bottom].text = " #{@feedback_message}".fg(@feedback_color || 156)
|
|
2265
|
+
@panes[:bottom].refresh
|
|
2266
|
+
end
|
|
2267
|
+
return
|
|
2268
|
+
elsif @feedback_expires_at && Time.now < @feedback_expires_at
|
|
2258
2269
|
if @panes[:bottom]
|
|
2259
2270
|
@panes[:bottom].text = " #{@feedback_message}".fg(@feedback_color || 156)
|
|
2260
2271
|
@panes[:bottom].refresh
|
|
@@ -3173,6 +3184,47 @@ module Heathrow
|
|
|
3173
3184
|
end
|
|
3174
3185
|
end
|
|
3175
3186
|
|
|
3187
|
+
def open_in_brrowser
|
|
3188
|
+
unless system("which brrowser > /dev/null 2>&1")
|
|
3189
|
+
set_feedback("brrowser not installed. See https://github.com/isene/brrowser", 196, 4)
|
|
3190
|
+
return
|
|
3191
|
+
end
|
|
3192
|
+
|
|
3193
|
+
msg = current_message
|
|
3194
|
+
return unless msg
|
|
3195
|
+
return if header_message?(msg)
|
|
3196
|
+
msg = ensure_full_message(msg)
|
|
3197
|
+
|
|
3198
|
+
# Build URL from message (same logic as open_message_external)
|
|
3199
|
+
url = nil
|
|
3200
|
+
if message_has_html?(msg)
|
|
3201
|
+
html = msg['html_content']
|
|
3202
|
+
html = msg['content'] if !html || html.to_s.strip.empty?
|
|
3203
|
+
tmpfile = "/tmp/heathrow-view-#{msg['id']}.html"
|
|
3204
|
+
File.write(tmpfile, html)
|
|
3205
|
+
url = "file://#{tmpfile}"
|
|
3206
|
+
else
|
|
3207
|
+
meta = msg['metadata']
|
|
3208
|
+
if meta
|
|
3209
|
+
parsed = meta.is_a?(Hash) ? meta : (JSON.parse(meta) rescue {})
|
|
3210
|
+
url = parsed['link'] || parsed['url']
|
|
3211
|
+
end
|
|
3212
|
+
url ||= msg['url'] || msg['link'] || msg['permalink']
|
|
3213
|
+
url ||= msg['external_id'] if msg['external_id']&.start_with?('http')
|
|
3214
|
+
end
|
|
3215
|
+
|
|
3216
|
+
unless url
|
|
3217
|
+
set_feedback("No URL or HTML content to open", 226, 3)
|
|
3218
|
+
return
|
|
3219
|
+
end
|
|
3220
|
+
|
|
3221
|
+
Rcurses.clear_screen
|
|
3222
|
+
system("brrowser '#{url}'")
|
|
3223
|
+
setup_display
|
|
3224
|
+
create_panes
|
|
3225
|
+
render_all
|
|
3226
|
+
end
|
|
3227
|
+
|
|
3176
3228
|
def view_attachments
|
|
3177
3229
|
msg = current_message
|
|
3178
3230
|
return unless msg
|
|
@@ -3250,14 +3302,14 @@ module Heathrow
|
|
|
3250
3302
|
end
|
|
3251
3303
|
|
|
3252
3304
|
def render_attachment_list(attachments, idx, tagged)
|
|
3253
|
-
lines = ["Attachments:".
|
|
3305
|
+
lines = ["Attachments:".bd.fg(226), ""]
|
|
3254
3306
|
attachments.each_with_index do |att, i|
|
|
3255
3307
|
name = att['name'] || att['filename'] || 'unnamed'
|
|
3256
3308
|
size = att['size'] ? " (#{human_size(att['size'])})" : ''
|
|
3257
3309
|
ctype = att['content_type']&.split(';')&.first || ''
|
|
3258
3310
|
tag = tagged.include?(i) ? "* ".fg(226) : " "
|
|
3259
3311
|
if i == idx
|
|
3260
|
-
lines << "→ ".fg(226) + tag + "#{name}#{size} #{ctype}".
|
|
3312
|
+
lines << "→ ".fg(226) + tag + "#{name}#{size} #{ctype}".bd.fg(255)
|
|
3261
3313
|
else
|
|
3262
3314
|
lines << " " + tag + "#{name}#{size} #{ctype}".fg(250)
|
|
3263
3315
|
end
|
|
@@ -3378,7 +3430,8 @@ module Heathrow
|
|
|
3378
3430
|
success = @db.mark_as_read(msg['id'])
|
|
3379
3431
|
if success
|
|
3380
3432
|
msg['is_read'] = 1
|
|
3381
|
-
|
|
3433
|
+
sync_maildir_flag(msg, 'S', true)
|
|
3434
|
+
|
|
3382
3435
|
# Re-sort if sorting by unread
|
|
3383
3436
|
if @sort_order == 'unread'
|
|
3384
3437
|
sort_messages
|
|
@@ -3829,7 +3882,7 @@ module Heathrow
|
|
|
3829
3882
|
star = @browser_favorites.include?(folder[:full_name]) ? "* ".fg(226) : " "
|
|
3830
3883
|
|
|
3831
3884
|
if i == @folder_browser_index
|
|
3832
|
-
line = "→ ".fg(226) + indent + arrow.fg(226) + star + folder[:name].
|
|
3885
|
+
line = "→ ".fg(226) + indent + arrow.fg(226) + star + folder[:name].bd.ul.fg(255)
|
|
3833
3886
|
else
|
|
3834
3887
|
line = " " + indent + arrow.fg(245) + star + folder[:name].fg(245)
|
|
3835
3888
|
end
|
|
@@ -3861,7 +3914,7 @@ module Heathrow
|
|
|
3861
3914
|
@folder_count_cache ||= {}
|
|
3862
3915
|
counts = @folder_count_cache[folder[:full_name]] ||= folder_message_count(folder[:full_name])
|
|
3863
3916
|
info = []
|
|
3864
|
-
info << "FOLDER: #{folder[:full_name]}".
|
|
3917
|
+
info << "FOLDER: #{folder[:full_name]}".bd.fg(226)
|
|
3865
3918
|
info << ""
|
|
3866
3919
|
info << "Messages: #{counts[:total]}".fg(39)
|
|
3867
3920
|
info << "Unread: #{counts[:unread]}".fg(counts[:unread] > 0 ? 208 : 245)
|
|
@@ -3876,7 +3929,7 @@ module Heathrow
|
|
|
3876
3929
|
# Update top bar (preserve Favorites title if in favorites mode)
|
|
3877
3930
|
browser_title = @in_favorites_browser ? "Favorites" : "Folder Browser"
|
|
3878
3931
|
browser_color = @in_favorites_browser ? 226 : 201
|
|
3879
|
-
@panes[:top].text = " Heathrow - ".
|
|
3932
|
+
@panes[:top].text = " Heathrow - ".bd.fg(255) + browser_title.bd.fg(browser_color) + " [#{@folder_display.size} folders]".fg(246)
|
|
3880
3933
|
@panes[:top].refresh
|
|
3881
3934
|
|
|
3882
3935
|
# Update bottom bar
|
|
@@ -3893,7 +3946,7 @@ module Heathrow
|
|
|
3893
3946
|
arrow = folder[:has_children] ? (folder[:collapsed] ? "▸ " : "▾ ") : " "
|
|
3894
3947
|
star = @browser_favorites.include?(folder[:full_name]) ? "* ".fg(226) : " "
|
|
3895
3948
|
if i == @folder_browser_index
|
|
3896
|
-
line = "→ ".fg(226) + indent + arrow.fg(226) + star + folder[:name].
|
|
3949
|
+
line = "→ ".fg(226) + indent + arrow.fg(226) + star + folder[:name].bd.ul.fg(255)
|
|
3897
3950
|
else
|
|
3898
3951
|
line = " " + indent + arrow.fg(245) + star + folder[:name].fg(245)
|
|
3899
3952
|
end
|
|
@@ -4082,7 +4135,7 @@ module Heathrow
|
|
|
4082
4135
|
end
|
|
4083
4136
|
|
|
4084
4137
|
render_folder_browser
|
|
4085
|
-
@panes[:top].text = " Heathrow - ".
|
|
4138
|
+
@panes[:top].text = " Heathrow - ".bd.fg(255) + "Favorites".bd.fg(226) + " [#{favorites.size} folders]".fg(246)
|
|
4086
4139
|
@panes[:top].refresh
|
|
4087
4140
|
@panes[:bottom].text = " j/k:Navigate | Enter:Open | C-Up/C-Down:Reorder | B:All folders | +:Remove fav | ESC:Back".fg(245)
|
|
4088
4141
|
@panes[:bottom].refresh
|
|
@@ -4121,7 +4174,7 @@ module Heathrow
|
|
|
4121
4174
|
@folder_browser_index = 0 if @folder_browser_index < 0
|
|
4122
4175
|
set_feedback("Removed #{folder[:full_name]} from favorites", 226, 2)
|
|
4123
4176
|
render_folder_browser
|
|
4124
|
-
@panes[:top].text = " Heathrow - ".
|
|
4177
|
+
@panes[:top].text = " Heathrow - ".bd.fg(255) + "Favorites".bd.fg(226) + " [#{@folder_display.size} folders]".fg(246)
|
|
4125
4178
|
@panes[:top].refresh
|
|
4126
4179
|
end
|
|
4127
4180
|
end
|
|
@@ -4181,7 +4234,7 @@ module Heathrow
|
|
|
4181
4234
|
shortcuts = get_folder_shortcuts
|
|
4182
4235
|
# Show shortcuts in right pane
|
|
4183
4236
|
info = []
|
|
4184
|
-
info << "FOLDER SHORTCUTS".
|
|
4237
|
+
info << "FOLDER SHORTCUTS".bd.fg(226)
|
|
4185
4238
|
info << "Press a key to jump to folder:".fg(245)
|
|
4186
4239
|
info << ""
|
|
4187
4240
|
shortcuts.sort_by { |k, _| k }.each do |key, folder|
|
|
@@ -4297,13 +4350,19 @@ module Heathrow
|
|
|
4297
4350
|
return if msgs.empty?
|
|
4298
4351
|
|
|
4299
4352
|
count = 0
|
|
4353
|
+
failed = 0
|
|
4354
|
+
filed_ids = Set.new
|
|
4300
4355
|
msgs.each do |msg|
|
|
4301
|
-
|
|
4302
|
-
|
|
4356
|
+
begin
|
|
4357
|
+
file_single_message(msg, dest)
|
|
4358
|
+
filed_ids << msg['id'] if msg['id']
|
|
4359
|
+
count += 1
|
|
4360
|
+
rescue => e
|
|
4361
|
+
failed += 1
|
|
4362
|
+
end
|
|
4303
4363
|
end
|
|
4304
4364
|
|
|
4305
4365
|
# Remove filed messages from current view (by id, works in both flat and threaded mode)
|
|
4306
|
-
filed_ids = msgs.map { |m| m['id'] }.compact.to_set
|
|
4307
4366
|
@filtered_messages.reject! { |m| m['id'] && filed_ids.include?(m['id']) }
|
|
4308
4367
|
if @show_threaded
|
|
4309
4368
|
@display_messages&.reject! { |m| m['id'] && filed_ids.include?(m['id']) }
|
|
@@ -4314,7 +4373,9 @@ module Heathrow
|
|
|
4314
4373
|
@index = [@index, (@filtered_messages.size - 1)].min
|
|
4315
4374
|
@index = 0 if @index < 0 || @filtered_messages.empty?
|
|
4316
4375
|
|
|
4317
|
-
|
|
4376
|
+
msg_text = "Moved #{count} message#{count > 1 ? 's' : ''} to #{dest}"
|
|
4377
|
+
msg_text += " (#{failed} failed)" if failed > 0
|
|
4378
|
+
set_feedback(msg_text, failed > 0 ? 208 : 156, 2)
|
|
4318
4379
|
render_all
|
|
4319
4380
|
end
|
|
4320
4381
|
|
|
@@ -4457,7 +4518,7 @@ module Heathrow
|
|
|
4457
4518
|
indent = " " * folder[:depth]
|
|
4458
4519
|
arrow = folder[:has_children] ? (folder[:collapsed] ? "▸ " : "▾ ") : " "
|
|
4459
4520
|
if i == idx
|
|
4460
|
-
"→ ".fg(226) + indent + arrow.fg(226) + folder[:name].
|
|
4521
|
+
"→ ".fg(226) + indent + arrow.fg(226) + folder[:name].bd.ul.fg(255)
|
|
4461
4522
|
else
|
|
4462
4523
|
" " + indent + arrow.fg(245) + folder[:name].fg(245)
|
|
4463
4524
|
end
|
|
@@ -4480,7 +4541,7 @@ module Heathrow
|
|
|
4480
4541
|
end
|
|
4481
4542
|
@panes[:left].refresh
|
|
4482
4543
|
|
|
4483
|
-
@panes[:top].text = " Heathrow - ".
|
|
4544
|
+
@panes[:top].text = " Heathrow - ".bd.fg(255) + title.bd.fg(226)
|
|
4484
4545
|
@panes[:top].refresh
|
|
4485
4546
|
@panes[:bottom].text = " j/k:Navigate | Enter:Save here | h/l:Collapse/Expand | ESC:Cancel".fg(245)
|
|
4486
4547
|
@panes[:bottom].refresh
|
|
@@ -4494,7 +4555,7 @@ module Heathrow
|
|
|
4494
4555
|
loop do
|
|
4495
4556
|
# Display current shortcuts in right pane
|
|
4496
4557
|
info = []
|
|
4497
|
-
info << "SAVE FOLDER SHORTCUTS".
|
|
4558
|
+
info << "SAVE FOLDER SHORTCUTS".bd.fg(226)
|
|
4498
4559
|
info << ""
|
|
4499
4560
|
if shortcuts.empty?
|
|
4500
4561
|
info << "No shortcuts configured".fg(245)
|
|
@@ -4609,7 +4670,7 @@ module Heathrow
|
|
|
4609
4670
|
return
|
|
4610
4671
|
end
|
|
4611
4672
|
|
|
4612
|
-
lines = ["LABELS IN USE".
|
|
4673
|
+
lines = ["LABELS IN USE".bd.fg(226), ""]
|
|
4613
4674
|
label_counts.sort_by { |_, c| -c }.each do |label, count|
|
|
4614
4675
|
lines << " #{label}".fg(51) + " (#{count})".fg(245)
|
|
4615
4676
|
end
|
|
@@ -4721,7 +4782,7 @@ module Heathrow
|
|
|
4721
4782
|
|
|
4722
4783
|
def ai_show_response(title, response)
|
|
4723
4784
|
lines = []
|
|
4724
|
-
lines << title.
|
|
4785
|
+
lines << title.bd.fg(226)
|
|
4725
4786
|
lines << ""
|
|
4726
4787
|
lines << response
|
|
4727
4788
|
@panes[:right].ix = 0
|
|
@@ -4838,7 +4899,7 @@ module Heathrow
|
|
|
4838
4899
|
|
|
4839
4900
|
# Show diff-like view
|
|
4840
4901
|
lines = []
|
|
4841
|
-
lines << "GRAMMAR/SPELLING FIX".
|
|
4902
|
+
lines << "GRAMMAR/SPELLING FIX".bd.fg(226)
|
|
4842
4903
|
lines << ""
|
|
4843
4904
|
lines << "Original:".fg(245)
|
|
4844
4905
|
lines << msg['content'].to_s
|
|
@@ -5398,7 +5459,7 @@ module Heathrow
|
|
|
5398
5459
|
idx = 0
|
|
5399
5460
|
loop do
|
|
5400
5461
|
ch = channels[idx]
|
|
5401
|
-
@panes[:bottom].text = " New message via: #{ch[:name].
|
|
5462
|
+
@panes[:bottom].text = " New message via: #{ch[:name].bd} (TAB to cycle, ENTER to confirm, ESC to cancel)".fg(226)
|
|
5402
5463
|
@panes[:bottom].refresh
|
|
5403
5464
|
|
|
5404
5465
|
key = getchr
|
|
@@ -5536,6 +5597,9 @@ module Heathrow
|
|
|
5536
5597
|
end
|
|
5537
5598
|
|
|
5538
5599
|
loop do
|
|
5600
|
+
# Ensure terminal is in raw mode with cursor hidden (safety after external tools)
|
|
5601
|
+
Cursor.hide
|
|
5602
|
+
|
|
5539
5603
|
# Show attachments and recipients in right pane
|
|
5540
5604
|
right_lines = []
|
|
5541
5605
|
if composed
|
|
@@ -5549,7 +5613,7 @@ module Heathrow
|
|
|
5549
5613
|
else
|
|
5550
5614
|
total_size = attachments.sum { |f| File.size(f) rescue 0 }
|
|
5551
5615
|
size_str = total_size < 1_000_000 ? "#{(total_size / 1024.0).round(1)}KB" : "#{(total_size / 1_000_000.0).round(1)}MB"
|
|
5552
|
-
right_lines << "Attachments (#{attachments.size}, #{size_str}):".
|
|
5616
|
+
right_lines << "Attachments (#{attachments.size}, #{size_str}):".bd
|
|
5553
5617
|
attachments.each_with_index do |f, i|
|
|
5554
5618
|
fsize = File.size(f) rescue 0
|
|
5555
5619
|
fs = fsize < 1_000_000 ? "#{(fsize / 1024.0).round(1)}KB" : "#{(fsize / 1_000_000.0).round(1)}MB"
|
|
@@ -5559,10 +5623,15 @@ module Heathrow
|
|
|
5559
5623
|
@panes[:right].text = right_lines.join("\n")
|
|
5560
5624
|
@panes[:right].refresh
|
|
5561
5625
|
|
|
5626
|
+
# Build prompt with compose plugin keys
|
|
5627
|
+
plugins = compose_plugins
|
|
5628
|
+
plugin_hint = plugins.map { |p| "#{p[:label]} (#{p[:key]})" }.join(" | ")
|
|
5629
|
+
plugin_hint = " | #{plugin_hint}" unless plugin_hint.empty?
|
|
5630
|
+
|
|
5562
5631
|
if attachments.empty?
|
|
5563
|
-
prompt = " Send (ENTER) | Edit (e) | Attach (a) | Postpone (p) | Cancel (ESC)"
|
|
5632
|
+
prompt = " Send (ENTER) | Edit (e) | Attach (a)#{plugin_hint} | Postpone (p) | Cancel (ESC)"
|
|
5564
5633
|
else
|
|
5565
|
-
prompt = " Send (ENTER) | Edit (e) | More (a) |
|
|
5634
|
+
prompt = " Send (ENTER) | Edit (e) | More (a)#{plugin_hint} | Remove (x) | Postpone (p) | ESC"
|
|
5566
5635
|
end
|
|
5567
5636
|
|
|
5568
5637
|
@panes[:bottom].text = prompt.fg(226)
|
|
@@ -5577,12 +5646,31 @@ module Heathrow
|
|
|
5577
5646
|
when 'p'
|
|
5578
5647
|
return :postpone
|
|
5579
5648
|
when 'e'
|
|
5649
|
+
composed[:attachments] = attachments.dup unless attachments.empty?
|
|
5580
5650
|
return :edit
|
|
5581
5651
|
when 'a', 'A'
|
|
5582
5652
|
new_files = run_rtfm_picker
|
|
5583
5653
|
attachments.concat(new_files) if new_files && !new_files.empty?
|
|
5584
5654
|
when 'x', 'X'
|
|
5585
|
-
attachments.
|
|
5655
|
+
if attachments.size == 1
|
|
5656
|
+
attachments.clear
|
|
5657
|
+
elsif attachments.size > 1
|
|
5658
|
+
@panes[:bottom].text = " Remove which? (1-#{attachments.size}, or 'a' for all): ".fg(226)
|
|
5659
|
+
@panes[:bottom].refresh
|
|
5660
|
+
ans = getchr
|
|
5661
|
+
if ans == 'a' || ans == 'A'
|
|
5662
|
+
attachments.clear
|
|
5663
|
+
elsif ans =~ /^\d$/ && (idx = ans.to_i) >= 1 && idx <= attachments.size
|
|
5664
|
+
attachments.delete_at(idx - 1)
|
|
5665
|
+
end
|
|
5666
|
+
end
|
|
5667
|
+
else
|
|
5668
|
+
# Check compose plugins
|
|
5669
|
+
plugin = plugins.find { |p| p[:key] == chr }
|
|
5670
|
+
if plugin
|
|
5671
|
+
new_files = run_compose_plugin(plugin)
|
|
5672
|
+
attachments.concat(new_files) if new_files && !new_files.empty?
|
|
5673
|
+
end
|
|
5586
5674
|
end
|
|
5587
5675
|
end
|
|
5588
5676
|
end
|
|
@@ -5592,7 +5680,8 @@ module Heathrow
|
|
|
5592
5680
|
pick_file = "/tmp/rtfm_pick_#{Process.pid}.txt"
|
|
5593
5681
|
File.delete(pick_file) if File.exist?(pick_file)
|
|
5594
5682
|
|
|
5595
|
-
#
|
|
5683
|
+
# Flush input, restore terminal for RTFM
|
|
5684
|
+
$stdin.getc while $stdin.wait_readable(0)
|
|
5596
5685
|
system("stty sane 2>/dev/null")
|
|
5597
5686
|
Cursor.show
|
|
5598
5687
|
|
|
@@ -5616,6 +5705,58 @@ module Heathrow
|
|
|
5616
5705
|
end
|
|
5617
5706
|
end
|
|
5618
5707
|
|
|
5708
|
+
|
|
5709
|
+
# Compose plugins: loaded from ~/.heathrow/plugins/compose/*.rb
|
|
5710
|
+
# Each plugin file should define a hash with :key, :label, :command
|
|
5711
|
+
# Example: { key: 'i', label: 'Insight', command: 'cd ~/myapp && ./picker --pick=%{pick_file}' }
|
|
5712
|
+
def compose_plugins
|
|
5713
|
+
@_compose_plugins ||= begin
|
|
5714
|
+
plugins = []
|
|
5715
|
+
dir = File.join(Dir.home, '.heathrow', 'plugins', 'compose')
|
|
5716
|
+
if Dir.exist?(dir)
|
|
5717
|
+
Dir.glob(File.join(dir, '*.rb')).each do |f|
|
|
5718
|
+
begin
|
|
5719
|
+
plugin = eval(File.read(f))
|
|
5720
|
+
plugins << plugin if plugin.is_a?(Hash) && plugin[:key] && plugin[:command]
|
|
5721
|
+
rescue => e
|
|
5722
|
+
File.open('/tmp/heathrow_debug.log', 'a') { |log| log.puts "Compose plugin error (#{f}): #{e.message}" }
|
|
5723
|
+
end
|
|
5724
|
+
end
|
|
5725
|
+
end
|
|
5726
|
+
plugins
|
|
5727
|
+
end
|
|
5728
|
+
end
|
|
5729
|
+
|
|
5730
|
+
def run_compose_plugin(plugin)
|
|
5731
|
+
pick_file = "/tmp/heathrow_plugin_pick_#{Process.pid}.txt"
|
|
5732
|
+
File.delete(pick_file) if File.exist?(pick_file)
|
|
5733
|
+
|
|
5734
|
+
# Flush any queued input, restore terminal, clear screen
|
|
5735
|
+
$stdin.getc while $stdin.wait_readable(0)
|
|
5736
|
+
system("stty sane 2>/dev/null")
|
|
5737
|
+
Cursor.show
|
|
5738
|
+
print "\e[2J\e[H" # Clear screen + home cursor
|
|
5739
|
+
|
|
5740
|
+
cmd = plugin[:command].gsub('%{pick_file}', Shellwords.escape(pick_file))
|
|
5741
|
+
system(cmd)
|
|
5742
|
+
|
|
5743
|
+
$stdin.raw!
|
|
5744
|
+
$stdin.echo = false
|
|
5745
|
+
Cursor.hide
|
|
5746
|
+
Rcurses.clear_screen
|
|
5747
|
+
setup_display
|
|
5748
|
+
create_panes
|
|
5749
|
+
render_all
|
|
5750
|
+
|
|
5751
|
+
if File.exist?(pick_file)
|
|
5752
|
+
files = File.read(pick_file).lines.map(&:strip).reject(&:empty?)
|
|
5753
|
+
File.delete(pick_file) rescue nil
|
|
5754
|
+
files.select { |f| File.exist?(f) && File.file?(f) }
|
|
5755
|
+
else
|
|
5756
|
+
[]
|
|
5757
|
+
end
|
|
5758
|
+
end
|
|
5759
|
+
|
|
5619
5760
|
# Unified send prompt loop: handles send, edit, postpone, cancel
|
|
5620
5761
|
def finalize_compose(source, composed, cancel_label = "cancelled")
|
|
5621
5762
|
require_relative '../message_composer'
|
|
@@ -5626,12 +5767,13 @@ module Heathrow
|
|
|
5626
5767
|
pending_attachments = Array(composed[:attachments]).dup
|
|
5627
5768
|
loop do
|
|
5628
5769
|
attachments = prompt_attachments(pending_attachments, composed: composed)
|
|
5629
|
-
pending_attachments = [] # Only seed on first iteration
|
|
5630
5770
|
case attachments
|
|
5631
5771
|
when :postpone
|
|
5632
5772
|
postpone_message(source, composed)
|
|
5633
5773
|
return
|
|
5634
5774
|
when :edit
|
|
5775
|
+
# Preserve attachments across edit
|
|
5776
|
+
pending_attachments = Array(composed[:attachments]).dup
|
|
5635
5777
|
# Re-open editor with current composed data
|
|
5636
5778
|
composer = MessageComposer.new(nil, identity: current_identity, address_book: @address_book, editor_args: @editor_args)
|
|
5637
5779
|
re_composed = composer.compose_draft(composed.transform_keys(&:to_s))
|
|
@@ -5640,10 +5782,9 @@ module Heathrow
|
|
|
5640
5782
|
render_all
|
|
5641
5783
|
if re_composed
|
|
5642
5784
|
composed = re_composed
|
|
5643
|
-
|
|
5644
|
-
set_feedback("Edit cancelled", 245, 1)
|
|
5645
|
-
return
|
|
5785
|
+
composed[:attachments] = pending_attachments unless pending_attachments.empty?
|
|
5646
5786
|
end
|
|
5787
|
+
# If unchanged (re_composed nil), just return to send prompt
|
|
5647
5788
|
when nil
|
|
5648
5789
|
set_feedback(cancel_label, 245, 1)
|
|
5649
5790
|
return
|
|
@@ -5698,7 +5839,7 @@ module Heathrow
|
|
|
5698
5839
|
|
|
5699
5840
|
build = -> {
|
|
5700
5841
|
popup.full_refresh
|
|
5701
|
-
lines = ["", " " + "Postponed Messages".
|
|
5842
|
+
lines = ["", " " + "Postponed Messages".bd.fg(theme[:accent])]
|
|
5702
5843
|
lines << " " + "\u2500" * [pw - 6, 1].max
|
|
5703
5844
|
drafts.each_with_index do |d, i|
|
|
5704
5845
|
data = JSON.parse(d['data']) rescue {}
|
|
@@ -5918,7 +6059,7 @@ module Heathrow
|
|
|
5918
6059
|
inner_w = pw - 4
|
|
5919
6060
|
lines = []
|
|
5920
6061
|
lines << ""
|
|
5921
|
-
lines << " " + "Settings".
|
|
6062
|
+
lines << " " + "Settings".bd.fg(theme[:accent])
|
|
5922
6063
|
lines << " " + "\u2500" * [inner_w - 3, 1].max
|
|
5923
6064
|
|
|
5924
6065
|
settings_rows.each_with_index do |key, i|
|
|
@@ -6140,7 +6281,7 @@ module Heathrow
|
|
|
6140
6281
|
lines = []
|
|
6141
6282
|
title = theme_name ? "Edit: #{theme_name}" : "Theme Editor (#{@color_theme})"
|
|
6142
6283
|
lines << ""
|
|
6143
|
-
lines << " " + title.
|
|
6284
|
+
lines << " " + title.bd.fg(editing[:accent] || 10)
|
|
6144
6285
|
lines << " " + "\u2500" * [inner_w - 3, 1].max
|
|
6145
6286
|
|
|
6146
6287
|
# Ensure scroll follows selection
|
|
@@ -6612,13 +6753,13 @@ module Heathrow
|
|
|
6612
6753
|
content = $2.chomp
|
|
6613
6754
|
case level
|
|
6614
6755
|
when 1
|
|
6615
|
-
colored += content.
|
|
6756
|
+
colored += content.bd.fg(226) + "\n" # Bright yellow bold for H1
|
|
6616
6757
|
when 2
|
|
6617
|
-
colored += content.
|
|
6758
|
+
colored += content.bd.fg(14) + "\n" # Cyan bold for H2
|
|
6618
6759
|
when 3
|
|
6619
|
-
colored += content.
|
|
6760
|
+
colored += content.bd.fg(10) + "\n" # Green bold for H3
|
|
6620
6761
|
else
|
|
6621
|
-
colored += content.
|
|
6762
|
+
colored += content.bd.fg(11) + "\n" # Yellow bold for H4-H6
|
|
6622
6763
|
end
|
|
6623
6764
|
when /^\s*[-*+]\s+(.+)$/ # Bullet points
|
|
6624
6765
|
indent = line[/^\s*/]
|
|
@@ -6648,8 +6789,8 @@ module Heathrow
|
|
|
6648
6789
|
processed = line.dup
|
|
6649
6790
|
|
|
6650
6791
|
# Bold text **text** or __text__
|
|
6651
|
-
processed.gsub!(/\*\*(.+?)\*\*/, '\1'.
|
|
6652
|
-
processed.gsub!(/__(.+?)__/, '\1'.
|
|
6792
|
+
processed.gsub!(/\*\*(.+?)\*\*/, '\1'.bd)
|
|
6793
|
+
processed.gsub!(/__(.+?)__/, '\1'.bd)
|
|
6653
6794
|
|
|
6654
6795
|
# Italic text *text* or _text_
|
|
6655
6796
|
processed.gsub!(/\*([^*]+)\*/, '\1'.fg(252))
|
|
@@ -6672,14 +6813,14 @@ module Heathrow
|
|
|
6672
6813
|
|
|
6673
6814
|
def get_extended_help_text
|
|
6674
6815
|
<<~HELP
|
|
6675
|
-
#{"HEATHROW - COMPREHENSIVE DOCUMENTATION".
|
|
6816
|
+
#{"HEATHROW - COMPREHENSIVE DOCUMENTATION".bd.fg(226)}
|
|
6676
6817
|
#{"=" * 60}
|
|
6677
6818
|
|
|
6678
6819
|
Heathrow is a unified terminal interface for all your communication sources.
|
|
6679
6820
|
It aggregates messages from email, WhatsApp, Telegram, Discord, Reddit,
|
|
6680
6821
|
RSS feeds, and more into a single, keyboard-driven interface.
|
|
6681
6822
|
|
|
6682
|
-
#{"KEYBOARD SHORTCUTS".
|
|
6823
|
+
#{"KEYBOARD SHORTCUTS".bd.fg(theme[:accent])}
|
|
6683
6824
|
|
|
6684
6825
|
#{"Navigation".fg(11)}
|
|
6685
6826
|
j/↓ Move down in message list
|
|
@@ -6740,7 +6881,7 @@ RSS feeds, and more into a single, keyboard-driven interface.
|
|
|
6740
6881
|
t = Translate message
|
|
6741
6882
|
a = Ask anything about the message
|
|
6742
6883
|
|
|
6743
|
-
#{"FILTER SYNTAX".
|
|
6884
|
+
#{"FILTER SYNTAX".bd.fg(theme[:accent])}
|
|
6744
6885
|
|
|
6745
6886
|
Filters support powerful pattern matching:
|
|
6746
6887
|
- Comma (,) = AND condition - all must match
|
|
@@ -6752,7 +6893,7 @@ RSS feeds, and more into a single, keyboard-driven interface.
|
|
|
6752
6893
|
- "critical,production" = matches critical AND production
|
|
6753
6894
|
- "error|warning,production" = (error OR warning) AND production
|
|
6754
6895
|
|
|
6755
|
-
#{"SOURCE TYPES".
|
|
6896
|
+
#{"SOURCE TYPES".bd.fg(theme[:accent])}
|
|
6756
6897
|
|
|
6757
6898
|
#{"Email (IMAP)".fg(39)}
|
|
6758
6899
|
Connect to any IMAP email server. Supports Gmail, Outlook, Yahoo, etc.
|
|
@@ -6782,7 +6923,7 @@ Required: Client ID, client secret
|
|
|
6782
6923
|
Monitor web pages for changes.
|
|
6783
6924
|
Required: URL, optional CSS selector
|
|
6784
6925
|
|
|
6785
|
-
#{"CONFIGURATION".
|
|
6926
|
+
#{"CONFIGURATION".bd.fg(theme[:accent])}
|
|
6786
6927
|
|
|
6787
6928
|
Config file: ~/.heathrow/config.yml
|
|
6788
6929
|
Database: ~/.heathrow/heathrow.db
|
|
@@ -6793,7 +6934,7 @@ Required: URL, optional CSS selector
|
|
|
6793
6934
|
- Notification settings
|
|
6794
6935
|
- Custom key bindings
|
|
6795
6936
|
|
|
6796
|
-
#{"TIPS & TRICKS".
|
|
6937
|
+
#{"TIPS & TRICKS".bd.fg(theme[:accent])}
|
|
6797
6938
|
|
|
6798
6939
|
1. Use numbered views (0-9) to organize messages by topic
|
|
6799
6940
|
2. Combine source filters with content patterns for precision
|
|
@@ -6807,16 +6948,16 @@ Required: URL, optional CSS selector
|
|
|
6807
6948
|
|
|
6808
6949
|
def get_help_text
|
|
6809
6950
|
<<~HELP
|
|
6810
|
-
#{"HEATHROW - Communication Hub In The Terminal".
|
|
6951
|
+
#{"HEATHROW - Communication Hub In The Terminal".bd.fg(226)}
|
|
6811
6952
|
|
|
6812
|
-
#{"BASIC KEYS".
|
|
6953
|
+
#{"BASIC KEYS".bd.fg(theme[:accent])}
|
|
6813
6954
|
#{"?".fg(10)} = Show this help text (press again for extended help)
|
|
6814
6955
|
#{"q".fg(10)} = Quit Heathrow
|
|
6815
6956
|
#{"Q".fg(10)} = QUIT (force quit without saving state)
|
|
6816
6957
|
#{"Ctrl-r".fg(10)} = Refresh current view (sync + reload)
|
|
6817
6958
|
#{"Ctrl-l".fg(10)} = Redraw panes (no fetch)
|
|
6818
6959
|
|
|
6819
|
-
#{"NAVIGATION".
|
|
6960
|
+
#{"NAVIGATION".bd.fg(theme[:accent])}
|
|
6820
6961
|
#{"j/↓".fg(10)} = Move down in message list (rounds to top)
|
|
6821
6962
|
#{"k/↑".fg(10)} = Move up in message list (rounds to bottom)
|
|
6822
6963
|
#{"h/←".fg(10)} = Go back / parent view
|
|
@@ -6827,7 +6968,7 @@ Required: URL, optional CSS selector
|
|
|
6827
6968
|
#{"End".fg(10)} = Go to last message
|
|
6828
6969
|
#{"J".fg(10)} = Jump to date (yyyy-mm-dd)
|
|
6829
6970
|
|
|
6830
|
-
#{"RIGHT PANE SCROLLING".
|
|
6971
|
+
#{"RIGHT PANE SCROLLING".bd.fg(theme[:accent])}
|
|
6831
6972
|
#{"S-↓".fg(10)} = Scroll content down one line
|
|
6832
6973
|
#{"S-↑".fg(10)} = Scroll content up one line
|
|
6833
6974
|
#{"S-PgDn".fg(10)} = Scroll content down one page
|
|
@@ -6836,7 +6977,7 @@ Required: URL, optional CSS selector
|
|
|
6836
6977
|
#{"S-LEFT".fg(10)} = Scroll content up one page
|
|
6837
6978
|
#{"TAB".fg(10)} = Scroll content down one page
|
|
6838
6979
|
|
|
6839
|
-
#{"VIEWS & FILTERS".
|
|
6980
|
+
#{"VIEWS & FILTERS".bd.fg(theme[:accent])}
|
|
6840
6981
|
#{"A".fg(10)} = Show all messages
|
|
6841
6982
|
#{"N".fg(10)} = Show new (unread) messages only
|
|
6842
6983
|
#{"S".fg(10)} = Sources configuration and management
|
|
@@ -6845,7 +6986,7 @@ Required: URL, optional CSS selector
|
|
|
6845
6986
|
#{"Ctrl-f".fg(10)} = Edit/create filter for current view (0-9, F1-F12)
|
|
6846
6987
|
#{"K".fg(10)} = Kill/delete a view (with confirmation)
|
|
6847
6988
|
|
|
6848
|
-
#{"MESSAGE ACTIONS".
|
|
6989
|
+
#{"MESSAGE ACTIONS".bd.fg(theme[:accent])}
|
|
6849
6990
|
#{"R".fg(10)} = Toggle read/unread status
|
|
6850
6991
|
#{"M".fg(10)} = Mark all messages in view as read
|
|
6851
6992
|
#{"Space".fg(10)} = Collapse/expand thread (threaded view)
|
|
@@ -6868,14 +7009,14 @@ Required: URL, optional CSS selector
|
|
|
6868
7009
|
#{"m".fg(10)} = Mail/compose new message
|
|
6869
7010
|
#{"y".fg(10)} = Copy message ID to clipboard (for CC sessions)
|
|
6870
7011
|
|
|
6871
|
-
#{"SOURCE MANAGEMENT".
|
|
7012
|
+
#{"SOURCE MANAGEMENT".bd.fg(theme[:accent])} (in Sources view with 'S')
|
|
6872
7013
|
#{"a".fg(10)} = Add new source
|
|
6873
7014
|
#{"e".fg(10)} = Edit selected source
|
|
6874
7015
|
#{"d".fg(10)} = Delete selected source
|
|
6875
7016
|
#{"Enter".fg(10)} = Show all messages from selected source
|
|
6876
7017
|
#{"Space".fg(10)} = Enable/disable source
|
|
6877
7018
|
|
|
6878
|
-
#{"FOLDER NAVIGATION".
|
|
7019
|
+
#{"FOLDER NAVIGATION".bd.fg(theme[:accent])}
|
|
6879
7020
|
#{"B".fg(10)} = Browse all folders (folder tree)
|
|
6880
7021
|
#{"F".fg(10)} = Browse favorite folders
|
|
6881
7022
|
#{"+".fg(10)} = Add/remove current folder from favorites
|
|
@@ -6888,7 +7029,7 @@ Required: URL, optional CSS selector
|
|
|
6888
7029
|
#{"l".fg(10)} = Add/remove labels (+label / -label / ? to list)
|
|
6889
7030
|
#{"/".fg(10)} = Full-text search (notmuch)
|
|
6890
7031
|
|
|
6891
|
-
#{"AI ASSISTANT".
|
|
7032
|
+
#{"AI ASSISTANT".bd.fg(theme[:accent])}
|
|
6892
7033
|
#{"I".fg(10)} = AI assistant (Claude Code integration)
|
|
6893
7034
|
#{" d".fg(10)} = Draft a reply
|
|
6894
7035
|
#{" f".fg(10)} = Fix grammar/spelling
|
|
@@ -6896,7 +7037,7 @@ Required: URL, optional CSS selector
|
|
|
6896
7037
|
#{" t".fg(10)} = Translate message
|
|
6897
7038
|
#{" a".fg(10)} = Ask anything about the message
|
|
6898
7039
|
|
|
6899
|
-
#{"UI CONTROLS".
|
|
7040
|
+
#{"UI CONTROLS".bd.fg(theme[:accent])}
|
|
6900
7041
|
#{"w".fg(10)} = Change left pane width (20% → 60%)
|
|
6901
7042
|
#{"Ctrl-b".fg(10)} = Cycle border style (none/single/double)
|
|
6902
7043
|
#{"D".fg(10)} = Cycle date/time format
|
|
@@ -6915,7 +7056,7 @@ Required: URL, optional CSS selector
|
|
|
6915
7056
|
bindings = @config.custom_bindings
|
|
6916
7057
|
return "" if bindings.empty?
|
|
6917
7058
|
|
|
6918
|
-
lines = ["\n #{" CUSTOM BINDINGS".
|
|
7059
|
+
lines = ["\n #{" CUSTOM BINDINGS".bd.fg(theme[:accent])}"]
|
|
6919
7060
|
bindings.each do |key, b|
|
|
6920
7061
|
desc = b[:description] || b[:shell] || b[:action].to_s
|
|
6921
7062
|
lines << " #{key.fg(10).ljust(16)}= #{desc}"
|
|
@@ -6925,7 +7066,7 @@ Required: URL, optional CSS selector
|
|
|
6925
7066
|
|
|
6926
7067
|
def render_sources_info
|
|
6927
7068
|
source_text = []
|
|
6928
|
-
source_text << "SOURCE MANAGEMENT".
|
|
7069
|
+
source_text << "SOURCE MANAGEMENT".bd.fg(226)
|
|
6929
7070
|
source_text << "=" * 40
|
|
6930
7071
|
source_text << ""
|
|
6931
7072
|
|
|
@@ -6934,7 +7075,7 @@ Required: URL, optional CSS selector
|
|
|
6934
7075
|
source_text << ""
|
|
6935
7076
|
source_text << "Press 'a' to add a new source"
|
|
6936
7077
|
source_text << ""
|
|
6937
|
-
source_text << "Available source types:".
|
|
7078
|
+
source_text << "Available source types:".bd.fg(39)
|
|
6938
7079
|
types = @source_manager.get_source_types
|
|
6939
7080
|
types.each do |key, info|
|
|
6940
7081
|
source_text << "• #{info[:icon]} #{info[:name]}".fg(226)
|
|
@@ -6945,7 +7086,7 @@ Required: URL, optional CSS selector
|
|
|
6945
7086
|
if selected
|
|
6946
7087
|
source = @source_manager.sources[selected['id']]
|
|
6947
7088
|
if source
|
|
6948
|
-
source_text << "Selected: #{source['name']}".
|
|
7089
|
+
source_text << "Selected: #{source['name']}".bd.fg(39)
|
|
6949
7090
|
source_text << "Type: #{source['plugin_type'] || source['type']}".fg(245)
|
|
6950
7091
|
source_text << "Status: #{source['enabled'] ? 'Enabled' : 'Disabled'}".fg(source['enabled'] ? 40 : 196)
|
|
6951
7092
|
interval = (source['poll_interval'] || 900).to_i
|
|
@@ -6981,7 +7122,7 @@ Required: URL, optional CSS selector
|
|
|
6981
7122
|
item_name = stype == 'rss' ? 'feed' : 'page'
|
|
6982
7123
|
items = config[stype == 'rss' ? 'feeds' : 'pages'] || []
|
|
6983
7124
|
unless items.empty?
|
|
6984
|
-
source_text << "#{items.size} #{item_name}s:".
|
|
7125
|
+
source_text << "#{items.size} #{item_name}s:".bd.fg(245)
|
|
6985
7126
|
items.each_with_index do |item, i|
|
|
6986
7127
|
name = item['title'] || item['url'] || item['name'] || "Item #{i}"
|
|
6987
7128
|
status = item['last_status']
|
|
@@ -7001,14 +7142,14 @@ Required: URL, optional CSS selector
|
|
|
7001
7142
|
end
|
|
7002
7143
|
|
|
7003
7144
|
# Context-sensitive actions
|
|
7004
|
-
source_text << "ACTIONS".
|
|
7145
|
+
source_text << "ACTIONS".bd.fg(226)
|
|
7005
7146
|
source_text << "-" * 40
|
|
7006
7147
|
source_text << "a - Add #{item_name}"
|
|
7007
7148
|
source_text << "d - Remove #{item_name}"
|
|
7008
7149
|
source_text << "e - Edit source settings"
|
|
7009
7150
|
else
|
|
7010
7151
|
# Show config (hide secrets)
|
|
7011
|
-
source_text << "Configuration:".
|
|
7152
|
+
source_text << "Configuration:".bd.fg(39)
|
|
7012
7153
|
config.each do |key, value|
|
|
7013
7154
|
next if key.to_s =~ /password|secret|token/
|
|
7014
7155
|
source_text << " #{key}: #{value}".fg(245)
|
|
@@ -7016,7 +7157,7 @@ Required: URL, optional CSS selector
|
|
|
7016
7157
|
source_text << ""
|
|
7017
7158
|
|
|
7018
7159
|
# Context-sensitive actions
|
|
7019
|
-
source_text << "ACTIONS".
|
|
7160
|
+
source_text << "ACTIONS".bd.fg(226)
|
|
7020
7161
|
source_text << "-" * 40
|
|
7021
7162
|
source_text << "a - Add new source"
|
|
7022
7163
|
source_text << "e - Edit this source"
|
|
@@ -7043,7 +7184,7 @@ Required: URL, optional CSS selector
|
|
|
7043
7184
|
|
|
7044
7185
|
# Build a 256-color grid in the right pane
|
|
7045
7186
|
lines = []
|
|
7046
|
-
lines << "COLOR PICKER for #{source['name']}".
|
|
7187
|
+
lines << "COLOR PICKER for #{source['name']}".bd.fg(226)
|
|
7047
7188
|
lines << "=" * 40
|
|
7048
7189
|
lines << ""
|
|
7049
7190
|
lines << "Enter color number (0-255) or RGB hex (e.g. ff8800):"
|
|
@@ -7265,7 +7406,7 @@ Required: URL, optional CSS selector
|
|
|
7265
7406
|
end
|
|
7266
7407
|
|
|
7267
7408
|
lines = []
|
|
7268
|
-
lines << "POLL INTERVAL for #{source['name']}".
|
|
7409
|
+
lines << "POLL INTERVAL for #{source['name']}".bd.fg(226)
|
|
7269
7410
|
lines << "=" * 40
|
|
7270
7411
|
lines << ""
|
|
7271
7412
|
lines << "Current: #{current_str}".fg(245)
|
|
@@ -7337,14 +7478,14 @@ Required: URL, optional CSS selector
|
|
|
7337
7478
|
|
|
7338
7479
|
def show_filter_details(view_num, view_config)
|
|
7339
7480
|
filter_text = []
|
|
7340
|
-
filter_text << "VIEW #{view_num} CONFIGURATION".
|
|
7481
|
+
filter_text << "VIEW #{view_num} CONFIGURATION".bd.fg(226)
|
|
7341
7482
|
filter_text << "=" * 40
|
|
7342
7483
|
filter_text << ""
|
|
7343
7484
|
|
|
7344
7485
|
if view_config[:filters] && !view_config[:filters].empty?
|
|
7345
|
-
filter_text << "Name:".
|
|
7486
|
+
filter_text << "Name:".bd.fg(39) + " #{view_config[:name] || 'View ' + view_num.to_s}"
|
|
7346
7487
|
filter_text << ""
|
|
7347
|
-
filter_text << "Active Filters:".
|
|
7488
|
+
filter_text << "Active Filters:".bd.fg(39)
|
|
7348
7489
|
filter_text << "-" * 20
|
|
7349
7490
|
|
|
7350
7491
|
filters = view_config[:filters]
|
|
@@ -7395,14 +7536,14 @@ Required: URL, optional CSS selector
|
|
|
7395
7536
|
filter_text << ""
|
|
7396
7537
|
filter_text << "-" * 40
|
|
7397
7538
|
filter_text << ""
|
|
7398
|
-
filter_text << "Matching Messages:".
|
|
7539
|
+
filter_text << "Matching Messages:".bd.fg(39) + " #{@filtered_messages.size}"
|
|
7399
7540
|
else
|
|
7400
7541
|
filter_text << "No filters configured".fg(245)
|
|
7401
7542
|
filter_text << ""
|
|
7402
7543
|
filter_text << "This view will show an empty list until"
|
|
7403
7544
|
filter_text << "you configure filters."
|
|
7404
7545
|
filter_text << ""
|
|
7405
|
-
filter_text << "Available filter options:".
|
|
7546
|
+
filter_text << "Available filter options:".bd.fg(39)
|
|
7406
7547
|
filter_text << "• Source types (email, whatsapp, etc.)"
|
|
7407
7548
|
filter_text << "• Sender pattern (pipe | for OR)"
|
|
7408
7549
|
filter_text << "• Subject pattern (pipe | for OR)"
|
|
@@ -7410,7 +7551,7 @@ Required: URL, optional CSS selector
|
|
|
7410
7551
|
filter_text << "• Label (use 'l' to add labels, filter here)"
|
|
7411
7552
|
filter_text << "• Read/unread status"
|
|
7412
7553
|
filter_text << ""
|
|
7413
|
-
filter_text << "Pattern Examples:".
|
|
7554
|
+
filter_text << "Pattern Examples:".bd.fg(39)
|
|
7414
7555
|
filter_text << "Sender: Mom|Dad|Sister (any of them)"
|
|
7415
7556
|
filter_text << "Content: error|warning,critical"
|
|
7416
7557
|
filter_text << " → (error OR warning) AND critical"
|
|
@@ -7629,7 +7770,7 @@ Required: URL, optional CSS selector
|
|
|
7629
7770
|
|
|
7630
7771
|
welcome = []
|
|
7631
7772
|
welcome << ""
|
|
7632
|
-
welcome << " " + "Welcome to Heathrow!".
|
|
7773
|
+
welcome << " " + "Welcome to Heathrow!".bd.fg(226)
|
|
7633
7774
|
welcome << " " + "Where all your messages connect.".fg(245)
|
|
7634
7775
|
welcome << ""
|
|
7635
7776
|
welcome << " " + "\u2500" * [pw - 6, 1].max
|
|
@@ -7637,7 +7778,7 @@ Required: URL, optional CSS selector
|
|
|
7637
7778
|
welcome << " No message sources configured yet."
|
|
7638
7779
|
welcome << " Let's get you started with your first source."
|
|
7639
7780
|
welcome << ""
|
|
7640
|
-
welcome << " Available source types:".
|
|
7781
|
+
welcome << " Available source types:".bd.fg(39)
|
|
7641
7782
|
welcome << ""
|
|
7642
7783
|
welcome << " " + "1".fg(226) + " - Maildir (local email, works with offlineimap/mbsync/fetchmail)"
|
|
7643
7784
|
welcome << " " + "2".fg(226) + " - RSS/Atom feeds"
|
|
@@ -7831,19 +7972,72 @@ Required: URL, optional CSS selector
|
|
|
7831
7972
|
if view && view[:filters] && view[:filters]['rules'].is_a?(Array)
|
|
7832
7973
|
rule = view[:filters]['rules'].find { |r| r['field'] == 'source_type' && r['op'] == '=' }
|
|
7833
7974
|
source_type = rule['value'] if rule
|
|
7975
|
+
# Also check source_id rules to determine source type
|
|
7976
|
+
if !source_type
|
|
7977
|
+
sid_rule = view[:filters]['rules'].find { |r| r['field'] == 'source_id' && r['op'] == '=' }
|
|
7978
|
+
if sid_rule
|
|
7979
|
+
src = @db.get_source_by_id(sid_rule['value'].to_i)
|
|
7980
|
+
source_type = src['plugin_type'] if src
|
|
7981
|
+
end
|
|
7982
|
+
end
|
|
7834
7983
|
end
|
|
7835
7984
|
folder = current_view_folder
|
|
7836
7985
|
|
|
7837
7986
|
set_feedback("Syncing #{source_type || 'view'}...", 226, 30)
|
|
7838
7987
|
@needs_redraw = true
|
|
7839
7988
|
|
|
7989
|
+
# Extract thread info BEFORE spawning thread (current_message depends on UI state)
|
|
7990
|
+
cur_msg = current_message
|
|
7991
|
+
cur_meta = cur_msg && cur_msg['metadata']
|
|
7992
|
+
cur_meta = JSON.parse(cur_meta) if cur_meta.is_a?(String) rescue nil
|
|
7993
|
+
cur_thread_id = cur_meta['thread_id'] if cur_meta.is_a?(Hash)
|
|
7994
|
+
# For thread headers, look for thread_id in section messages
|
|
7995
|
+
if !cur_thread_id && cur_msg && cur_msg['section_messages']
|
|
7996
|
+
first_msg = cur_msg['section_messages'].first
|
|
7997
|
+
if first_msg
|
|
7998
|
+
fm = first_msg['metadata']
|
|
7999
|
+
fm = JSON.parse(fm) if fm.is_a?(String) rescue nil
|
|
8000
|
+
cur_thread_id = fm['thread_id'] if fm.is_a?(Hash)
|
|
8001
|
+
end
|
|
8002
|
+
end
|
|
8003
|
+
cur_thread_name = cur_msg['subject'] if cur_msg
|
|
8004
|
+
File.open('/tmp/heathrow_sync_debug.log', 'w') { |f|
|
|
8005
|
+
f.puts "source_type=#{source_type}"
|
|
8006
|
+
f.puts "cur_msg_id=#{cur_msg&.[]('id')}"
|
|
8007
|
+
f.puts "cur_msg_keys=#{cur_msg&.keys}"
|
|
8008
|
+
f.puts "cur_meta_class=#{cur_meta.class}"
|
|
8009
|
+
f.puts "cur_meta=#{cur_meta.inspect}"
|
|
8010
|
+
f.puts "cur_thread_id=#{cur_thread_id}"
|
|
8011
|
+
f.puts "cur_thread_name=#{cur_thread_name}"
|
|
8012
|
+
f.puts "is_header=#{cur_msg&.[]('is_header')}"
|
|
8013
|
+
f.puts "section_messages=#{cur_msg&.[]('section_messages')&.length}"
|
|
8014
|
+
}
|
|
8015
|
+
|
|
7840
8016
|
Thread.new do
|
|
7841
8017
|
thread_db = Heathrow::Database.new
|
|
8018
|
+
|
|
7842
8019
|
case source_type
|
|
7843
8020
|
when 'maildir' then sync_maildir(folder: folder, db: thread_db)
|
|
7844
8021
|
when 'rss' then sync_rss(db: thread_db)
|
|
7845
8022
|
when 'web' then sync_webwatch(db: thread_db)
|
|
7846
|
-
when 'messenger'
|
|
8023
|
+
when 'messenger'
|
|
8024
|
+
if cur_thread_id && cur_thread_name
|
|
8025
|
+
require_relative '../sources/messenger'
|
|
8026
|
+
src = thread_db.get_sources.find { |s| s['plugin_type'] == 'messenger' }
|
|
8027
|
+
if src
|
|
8028
|
+
config = src['config']
|
|
8029
|
+
config = JSON.parse(config) if config.is_a?(String)
|
|
8030
|
+
instance = Heathrow::Sources::Messenger.new(src['name'], config || {}, thread_db)
|
|
8031
|
+
count = instance.sync_thread(src['id'], cur_thread_id.to_s, cur_thread_name)
|
|
8032
|
+
if instance.sync_error
|
|
8033
|
+
@last_sync_errors = (@last_sync_errors || []) << instance.sync_error
|
|
8034
|
+
else
|
|
8035
|
+
@_thread_sync_count = count
|
|
8036
|
+
end
|
|
8037
|
+
end
|
|
8038
|
+
else
|
|
8039
|
+
sync_messenger(db: thread_db)
|
|
8040
|
+
end
|
|
7847
8041
|
when 'instagram' then sync_instagram(db: thread_db)
|
|
7848
8042
|
when 'weechat' then sync_weechat(db: thread_db)
|
|
7849
8043
|
else
|
|
@@ -7856,7 +8050,15 @@ Required: URL, optional CSS selector
|
|
|
7856
8050
|
end
|
|
7857
8051
|
thread_db.close rescue nil
|
|
7858
8052
|
@pending_view_refresh = true
|
|
7859
|
-
|
|
8053
|
+
if @last_sync_errors && !@last_sync_errors.empty?
|
|
8054
|
+
set_feedback("Sync errors: #{@last_sync_errors.join('; ')}", 208, 0)
|
|
8055
|
+
@last_sync_errors = nil
|
|
8056
|
+
elsif @_thread_sync_count
|
|
8057
|
+
set_feedback("Fetched #{@_thread_sync_count} messages from #{cur_thread_name}", @_thread_sync_count > 0 ? 156 : 208, 0)
|
|
8058
|
+
@_thread_sync_count = nil
|
|
8059
|
+
else
|
|
8060
|
+
set_feedback("Synced", 46, 2)
|
|
8061
|
+
end
|
|
7860
8062
|
rescue => e
|
|
7861
8063
|
set_feedback("Refresh error: #{e.message}", 196, 3)
|
|
7862
8064
|
end
|
|
@@ -8241,7 +8443,7 @@ Required: URL, optional CSS selector
|
|
|
8241
8443
|
|
|
8242
8444
|
# Format the event for display
|
|
8243
8445
|
lines = []
|
|
8244
|
-
lines << "Calendar Event".
|
|
8446
|
+
lines << "Calendar Event".bd.fg(226)
|
|
8245
8447
|
lines << ""
|
|
8246
8448
|
lines << "WHAT: #{event[:summary]}".fg(156) if event[:summary]
|
|
8247
8449
|
if event[:dates]
|
|
@@ -8365,7 +8567,7 @@ Required: URL, optional CSS selector
|
|
|
8365
8567
|
def format_attachments(attachments)
|
|
8366
8568
|
return nil unless attachments.is_a?(Array) && !attachments.empty?
|
|
8367
8569
|
lines = []
|
|
8368
|
-
lines << "Attachments:".
|
|
8570
|
+
lines << "Attachments:".bd.fg(208)
|
|
8369
8571
|
attachments.each_with_index do |att, i|
|
|
8370
8572
|
name = att['name'] || att['filename'] || 'unnamed'
|
|
8371
8573
|
size = att['size'] ? " (#{human_size(att['size'])})" : ''
|
|
@@ -8403,7 +8605,7 @@ Required: URL, optional CSS selector
|
|
|
8403
8605
|
result = ""
|
|
8404
8606
|
parts.each_with_index do |part, i|
|
|
8405
8607
|
result += base_color ? part.fg(base_color) : part
|
|
8406
|
-
result += urls[i].
|
|
8608
|
+
result += urls[i].ul.fg(link_color) if urls[i]
|
|
8407
8609
|
end
|
|
8408
8610
|
result
|
|
8409
8611
|
end
|
|
@@ -8535,14 +8737,18 @@ Required: URL, optional CSS selector
|
|
|
8535
8737
|
|
|
8536
8738
|
require_relative '../sources/messenger'
|
|
8537
8739
|
total = 0
|
|
8740
|
+
errors = []
|
|
8538
8741
|
sources.each do |source|
|
|
8539
8742
|
config = source['config']
|
|
8540
8743
|
config = JSON.parse(config) if config.is_a?(String)
|
|
8541
8744
|
instance = Heathrow::Sources::Messenger.new(source['name'], config, db)
|
|
8542
8745
|
total += (instance.sync(source['id']) || 0)
|
|
8746
|
+
errors << instance.sync_error if instance.sync_error
|
|
8543
8747
|
end
|
|
8748
|
+
@last_sync_errors = (@last_sync_errors || []) + errors if errors.any?
|
|
8544
8749
|
total > 0
|
|
8545
8750
|
rescue => e
|
|
8751
|
+
@last_sync_errors = (@last_sync_errors || []) << "Messenger: #{e.message}"
|
|
8546
8752
|
File.open('/tmp/heathrow_debug.log', 'a') { |f| f.puts "Messenger sync error: #{e.message}\n#{e.backtrace.first(3).join("\n")}" }
|
|
8547
8753
|
false
|
|
8548
8754
|
end
|