heathrow 0.7.2 → 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/ui/application.rb +122 -46
- data/lib/heathrow/ui/threaded_view.rb +23 -19
- data/lib/heathrow/version.rb +1 -1
- metadata +1 -1
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
|
|
@@ -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
|
|
|
@@ -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,9 +5717,16 @@ 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?
|
|
@@ -7670,7 +7715,9 @@ 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]
|
|
@@ -7678,23 +7725,41 @@ Required: URL, optional CSS selector
|
|
|
7678
7725
|
link_color = theme[:link] || 4 # blue (vim String)
|
|
7679
7726
|
email_color = theme[:email] || 5 # magenta (vim Special)
|
|
7680
7727
|
in_signature = false
|
|
7728
|
+
indent_quote_level = 0 # tracks nesting from "wrote:" attribution lines
|
|
7681
7729
|
|
|
7682
|
-
content.lines
|
|
7730
|
+
lines = content.lines
|
|
7731
|
+
result = []
|
|
7732
|
+
lines.each_with_index do |line, i|
|
|
7683
7733
|
stripped = line.rstrip
|
|
7684
7734
|
# Detect signature delimiter (RFC 3676: "-- " on its own line)
|
|
7685
7735
|
if stripped == '-- ' || stripped == '--'
|
|
7686
7736
|
in_signature = true
|
|
7687
|
-
|
|
7737
|
+
indent_quote_level = 0
|
|
7738
|
+
result << colorize_links(stripped, sig_color, link_color)
|
|
7688
7739
|
elsif in_signature
|
|
7689
|
-
colorize_links(stripped, sig_color, link_color)
|
|
7740
|
+
result << colorize_links(stripped, sig_color, link_color)
|
|
7690
7741
|
elsif stripped =~ /^(>{1,})\s?/
|
|
7691
7742
|
level = $1.length
|
|
7692
7743
|
color = quote_colors[[level - 1, quote_colors.length - 1].min]
|
|
7693
|
-
colorize_links(stripped, color, link_color)
|
|
7744
|
+
result << colorize_links(stripped, color, link_color)
|
|
7694
7745
|
else
|
|
7695
|
-
|
|
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
|
|
7696
7760
|
end
|
|
7697
|
-
end
|
|
7761
|
+
end
|
|
7762
|
+
result.join("\n")
|
|
7698
7763
|
end
|
|
7699
7764
|
|
|
7700
7765
|
# Convert HTML to readable text via w3m
|
|
@@ -7739,12 +7804,13 @@ Required: URL, optional CSS selector
|
|
|
7739
7804
|
tags.each do |tag|
|
|
7740
7805
|
src = tag[/src=["']([^"']+)["']/i, 1]
|
|
7741
7806
|
next unless src
|
|
7742
|
-
# Skip by URL patterns
|
|
7743
|
-
next if src =~ /track|pixel|spacer|beacon|\.gif
|
|
7744
|
-
# 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)
|
|
7745
7810
|
w = tag[/width=["']?(\d+)/i, 1]&.to_i
|
|
7746
7811
|
h = tag[/height=["']?(\d+)/i, 1]&.to_i
|
|
7747
|
-
next if w &&
|
|
7812
|
+
next if w && w <= 40
|
|
7813
|
+
next if h && h <= 40
|
|
7748
7814
|
urls << src
|
|
7749
7815
|
end
|
|
7750
7816
|
urls
|
|
@@ -7826,7 +7892,7 @@ Required: URL, optional CSS selector
|
|
|
7826
7892
|
# Clear right pane and show images
|
|
7827
7893
|
n = image_paths.size
|
|
7828
7894
|
label = n == 1 ? "1 image" : "#{n} images"
|
|
7829
|
-
@panes[:right].text = " [#{label}] Press
|
|
7895
|
+
@panes[:right].text = " [#{label}] Press ESC to return".fg(245)
|
|
7830
7896
|
@panes[:right].full_refresh # Full refresh to clear image area
|
|
7831
7897
|
|
|
7832
7898
|
pane_w = @panes[:right].w - 2
|
|
@@ -7909,7 +7975,6 @@ Required: URL, optional CSS selector
|
|
|
7909
7975
|
def format_attachments(attachments)
|
|
7910
7976
|
return nil unless attachments.is_a?(Array) && !attachments.empty?
|
|
7911
7977
|
lines = []
|
|
7912
|
-
lines << ("─" * 60).fg(238)
|
|
7913
7978
|
lines << "Attachments:".b.fg(208)
|
|
7914
7979
|
attachments.each_with_index do |att, i|
|
|
7915
7980
|
name = att['name'] || att['filename'] || 'unnamed'
|
|
@@ -7930,6 +7995,17 @@ Required: URL, optional CSS selector
|
|
|
7930
7995
|
end
|
|
7931
7996
|
|
|
7932
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
|
+
|
|
7933
8009
|
def colorize_links(line, base_color, link_color)
|
|
7934
8010
|
url_re = %r{https?://[^\s<>\[\]()]+}
|
|
7935
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