heathrow 0.7.1 → 0.7.3
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/sources/maildir.rb +10 -2
- data/lib/heathrow/ui/application.rb +133 -56
- data/lib/heathrow/ui/threaded_view.rb +23 -19
- 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: 123f2c7c6134d4ea1add0517f649b94353918d1e76583a9f714a1e49d7879f96
|
|
4
|
+
data.tar.gz: 26e26497ec050fc246cb2ee1eff2389ceb3088dccb8c83d09257425dd152888a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2130ee038cbc2d9bc8ee4b7855809afaa1c171aba9b8b0967a85d71c9397b39b2ef86afcef58b3301d15556dfc3d07ab8d91d8d12ed77d4a90879904562829a5
|
|
7
|
+
data.tar.gz: 3557ffc9cf7af99441c6c4062e63d7bc9c18145b1f537d10607d420f059221f36c33d1f1a86cad76036a69d003eedb39f26c471013c2dba839187350a10c6aff
|
|
@@ -170,9 +170,17 @@ module Heathrow
|
|
|
170
170
|
changed_base_ids.each do |base_id|
|
|
171
171
|
flags = self.class.parse_maildir_flags(disk_files[base_id])
|
|
172
172
|
db_row = db_index[base_id]
|
|
173
|
-
|
|
173
|
+
# For replied: DB is authoritative. If DB says replied but disk
|
|
174
|
+
# doesn't have R flag, add it to disk rather than clearing DB.
|
|
175
|
+
disk_replied = flags[:replied]
|
|
176
|
+
db_replied = db_row[:replied] == 1
|
|
177
|
+
if db_replied && !disk_replied
|
|
178
|
+
rename_with_flag(disk_files[base_id], 'R', add: true)
|
|
179
|
+
disk_replied = true
|
|
180
|
+
end
|
|
181
|
+
if flags[:seen] != (db_row[:read] == 1) || flags[:flagged] != (db_row[:starred] == 1) || disk_replied != db_replied
|
|
174
182
|
db.execute("UPDATE messages SET read = ?, starred = ?, replied = ? WHERE id = ?",
|
|
175
|
-
flags[:seen] ? 1 : 0, flags[:flagged] ? 1 : 0,
|
|
183
|
+
flags[:seen] ? 1 : 0, flags[:flagged] ? 1 : 0, disk_replied ? 1 : 0, db_row[:id])
|
|
176
184
|
end
|
|
177
185
|
# Update external_id and metadata if filename changed
|
|
178
186
|
current_filename = File.basename(disk_files[base_id])
|
|
@@ -725,6 +725,8 @@ module Heathrow
|
|
|
725
725
|
page_down
|
|
726
726
|
when 'PgUP'
|
|
727
727
|
page_up
|
|
728
|
+
when 'L'
|
|
729
|
+
load_more_messages
|
|
728
730
|
when 'w'
|
|
729
731
|
change_width
|
|
730
732
|
when 'Y'
|
|
@@ -1498,28 +1500,32 @@ module Heathrow
|
|
|
1498
1500
|
available_width -= 2 if @panes[:left].border # Extra space for border chars
|
|
1499
1501
|
available_width -= 3 # Space for N flag + replied flag + indicator column (tag/star/attachment/D)
|
|
1500
1502
|
|
|
1501
|
-
# Truncate sender to fit
|
|
1503
|
+
# Truncate sender to fit (use display_width for CJK characters)
|
|
1502
1504
|
sender_max = 15
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
+
dw = Rcurses.display_width(sender)
|
|
1506
|
+
sender_display = if dw > sender_max
|
|
1507
|
+
truncate_to_width(sender, sender_max - 1) + '…'
|
|
1508
|
+
else
|
|
1509
|
+
sender + ' ' * [sender_max - dw, 0].max
|
|
1510
|
+
end
|
|
1511
|
+
|
|
1505
1512
|
# Build the line with timestamp, icon and sender
|
|
1506
1513
|
icon = respond_to?(:get_source_icon) ? get_source_icon(msg['source_type']) : '•'
|
|
1507
1514
|
line_prefix = "#{timestamp} #{icon} #{sender_display} "
|
|
1508
|
-
|
|
1509
|
-
# Calculate remaining space for subject
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1515
|
+
|
|
1516
|
+
# Calculate remaining space for subject (use display_width for CJK)
|
|
1517
|
+
prefix_dw = Rcurses.display_width(line_prefix)
|
|
1518
|
+
subject_width = available_width - prefix_dw - 1 # -1 for safety
|
|
1519
|
+
subject_dw = Rcurses.display_width(subject)
|
|
1520
|
+
if subject_width > 0 && subject_dw > subject_width
|
|
1521
|
+
subject = truncate_to_width(subject, subject_width - 1) + '…'
|
|
1522
|
+
subject_dw = Rcurses.display_width(subject)
|
|
1513
1523
|
end
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
line = line_prefix + subject
|
|
1517
|
-
# Pad line to full available width so background color spans entire width
|
|
1518
|
-
line = line.ljust(available_width)
|
|
1519
|
-
|
|
1520
|
-
prefix_part = "#{timestamp} #{icon} #{sender_display} "
|
|
1524
|
+
|
|
1525
|
+
prefix_part = line_prefix
|
|
1521
1526
|
subject_part = subject.strip
|
|
1522
|
-
|
|
1527
|
+
total_dw = Rcurses.display_width(prefix_part) + Rcurses.display_width(subject_part)
|
|
1528
|
+
padding = " " * [available_width - total_dw, 0].max
|
|
1523
1529
|
finalize_line(msg, selected, prefix_part, subject_part, source_color, padding)
|
|
1524
1530
|
end
|
|
1525
1531
|
|
|
@@ -1885,7 +1891,7 @@ module Heathrow
|
|
|
1885
1891
|
# Show view name instantly
|
|
1886
1892
|
render_top_bar
|
|
1887
1893
|
|
|
1888
|
-
@load_limit =
|
|
1894
|
+
@load_limit = 200
|
|
1889
1895
|
@filtered_messages = @db.get_messages({}, @load_limit, 0, light: true)
|
|
1890
1896
|
sort_messages
|
|
1891
1897
|
@index = 0
|
|
@@ -1911,7 +1917,11 @@ module Heathrow
|
|
|
1911
1917
|
clear_inline_image
|
|
1912
1918
|
end
|
|
1913
1919
|
|
|
1914
|
-
|
|
1920
|
+
unless current_msg
|
|
1921
|
+
@panes[:right].text = ""
|
|
1922
|
+
@panes[:right].refresh
|
|
1923
|
+
return
|
|
1924
|
+
end
|
|
1915
1925
|
|
|
1916
1926
|
msg = current_msg
|
|
1917
1927
|
|
|
@@ -1979,28 +1989,28 @@ module Heathrow
|
|
|
1979
1989
|
end
|
|
1980
1990
|
else
|
|
1981
1991
|
# Regular message display
|
|
1982
|
-
header << "From: #{msg['sender']}".fg(
|
|
1992
|
+
header << "From: #{msg['sender']}".fg(2) if msg['sender']
|
|
1983
1993
|
# Show recipients (To field)
|
|
1984
1994
|
to = msg['recipients'] || msg['recipient']
|
|
1985
1995
|
if to
|
|
1986
1996
|
to_list = to.is_a?(String) ? (JSON.parse(to) rescue [to]) : to
|
|
1987
1997
|
to_str = to_list.is_a?(Array) ? to_list.join(', ') : to_list.to_s
|
|
1988
|
-
header << "To: #{to_str}".fg(
|
|
1998
|
+
header << "To: #{to_str}".fg(2) unless to_str.empty?
|
|
1989
1999
|
end
|
|
1990
2000
|
# Show CC recipients
|
|
1991
2001
|
cc = msg['cc']
|
|
1992
2002
|
if cc
|
|
1993
2003
|
cc_list = cc.is_a?(String) ? (JSON.parse(cc) rescue [cc]) : cc
|
|
1994
2004
|
cc_str = cc_list.is_a?(Array) ? cc_list.join(', ') : cc_list.to_s
|
|
1995
|
-
header << "Cc: #{cc_str}".fg(
|
|
2005
|
+
header << "Cc: #{cc_str}".fg(2) unless cc_str.empty?
|
|
1996
2006
|
end
|
|
1997
2007
|
# For weechat, show channel name from metadata instead of content preview
|
|
1998
2008
|
meta = msg['metadata']
|
|
1999
2009
|
meta = JSON.parse(meta) if meta.is_a?(String) rescue nil
|
|
2000
2010
|
if meta.is_a?(Hash) && meta['channel_name']
|
|
2001
|
-
header << "Subject: #{meta['channel_name']}".b.fg(
|
|
2011
|
+
header << "Subject: #{meta['channel_name']}".b.fg(1)
|
|
2002
2012
|
elsif msg['subject']
|
|
2003
|
-
header << "Subject: #{msg['subject']}".b.fg(
|
|
2013
|
+
header << "Subject: #{msg['subject']}".b.fg(1)
|
|
2004
2014
|
end
|
|
2005
2015
|
end
|
|
2006
2016
|
|
|
@@ -2314,18 +2324,22 @@ module Heathrow
|
|
|
2314
2324
|
|
|
2315
2325
|
def check_load_more
|
|
2316
2326
|
return unless @load_limit && @filtered_messages
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2327
|
+
return if @filtered_messages.size < @load_limit # Haven't hit the limit yet
|
|
2328
|
+
display_size = message_count
|
|
2329
|
+
return if display_size == 0
|
|
2330
|
+
return unless @index >= display_size - 10
|
|
2331
|
+
return if @last_autoload_index == @index
|
|
2332
|
+
@last_autoload_index = @index
|
|
2333
|
+
load_more_messages
|
|
2323
2334
|
end
|
|
2324
2335
|
|
|
2325
2336
|
def load_more_messages
|
|
2326
2337
|
return unless @load_limit
|
|
2338
|
+
# Remember current message so we can restore position after reload
|
|
2339
|
+
cur = current_message
|
|
2340
|
+
cur_id = cur['id'] if cur
|
|
2327
2341
|
old_count = @filtered_messages.size
|
|
2328
|
-
@load_limit +=
|
|
2342
|
+
@load_limit += 200
|
|
2329
2343
|
|
|
2330
2344
|
if @current_folder
|
|
2331
2345
|
light_cols = "id, source_id, external_id, thread_id, parent_id, sender, sender_name, recipients, subject, substr(content, 1, 200) as content, timestamp, received_at, read AS is_read, starred AS is_starred, archived, labels, metadata, attachments, folder, replied"
|
|
@@ -2345,7 +2359,21 @@ module Heathrow
|
|
|
2345
2359
|
|
|
2346
2360
|
sort_messages
|
|
2347
2361
|
new_count = @filtered_messages.size
|
|
2348
|
-
|
|
2362
|
+
return if new_count <= old_count
|
|
2363
|
+
|
|
2364
|
+
# In threaded mode, set pending restore so the threaded rebuild finds our position
|
|
2365
|
+
if @show_threaded && cur_id
|
|
2366
|
+
@pending_restore_id = cur_id
|
|
2367
|
+
end
|
|
2368
|
+
|
|
2369
|
+
# Force threaded view to rebuild organizer with new messages
|
|
2370
|
+
if @show_threaded && respond_to?(:organize_current_messages)
|
|
2371
|
+
organize_current_messages(true)
|
|
2372
|
+
end
|
|
2373
|
+
|
|
2374
|
+
set_feedback("Loaded #{new_count} messages (+#{new_count - old_count})", 156, 2)
|
|
2375
|
+
render_message_list
|
|
2376
|
+
render_top_bar
|
|
2349
2377
|
end
|
|
2350
2378
|
|
|
2351
2379
|
# Ensure all feeds/channels from a source have at least some messages loaded
|
|
@@ -2512,7 +2540,7 @@ module Heathrow
|
|
|
2512
2540
|
|
|
2513
2541
|
render_top_bar
|
|
2514
2542
|
|
|
2515
|
-
@load_limit =
|
|
2543
|
+
@load_limit = 200
|
|
2516
2544
|
@filtered_messages = @db.get_messages({is_read: false}, @load_limit, 0, light: true)
|
|
2517
2545
|
sort_messages
|
|
2518
2546
|
@index = 0
|
|
@@ -2559,7 +2587,7 @@ module Heathrow
|
|
|
2559
2587
|
@section_order = view[:filters]['section_order'].dup
|
|
2560
2588
|
end
|
|
2561
2589
|
|
|
2562
|
-
@load_limit =
|
|
2590
|
+
@load_limit = 200
|
|
2563
2591
|
if view && view[:filters] && !view[:filters].empty?
|
|
2564
2592
|
apply_view_filters(view)
|
|
2565
2593
|
sort_messages
|
|
@@ -3701,6 +3729,7 @@ module Heathrow
|
|
|
3701
3729
|
|
|
3702
3730
|
def show_folder_browser
|
|
3703
3731
|
@in_folder_browser = true
|
|
3732
|
+
@in_favorites_browser = false
|
|
3704
3733
|
@folder_browser_index = 0
|
|
3705
3734
|
@panes[:top].bg = @topcolor
|
|
3706
3735
|
@folder_collapsed ||= {}
|
|
@@ -3781,8 +3810,10 @@ module Heathrow
|
|
|
3781
3810
|
@panes[:right].refresh
|
|
3782
3811
|
end
|
|
3783
3812
|
|
|
3784
|
-
# Update top bar
|
|
3785
|
-
|
|
3813
|
+
# Update top bar (preserve Favorites title if in favorites mode)
|
|
3814
|
+
browser_title = @in_favorites_browser ? "Favorites" : "Folder Browser"
|
|
3815
|
+
browser_color = @in_favorites_browser ? 226 : 201
|
|
3816
|
+
@panes[:top].text = " Heathrow - ".b.fg(255) + browser_title.b.fg(browser_color) + " [#{@folder_display.size} folders]".fg(246)
|
|
3786
3817
|
@panes[:top].refresh
|
|
3787
3818
|
|
|
3788
3819
|
# Update bottom bar
|
|
@@ -3902,6 +3933,7 @@ module Heathrow
|
|
|
3902
3933
|
render_folder_browser
|
|
3903
3934
|
when 'q', 'ESC', "\e"
|
|
3904
3935
|
@in_folder_browser = false
|
|
3936
|
+
@in_favorites_browser = false
|
|
3905
3937
|
render_all
|
|
3906
3938
|
break
|
|
3907
3939
|
else
|
|
@@ -3918,6 +3950,7 @@ module Heathrow
|
|
|
3918
3950
|
# Open a specific folder and show its messages
|
|
3919
3951
|
def open_folder(folder_name)
|
|
3920
3952
|
@in_folder_browser = false
|
|
3953
|
+
@in_favorites_browser = false
|
|
3921
3954
|
@current_folder = folder_name
|
|
3922
3955
|
@current_view = 'A'
|
|
3923
3956
|
@in_source_view = false
|
|
@@ -3932,7 +3965,7 @@ module Heathrow
|
|
|
3932
3965
|
@panes[:bottom].refresh
|
|
3933
3966
|
|
|
3934
3967
|
# Light query with limit (full content loaded lazily when viewing)
|
|
3935
|
-
@load_limit =
|
|
3968
|
+
@load_limit = 200
|
|
3936
3969
|
light_cols = "id, source_id, external_id, thread_id, parent_id, sender, sender_name, recipients, subject, substr(content, 1, 200) as content, timestamp, received_at, read AS is_read, starred AS is_starred, archived, labels, metadata, attachments, folder, replied"
|
|
3937
3970
|
results = @db.execute(
|
|
3938
3971
|
"SELECT #{light_cols} FROM messages WHERE folder >= ? AND folder < ? ORDER BY timestamp DESC LIMIT ?",
|
|
@@ -3969,6 +4002,7 @@ module Heathrow
|
|
|
3969
4002
|
def show_favorites_browser
|
|
3970
4003
|
favorites = get_favorite_folders
|
|
3971
4004
|
@in_folder_browser = true
|
|
4005
|
+
@in_favorites_browser = true
|
|
3972
4006
|
@folder_browser_index = 0
|
|
3973
4007
|
@panes[:top].bg = @topcolor
|
|
3974
4008
|
@folder_count_cache = {} # Fresh counts each time
|
|
@@ -3999,7 +4033,7 @@ module Heathrow
|
|
|
3999
4033
|
when 'k', 'UP'
|
|
4000
4034
|
@folder_browser_index = (@folder_browser_index - 1) % @folder_display.size if @folder_display.size > 0
|
|
4001
4035
|
render_folder_browser
|
|
4002
|
-
when 'ENTER'
|
|
4036
|
+
when 'l', 'RIGHT', 'ENTER'
|
|
4003
4037
|
folder = @folder_display[@folder_browser_index]
|
|
4004
4038
|
if folder
|
|
4005
4039
|
open_folder(folder[:full_name])
|
|
@@ -4052,6 +4086,7 @@ module Heathrow
|
|
|
4052
4086
|
end
|
|
4053
4087
|
when 'q', 'ESC', "\e", 'h', 'LEFT'
|
|
4054
4088
|
@in_folder_browser = false
|
|
4089
|
+
@in_favorites_browser = false
|
|
4055
4090
|
render_all
|
|
4056
4091
|
break
|
|
4057
4092
|
end
|
|
@@ -4314,6 +4349,9 @@ module Heathrow
|
|
|
4314
4349
|
@folder_collapsed.delete(folder[:full_name])
|
|
4315
4350
|
@folder_display = flatten_folder_tree(@folder_tree, '', 0, @folder_collapsed)
|
|
4316
4351
|
render_save_folder_picker(idx, title)
|
|
4352
|
+
elsif folder
|
|
4353
|
+
render_all
|
|
4354
|
+
return folder[:full_name]
|
|
4317
4355
|
end
|
|
4318
4356
|
when 'h', 'LEFT'
|
|
4319
4357
|
folder = @folder_display[idx]
|
|
@@ -5679,23 +5717,30 @@ module Heathrow
|
|
|
5679
5717
|
|
|
5680
5718
|
if result[:success]
|
|
5681
5719
|
if orig_id
|
|
5720
|
+
# Re-read metadata from DB to get current maildir_file path
|
|
5721
|
+
# (poller may have renamed the file since we captured orig_msg)
|
|
5722
|
+
fresh = @db.get_message(orig_id)
|
|
5723
|
+
if fresh
|
|
5724
|
+
orig_msg = fresh
|
|
5725
|
+
end
|
|
5726
|
+
# Sync disk flag first, then DB, to avoid poller race condition
|
|
5727
|
+
sync_maildir_flag(orig_msg, 'R', true) if orig_msg
|
|
5682
5728
|
@db.execute("UPDATE messages SET replied = 1 WHERE id = ?", [orig_id])
|
|
5683
5729
|
orig_msg['replied'] = 1 if orig_msg
|
|
5684
|
-
sync_maildir_flag(orig_msg, 'R', true) if orig_msg
|
|
5685
5730
|
end
|
|
5686
5731
|
msg = result[:message]
|
|
5687
5732
|
if composed[:attachments] && !composed[:attachments].empty?
|
|
5688
5733
|
msg += " (#{composed[:attachments].size} attachment(s))"
|
|
5689
5734
|
end
|
|
5690
5735
|
set_feedback(msg, 156, 3)
|
|
5736
|
+
render_left_pane if orig_id
|
|
5691
5737
|
else
|
|
5692
5738
|
set_feedback(result[:message], 196, 4)
|
|
5693
5739
|
end
|
|
5694
5740
|
rescue => e
|
|
5695
5741
|
set_feedback("Send error: #{e.message}", 196, 4)
|
|
5696
|
-
|
|
5697
|
-
|
|
5698
|
-
end
|
|
5742
|
+
File.open('/tmp/heathrow_debug.log', 'a') { |f| f.puts "#{Time.now} send_composed_message error: #{e.message}\n#{e.backtrace.first(5).join("\n")}" }
|
|
5743
|
+
|
|
5699
5744
|
end
|
|
5700
5745
|
end
|
|
5701
5746
|
end
|
|
@@ -7670,30 +7715,51 @@ Required: URL, optional CSS selector
|
|
|
7670
7715
|
Config.identity_for_folder(folder)
|
|
7671
7716
|
end
|
|
7672
7717
|
|
|
7673
|
-
# Colorize email content with mutt-style quote levels and signature dimming
|
|
7718
|
+
# Colorize email content with mutt-style quote levels and signature dimming.
|
|
7719
|
+
# Detects both ">" prefix quoting and indentation-based quoting (from HTML
|
|
7720
|
+
# emails rendered via w3m, where blockquotes become indented text).
|
|
7674
7721
|
def colorize_email_content(content)
|
|
7675
7722
|
quote_colors = [theme[:quote1] || 114, theme[:quote2] || 180,
|
|
7676
7723
|
theme[:quote3] || 139, theme[:quote4] || 109]
|
|
7677
|
-
sig_color = theme[:sig] ||
|
|
7678
|
-
link_color = theme[:
|
|
7724
|
+
sig_color = theme[:sig] || 5 # magenta (vim PreProc)
|
|
7725
|
+
link_color = theme[:link] || 4 # blue (vim String)
|
|
7726
|
+
email_color = theme[:email] || 5 # magenta (vim Special)
|
|
7679
7727
|
in_signature = false
|
|
7728
|
+
indent_quote_level = 0 # tracks nesting from "wrote:" attribution lines
|
|
7680
7729
|
|
|
7681
|
-
content.lines
|
|
7730
|
+
lines = content.lines
|
|
7731
|
+
result = []
|
|
7732
|
+
lines.each_with_index do |line, i|
|
|
7682
7733
|
stripped = line.rstrip
|
|
7683
7734
|
# Detect signature delimiter (RFC 3676: "-- " on its own line)
|
|
7684
7735
|
if stripped == '-- ' || stripped == '--'
|
|
7685
7736
|
in_signature = true
|
|
7686
|
-
|
|
7737
|
+
indent_quote_level = 0
|
|
7738
|
+
result << colorize_links(stripped, sig_color, link_color)
|
|
7687
7739
|
elsif in_signature
|
|
7688
|
-
colorize_links(stripped, sig_color, link_color)
|
|
7740
|
+
result << colorize_links(stripped, sig_color, link_color)
|
|
7689
7741
|
elsif stripped =~ /^(>{1,})\s?/
|
|
7690
7742
|
level = $1.length
|
|
7691
7743
|
color = quote_colors[[level - 1, quote_colors.length - 1].min]
|
|
7692
|
-
colorize_links(stripped, color, link_color)
|
|
7744
|
+
result << colorize_links(stripped, color, link_color)
|
|
7693
7745
|
else
|
|
7694
|
-
|
|
7746
|
+
# Detect "wrote:" attribution lines (start of indented quote block)
|
|
7747
|
+
if stripped =~ /\bwrote:\s*$/
|
|
7748
|
+
indent_quote_level += 1
|
|
7749
|
+
color = quote_colors[[indent_quote_level - 1, quote_colors.length - 1].min]
|
|
7750
|
+
result << colorize_links(stripped, color, link_color)
|
|
7751
|
+
elsif indent_quote_level > 0 && stripped =~ /^\s{2,}/
|
|
7752
|
+
# Indented line inside a quote block
|
|
7753
|
+
color = quote_colors[[indent_quote_level - 1, quote_colors.length - 1].min]
|
|
7754
|
+
result << colorize_links(stripped, color, link_color)
|
|
7755
|
+
else
|
|
7756
|
+
# Non-indented line resets indent quoting
|
|
7757
|
+
indent_quote_level = 0 if indent_quote_level > 0 && !stripped.empty?
|
|
7758
|
+
result << colorize_links(stripped, nil, link_color)
|
|
7759
|
+
end
|
|
7695
7760
|
end
|
|
7696
|
-
end
|
|
7761
|
+
end
|
|
7762
|
+
result.join("\n")
|
|
7697
7763
|
end
|
|
7698
7764
|
|
|
7699
7765
|
# Convert HTML to readable text via w3m
|
|
@@ -7738,12 +7804,13 @@ Required: URL, optional CSS selector
|
|
|
7738
7804
|
tags.each do |tag|
|
|
7739
7805
|
src = tag[/src=["']([^"']+)["']/i, 1]
|
|
7740
7806
|
next unless src
|
|
7741
|
-
# Skip by URL patterns
|
|
7742
|
-
next if src =~ /track|pixel|spacer|beacon|\.gif
|
|
7743
|
-
# Skip by HTML dimensions (
|
|
7807
|
+
# Skip by URL patterns (tracking, icons, social media badges)
|
|
7808
|
+
next if src =~ /track|pixel|spacer|beacon|\.gif$|icon|logo|badge|button|social|facebook|linkedin|twitter|instagram/i
|
|
7809
|
+
# Skip by HTML dimensions (tracking pixels and small icons)
|
|
7744
7810
|
w = tag[/width=["']?(\d+)/i, 1]&.to_i
|
|
7745
7811
|
h = tag[/height=["']?(\d+)/i, 1]&.to_i
|
|
7746
|
-
next if w &&
|
|
7812
|
+
next if w && w <= 40
|
|
7813
|
+
next if h && h <= 40
|
|
7747
7814
|
urls << src
|
|
7748
7815
|
end
|
|
7749
7816
|
urls
|
|
@@ -7825,7 +7892,7 @@ Required: URL, optional CSS selector
|
|
|
7825
7892
|
# Clear right pane and show images
|
|
7826
7893
|
n = image_paths.size
|
|
7827
7894
|
label = n == 1 ? "1 image" : "#{n} images"
|
|
7828
|
-
@panes[:right].text = " [#{label}] Press
|
|
7895
|
+
@panes[:right].text = " [#{label}] Press ESC to return".fg(245)
|
|
7829
7896
|
@panes[:right].full_refresh # Full refresh to clear image area
|
|
7830
7897
|
|
|
7831
7898
|
pane_w = @panes[:right].w - 2
|
|
@@ -7908,7 +7975,6 @@ Required: URL, optional CSS selector
|
|
|
7908
7975
|
def format_attachments(attachments)
|
|
7909
7976
|
return nil unless attachments.is_a?(Array) && !attachments.empty?
|
|
7910
7977
|
lines = []
|
|
7911
|
-
lines << ("─" * 60).fg(238)
|
|
7912
7978
|
lines << "Attachments:".b.fg(208)
|
|
7913
7979
|
attachments.each_with_index do |att, i|
|
|
7914
7980
|
name = att['name'] || att['filename'] || 'unnamed'
|
|
@@ -7929,6 +7995,17 @@ Required: URL, optional CSS selector
|
|
|
7929
7995
|
end
|
|
7930
7996
|
|
|
7931
7997
|
# Highlight URLs in a line, applying base_color to non-URL text
|
|
7998
|
+
# Truncate a string to fit within a given display width (CJK-aware)
|
|
7999
|
+
def truncate_to_width(str, max_width)
|
|
8000
|
+
w = 0
|
|
8001
|
+
str.each_char.with_index do |c, i|
|
|
8002
|
+
cw = Rcurses.display_width(c)
|
|
8003
|
+
return str[0...i] if w + cw > max_width
|
|
8004
|
+
w += cw
|
|
8005
|
+
end
|
|
8006
|
+
str
|
|
8007
|
+
end
|
|
8008
|
+
|
|
7932
8009
|
def colorize_links(line, base_color, link_color)
|
|
7933
8010
|
url_re = %r{https?://[^\s<>\[\]()]+}
|
|
7934
8011
|
parts = line.split(url_re, -1)
|
|
@@ -533,7 +533,7 @@ module Heathrow
|
|
|
533
533
|
if (source_type || msg['source_type']) == 'rss'
|
|
534
534
|
sender = '' # Don't show sender for RSS
|
|
535
535
|
else
|
|
536
|
-
sender = sender
|
|
536
|
+
sender = truncate_to_width(sender, 14) + '…' if Rcurses.display_width(sender) > 15
|
|
537
537
|
end
|
|
538
538
|
|
|
539
539
|
# For Discord/Slack channels, show content not channel name
|
|
@@ -558,14 +558,14 @@ module Heathrow
|
|
|
558
558
|
if sender.empty?
|
|
559
559
|
prefix = "#{unread} "
|
|
560
560
|
else
|
|
561
|
-
prefix = "#{unread} #{sender.
|
|
561
|
+
prefix = "#{unread} #{sender + ' ' * [15 - Rcurses.display_width(sender), 0].max} "
|
|
562
562
|
end
|
|
563
563
|
|
|
564
564
|
# Truncate content to fit single line (prefix + 2 for nflag+ind from finalize_line)
|
|
565
565
|
pane_width = @panes[:left].w - 5
|
|
566
|
-
available = pane_width - prefix
|
|
567
|
-
if display_content && display_content
|
|
568
|
-
display_content = display_content
|
|
566
|
+
available = pane_width - Rcurses.display_width(prefix)
|
|
567
|
+
if display_content && Rcurses.display_width(display_content) > available && available > 0
|
|
568
|
+
display_content = truncate_to_width(display_content, available - 1) + "…"
|
|
569
569
|
end
|
|
570
570
|
|
|
571
571
|
finalize_line(msg, selected, prefix, display_content, color)
|
|
@@ -574,17 +574,17 @@ module Heathrow
|
|
|
574
574
|
# Format a thread reply
|
|
575
575
|
def format_thread_reply(msg, selected, indent)
|
|
576
576
|
sender = display_sender(msg)
|
|
577
|
-
sender = sender
|
|
578
|
-
|
|
577
|
+
sender = truncate_to_width(sender, 12) if Rcurses.display_width(sender) > 12
|
|
578
|
+
|
|
579
579
|
content = msg['content'] || ''
|
|
580
580
|
content = content.gsub(/\n/, ' ')
|
|
581
|
-
|
|
581
|
+
|
|
582
582
|
# Truncate based on pane width
|
|
583
583
|
pane_width = @panes[:left].w - 5
|
|
584
|
-
used_space = 2 + indent
|
|
584
|
+
used_space = 2 + Rcurses.display_width(indent) + 3 + Rcurses.display_width(sender) + 2
|
|
585
585
|
available = pane_width - used_space
|
|
586
|
-
if content
|
|
587
|
-
content = content
|
|
586
|
+
if Rcurses.display_width(content) > available && available > 0
|
|
587
|
+
content = truncate_to_width(content, available - 1) + "…"
|
|
588
588
|
end
|
|
589
589
|
|
|
590
590
|
prefix = "#{indent}└─ #{sender}: "
|
|
@@ -599,17 +599,20 @@ module Heathrow
|
|
|
599
599
|
|
|
600
600
|
timestamp = (parse_timestamp(msg['timestamp']) || "").ljust(6)
|
|
601
601
|
sender_max = 15
|
|
602
|
-
|
|
602
|
+
sdw = Rcurses.display_width(sender)
|
|
603
|
+
sender = sdw > sender_max ? truncate_to_width(sender, sender_max - 1) + '…' : sender + ' ' * [sender_max - sdw, 0].max
|
|
603
604
|
child_indent = indent.empty? ? "" : " "
|
|
604
605
|
prefix = "#{child_indent}#{timestamp} #{icon} #{sender} "
|
|
605
606
|
|
|
606
607
|
content = msg['subject'] || msg['content'] || ''
|
|
608
|
+
content = content.dup if content.frozen?
|
|
609
|
+
content = content.encode('UTF-8', invalid: :replace, undef: :replace, replace: '?') rescue content.force_encoding('UTF-8').scrub('?')
|
|
607
610
|
content = content.gsub(/\n/, ' ')
|
|
608
611
|
|
|
609
|
-
used_space = prefix
|
|
612
|
+
used_space = Rcurses.display_width(prefix) + 1
|
|
610
613
|
available = pane_width - used_space
|
|
611
|
-
if content
|
|
612
|
-
content = content
|
|
614
|
+
if Rcurses.display_width(content) > available && available > 0
|
|
615
|
+
content = truncate_to_width(content, available - 1) + "…"
|
|
613
616
|
end
|
|
614
617
|
|
|
615
618
|
color = get_source_color(msg)
|
|
@@ -623,7 +626,8 @@ module Heathrow
|
|
|
623
626
|
|
|
624
627
|
# Truncate sender to fixed width
|
|
625
628
|
sender_max = 15
|
|
626
|
-
|
|
629
|
+
sdw = Rcurses.display_width(sender)
|
|
630
|
+
sender = sdw > sender_max ? truncate_to_width(sender, sender_max - 1) + '…' : sender + ' ' * [sender_max - sdw, 0].max
|
|
627
631
|
|
|
628
632
|
content = msg['content'] || ''
|
|
629
633
|
content = content.gsub(/\n/, ' ')
|
|
@@ -633,9 +637,9 @@ module Heathrow
|
|
|
633
637
|
|
|
634
638
|
# Truncate content to fit single line
|
|
635
639
|
pane_width = @panes[:left].w - 5
|
|
636
|
-
available = pane_width - prefix
|
|
637
|
-
if content
|
|
638
|
-
content = content
|
|
640
|
+
available = pane_width - Rcurses.display_width(prefix)
|
|
641
|
+
if Rcurses.display_width(content) > available && available > 0
|
|
642
|
+
content = truncate_to_width(content, available - 1) + "…"
|
|
639
643
|
end
|
|
640
644
|
|
|
641
645
|
color = get_source_color(msg)
|
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.3
|
|
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-18 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: rcurses
|