sup 1.3 → 1.4

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.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/checks.yml +10 -9
  3. data/.rubocop.yml +1 -1
  4. data/History.txt +17 -0
  5. data/Manifest.txt +9 -1
  6. data/README.md +5 -3
  7. data/Rakefile +1 -1
  8. data/bin/sup +5 -3
  9. data/contrib/nix/Gemfile.lock +39 -32
  10. data/contrib/nix/gemset.nix +80 -58
  11. data/contrib/nix/ruby2.4-Gemfile.lock +1 -1
  12. data/contrib/nix/ruby2.4-gemset.nix +2 -2
  13. data/contrib/nix/ruby2.5-Gemfile.lock +1 -1
  14. data/contrib/nix/ruby2.5-gemset.nix +2 -2
  15. data/contrib/nix/ruby2.6-Gemfile.lock +1 -1
  16. data/contrib/nix/ruby2.6-gemset.nix +2 -2
  17. data/contrib/nix/ruby2.7-Gemfile.lock +1 -1
  18. data/contrib/nix/ruby2.7-gemset.nix +2 -2
  19. data/contrib/nix/ruby3.0-Gemfile.lock +1 -1
  20. data/contrib/nix/ruby3.0-gemset.nix +2 -2
  21. data/contrib/nix/ruby3.1-Gemfile.lock +101 -0
  22. data/contrib/nix/ruby3.1-gemset.nix +391 -0
  23. data/contrib/nix/ruby3.1-shell.nix +2 -13
  24. data/contrib/nix/ruby3.2-Gemfile.lock +101 -0
  25. data/contrib/nix/ruby3.2-gemset.nix +391 -0
  26. data/contrib/nix/ruby3.2-shell.nix +2 -13
  27. data/contrib/nix/ruby3.3-shell.nix +1 -12
  28. data/contrib/nix/ruby3.4-shell.nix +1 -10
  29. data/contrib/nix/ruby4.0-shell.nix +40 -0
  30. data/lib/sup/account.rb +2 -0
  31. data/lib/sup/buffer.rb +1 -0
  32. data/lib/sup/contact.rb +3 -4
  33. data/lib/sup/draft.rb +15 -11
  34. data/lib/sup/index.rb +8 -4
  35. data/lib/sup/message_chunks.rb +0 -24
  36. data/lib/sup/mode.rb +1 -0
  37. data/lib/sup/modes/contact_list_mode.rb +0 -1
  38. data/lib/sup/modes/edit_message_mode.rb +1 -1
  39. data/lib/sup/modes/label_search_results_mode.rb +1 -2
  40. data/lib/sup/modes/line_cursor_mode.rb +22 -20
  41. data/lib/sup/modes/search_results_mode.rb +0 -1
  42. data/lib/sup/modes/thread_view_mode.rb +1 -2
  43. data/lib/sup/rfc2047.rb +5 -2
  44. data/lib/sup/source.rb +2 -0
  45. data/lib/sup/version.rb +1 -1
  46. data/lib/sup.rb +1 -1
  47. data/man/sup-add.1 +6 -6
  48. data/man/sup-config.1 +6 -6
  49. data/man/sup-dump.1 +5 -5
  50. data/man/sup-import-dump.1 +7 -7
  51. data/man/sup-recover-sources.1 +5 -5
  52. data/man/sup-sync-back-maildir.1 +9 -9
  53. data/man/sup-sync.1 +11 -11
  54. data/man/sup-tweak-labels.1 +10 -10
  55. data/man/sup.1 +11 -11
  56. data/sup.gemspec +1 -1
  57. data/test/dummy_buffer.rb +34 -0
  58. data/test/fixtures/contacts.txt +2 -1
  59. data/test/fixtures/rfc2047-header-encoding.eml +1 -1
  60. data/test/integration/test_draft.rb +128 -0
  61. data/test/integration/test_maildir.rb +2 -0
  62. data/test/integration/test_mbox.rb +2 -0
  63. data/test/integration/test_sup-add.rb +4 -0
  64. data/test/test_crypto.rb +7 -2
  65. data/test/test_message.rb +1 -0
  66. data/test/unit/test_contact.rb +15 -3
  67. data/test/unit/test_edit_message_mode.rb +6 -1
  68. data/test/unit/test_index.rb +65 -0
  69. data/test/unit/test_line_cursor_mode.rb +208 -0
  70. metadata +17 -7
  71. data/shell.nix +0 -1
@@ -1,4 +1,4 @@
1
- From: test@example.invalid
1
+ From: =?utf-8?q?YouTube-tj=E4nst?= <service@youtube.com>
2
2
  To: test@example.invalid
3
3
  Date: Sun, 19 Jul 2020 17:03:56 +1000
4
4
  Subject:
@@ -0,0 +1,128 @@
1
+ require "sup"
2
+ require "test_helper"
3
+
4
+ class TestDraft < Minitest::Test
5
+ include Redwood
6
+
7
+ def setup
8
+ @path = Dir.mktmpdir
9
+ start
10
+ Logger.remove_sink $stderr
11
+ @draft_dir = File.join @path, "drafts"
12
+ @test_message_1 = <<EOS
13
+ From: Some Person <someone@example.invalid>
14
+ To:
15
+ Cc:
16
+ Bcc:
17
+ Subject: draft
18
+ Date: Fri, 11 Apr 2025 22:34:05 +1000
19
+ Message-ID: <123@example.invalid>
20
+
21
+ My incomplete message
22
+ EOS
23
+ DraftManager.instance.instance_eval "@dir = '#{@draft_dir}'"
24
+ Index.init @path
25
+ Index.load
26
+ SourceManager.instance.instance_eval "@sources = {}"
27
+ @draft_source = DraftManager.new_source
28
+ SourceManager.add_source @draft_source
29
+ end
30
+
31
+ def teardown
32
+ Index.save_index
33
+ ObjectSpace.each_object(Class).select {|a| a < Redwood::Singleton}.each do |klass|
34
+ klass.deinstantiate! unless klass == Redwood::Logger
35
+ end
36
+ FileUtils.rm_r @path
37
+ end
38
+
39
+ def test_write_draft
40
+ DraftManager.write_draft { |f| f.write @test_message_1 }
41
+
42
+ draft_filename = File.join @draft_dir, "0"
43
+ assert File.exist? draft_filename
44
+ assert_equal @test_message_1, (File.read draft_filename)
45
+
46
+ ## Check that it is loaded back into the index successfully too.
47
+ messages_in_index = Index.instance.enum_for(:each_message).to_a
48
+ assert_equal @test_message_1, messages_in_index.first.raw_message
49
+ assert_equal [:draft, :inbox].to_set, messages_in_index.first.labels
50
+ end
51
+
52
+ def test_discard_draft
53
+ DraftManager.write_draft { |f| f.write @test_message_1 }
54
+ draft_filename = File.join @draft_dir, "0"
55
+ assert File.exist? draft_filename
56
+ message_in_index = Index.instance.enum_for(:each_message).to_a.first
57
+
58
+ DraftManager.discard message_in_index
59
+ refute File.exist? draft_filename
60
+ end
61
+
62
+ def test_discard_already_deleted_from_disk
63
+ DraftManager.write_draft { |f| f.write @test_message_1 }
64
+ draft_filename = File.join @draft_dir, "0"
65
+ assert File.exist? draft_filename
66
+ message_in_index = Index.instance.enum_for(:each_message).to_a.first
67
+
68
+ File.delete draft_filename
69
+
70
+ DraftManager.discard message_in_index
71
+ refute File.exist? draft_filename
72
+ end
73
+
74
+ def test_load_malformed_draft
75
+ ## Sup always writes drafts by serialising a Message, meaning the draft is
76
+ ## guaranteed to have certain headers like Date. But it's always possible
77
+ ## for the user to edit the draft directly on the filesystem and leave it
78
+ ## in some kind of malformed state. Sup should handle it without crashing.
79
+ draft_filename = File.join @draft_dir, "0"
80
+ fallback_date = Time.new 2025, 5, 3, 15, 47, 41
81
+ File.write draft_filename, <<EOS
82
+ Some-Header: Value
83
+
84
+ body
85
+ EOS
86
+ File.utime fallback_date, fallback_date, draft_filename
87
+ PollManager.poll_from @draft_source
88
+ messages_in_index = Index.instance.enum_for(:each_message).to_a
89
+ assert_equal "", messages_in_index[0].subj
90
+ assert_equal fallback_date, messages_in_index[0].date
91
+
92
+ File.write (File.join @draft_dir, "1"), <<EOS
93
+ missing a header!
94
+ EOS
95
+ PollManager.poll_from @draft_source
96
+ messages_in_index = Index.instance.enum_for(:each_message).to_a
97
+ assert_equal "", messages_in_index[0].subj
98
+
99
+ File.write (File.join @draft_dir, "2"), ""
100
+ PollManager.poll_from @draft_source
101
+ messages_in_index = Index.instance.enum_for(:each_message).to_a
102
+ assert_equal "", messages_in_index[0].subj
103
+ end
104
+
105
+ def test_draft_with_non_ascii_chars
106
+ message = <<EOS
107
+ From: Some Person <someone@example.invalid>
108
+ To:
109
+ Cc:
110
+ Bcc:
111
+ Subject: UTF-8 draft 🤐
112
+ Date: Fri, 11 Apr 2025 22:34:05 +1000
113
+ Message-ID: <123@example.invalid>
114
+
115
+ ¡Buen día! Доброго ранку! おはよう!
116
+ EOS
117
+ DraftManager.write_draft { |f| f.write message }
118
+ draft_filename = File.join @draft_dir, "0"
119
+ assert_equal message, (File.read draft_filename)
120
+
121
+ PollManager.poll_from @draft_source
122
+ messages_in_index = Index.instance.enum_for(:each_message).to_a
123
+ assert_equal "UTF-8 draft 🤐", messages_in_index[0].subj
124
+ assert_equal message, messages_in_index.first.raw_message
125
+ assert_equal "¡Buen día! Доброго ранку! おはよう!", \
126
+ messages_in_index[0].chunks[0].lines[0]
127
+ end
128
+ end
@@ -41,11 +41,13 @@ EOS
41
41
 
42
42
  def start_sup_and_add_source(source)
43
43
  start
44
+ Logger.remove_sink $stderr
44
45
  Index.init @path
45
46
  Index.load
46
47
  SourceManager.instance.instance_eval '@sources = {}'
47
48
  SourceManager.instance.add_source source
48
49
  PollManager.poll_from source
50
+ Index.save_index
49
51
  end
50
52
 
51
53
  # and now, let the tests begin!
@@ -34,11 +34,13 @@ EOS
34
34
 
35
35
  def start_sup_and_add_source(source)
36
36
  start
37
+ Logger.remove_sink $stderr
37
38
  Index.init @path
38
39
  Index.load
39
40
  SourceManager.instance.instance_eval '@sources = {}'
40
41
  SourceManager.instance.add_source source
41
42
  PollManager.poll_from source
43
+ Index.save_index
42
44
  end
43
45
 
44
46
  # and now, let the tests begin!
@@ -26,6 +26,8 @@ class TestSupAdd < Minitest::Test
26
26
  EOS
27
27
  end
28
28
 
29
+ ## https://github.com/sup-heliotrope/sup/issues/577
30
+ ## https://github.com/sup-heliotrope/sup/issues/516
29
31
  def test_fixes_old_tag_uri_syntax
30
32
  File.write "#{@path}/sources.yaml", <<EOS
31
33
  ---
@@ -36,6 +38,7 @@ EOS
36
38
  sync_back: true
37
39
  id: 1
38
40
  labels: []
41
+ - !supmua.org,2006-10-01/Redwood/SentLoader {}
39
42
  EOS
40
43
  _out, _err = capture_subprocess_io do
41
44
  assert system({"SUP_BASE" => @path}, "bin/sup-add", "maildir:///other/path")
@@ -58,6 +61,7 @@ EOS
58
61
  sync_back: true
59
62
  id: 2
60
63
  labels: []
64
+ - !<tag:supmua.org,2006-10-01/Redwood/SentLoader> {}
61
65
  EOS
62
66
  end
63
67
 
data/test/test_crypto.rb CHANGED
@@ -38,8 +38,13 @@ class TestCryptoManager < Minitest::Test
38
38
  @path = Dir.mktmpdir
39
39
  Redwood::HookManager.init File.join(@path, 'hooks')
40
40
 
41
- am = {:default=> {name: +"test", email: @from_email.dup, alternates: [@from_email_ecc.dup]}}
42
- Redwood::AccountManager.init am
41
+ account = {
42
+ :name => +"test",
43
+ :email => @from_email.dup,
44
+ :alternates => [@from_email_ecc.dup],
45
+ :sendmail => "/bin/false",
46
+ }
47
+ Redwood::AccountManager.init :default => account
43
48
 
44
49
  Redwood::CryptoManager.init
45
50
  end
data/test/test_message.rb CHANGED
@@ -253,6 +253,7 @@ class TestMessage < Minitest::Test
253
253
  "bad: =?UTF16?q?badcharsetname?==?US-ASCII?b?/w?=" +
254
254
  "=?UTF-7?Q?=41=6D=65=72=69=63=61=E2=80=99=73?=",
255
255
  sup_message.subj)
256
+ assert_equal "=?utf-8?q?YouTube-tj=E4nst?=", sup_message.from.name
256
257
  end
257
258
 
258
259
  def test_nonascii_header
@@ -1,4 +1,5 @@
1
1
  require 'test_helper'
2
+ require 'sup'
2
3
  require 'sup/contact'
3
4
 
4
5
  module Redwood
@@ -16,9 +17,20 @@ class TestContact < Minitest::Test
16
17
 
17
18
  def test_contact_manager
18
19
  assert @contact
19
- ## 1 contact is imported from the fixture file.
20
- assert_equal 1, @contact.contacts.count
21
- assert_equal @contact.contact_for("RC").name, "Random Contact"
20
+
21
+ ## 2 contacts are imported from the fixture file.
22
+ assert_equal 2, @contact.contacts.count
23
+
24
+ rc = @contact.contact_for "RC"
25
+ assert_equal "Random Contact", rc.name
26
+ assert @contact.is_aliased_contact? rc
27
+ assert_equal "RC", @contact.alias_for(rc)
28
+
29
+ uc = @contact.person_for "unaliased@example.invalid"
30
+ refute @contact.is_aliased_contact? uc
31
+ assert_nil @contact.alias_for uc
32
+ assert_equal [rc, uc], @contact.contacts
33
+ assert_equal [rc], @contact.contacts_with_aliases
22
34
 
23
35
  assert_nil @contact.contact_for "TN"
24
36
  @contact.update_alias @person, "TN"
@@ -24,7 +24,12 @@ class TestEditMessageMode < Minitest::Test
24
24
  $config = {}
25
25
  @path = Dir.mktmpdir
26
26
  Redwood::HookManager.init File.join(@path, "hooks")
27
- Redwood::AccountManager.init :default => {name: +"test", email: +"sender@example.invalid"}
27
+ account = {
28
+ :name => +"test",
29
+ :email => +"sender@example.invalid",
30
+ :sendmail => "/bin/false",
31
+ }
32
+ Redwood::AccountManager.init :default => account
28
33
  Redwood::CryptoManager.instance_variable_set :@instance, DummyCryptoManager.new
29
34
  end
30
35
 
@@ -0,0 +1,65 @@
1
+ require "minitest/mock"
2
+ require "test_helper"
3
+
4
+ require "sup"
5
+
6
+ class TestIndex < Minitest::Test
7
+ def setup
8
+ @path = Dir.mktmpdir
9
+ Redwood::start
10
+ Redwood::Logger.remove_sink $stderr
11
+ Redwood::Index.init @path
12
+ Redwood::Index.load
13
+ end
14
+
15
+ def teardown
16
+ Redwood::Index.save_index
17
+ ObjectSpace.each_object(Class).select {|a| a < Redwood::Singleton}.each do |klass|
18
+ klass.deinstantiate! unless klass == Redwood::Logger
19
+ end
20
+ FileUtils.rm_r @path
21
+ end
22
+
23
+ def with_fake_time &block
24
+ Time.stub :now, Time.utc(2000) do
25
+ ## Also stub Time.local to behave like Time.utc so that Chronic.parse
26
+ ## doesn't pick up the timezone of the test runner.
27
+ Time.stub :local, ->(*args) { Time.utc(*args) }, &block
28
+ end
29
+ end
30
+
31
+ def test_date_query_parsing
32
+ parsed = with_fake_time do
33
+ Redwood::Index.parse_query "after:yesterday"
34
+ end
35
+ expected_qobj = Xapian::Query.new(
36
+ Xapian::Query::OP_VALUE_RANGE,
37
+ Redwood::Index::DATE_VALUENO,
38
+ Xapian.sortable_serialise(Time.utc(2000, 1, 1).to_i),
39
+ Xapian.sortable_serialise(2**32),
40
+ )
41
+ assert_equal expected_qobj.description, parsed[:qobj].description
42
+
43
+ parsed = with_fake_time do
44
+ Redwood::Index.parse_query "before:yesterday"
45
+ end
46
+ expected_qobj = Xapian::Query.new(
47
+ Xapian::Query::OP_VALUE_RANGE,
48
+ Redwood::Index::DATE_VALUENO,
49
+ Xapian.sortable_serialise(0),
50
+ Xapian.sortable_serialise(Time.utc(2000, 1, 1).to_i),
51
+ )
52
+ assert_equal expected_qobj.description, parsed[:qobj].description
53
+
54
+ parsed = with_fake_time do
55
+ Redwood::Index.parse_query "during:yesterday"
56
+ end
57
+ expected_qobj = Xapian::Query.new(
58
+ Xapian::Query::OP_VALUE_RANGE,
59
+ Redwood::Index::DATE_VALUENO,
60
+ Xapian.sortable_serialise(Time.utc(1999, 12, 31).to_i),
61
+ Xapian.sortable_serialise(Time.utc(2000, 1, 1).to_i),
62
+ )
63
+ assert_equal expected_qobj.description, parsed[:qobj].description
64
+ end
65
+ end
@@ -0,0 +1,208 @@
1
+ require "test_helper"
2
+ require "dummy_buffer"
3
+
4
+ require "sup"
5
+
6
+ class TestLineCursorMode < Minitest::Test
7
+ def setup
8
+ $config = {
9
+ :load_more_threads_hysteresis => 0,
10
+ :load_more_threads_when_scrolling => true,
11
+ :continuous_scroll => false,
12
+ }
13
+ Redwood::BufferManager.init
14
+ @modes_to_cleanup = []
15
+ @lines = []
16
+ @load_more = Thread::Queue.new
17
+ @buffer_height = 41 # 1 for status line, 40 usable lines
18
+ end
19
+
20
+ def teardown
21
+ @modes_to_cleanup.each { |mode| mode.cleanup }
22
+ Redwood::BufferManager.deinstantiate!
23
+ $config = nil
24
+ end
25
+
26
+ def make_mode
27
+ mode = Redwood::LineCursorMode.new
28
+ @modes_to_cleanup << mode
29
+ lines = @lines
30
+ mode.define_singleton_method(:lines) { lines.length }
31
+ mode.define_singleton_method(:[]) { |i| lines[i] }
32
+ mode.send(:to_load_more) { |n| @load_more << n }
33
+ mode.buffer = Redwood::DummyBuffer.new 100, @buffer_height
34
+ mode.spawned
35
+ mode.draw
36
+ mode
37
+ end
38
+
39
+ def expect_load_more n
40
+ begin
41
+ requested = @load_more.pop :timeout => 0.1
42
+ rescue ThreadError
43
+ ## Ruby < 3.2 does not obey the timeout for Queue#pop
44
+ sleep 0.1
45
+ requested = @load_more.pop true
46
+ end
47
+ refute_nil requested, "Expected load_more callbacks to fire"
48
+ assert_equal n, requested
49
+ (0...n).map { |i| @lines << "line #{i}" }
50
+ end
51
+
52
+ def test_cursor_down
53
+ mode = make_mode
54
+ expect_load_more 41
55
+ mode.draw # curpos gets messed up without this call, why?
56
+
57
+ 21.times do
58
+ mode.handle_input Ncurses::CharCode.character('j')
59
+ end
60
+ assert_equal 21, mode.curpos
61
+ assert_equal 0, mode.topline
62
+
63
+ ## Two past halfway, load_more callbacks are triggered.
64
+ mode.handle_input Ncurses::CharCode.character('j')
65
+ assert_equal 22, mode.curpos
66
+ expect_load_more 40
67
+
68
+ 17.times do
69
+ mode.handle_input Ncurses::CharCode.character('j')
70
+ end
71
+ assert_equal 39, mode.curpos
72
+ assert_equal 0, mode.topline
73
+
74
+ ## From the bottom line, it wraps back to the top of the next page.
75
+ mode.handle_input Ncurses::CharCode.character('j')
76
+ assert_equal 40, mode.curpos
77
+ assert_equal 40, mode.topline
78
+ end
79
+
80
+ def test_scroll_down
81
+ mode = make_mode
82
+ expect_load_more 41
83
+
84
+ ## When the cursor is already at the top, it moves with the scroll.
85
+ assert_equal 0, mode.curpos
86
+ assert_equal 0, mode.topline
87
+ mode.handle_input Ncurses::CharCode.character('J')
88
+ assert_equal 1, mode.curpos
89
+ assert_equal 1, mode.topline
90
+
91
+ 3.times do
92
+ mode.handle_input Ncurses::CharCode.character('j')
93
+ end
94
+ assert_equal 4, mode.curpos
95
+ assert_equal 1, mode.topline
96
+
97
+ ## When the cursor is not at the top, it keeps its place and the
98
+ ## buffer scrolls underneath it.
99
+ mode.handle_input Ncurses::CharCode.character('J')
100
+ assert_equal 4, mode.curpos
101
+ assert_equal 2, mode.topline
102
+
103
+ ## It always loads 10 more if we would scroll past the bottom.
104
+ expect_load_more 10
105
+ end
106
+
107
+ def test_page_down
108
+ mode = make_mode
109
+ expect_load_more 41
110
+
111
+ mode.handle_input Ncurses::CharCode.keycode(Ncurses::KEY_NPAGE)
112
+ assert_equal 40, mode.curpos
113
+ assert_equal 40, mode.topline
114
+ expect_load_more 40
115
+
116
+ mode.handle_input Ncurses::CharCode.keycode(Ncurses::KEY_NPAGE)
117
+ assert_equal 80, mode.curpos
118
+ assert_equal 80, mode.topline
119
+ assert_equal 81, mode.lines
120
+ expect_load_more 40
121
+ end
122
+
123
+ def test_page_down_when_fully_populated
124
+ mode = make_mode
125
+ expect_load_more 41
126
+ (0...119).map { |i| @lines << "more line #{i}" } # enough for 4 full pages
127
+
128
+ mode.handle_input Ncurses::CharCode.keycode(Ncurses::KEY_NPAGE)
129
+ assert_equal 40, mode.curpos
130
+ assert_equal 40, mode.topline
131
+
132
+ ## Relative cursor position is preserved when paging down.
133
+ 3.times do
134
+ mode.handle_input Ncurses::CharCode.character('j')
135
+ end
136
+ assert_equal 43, mode.curpos
137
+ assert_equal 40, mode.topline
138
+ mode.handle_input Ncurses::CharCode.keycode(Ncurses::KEY_NPAGE)
139
+ assert_equal 83, mode.curpos
140
+ assert_equal 80, mode.topline
141
+ end
142
+
143
+ def test_half_page_down
144
+ mode = make_mode
145
+ expect_load_more 41
146
+
147
+ mode.handle_input Ncurses::CharCode.character("\C-d")
148
+ assert_equal 20, mode.curpos
149
+ assert_equal 20, mode.topline
150
+ expect_load_more 40
151
+
152
+ mode.handle_input Ncurses::CharCode.character("\C-d")
153
+ assert_equal 40, mode.curpos
154
+ assert_equal 40, mode.topline
155
+ end
156
+
157
+ def test_half_page_down_when_fully_populated
158
+ mode = make_mode
159
+ expect_load_more 41
160
+ (0...119).map { |i| @lines << "more line #{i}" } # enough for 4 full pages
161
+
162
+ mode.handle_input Ncurses::CharCode.character("\C-d")
163
+ assert_equal 20, mode.curpos
164
+ assert_equal 20, mode.topline
165
+
166
+ 25.times do
167
+ mode.handle_input Ncurses::CharCode.character('j')
168
+ end
169
+ assert_equal 45, mode.curpos
170
+ assert_equal 20, mode.topline
171
+ mode.handle_input Ncurses::CharCode.character("\C-d")
172
+ assert_equal 45, mode.curpos
173
+ assert_equal 40, mode.topline
174
+ end
175
+
176
+ def test_half_page_up
177
+ mode = make_mode
178
+ expect_load_more 41
179
+ (0...119).map { |i| @lines << "more line #{i}" } # enough for 4 full pages
180
+
181
+ mode.handle_input Ncurses::CharCode.keycode(Ncurses::KEY_NPAGE)
182
+ assert_equal 40, mode.curpos
183
+ assert_equal 40, mode.topline
184
+
185
+ mode.handle_input Ncurses::CharCode.character("\C-u")
186
+ assert_equal 40, mode.curpos
187
+ assert_equal 20, mode.topline
188
+
189
+ mode.handle_input Ncurses::CharCode.character('j')
190
+ assert_equal 41, mode.curpos
191
+ assert_equal 20, mode.topline
192
+
193
+ mode.handle_input Ncurses::CharCode.character("\C-u")
194
+ assert_equal 40, mode.curpos
195
+ assert_equal 0, mode.topline
196
+ end
197
+
198
+ def test_jump_to_end
199
+ mode = make_mode
200
+ expect_load_more 41
201
+
202
+ mode.handle_input Ncurses::CharCode.keycode(Ncurses::KEY_END)
203
+ assert_equal 40, mode.curpos
204
+ assert_equal 1, mode.topline
205
+ assert_equal 41, mode.botline
206
+ expect_load_more 40
207
+ end
208
+ end
metadata CHANGED
@@ -1,17 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sup
3
3
  version: !ruby/object:Gem::Version
4
- version: '1.3'
4
+ version: '1.4'
5
5
  platform: ruby
6
6
  authors:
7
7
  - William Morgan
8
8
  - Gaute Hope
9
9
  - Hamish Downer
10
10
  - Matthieu Rakotojaona
11
- autorequire:
12
11
  bindir: bin
13
12
  cert_chain: []
14
- date: 2025-04-21 00:00:00.000000000 Z
13
+ date: 2026-04-21 00:00:00.000000000 Z
15
14
  dependencies:
16
15
  - !ruby/object:Gem::Dependency
17
16
  name: ncursesw
@@ -370,10 +369,15 @@ files:
370
369
  - contrib/nix/ruby3.0-Gemfile.lock
371
370
  - contrib/nix/ruby3.0-gemset.nix
372
371
  - contrib/nix/ruby3.0-shell.nix
372
+ - contrib/nix/ruby3.1-Gemfile.lock
373
+ - contrib/nix/ruby3.1-gemset.nix
373
374
  - contrib/nix/ruby3.1-shell.nix
375
+ - contrib/nix/ruby3.2-Gemfile.lock
376
+ - contrib/nix/ruby3.2-gemset.nix
374
377
  - contrib/nix/ruby3.2-shell.nix
375
378
  - contrib/nix/ruby3.3-shell.nix
376
379
  - contrib/nix/ruby3.4-shell.nix
380
+ - contrib/nix/ruby4.0-shell.nix
377
381
  - contrib/nix/test-all-rubies.sh
378
382
  - devel/console.sh
379
383
  - devel/count-loc.sh
@@ -461,8 +465,8 @@ files:
461
465
  - man/sup-sync.1
462
466
  - man/sup-tweak-labels.1
463
467
  - man/sup.1
464
- - shell.nix
465
468
  - sup.gemspec
469
+ - test/dummy_buffer.rb
466
470
  - test/dummy_source.rb
467
471
  - test/fixtures/bad-content-transfer-encoding-1.eml
468
472
  - test/fixtures/binary-content-transfer-encoding-2.eml
@@ -496,6 +500,7 @@ files:
496
500
  - test/gnupg_test_home/regen_keys.sh
497
501
  - test/gnupg_test_home/secring.gpg
498
502
  - test/gnupg_test_home/sup-test-2@foo.bar.asc
503
+ - test/integration/test_draft.rb
499
504
  - test/integration/test_maildir.rb
500
505
  - test/integration/test_mbox.rb
501
506
  - test/integration/test_sup-add.rb
@@ -510,13 +515,15 @@ files:
510
515
  - test/unit/test_contact.rb
511
516
  - test/unit/test_edit_message_mode.rb
512
517
  - test/unit/test_horizontal_selector.rb
518
+ - test/unit/test_index.rb
519
+ - test/unit/test_line_cursor_mode.rb
513
520
  - test/unit/test_locale_fiddler.rb
514
521
  - test/unit/test_person.rb
515
522
  - test/unit/test_rmail_message.rb
516
523
  - test/unit/util/test_query.rb
517
524
  - test/unit/util/test_string.rb
518
525
  - test/unit/util/test_uri.rb
519
- homepage: https://sup-heliotrope.github.io/
526
+ homepage: https://supmua.dev/
520
527
  licenses:
521
528
  - GPL-2.0
522
529
  metadata: {}
@@ -543,11 +550,11 @@ required_rubygems_version: !ruby/object:Gem::Requirement
543
550
  - !ruby/object:Gem::Version
544
551
  version: '0'
545
552
  requirements: []
546
- rubygems_version: 3.5.22
547
- signing_key:
553
+ rubygems_version: 3.7.2
548
554
  specification_version: 4
549
555
  summary: A console-based email client with the best features of GMail, mutt and Emacs
550
556
  test_files:
557
+ - test/dummy_buffer.rb
551
558
  - test/dummy_source.rb
552
559
  - test/fixtures/bad-content-transfer-encoding-1.eml
553
560
  - test/fixtures/binary-content-transfer-encoding-2.eml
@@ -581,6 +588,7 @@ test_files:
581
588
  - test/gnupg_test_home/regen_keys.sh
582
589
  - test/gnupg_test_home/secring.gpg
583
590
  - test/gnupg_test_home/sup-test-2@foo.bar.asc
591
+ - test/integration/test_draft.rb
584
592
  - test/integration/test_maildir.rb
585
593
  - test/integration/test_mbox.rb
586
594
  - test/integration/test_sup-add.rb
@@ -595,6 +603,8 @@ test_files:
595
603
  - test/unit/test_contact.rb
596
604
  - test/unit/test_edit_message_mode.rb
597
605
  - test/unit/test_horizontal_selector.rb
606
+ - test/unit/test_index.rb
607
+ - test/unit/test_line_cursor_mode.rb
598
608
  - test/unit/test_locale_fiddler.rb
599
609
  - test/unit/test_person.rb
600
610
  - test/unit/test_rmail_message.rb
data/shell.nix DELETED
@@ -1 +0,0 @@
1
- contrib/nix/ruby3.3-shell.nix