heathrow 0.7.5 → 0.7.7
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/lib/heathrow/database.rb +2 -2
- data/lib/heathrow/ui/application.rb +230 -175
- data/lib/heathrow/ui/threaded_view.rb +27 -39
- data/lib/heathrow/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d7c8e6773f02f56ba4ac097d508bafaf526e2aed3bb327220614640d5c1bc3bd
|
|
4
|
+
data.tar.gz: 78db591713e7d944436a7c797c7099f337e5050bd4bce8689b38637330665056
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c7b866bfaaf9c2f2edd272be1950fd1730d0e40df9500779dbc51fa8f166e9a149472edd7b1099eea0ece4b713b62201e010cd165161c1a8a0f15725400a00ec
|
|
7
|
+
data.tar.gz: b1bde82078786c429268cf54cce8bef68de0ef03368612e90414c9b484e76181fe3aba59e1157daa7808c968128c750e565d273ea4c30044fa19a847314d76fb
|
data/lib/heathrow/database.rb
CHANGED
|
@@ -375,9 +375,9 @@ module Heathrow
|
|
|
375
375
|
# Handle sender pattern (supports regex via pipe separation)
|
|
376
376
|
if filters[:sender_pattern]
|
|
377
377
|
patterns = filters[:sender_pattern].split('|')
|
|
378
|
-
conditions = patterns.map { "sender LIKE ?" }.join(' OR ')
|
|
378
|
+
conditions = patterns.map { "(sender LIKE ? OR sender_name LIKE ?)" }.join(' OR ')
|
|
379
379
|
query += " AND (#{conditions})"
|
|
380
|
-
|
|
380
|
+
patterns.each { |p| params += ["%#{p}%", "%#{p}%"] }
|
|
381
381
|
end
|
|
382
382
|
|
|
383
383
|
# Handle subject pattern
|
|
@@ -206,14 +206,24 @@ module Heathrow
|
|
|
206
206
|
selected ? base.merge(selected) : base
|
|
207
207
|
end
|
|
208
208
|
|
|
209
|
+
def header_message?(msg)
|
|
210
|
+
msg['is_header'] || msg['is_channel_header'] || msg['is_thread_header'] || msg['is_dm_header']
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def invalidate_counts
|
|
214
|
+
@cached_unread = nil
|
|
215
|
+
@cached_starred = nil
|
|
216
|
+
@cached_total = nil
|
|
217
|
+
end
|
|
218
|
+
|
|
209
219
|
# Get the current message at @index (threaded or flat view)
|
|
210
220
|
def current_message
|
|
211
|
-
|
|
221
|
+
current_message_for_navigation
|
|
212
222
|
end
|
|
213
223
|
|
|
214
224
|
# Get the current message count (threaded or flat view)
|
|
215
225
|
def message_count
|
|
216
|
-
|
|
226
|
+
filtered_messages_size
|
|
217
227
|
end
|
|
218
228
|
|
|
219
229
|
# Mark the previous message as read when moving away from it
|
|
@@ -223,7 +233,7 @@ module Heathrow
|
|
|
223
233
|
# Remember current message so it gets marked read when we leave it
|
|
224
234
|
msg = current_message
|
|
225
235
|
return unless msg
|
|
226
|
-
return if msg
|
|
236
|
+
return if header_message?(msg)
|
|
227
237
|
return unless msg['id'] && !msg['id'].to_s.start_with?('header_')
|
|
228
238
|
|
|
229
239
|
if msg['is_read'].to_i == 0
|
|
@@ -246,6 +256,7 @@ module Heathrow
|
|
|
246
256
|
|
|
247
257
|
@db.mark_as_read(msg['id'])
|
|
248
258
|
msg['is_read'] = 1
|
|
259
|
+
invalidate_counts
|
|
249
260
|
sync_maildir_flag(msg, 'S', true)
|
|
250
261
|
|
|
251
262
|
# Also update any other references to this message in filtered/display lists
|
|
@@ -281,21 +292,22 @@ module Heathrow
|
|
|
281
292
|
count = @browsed_message_ids.size
|
|
282
293
|
return if count == 0
|
|
283
294
|
|
|
295
|
+
msg_by_id = @filtered_messages.each_with_object({}) { |m, h| h[m['id']] = m }
|
|
284
296
|
@browsed_message_ids.each do |msg_id|
|
|
285
297
|
@db.mark_as_read(msg_id)
|
|
286
|
-
|
|
287
|
-
msg = @filtered_messages.find { |m| m['id'] == msg_id }
|
|
298
|
+
msg = msg_by_id[msg_id]
|
|
288
299
|
msg['is_read'] = 1 if msg
|
|
289
300
|
end
|
|
290
301
|
|
|
291
302
|
@browsed_message_ids.clear
|
|
303
|
+
invalidate_counts
|
|
292
304
|
set_feedback("Marked #{count} browsed messages as read", 156, 3)
|
|
293
305
|
|
|
294
306
|
# Remove from view if in unread view
|
|
295
307
|
if is_unread_view?
|
|
296
308
|
@filtered_messages.select! { |m| m['is_read'].to_i == 0 }
|
|
297
309
|
@index = 0
|
|
298
|
-
reset_threading
|
|
310
|
+
reset_threading
|
|
299
311
|
end
|
|
300
312
|
|
|
301
313
|
render_all
|
|
@@ -391,16 +403,13 @@ module Heathrow
|
|
|
391
403
|
|
|
392
404
|
# EXACTLY LIKE RTFM
|
|
393
405
|
# Initialize rcurses
|
|
394
|
-
File.write('/tmp/heathrow_start.txt', "Starting run method\n") if ENV['DEBUG']
|
|
395
406
|
Rcurses.init!
|
|
396
|
-
File.write('/tmp/heathrow_start.txt', "Rcurses initialized\n", mode: 'a') if ENV['DEBUG']
|
|
397
407
|
|
|
398
408
|
# Clear screen to remove any artifacts
|
|
399
409
|
Rcurses.clear_screen
|
|
400
410
|
|
|
401
411
|
# Get terminal size
|
|
402
412
|
setup_display
|
|
403
|
-
File.write('/tmp/heathrow_start.txt', "Display setup complete: #{@w}x#{@h}\n", mode: 'a') if ENV['DEBUG']
|
|
404
413
|
|
|
405
414
|
# Create panes and show skeleton immediately
|
|
406
415
|
create_panes
|
|
@@ -440,8 +449,8 @@ module Heathrow
|
|
|
440
449
|
end
|
|
441
450
|
sort_messages
|
|
442
451
|
@index = 0
|
|
443
|
-
reset_threading
|
|
444
|
-
restore_view_thread_mode
|
|
452
|
+
reset_threading
|
|
453
|
+
restore_view_thread_mode
|
|
445
454
|
@initial_load_done = true
|
|
446
455
|
@needs_redraw = true
|
|
447
456
|
# Preload the heavy mail gem so 'v' (attachments) doesn't lag
|
|
@@ -481,14 +490,14 @@ module Heathrow
|
|
|
481
490
|
sort_messages
|
|
482
491
|
end
|
|
483
492
|
# Force re-organization in threaded mode
|
|
484
|
-
if @show_threaded
|
|
493
|
+
if @show_threaded
|
|
485
494
|
reset_threading(true)
|
|
486
|
-
organize_current_messages(true)
|
|
495
|
+
organize_current_messages(true)
|
|
487
496
|
end
|
|
488
497
|
# Defer index restoration for threaded views (display_messages is empty
|
|
489
498
|
# after reset_threading; it gets populated during render). For flat views,
|
|
490
499
|
# resolve immediately since @filtered_messages is the navigation list.
|
|
491
|
-
if @show_threaded
|
|
500
|
+
if @show_threaded
|
|
492
501
|
@pending_restore_id = selected_id
|
|
493
502
|
@index = 0
|
|
494
503
|
elsif selected_id
|
|
@@ -826,6 +835,8 @@ module Heathrow
|
|
|
826
835
|
label_message
|
|
827
836
|
when 'v'
|
|
828
837
|
view_attachments
|
|
838
|
+
when 'Z'
|
|
839
|
+
open_in_timely
|
|
829
840
|
when 'I'
|
|
830
841
|
ai_assistant
|
|
831
842
|
when 'V'
|
|
@@ -1143,7 +1154,7 @@ module Heathrow
|
|
|
1143
1154
|
set_feedback("Deleted: #{name} (#{deleted_msgs} messages removed)", 156, 3)
|
|
1144
1155
|
|
|
1145
1156
|
# Re-load the view to reflect removed messages
|
|
1146
|
-
reset_threading
|
|
1157
|
+
reset_threading
|
|
1147
1158
|
switch_to_view(@current_view) if @current_view
|
|
1148
1159
|
end
|
|
1149
1160
|
|
|
@@ -1362,10 +1373,15 @@ module Heathrow
|
|
|
1362
1373
|
count_text = "#{total} sources"
|
|
1363
1374
|
else
|
|
1364
1375
|
# For message views, show unread/total plus starred
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1376
|
+
if @cached_unread.nil?
|
|
1377
|
+
real_msgs = @filtered_messages.reject { |m| header_message?(m) }
|
|
1378
|
+
@cached_unread = real_msgs.count { |m| m['is_read'].to_i == 0 }
|
|
1379
|
+
@cached_starred = real_msgs.count { |m| m['is_starred'].to_i == 1 }
|
|
1380
|
+
@cached_total = real_msgs.size
|
|
1381
|
+
end
|
|
1382
|
+
unread = @cached_unread
|
|
1383
|
+
starred = @cached_starred
|
|
1384
|
+
total = @cached_total
|
|
1369
1385
|
count_text = "#{unread} unread / #{total} msgs"
|
|
1370
1386
|
count_text += " / #{starred}*" if starred > 0
|
|
1371
1387
|
end
|
|
@@ -1381,6 +1397,7 @@ module Heathrow
|
|
|
1381
1397
|
when 'latest' then 'Latest'
|
|
1382
1398
|
when 'alphabetical' then 'A-Z'
|
|
1383
1399
|
when 'sender' then 'Sender'
|
|
1400
|
+
when 'from' then 'From'
|
|
1384
1401
|
when 'unread' then 'Unread'
|
|
1385
1402
|
when 'source' then 'Source'
|
|
1386
1403
|
else @sort_order.capitalize
|
|
@@ -1392,7 +1409,7 @@ module Heathrow
|
|
|
1392
1409
|
|
|
1393
1410
|
# Threading mode indicator
|
|
1394
1411
|
mode_part = ""
|
|
1395
|
-
if @current_view != 'S'
|
|
1412
|
+
if @current_view != 'S'
|
|
1396
1413
|
mode_part = " [#{thread_mode_label}]".fg(245)
|
|
1397
1414
|
end
|
|
1398
1415
|
|
|
@@ -1520,7 +1537,7 @@ module Heathrow
|
|
|
1520
1537
|
end
|
|
1521
1538
|
|
|
1522
1539
|
# Build the line with timestamp, icon and sender
|
|
1523
|
-
icon =
|
|
1540
|
+
icon = get_source_icon(msg['source_type'])
|
|
1524
1541
|
line_prefix = "#{timestamp} #{icon} #{sender_display} "
|
|
1525
1542
|
|
|
1526
1543
|
# Calculate remaining space for subject (use display_width for CJK)
|
|
@@ -1837,7 +1854,7 @@ module Heathrow
|
|
|
1837
1854
|
show_loading("Loading sources...")
|
|
1838
1855
|
|
|
1839
1856
|
# Reset threading state when changing views
|
|
1840
|
-
reset_threading
|
|
1857
|
+
reset_threading
|
|
1841
1858
|
|
|
1842
1859
|
# Reload source colors to ensure they're fresh
|
|
1843
1860
|
load_source_colors
|
|
@@ -1885,6 +1902,7 @@ module Heathrow
|
|
|
1885
1902
|
|
|
1886
1903
|
def show_all_messages
|
|
1887
1904
|
flush_pending_read
|
|
1905
|
+
invalidate_counts
|
|
1888
1906
|
@current_view = 'A'
|
|
1889
1907
|
@current_folder = nil
|
|
1890
1908
|
@in_source_view = false
|
|
@@ -1895,8 +1913,8 @@ module Heathrow
|
|
|
1895
1913
|
@last_rendered_index = nil # Force right pane refresh
|
|
1896
1914
|
|
|
1897
1915
|
# Restore per-view threading mode
|
|
1898
|
-
reset_threading
|
|
1899
|
-
restore_view_thread_mode
|
|
1916
|
+
reset_threading
|
|
1917
|
+
restore_view_thread_mode
|
|
1900
1918
|
|
|
1901
1919
|
# Show view name instantly
|
|
1902
1920
|
render_top_bar
|
|
@@ -1912,14 +1930,6 @@ module Heathrow
|
|
|
1912
1930
|
def render_message_content
|
|
1913
1931
|
current_msg = current_message
|
|
1914
1932
|
|
|
1915
|
-
if ENV['DEBUG']
|
|
1916
|
-
File.open('/tmp/heathrow_debug.log', 'a') do |f|
|
|
1917
|
-
f.puts "render_message_content START"
|
|
1918
|
-
f.puts " @index: #{@index}"
|
|
1919
|
-
f.puts " @filtered_messages[@index] exists: #{!current_msg.nil?}"
|
|
1920
|
-
end
|
|
1921
|
-
end
|
|
1922
|
-
|
|
1923
1933
|
# Reset scroll position when switching messages
|
|
1924
1934
|
if @last_rendered_index != @index
|
|
1925
1935
|
@panes[:right].ix = 0
|
|
@@ -1947,23 +1957,11 @@ module Heathrow
|
|
|
1947
1957
|
end
|
|
1948
1958
|
|
|
1949
1959
|
# Special handling for headers (channel headers, thread headers, etc.)
|
|
1950
|
-
if msg
|
|
1960
|
+
if header_message?(msg)
|
|
1951
1961
|
render_header_summary(msg)
|
|
1952
1962
|
return
|
|
1953
1963
|
end
|
|
1954
1964
|
|
|
1955
|
-
if ENV['DEBUG']
|
|
1956
|
-
File.open('/tmp/heathrow_debug.log', 'a') do |f|
|
|
1957
|
-
f.puts " msg details:"
|
|
1958
|
-
f.puts " id: #{msg['id']}"
|
|
1959
|
-
f.puts " sender: #{msg['sender']}"
|
|
1960
|
-
f.puts " recipient: #{msg['recipient']}"
|
|
1961
|
-
f.puts " subject: #{msg['subject']}"
|
|
1962
|
-
f.puts " timestamp: #{msg['timestamp'].inspect}"
|
|
1963
|
-
f.puts " source_type: #{msg['source_type'].inspect}"
|
|
1964
|
-
end
|
|
1965
|
-
end
|
|
1966
|
-
|
|
1967
1965
|
# Format message header
|
|
1968
1966
|
header = []
|
|
1969
1967
|
|
|
@@ -2000,23 +1998,20 @@ module Heathrow
|
|
|
2000
1998
|
else
|
|
2001
1999
|
# Regular message display
|
|
2002
2000
|
header << "From: #{msg['sender']}".fg(2) if msg['sender']
|
|
2003
|
-
# Show recipients (To field)
|
|
2001
|
+
# Show recipients (To field, already parsed by normalize_message_row)
|
|
2004
2002
|
to = msg['recipients'] || msg['recipient']
|
|
2005
2003
|
if to
|
|
2006
|
-
|
|
2007
|
-
to_str = to_list.is_a?(Array) ? to_list.join(', ') : to_list.to_s
|
|
2004
|
+
to_str = to.is_a?(Array) ? to.join(', ') : to.to_s
|
|
2008
2005
|
header << "To: #{to_str}".fg(2) unless to_str.empty?
|
|
2009
2006
|
end
|
|
2010
|
-
# Show CC recipients
|
|
2007
|
+
# Show CC recipients (already parsed by normalize_message_row)
|
|
2011
2008
|
cc = msg['cc']
|
|
2012
2009
|
if cc
|
|
2013
|
-
|
|
2014
|
-
cc_str = cc_list.is_a?(Array) ? cc_list.join(', ') : cc_list.to_s
|
|
2010
|
+
cc_str = cc.is_a?(Array) ? cc.join(', ') : cc.to_s
|
|
2015
2011
|
header << "Cc: #{cc_str}".fg(2) unless cc_str.empty?
|
|
2016
2012
|
end
|
|
2017
2013
|
# For weechat, show channel name from metadata instead of content preview
|
|
2018
2014
|
meta = msg['metadata']
|
|
2019
|
-
meta = JSON.parse(meta) if meta.is_a?(String) rescue nil
|
|
2020
2015
|
if meta.is_a?(Hash) && meta['channel_name']
|
|
2021
2016
|
header << "Subject: #{meta['channel_name']}".b.fg(1)
|
|
2022
2017
|
elsif msg['subject']
|
|
@@ -2024,22 +2019,9 @@ module Heathrow
|
|
|
2024
2019
|
end
|
|
2025
2020
|
end
|
|
2026
2021
|
|
|
2027
|
-
if ENV['DEBUG']
|
|
2028
|
-
File.open('/tmp/heathrow_debug.log', 'a') do |f|
|
|
2029
|
-
f.puts " About to parse timestamp: #{msg['timestamp'].inspect}"
|
|
2030
|
-
end
|
|
2031
|
-
end
|
|
2032
|
-
|
|
2033
2022
|
# Parse timestamp using helper method
|
|
2034
2023
|
date_str = parse_timestamp(msg['timestamp'], '%Y-%m-%d %H:%M:%S') || "Unknown date"
|
|
2035
|
-
|
|
2036
|
-
if ENV['DEBUG']
|
|
2037
|
-
File.open('/tmp/heathrow_debug.log', 'a') do |f|
|
|
2038
|
-
f.puts " Final date_str: #{date_str}"
|
|
2039
|
-
f.puts " About to add header lines"
|
|
2040
|
-
end
|
|
2041
|
-
end
|
|
2042
|
-
|
|
2024
|
+
|
|
2043
2025
|
header << "Date: #{date_str}".fg(240)
|
|
2044
2026
|
if msg['source_type']
|
|
2045
2027
|
source_label = case msg['source_type']
|
|
@@ -2050,10 +2032,10 @@ module Heathrow
|
|
|
2050
2032
|
header << "Type: #{source_label}".fg(get_source_color(msg))
|
|
2051
2033
|
end
|
|
2052
2034
|
|
|
2053
|
-
# Show labels (if any beyond the folder name)
|
|
2035
|
+
# Show labels (if any beyond the folder name, already parsed by normalize_message_row)
|
|
2054
2036
|
labels = msg['labels']
|
|
2055
|
-
labels =
|
|
2056
|
-
if labels.
|
|
2037
|
+
labels = [] unless labels.is_a?(Array)
|
|
2038
|
+
if labels.size > 0
|
|
2057
2039
|
# Skip folder name (first label) if it matches the folder
|
|
2058
2040
|
display_labels = labels.reject { |l| l == msg['folder'] }
|
|
2059
2041
|
unless display_labels.empty?
|
|
@@ -2061,11 +2043,6 @@ module Heathrow
|
|
|
2061
2043
|
end
|
|
2062
2044
|
end
|
|
2063
2045
|
|
|
2064
|
-
if ENV['DEBUG']
|
|
2065
|
-
File.open('/tmp/heathrow_debug.log', 'a') do |f|
|
|
2066
|
-
f.puts " Header built successfully, about to format content"
|
|
2067
|
-
end
|
|
2068
|
-
end
|
|
2069
2046
|
header << ("─" * 60).fg(238)
|
|
2070
2047
|
|
|
2071
2048
|
# Format message body — prefer HTML rendered via w3m
|
|
@@ -2134,13 +2111,6 @@ module Heathrow
|
|
|
2134
2111
|
content = content_parts.join("\n")
|
|
2135
2112
|
end
|
|
2136
2113
|
|
|
2137
|
-
if ENV['DEBUG']
|
|
2138
|
-
File.open('/tmp/heathrow_debug.log', 'a') do |f|
|
|
2139
|
-
f.puts " Content formatted, about to set pane text"
|
|
2140
|
-
f.puts " Content length: #{content.length}"
|
|
2141
|
-
end
|
|
2142
|
-
end
|
|
2143
|
-
|
|
2144
2114
|
# Ensure content is UTF-8 compatible (handle emails with various encodings)
|
|
2145
2115
|
# Create a mutable copy if frozen, then fix encoding
|
|
2146
2116
|
content = content.dup if content.frozen?
|
|
@@ -2154,9 +2124,8 @@ module Heathrow
|
|
|
2154
2124
|
# Colorize email content (quote levels + signature)
|
|
2155
2125
|
content = colorize_email_content(content)
|
|
2156
2126
|
|
|
2157
|
-
# Store maildir file path for calendar parser
|
|
2127
|
+
# Store maildir file path for calendar parser (metadata already parsed by normalize_message_row)
|
|
2158
2128
|
meta = msg['metadata']
|
|
2159
|
-
meta = JSON.parse(meta) if meta.is_a?(String) rescue nil
|
|
2160
2129
|
@_current_render_msg_file = meta['maildir_file'] if meta.is_a?(Hash)
|
|
2161
2130
|
|
|
2162
2131
|
# Attachment list under header, before body
|
|
@@ -2193,11 +2162,6 @@ module Heathrow
|
|
|
2193
2162
|
@panes[:right].text = full_text
|
|
2194
2163
|
@panes[:right].refresh
|
|
2195
2164
|
|
|
2196
|
-
if ENV['DEBUG']
|
|
2197
|
-
File.open('/tmp/heathrow_debug.log', 'a') do |f|
|
|
2198
|
-
f.puts "render_message_content END - success"
|
|
2199
|
-
end
|
|
2200
|
-
end
|
|
2201
2165
|
end
|
|
2202
2166
|
|
|
2203
2167
|
def render_header_summary(header_msg)
|
|
@@ -2392,7 +2356,7 @@ module Heathrow
|
|
|
2392
2356
|
end
|
|
2393
2357
|
|
|
2394
2358
|
# Force threaded view to rebuild organizer with new messages
|
|
2395
|
-
if @show_threaded
|
|
2359
|
+
if @show_threaded
|
|
2396
2360
|
organize_current_messages(true)
|
|
2397
2361
|
end
|
|
2398
2362
|
|
|
@@ -2455,7 +2419,7 @@ module Heathrow
|
|
|
2455
2419
|
}
|
|
2456
2420
|
end
|
|
2457
2421
|
|
|
2458
|
-
def
|
|
2422
|
+
def build_db_filters(view)
|
|
2459
2423
|
filters = view[:filters] || {}
|
|
2460
2424
|
db_filters = {}
|
|
2461
2425
|
if filters['rules'].is_a?(Array)
|
|
@@ -2476,6 +2440,11 @@ module Heathrow
|
|
|
2476
2440
|
end
|
|
2477
2441
|
end
|
|
2478
2442
|
end
|
|
2443
|
+
db_filters
|
|
2444
|
+
end
|
|
2445
|
+
|
|
2446
|
+
def apply_view_filters_with_limit(view, limit)
|
|
2447
|
+
db_filters = build_db_filters(view)
|
|
2479
2448
|
@filtered_messages = @db.get_messages(db_filters, limit, 0, light: true)
|
|
2480
2449
|
end
|
|
2481
2450
|
|
|
@@ -2552,6 +2521,7 @@ module Heathrow
|
|
|
2552
2521
|
# View switching
|
|
2553
2522
|
def show_new_messages
|
|
2554
2523
|
flush_pending_read
|
|
2524
|
+
invalidate_counts
|
|
2555
2525
|
@current_view = 'N'
|
|
2556
2526
|
@current_folder = nil
|
|
2557
2527
|
@in_source_view = false
|
|
@@ -2561,8 +2531,8 @@ module Heathrow
|
|
|
2561
2531
|
@last_rendered_index = nil # Force right pane refresh
|
|
2562
2532
|
|
|
2563
2533
|
# Restore per-view threading mode
|
|
2564
|
-
reset_threading
|
|
2565
|
-
restore_view_thread_mode
|
|
2534
|
+
reset_threading
|
|
2535
|
+
restore_view_thread_mode
|
|
2566
2536
|
|
|
2567
2537
|
render_top_bar
|
|
2568
2538
|
|
|
@@ -2585,6 +2555,7 @@ module Heathrow
|
|
|
2585
2555
|
|
|
2586
2556
|
def switch_to_view(key)
|
|
2587
2557
|
flush_pending_read
|
|
2558
|
+
invalidate_counts
|
|
2588
2559
|
@current_view = key
|
|
2589
2560
|
@current_folder = nil
|
|
2590
2561
|
@in_source_view = false
|
|
@@ -2592,8 +2563,8 @@ module Heathrow
|
|
|
2592
2563
|
@last_rendered_index = nil # Force right pane refresh
|
|
2593
2564
|
|
|
2594
2565
|
# Restore per-view threading mode
|
|
2595
|
-
reset_threading
|
|
2596
|
-
restore_view_thread_mode
|
|
2566
|
+
reset_threading
|
|
2567
|
+
restore_view_thread_mode
|
|
2597
2568
|
|
|
2598
2569
|
render_top_bar
|
|
2599
2570
|
|
|
@@ -2631,12 +2602,12 @@ module Heathrow
|
|
|
2631
2602
|
|
|
2632
2603
|
def apply_view_filters(view)
|
|
2633
2604
|
filters = view[:filters] || {}
|
|
2634
|
-
|
|
2605
|
+
|
|
2635
2606
|
# Check for special filter types
|
|
2636
2607
|
if filters['special'] == 'uncategorized'
|
|
2637
2608
|
# Get all messages first
|
|
2638
2609
|
all_messages = @db.get_messages({}, 1000, 0, light: true)
|
|
2639
|
-
|
|
2610
|
+
|
|
2640
2611
|
# Get messages from all other configured views
|
|
2641
2612
|
categorized_ids = Set.new
|
|
2642
2613
|
|
|
@@ -2648,7 +2619,7 @@ module Heathrow
|
|
|
2648
2619
|
other_view[:filters].each do |key, value|
|
|
2649
2620
|
symbolized_filters[key.to_sym] = value
|
|
2650
2621
|
end
|
|
2651
|
-
|
|
2622
|
+
|
|
2652
2623
|
view_messages = @db.get_messages(symbolized_filters, 1000, 0, light: true)
|
|
2653
2624
|
view_messages.each { |msg| categorized_ids.add(msg['id']) }
|
|
2654
2625
|
end
|
|
@@ -2657,32 +2628,12 @@ module Heathrow
|
|
|
2657
2628
|
# Filter to only uncategorized messages
|
|
2658
2629
|
@filtered_messages = all_messages.reject { |msg| categorized_ids.include?(msg['id']) }
|
|
2659
2630
|
else
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
if filters['rules'].is_a?(Array)
|
|
2664
|
-
# Convert rules array to simple database filters
|
|
2665
|
-
filters['rules'].each do |rule|
|
|
2666
|
-
field = rule['field']
|
|
2667
|
-
value = rule['value']
|
|
2668
|
-
case rule['op']
|
|
2669
|
-
when '='
|
|
2670
|
-
db_filters[field.to_sym] = value
|
|
2671
|
-
when 'like'
|
|
2672
|
-
case field
|
|
2673
|
-
when 'search' then db_filters[:search] = value
|
|
2674
|
-
when 'sender' then db_filters[:sender_pattern] = value
|
|
2675
|
-
when 'subject' then db_filters[:subject_pattern] = value
|
|
2676
|
-
when 'folder' then db_filters[:maildir_folder] = value
|
|
2677
|
-
when 'label' then db_filters[:label] = value
|
|
2678
|
-
when 'source' then db_filters[:source_name] = value
|
|
2679
|
-
end
|
|
2680
|
-
end
|
|
2681
|
-
end
|
|
2682
|
-
else
|
|
2683
|
-
# Legacy simple filters - convert string keys to symbols
|
|
2631
|
+
db_filters = build_db_filters(view)
|
|
2632
|
+
|
|
2633
|
+
# Legacy simple filters (no rules array)
|
|
2634
|
+
if !filters['rules'].is_a?(Array)
|
|
2684
2635
|
filters.each do |key, value|
|
|
2685
|
-
next if key == 'rules'
|
|
2636
|
+
next if key == 'rules'
|
|
2686
2637
|
db_filters[key.to_sym] = value
|
|
2687
2638
|
end
|
|
2688
2639
|
end
|
|
@@ -2694,7 +2645,6 @@ module Heathrow
|
|
|
2694
2645
|
ensure_all_feeds_loaded(db_filters[:source_id].to_i)
|
|
2695
2646
|
end
|
|
2696
2647
|
end
|
|
2697
|
-
|
|
2698
2648
|
end
|
|
2699
2649
|
|
|
2700
2650
|
# Message operations
|
|
@@ -2748,7 +2698,7 @@ module Heathrow
|
|
|
2748
2698
|
end
|
|
2749
2699
|
|
|
2750
2700
|
# Re-render
|
|
2751
|
-
render_message_list_threaded
|
|
2701
|
+
render_message_list_threaded
|
|
2752
2702
|
render_message_content
|
|
2753
2703
|
end
|
|
2754
2704
|
|
|
@@ -2786,7 +2736,7 @@ module Heathrow
|
|
|
2786
2736
|
end
|
|
2787
2737
|
|
|
2788
2738
|
# Re-render
|
|
2789
|
-
render_message_list_threaded
|
|
2739
|
+
render_message_list_threaded
|
|
2790
2740
|
|
|
2791
2741
|
# Restore position if we had one saved
|
|
2792
2742
|
if restore_position
|
|
@@ -2820,7 +2770,7 @@ module Heathrow
|
|
|
2820
2770
|
def toggle_tag
|
|
2821
2771
|
msg = current_message
|
|
2822
2772
|
return unless msg
|
|
2823
|
-
return if msg
|
|
2773
|
+
return if header_message?(msg)
|
|
2824
2774
|
return unless msg['id']
|
|
2825
2775
|
|
|
2826
2776
|
if @tagged_messages.include?(msg['id'])
|
|
@@ -2847,7 +2797,7 @@ module Heathrow
|
|
|
2847
2797
|
|
|
2848
2798
|
count = 0
|
|
2849
2799
|
@filtered_messages.each do |msg|
|
|
2850
|
-
next if msg
|
|
2800
|
+
next if header_message?(msg)
|
|
2851
2801
|
next unless msg['id']
|
|
2852
2802
|
|
|
2853
2803
|
match = [msg['sender'], msg['subject'], msg['content']].compact.any? { |f| f.match?(regex) }
|
|
@@ -2867,7 +2817,7 @@ module Heathrow
|
|
|
2867
2817
|
|
|
2868
2818
|
# Tag/untag all messages in current view
|
|
2869
2819
|
def tag_all_toggle
|
|
2870
|
-
msgs = @filtered_messages.reject { |m| m
|
|
2820
|
+
msgs = @filtered_messages.reject { |m| header_message?(m) }
|
|
2871
2821
|
ids = msgs.map { |m| m['id'] }.compact
|
|
2872
2822
|
if ids.all? { |id| @tagged_messages.include?(id) }
|
|
2873
2823
|
# All tagged, untag all
|
|
@@ -2895,7 +2845,7 @@ module Heathrow
|
|
|
2895
2845
|
idx = (@index + 1 + i) % display.size
|
|
2896
2846
|
msg = display[idx]
|
|
2897
2847
|
next unless msg
|
|
2898
|
-
next if msg
|
|
2848
|
+
next if header_message?(msg)
|
|
2899
2849
|
if msg['is_read'].to_i == 0
|
|
2900
2850
|
@index = idx
|
|
2901
2851
|
track_browsed_message
|
|
@@ -2914,14 +2864,14 @@ module Heathrow
|
|
|
2914
2864
|
if unread_msgs.any?
|
|
2915
2865
|
# Re-render to rebuild display_messages with uncollapsed sections
|
|
2916
2866
|
organize_current_messages(true)
|
|
2917
|
-
render_message_list_threaded
|
|
2867
|
+
render_message_list_threaded
|
|
2918
2868
|
new_display = @display_messages || @filtered_messages
|
|
2919
2869
|
# Find the first visible unread after current position
|
|
2920
2870
|
new_display.size.times do |i|
|
|
2921
2871
|
idx = (@index + 1 + i) % new_display.size
|
|
2922
2872
|
m = new_display[idx]
|
|
2923
2873
|
next unless m && m['id'] && m['is_read'].to_i == 0
|
|
2924
|
-
next if m
|
|
2874
|
+
next if header_message?(m)
|
|
2925
2875
|
@index = idx
|
|
2926
2876
|
track_browsed_message
|
|
2927
2877
|
render_all
|
|
@@ -2943,7 +2893,7 @@ module Heathrow
|
|
|
2943
2893
|
idx = (@index - 1 - i) % size
|
|
2944
2894
|
msg = display[idx]
|
|
2945
2895
|
next unless msg
|
|
2946
|
-
next if msg
|
|
2896
|
+
next if header_message?(msg)
|
|
2947
2897
|
if msg['is_read'].to_i == 0
|
|
2948
2898
|
@index = idx
|
|
2949
2899
|
track_browsed_message
|
|
@@ -2960,14 +2910,14 @@ module Heathrow
|
|
|
2960
2910
|
end
|
|
2961
2911
|
if unread_msgs.any?
|
|
2962
2912
|
organize_current_messages(true)
|
|
2963
|
-
render_message_list_threaded
|
|
2913
|
+
render_message_list_threaded
|
|
2964
2914
|
new_display = @display_messages || @filtered_messages
|
|
2965
2915
|
new_size = new_display.size
|
|
2966
2916
|
new_size.times do |i|
|
|
2967
2917
|
idx = (@index - 1 - i) % new_size
|
|
2968
2918
|
m = new_display[idx]
|
|
2969
2919
|
next unless m && m['id'] && m['is_read'].to_i == 0
|
|
2970
|
-
next if m
|
|
2920
|
+
next if header_message?(m)
|
|
2971
2921
|
@index = idx
|
|
2972
2922
|
track_browsed_message
|
|
2973
2923
|
render_all
|
|
@@ -3050,10 +3000,87 @@ module Heathrow
|
|
|
3050
3000
|
(msg['content'] && msg['content'] =~ /\A\s*<(!DOCTYPE|html|head|body)\b/i)
|
|
3051
3001
|
end
|
|
3052
3002
|
|
|
3003
|
+
def open_in_timely
|
|
3004
|
+
msg = current_message
|
|
3005
|
+
return unless msg
|
|
3006
|
+
msg = ensure_full_message(msg)
|
|
3007
|
+
|
|
3008
|
+
# Try to find a date from calendar data or message timestamp
|
|
3009
|
+
timely_home = File.expand_path('~/.timely')
|
|
3010
|
+
return set_feedback("Timely not configured (~/.timely missing)", 196, 3) unless File.directory?(timely_home)
|
|
3011
|
+
|
|
3012
|
+
# Check for ICS attachment or inline calendar data
|
|
3013
|
+
date_str = nil
|
|
3014
|
+
meta = msg['metadata']
|
|
3015
|
+
meta = JSON.parse(meta) if meta.is_a?(String) rescue nil
|
|
3016
|
+
file = meta['maildir_file'] if meta.is_a?(Hash)
|
|
3017
|
+
|
|
3018
|
+
if file && File.exist?(file)
|
|
3019
|
+
begin
|
|
3020
|
+
require 'mail'
|
|
3021
|
+
mail = Mail.read(file)
|
|
3022
|
+
if mail.multipart?
|
|
3023
|
+
mail.parts.each do |part|
|
|
3024
|
+
ct = (part.content_type || '').downcase
|
|
3025
|
+
if ct.include?('calendar') || ct.include?('ics')
|
|
3026
|
+
ics = part.decoded
|
|
3027
|
+
vevent = ics[/BEGIN:VEVENT(.*?)END:VEVENT/m, 1]
|
|
3028
|
+
if vevent
|
|
3029
|
+
vevent = vevent.gsub(/\r?\n[ \t]/, '')
|
|
3030
|
+
if vevent =~ /^DTSTART;TZID=[^:]*:(\d{8})/i ||
|
|
3031
|
+
vevent =~ /^DTSTART:(\d{8})/i ||
|
|
3032
|
+
vevent =~ /^DTSTART;VALUE=DATE:(\d{8})/i
|
|
3033
|
+
d = $1
|
|
3034
|
+
date_str = "#{d[0,4]}-#{d[4,2]}-#{d[6,2]}"
|
|
3035
|
+
end
|
|
3036
|
+
end
|
|
3037
|
+
break
|
|
3038
|
+
end
|
|
3039
|
+
end
|
|
3040
|
+
end
|
|
3041
|
+
rescue => e
|
|
3042
|
+
# Fall through to timestamp
|
|
3043
|
+
end
|
|
3044
|
+
end
|
|
3045
|
+
|
|
3046
|
+
# Fallback: use message timestamp
|
|
3047
|
+
unless date_str
|
|
3048
|
+
ts = msg['timestamp'].to_i
|
|
3049
|
+
date_str = Time.at(ts).strftime('%Y-%m-%d') if ts > 0
|
|
3050
|
+
end
|
|
3051
|
+
|
|
3052
|
+
return set_feedback("Could not determine date for Timely", 245, 2) unless date_str
|
|
3053
|
+
|
|
3054
|
+
# Write goto file for Timely
|
|
3055
|
+
File.write(File.join(timely_home, 'goto'), date_str)
|
|
3056
|
+
|
|
3057
|
+
# Also copy ICS to incoming if it has calendar data
|
|
3058
|
+
if file && File.exist?(file)
|
|
3059
|
+
begin
|
|
3060
|
+
incoming = File.join(timely_home, 'incoming')
|
|
3061
|
+
FileUtils.mkdir_p(incoming)
|
|
3062
|
+
require 'mail'
|
|
3063
|
+
mail = Mail.read(file)
|
|
3064
|
+
mail.parts.each do |part|
|
|
3065
|
+
ct = (part.content_type || '').downcase
|
|
3066
|
+
if ct.include?('calendar') || ct.include?('ics')
|
|
3067
|
+
ics_file = File.join(incoming, "heathrow_#{msg['id']}.ics")
|
|
3068
|
+
File.write(ics_file, part.decoded) unless File.exist?(ics_file)
|
|
3069
|
+
break
|
|
3070
|
+
end
|
|
3071
|
+
end
|
|
3072
|
+
rescue => e
|
|
3073
|
+
# Non-fatal
|
|
3074
|
+
end
|
|
3075
|
+
end
|
|
3076
|
+
|
|
3077
|
+
set_feedback("Sent to Timely: #{date_str}", 156, 0)
|
|
3078
|
+
end
|
|
3079
|
+
|
|
3053
3080
|
def open_message_external
|
|
3054
3081
|
msg = current_message
|
|
3055
3082
|
return unless msg
|
|
3056
|
-
return if msg
|
|
3083
|
+
return if header_message?(msg)
|
|
3057
3084
|
msg = ensure_full_message(msg)
|
|
3058
3085
|
|
|
3059
3086
|
# Mark as read
|
|
@@ -3147,7 +3174,7 @@ module Heathrow
|
|
|
3147
3174
|
def view_attachments
|
|
3148
3175
|
msg = current_message
|
|
3149
3176
|
return unless msg
|
|
3150
|
-
return set_feedback("Select a message first", 245, 2) if msg
|
|
3177
|
+
return set_feedback("Select a message first", 245, 2) if header_message?(msg)
|
|
3151
3178
|
|
|
3152
3179
|
# Load full message if needed (use numeric id only)
|
|
3153
3180
|
msg_id = msg['id']
|
|
@@ -3354,7 +3381,7 @@ module Heathrow
|
|
|
3354
3381
|
if @sort_order == 'unread'
|
|
3355
3382
|
sort_messages
|
|
3356
3383
|
# Reset threading to reorganize with new unread counts (preserve collapsed state)
|
|
3357
|
-
reset_threading(true)
|
|
3384
|
+
reset_threading(true)
|
|
3358
3385
|
end
|
|
3359
3386
|
|
|
3360
3387
|
# Update displays if UI is initialized
|
|
@@ -3451,6 +3478,8 @@ module Heathrow
|
|
|
3451
3478
|
rows = nil
|
|
3452
3479
|
end
|
|
3453
3480
|
|
|
3481
|
+
organized_sections = (@show_threaded && @organizer) ? @organizer.get_organized_view(@sort_order, @sort_inverted) : nil
|
|
3482
|
+
|
|
3454
3483
|
if rows
|
|
3455
3484
|
# Sync maildir flags for all affected messages
|
|
3456
3485
|
count = rows.size
|
|
@@ -3465,8 +3494,8 @@ module Heathrow
|
|
|
3465
3494
|
next unless list
|
|
3466
3495
|
list.each { |m| m['is_read'] = 1 if m['id'] && m['is_read'].to_i == 0 }
|
|
3467
3496
|
end
|
|
3468
|
-
if
|
|
3469
|
-
|
|
3497
|
+
if organized_sections
|
|
3498
|
+
organized_sections.each do |section|
|
|
3470
3499
|
next unless section[:messages]
|
|
3471
3500
|
section[:messages].each { |m| m['is_read'] = 1 if m['is_read'].to_i == 0 }
|
|
3472
3501
|
end
|
|
@@ -3481,14 +3510,15 @@ module Heathrow
|
|
|
3481
3510
|
return if msgs.nil? || msgs.empty?
|
|
3482
3511
|
count = mark_messages_read(msgs)
|
|
3483
3512
|
# Also mark messages inside collapsed sections
|
|
3484
|
-
if
|
|
3485
|
-
|
|
3513
|
+
if organized_sections
|
|
3514
|
+
organized_sections.each do |section|
|
|
3486
3515
|
next unless section[:messages]
|
|
3487
3516
|
count += mark_messages_read(section[:messages])
|
|
3488
3517
|
end
|
|
3489
3518
|
end
|
|
3490
3519
|
end
|
|
3491
3520
|
|
|
3521
|
+
invalidate_counts
|
|
3492
3522
|
set_feedback("Marked #{count} messages as read", 156, 3)
|
|
3493
3523
|
render_top_bar
|
|
3494
3524
|
render_message_list
|
|
@@ -3499,7 +3529,7 @@ module Heathrow
|
|
|
3499
3529
|
def mark_messages_read(msgs)
|
|
3500
3530
|
count = 0
|
|
3501
3531
|
msgs.each do |msg|
|
|
3502
|
-
next if msg
|
|
3532
|
+
next if header_message?(msg)
|
|
3503
3533
|
next if msg['is_read'].to_i == 1
|
|
3504
3534
|
next unless msg['id']
|
|
3505
3535
|
@db.mark_as_read(msg['id'])
|
|
@@ -3516,7 +3546,7 @@ module Heathrow
|
|
|
3516
3546
|
return nil unless msg && @show_threaded && @organizer
|
|
3517
3547
|
|
|
3518
3548
|
# If standing on a header, use its section_messages directly
|
|
3519
|
-
if msg
|
|
3549
|
+
if header_message?(msg)
|
|
3520
3550
|
return msg['section_messages'] if msg['section_messages']
|
|
3521
3551
|
end
|
|
3522
3552
|
|
|
@@ -3524,7 +3554,7 @@ module Heathrow
|
|
|
3524
3554
|
idx = @index - 1
|
|
3525
3555
|
while idx >= 0
|
|
3526
3556
|
prev = @display_messages[idx]
|
|
3527
|
-
if prev && (prev
|
|
3557
|
+
if prev && (header_message?(prev))
|
|
3528
3558
|
return prev['section_messages'] if prev['section_messages']
|
|
3529
3559
|
break
|
|
3530
3560
|
end
|
|
@@ -3566,7 +3596,7 @@ module Heathrow
|
|
|
3566
3596
|
# Toggle all messages in this section
|
|
3567
3597
|
messages.each do |msg|
|
|
3568
3598
|
next unless msg['id'] && !msg['id'].to_s.start_with?('header_') # Skip synthetic headers
|
|
3569
|
-
|
|
3599
|
+
|
|
3570
3600
|
if all_read
|
|
3571
3601
|
@db.mark_as_unread(msg['id'])
|
|
3572
3602
|
msg['is_read'] = 0
|
|
@@ -3575,12 +3605,13 @@ module Heathrow
|
|
|
3575
3605
|
msg['is_read'] = 1
|
|
3576
3606
|
end
|
|
3577
3607
|
end
|
|
3578
|
-
|
|
3608
|
+
invalidate_counts
|
|
3609
|
+
|
|
3579
3610
|
# Re-sort if sorting by unread
|
|
3580
3611
|
if @sort_order == 'unread'
|
|
3581
3612
|
sort_messages
|
|
3582
3613
|
# Reset threading to reorganize with new unread counts (preserve collapsed state)
|
|
3583
|
-
reset_threading(true)
|
|
3614
|
+
reset_threading(true)
|
|
3584
3615
|
end
|
|
3585
3616
|
|
|
3586
3617
|
# Update displays if UI is initialized
|
|
@@ -3601,6 +3632,7 @@ module Heathrow
|
|
|
3601
3632
|
|
|
3602
3633
|
# Toggle the specific message
|
|
3603
3634
|
current_read_status = msg['is_read'].to_i
|
|
3635
|
+
invalidate_counts
|
|
3604
3636
|
|
|
3605
3637
|
if current_read_status == 1
|
|
3606
3638
|
success = @db.mark_as_unread(msg['id'])
|
|
@@ -3620,10 +3652,10 @@ module Heathrow
|
|
|
3620
3652
|
if is_unread_view?
|
|
3621
3653
|
if @show_threaded
|
|
3622
3654
|
# In threaded view, need to reload filtered messages to rebuild threads
|
|
3623
|
-
reset_threading
|
|
3655
|
+
reset_threading
|
|
3624
3656
|
@filtered_messages = @db.get_messages({is_read: false}, 1000, 0, light: true)
|
|
3625
3657
|
sort_messages
|
|
3626
|
-
organize_current_messages(force_reinit: true)
|
|
3658
|
+
organize_current_messages(force_reinit: true)
|
|
3627
3659
|
@index = [@index, filtered_messages_size - 1].min
|
|
3628
3660
|
@index = 0 if @index < 0
|
|
3629
3661
|
else
|
|
@@ -3640,7 +3672,7 @@ module Heathrow
|
|
|
3640
3672
|
if @sort_order == 'unread'
|
|
3641
3673
|
sort_messages
|
|
3642
3674
|
# Reset threading to reorganize with new unread counts (preserve collapsed state)
|
|
3643
|
-
reset_threading(true)
|
|
3675
|
+
reset_threading(true)
|
|
3644
3676
|
end
|
|
3645
3677
|
|
|
3646
3678
|
# Update displays immediately if UI is initialized
|
|
@@ -3696,6 +3728,7 @@ module Heathrow
|
|
|
3696
3728
|
return unless msg
|
|
3697
3729
|
@db.toggle_star(msg['id'])
|
|
3698
3730
|
msg['is_starred'] = msg['is_starred'] == 1 ? 0 : 1
|
|
3731
|
+
invalidate_counts
|
|
3699
3732
|
|
|
3700
3733
|
# Sync flagged status to Maildir file
|
|
3701
3734
|
sync_maildir_flag(msg, 'F', msg['is_starred'] == 1)
|
|
@@ -3986,7 +4019,7 @@ module Heathrow
|
|
|
3986
4019
|
@current_source_filter = "Folder: #{folder_name}"
|
|
3987
4020
|
|
|
3988
4021
|
# Reset threading state so old @display_messages don't persist
|
|
3989
|
-
reset_threading
|
|
4022
|
+
reset_threading
|
|
3990
4023
|
|
|
3991
4024
|
# Show progress while loading
|
|
3992
4025
|
@panes[:bottom].text = " Loading #{folder_name}...".fg(226)
|
|
@@ -4273,7 +4306,7 @@ module Heathrow
|
|
|
4273
4306
|
if @show_threaded
|
|
4274
4307
|
@display_messages&.reject! { |m| m['id'] && filed_ids.include?(m['id']) }
|
|
4275
4308
|
# Force organizer to rebuild from updated @filtered_messages
|
|
4276
|
-
organize_current_messages(true)
|
|
4309
|
+
organize_current_messages(true)
|
|
4277
4310
|
end
|
|
4278
4311
|
@tagged_messages.clear if @tagged_messages.size > 0
|
|
4279
4312
|
@index = [@index, (@filtered_messages.size - 1)].min
|
|
@@ -4963,7 +4996,7 @@ module Heathrow
|
|
|
4963
4996
|
@filtered_messages = results
|
|
4964
4997
|
sort_messages
|
|
4965
4998
|
@index = 0
|
|
4966
|
-
reset_threading
|
|
4999
|
+
reset_threading
|
|
4967
5000
|
set_feedback("#{results.size} results for: #{query}", 156, 0)
|
|
4968
5001
|
render_all
|
|
4969
5002
|
end
|
|
@@ -4987,7 +5020,7 @@ module Heathrow
|
|
|
4987
5020
|
else
|
|
4988
5021
|
msg = current_message
|
|
4989
5022
|
return unless msg
|
|
4990
|
-
return if msg
|
|
5023
|
+
return if header_message?(msg)
|
|
4991
5024
|
|
|
4992
5025
|
if @delete_marked.include?(msg['id'])
|
|
4993
5026
|
@delete_marked.delete(msg['id'])
|
|
@@ -5052,7 +5085,7 @@ module Heathrow
|
|
|
5052
5085
|
@delete_marked.clear
|
|
5053
5086
|
|
|
5054
5087
|
# Force threaded view to rebuild with purged messages gone
|
|
5055
|
-
reset_threading(true)
|
|
5088
|
+
reset_threading(true)
|
|
5056
5089
|
|
|
5057
5090
|
# Position cursor on the message above the first deleted one
|
|
5058
5091
|
new_display = @display_messages || @filtered_messages
|
|
@@ -5100,7 +5133,7 @@ module Heathrow
|
|
|
5100
5133
|
msg = ensure_full_message(msg)
|
|
5101
5134
|
|
|
5102
5135
|
# Don't allow replying to header messages
|
|
5103
|
-
if msg
|
|
5136
|
+
if header_message?(msg)
|
|
5104
5137
|
set_feedback("Cannot reply to section headers. Select a message.", 226, 3)
|
|
5105
5138
|
render_bottom_bar
|
|
5106
5139
|
return
|
|
@@ -5241,7 +5274,7 @@ module Heathrow
|
|
|
5241
5274
|
def edit_message_content
|
|
5242
5275
|
msg = current_message
|
|
5243
5276
|
return unless msg
|
|
5244
|
-
return if msg
|
|
5277
|
+
return if header_message?(msg)
|
|
5245
5278
|
msg = ensure_full_message(msg)
|
|
5246
5279
|
|
|
5247
5280
|
source_id = msg['source_id']
|
|
@@ -5866,7 +5899,7 @@ module Heathrow
|
|
|
5866
5899
|
['%d %b %H:%M', 'DD Mon HH:MM'],
|
|
5867
5900
|
['%b %d %H:%M', 'Mon DD HH:MM']
|
|
5868
5901
|
]
|
|
5869
|
-
sort_orders = ['latest', 'alphabetical', 'sender', 'conversation', 'unread', 'source']
|
|
5902
|
+
sort_orders = ['latest', 'alphabetical', 'sender', 'from', 'conversation', 'unread', 'source']
|
|
5870
5903
|
border_labels = ['none', 'right', 'both', 'left']
|
|
5871
5904
|
# Build view choices: A, N, plus any user-defined views
|
|
5872
5905
|
view_choices = [['A', 'All'], ['N', 'New/Unread']]
|
|
@@ -6051,9 +6084,9 @@ module Heathrow
|
|
|
6051
6084
|
@panes[:right].w = @w - @panes[:left].w - 4
|
|
6052
6085
|
|
|
6053
6086
|
# Reset threading for sort changes
|
|
6054
|
-
reset_threading(true)
|
|
6087
|
+
reset_threading(true)
|
|
6055
6088
|
sort_messages
|
|
6056
|
-
organize_current_messages(true)
|
|
6089
|
+
organize_current_messages(true)
|
|
6057
6090
|
|
|
6058
6091
|
Rcurses.clear_screen
|
|
6059
6092
|
@panes.each_value { |p| p.cleanup if p.respond_to?(:cleanup) }
|
|
@@ -6296,11 +6329,12 @@ module Heathrow
|
|
|
6296
6329
|
end
|
|
6297
6330
|
|
|
6298
6331
|
def cycle_sort_order
|
|
6299
|
-
# Cycle through: latest -> alphabetical -> sender -> conversation -> unread -> source -> latest
|
|
6332
|
+
# Cycle through: latest -> alphabetical -> sender -> from -> conversation -> unread -> source -> latest
|
|
6300
6333
|
@sort_order = case @sort_order
|
|
6301
6334
|
when 'latest' then 'alphabetical'
|
|
6302
6335
|
when 'alphabetical' then 'sender'
|
|
6303
|
-
when 'sender' then '
|
|
6336
|
+
when 'sender' then 'from'
|
|
6337
|
+
when 'from' then 'conversation'
|
|
6304
6338
|
when 'conversation' then 'unread'
|
|
6305
6339
|
when 'unread' then 'source'
|
|
6306
6340
|
else 'latest' # This handles 'source' and any other value
|
|
@@ -6322,13 +6356,13 @@ module Heathrow
|
|
|
6322
6356
|
end
|
|
6323
6357
|
|
|
6324
6358
|
# Reset threading state to force reorganization with new sort (preserve collapsed state)
|
|
6325
|
-
reset_threading(true)
|
|
6359
|
+
reset_threading(true)
|
|
6326
6360
|
|
|
6327
6361
|
# Re-sort and redisplay messages
|
|
6328
6362
|
sort_messages
|
|
6329
6363
|
|
|
6330
6364
|
# Force reinit the organizer with the newly sorted messages
|
|
6331
|
-
organize_current_messages(true)
|
|
6365
|
+
organize_current_messages(true)
|
|
6332
6366
|
|
|
6333
6367
|
@index = 0 # Reset to top
|
|
6334
6368
|
render_all # Re-render everything to show the new sort
|
|
@@ -6354,13 +6388,13 @@ module Heathrow
|
|
|
6354
6388
|
end
|
|
6355
6389
|
|
|
6356
6390
|
# Reset threading state to force reorganization with new sort (preserve collapsed state)
|
|
6357
|
-
reset_threading(true)
|
|
6391
|
+
reset_threading(true)
|
|
6358
6392
|
|
|
6359
6393
|
# Re-sort and redisplay messages
|
|
6360
6394
|
sort_messages
|
|
6361
6395
|
|
|
6362
6396
|
# Force reinit the organizer with the newly sorted messages
|
|
6363
|
-
organize_current_messages(true)
|
|
6397
|
+
organize_current_messages(true)
|
|
6364
6398
|
|
|
6365
6399
|
@index = 0 # Reset to top
|
|
6366
6400
|
render_all # Re-render everything
|
|
@@ -6386,7 +6420,11 @@ module Heathrow
|
|
|
6386
6420
|
|
|
6387
6421
|
# Make sure we have a mutable array
|
|
6388
6422
|
@filtered_messages = @filtered_messages.dup if @filtered_messages.frozen?
|
|
6389
|
-
|
|
6423
|
+
|
|
6424
|
+
# Pre-compute timestamp cache to avoid repeated parsing in sort comparisons
|
|
6425
|
+
ts_cache = {}
|
|
6426
|
+
@filtered_messages.each { |m| ts_cache[m.object_id] = timestamp_to_time(m['timestamp']) }
|
|
6427
|
+
|
|
6390
6428
|
begin
|
|
6391
6429
|
case @sort_order
|
|
6392
6430
|
when 'alphabetical'
|
|
@@ -6410,7 +6448,7 @@ module Heathrow
|
|
|
6410
6448
|
else
|
|
6411
6449
|
# If subjects are the same, sort by timestamp
|
|
6412
6450
|
begin
|
|
6413
|
-
|
|
6451
|
+
ts_cache[b.object_id] <=> ts_cache[a.object_id]
|
|
6414
6452
|
rescue
|
|
6415
6453
|
0
|
|
6416
6454
|
end
|
|
@@ -6428,12 +6466,30 @@ module Heathrow
|
|
|
6428
6466
|
else
|
|
6429
6467
|
# Safe timestamp comparison
|
|
6430
6468
|
begin
|
|
6431
|
-
|
|
6469
|
+
ts_cache[b.object_id] <=> ts_cache[a.object_id]
|
|
6432
6470
|
rescue
|
|
6433
6471
|
0
|
|
6434
6472
|
end
|
|
6435
6473
|
end
|
|
6436
6474
|
end
|
|
6475
|
+
when 'from'
|
|
6476
|
+
# Group by sender, most recently active sender first
|
|
6477
|
+
# Within each sender group, newest message first
|
|
6478
|
+
latest_per_sender = {}
|
|
6479
|
+
@filtered_messages.each do |m|
|
|
6480
|
+
s = display_sender(m).downcase
|
|
6481
|
+
t = ts_cache[m.object_id] || Time.at(0)
|
|
6482
|
+
latest_per_sender[s] = t if !latest_per_sender[s] || t > latest_per_sender[s]
|
|
6483
|
+
end
|
|
6484
|
+
@filtered_messages.sort! do |a, b|
|
|
6485
|
+
sa = display_sender(a).downcase
|
|
6486
|
+
sb = display_sender(b).downcase
|
|
6487
|
+
if sa == sb
|
|
6488
|
+
(ts_cache[b.object_id] || Time.at(0)) <=> (ts_cache[a.object_id] || Time.at(0))
|
|
6489
|
+
else
|
|
6490
|
+
(latest_per_sender[sb] || Time.at(0)) <=> (latest_per_sender[sa] || Time.at(0))
|
|
6491
|
+
end
|
|
6492
|
+
end
|
|
6437
6493
|
when 'unread'
|
|
6438
6494
|
# Sort by unread first, then by timestamp
|
|
6439
6495
|
@filtered_messages.sort! do |a, b|
|
|
@@ -6443,7 +6499,7 @@ module Heathrow
|
|
|
6443
6499
|
else
|
|
6444
6500
|
# Safe timestamp comparison
|
|
6445
6501
|
begin
|
|
6446
|
-
|
|
6502
|
+
ts_cache[b.object_id] <=> ts_cache[a.object_id]
|
|
6447
6503
|
rescue
|
|
6448
6504
|
0 # If timestamp parsing fails, consider them equal
|
|
6449
6505
|
end
|
|
@@ -6460,7 +6516,7 @@ module Heathrow
|
|
|
6460
6516
|
conv_cmp
|
|
6461
6517
|
else
|
|
6462
6518
|
begin
|
|
6463
|
-
|
|
6519
|
+
ts_cache[b.object_id] <=> ts_cache[a.object_id]
|
|
6464
6520
|
rescue
|
|
6465
6521
|
0
|
|
6466
6522
|
end
|
|
@@ -6478,7 +6534,7 @@ module Heathrow
|
|
|
6478
6534
|
else
|
|
6479
6535
|
# Safe timestamp comparison
|
|
6480
6536
|
begin
|
|
6481
|
-
|
|
6537
|
+
ts_cache[b.object_id] <=> ts_cache[a.object_id]
|
|
6482
6538
|
rescue
|
|
6483
6539
|
0 # If timestamp parsing fails, consider them equal
|
|
6484
6540
|
end
|
|
@@ -6488,7 +6544,7 @@ module Heathrow
|
|
|
6488
6544
|
# Sort by timestamp descending (newest first)
|
|
6489
6545
|
@filtered_messages.sort! do |a, b|
|
|
6490
6546
|
begin
|
|
6491
|
-
|
|
6547
|
+
ts_cache[b.object_id] <=> ts_cache[a.object_id]
|
|
6492
6548
|
rescue
|
|
6493
6549
|
0 # If timestamp parsing fails, consider them equal
|
|
6494
6550
|
end
|
|
@@ -6512,8 +6568,7 @@ module Heathrow
|
|
|
6512
6568
|
@panes[:right].ix = 0 # Reset scroll position
|
|
6513
6569
|
help_text = get_help_text # Use the colored version directly
|
|
6514
6570
|
|
|
6515
|
-
|
|
6516
|
-
File.write('/tmp/heathrow_help_debug.txt', help_text) if ENV['DEBUG']
|
|
6571
|
+
|
|
6517
6572
|
|
|
6518
6573
|
# Just set the text and let rcurses handle everything
|
|
6519
6574
|
@panes[:right].text = help_text
|
|
@@ -21,6 +21,8 @@ module Heathrow
|
|
|
21
21
|
@all_start_collapsed = true # Start with everything collapsed
|
|
22
22
|
@section_order = nil # Custom section order (array of names)
|
|
23
23
|
@view_thread_modes = {} # Per-view threading mode: key => {threaded:, folder:}
|
|
24
|
+
@organized_cache = nil
|
|
25
|
+
@organized_cache_key = nil
|
|
24
26
|
end
|
|
25
27
|
|
|
26
28
|
# Toggle between flat and threaded view
|
|
@@ -69,20 +71,10 @@ module Heathrow
|
|
|
69
71
|
def save_view_thread_mode
|
|
70
72
|
return unless @current_view
|
|
71
73
|
@view_thread_modes[@current_view] = { threaded: @show_threaded, folder: @group_by_folder }
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
view[:filters]['view_thread_mode'] = thread_mode_key
|
|
77
|
-
@db.execute("UPDATE views SET filters = ?, updated_at = ? WHERE id = ?",
|
|
78
|
-
[JSON.generate(view[:filters]), Time.now.to_i, view[:id]])
|
|
79
|
-
else
|
|
80
|
-
# Built-in views (A, N): store in settings table
|
|
81
|
-
@db.execute(
|
|
82
|
-
"INSERT OR REPLACE INTO settings (key, value, updated_at) VALUES (?, ?, ?)",
|
|
83
|
-
["thread_mode_#{@current_view}", thread_mode_key, Time.now.to_i]
|
|
84
|
-
)
|
|
85
|
-
end
|
|
74
|
+
@db.execute(
|
|
75
|
+
"INSERT OR REPLACE INTO settings (key, value, updated_at) VALUES (?, ?, ?)",
|
|
76
|
+
["thread_mode_#{@current_view}", thread_mode_key, Time.now.to_i]
|
|
77
|
+
)
|
|
86
78
|
end
|
|
87
79
|
|
|
88
80
|
# Restore threading mode for the active view
|
|
@@ -97,19 +89,12 @@ module Heathrow
|
|
|
97
89
|
return true
|
|
98
90
|
end
|
|
99
91
|
|
|
100
|
-
# Load from
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
unless mode_key
|
|
107
|
-
row = @db.db.get_first_row(
|
|
108
|
-
"SELECT value FROM settings WHERE key = ?",
|
|
109
|
-
["thread_mode_#{@current_view}"]
|
|
110
|
-
)
|
|
111
|
-
mode_key = row && row['value']
|
|
112
|
-
end
|
|
92
|
+
# Load from settings table
|
|
93
|
+
row = @db.db.get_first_row(
|
|
94
|
+
"SELECT value FROM settings WHERE key = ?",
|
|
95
|
+
["thread_mode_#{@current_view}"]
|
|
96
|
+
)
|
|
97
|
+
mode_key = row && row['value']
|
|
113
98
|
return false unless mode_key
|
|
114
99
|
|
|
115
100
|
apply_thread_mode_key(mode_key)
|
|
@@ -161,6 +146,8 @@ module Heathrow
|
|
|
161
146
|
@threading_initialized = false
|
|
162
147
|
@display_messages = []
|
|
163
148
|
@scroll_offset = 0 # Reset scroll position
|
|
149
|
+
@organized_cache = nil
|
|
150
|
+
@organized_cache_key = nil
|
|
164
151
|
|
|
165
152
|
# Preserve or reset collapsed states
|
|
166
153
|
if !preserve_collapsed_state
|
|
@@ -195,17 +182,15 @@ module Heathrow
|
|
|
195
182
|
# Organize messages for current view
|
|
196
183
|
def organize_current_messages(force_reinit = false)
|
|
197
184
|
return unless @show_threaded
|
|
198
|
-
|
|
185
|
+
|
|
199
186
|
# Only organize once per message set - capture the base messages on first run
|
|
200
187
|
# OR if we're forcing reinitialization after a sort change
|
|
201
188
|
if !@threading_initialized || force_reinit
|
|
202
189
|
@base_messages = @filtered_messages.dup
|
|
203
190
|
@threading_initialized = true
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
end if ENV['DEBUG']
|
|
208
|
-
|
|
191
|
+
@organized_cache = nil
|
|
192
|
+
@organized_cache_key = nil
|
|
193
|
+
|
|
209
194
|
require_relative '../message_organizer'
|
|
210
195
|
@organizer = MessageOrganizer.new(@base_messages, @db, group_by_folder: @group_by_folder)
|
|
211
196
|
end
|
|
@@ -305,8 +290,15 @@ module Heathrow
|
|
|
305
290
|
visible_messages = []
|
|
306
291
|
current_index = 0
|
|
307
292
|
@scroll_offset ||= 0 # Track scroll position
|
|
308
|
-
|
|
309
|
-
|
|
293
|
+
|
|
294
|
+
cache_key = "#{@sort_order}|#{@sort_inverted}|#{@filtered_messages.size}"
|
|
295
|
+
if @organized_cache && @organized_cache_key == cache_key
|
|
296
|
+
organized = @organized_cache
|
|
297
|
+
else
|
|
298
|
+
organized = @organizer.get_organized_view(@sort_order, @sort_inverted)
|
|
299
|
+
@organized_cache = organized
|
|
300
|
+
@organized_cache_key = cache_key
|
|
301
|
+
end
|
|
310
302
|
|
|
311
303
|
# Apply custom section order if set
|
|
312
304
|
if @section_order && !@section_order.empty?
|
|
@@ -404,10 +396,6 @@ module Heathrow
|
|
|
404
396
|
end
|
|
405
397
|
end
|
|
406
398
|
|
|
407
|
-
File.open('/tmp/heathrow_debug.log', 'a') do |f|
|
|
408
|
-
f.puts "THREADING: Created #{@display_messages.size} display messages for navigation"
|
|
409
|
-
end if ENV['DEBUG']
|
|
410
|
-
|
|
411
399
|
# Give full text to rcurses and use its scrolling with markers
|
|
412
400
|
@panes[:left].scroll = true
|
|
413
401
|
new_text = lines.join("\n")
|
data/lib/heathrow/version.rb
CHANGED
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.
|
|
4
|
+
version: 0.7.7
|
|
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-
|
|
12
|
+
date: 2026-03-24 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: rcurses
|