pwn 0.5.762 → 0.5.763

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 20f94659af48e0dba30534ed36cfaf8942497dcc5851e346a44ef40c51abb130
4
- data.tar.gz: 8f91abcce730d25f6721d89aa6389090aa572b40ffe7449cd419a874d15d1932
3
+ metadata.gz: fec3b367c44874a819aa4892a5b3c507cf858ed4649eb31d295b0509e8747c83
4
+ data.tar.gz: d8a004b8eee5f7bd996ae8c9208f895d93354b09f9149050cb3e93d8632fda8c
5
5
  SHA512:
6
- metadata.gz: 8174ba5855b3cb25e41eabe9c34a57add502f6b1d52caef2809bd603e66eab5dd1a169e01e1e06feb5c745ffddaa817fbaae9748458933c35c727a5a9c18b736
7
- data.tar.gz: 111e5b05d8ad0c54f82ba0ae1836fb9ab1f8f3c1845c992d2dbacf7a241286e6ea139c9714d91e58ee80a246a119bdadf59e71d6a0f65e30fc224b96a9de2b7a
6
+ metadata.gz: 7490168d3e50c940ff771fe6788f7373e89b15f96541a8d0cec29bd13b1cccdce7c377799a415628fcd899a0e912928bf3aeb5fef68c0cf0fda2217871f60d95
7
+ data.tar.gz: eb50e5646ee60e9394f92139b61c38901303fc6167b06f95cf559a413d88a09dbfa21eadeb1e46e8e9418295d348e53f81a62d4d4f25956ebb0284f7e9c2e572
data/Gemfile CHANGED
@@ -77,7 +77,7 @@ gem 'rbvmomi2', '3.10.0'
77
77
  gem 'rdoc', '7.0.4'
78
78
  gem 'rest-client', '2.1.0'
79
79
  gem 'rex', '2.0.13'
80
- gem 'rmagick', '7.1.6'
80
+ gem 'rmagick', '7.1.7'
81
81
  gem 'rqrcode', '3.2.0'
82
82
  gem 'rspec', '3.13.2'
83
83
  gem 'rtesseract', '3.1.4'
@@ -14,4 +14,22 @@ welcome-banner # REPL command: redraw a random one
14
14
 
15
15
  Add your own: drop a module in `lib/pwn/banner/` implementing `self.get`.
16
16
 
17
+ ## pwn-ai startup pane
18
+
19
+ The curses console starts with a white miniature derived directly from
20
+ `PWN::Banner::WhiteRabbit.get`, without its dedication or wordmark. The full
21
+ banner remains unchanged. The rabbit is centered and aspect-contained in the
22
+ current pane: small panes use area-filtered 3×6 character strokes packed into
23
+ ordinary 2×4 Braille text dots; when the original ASCII fits, it is displayed
24
+ verbatim, centered without enlargement. This assumes typical 1:2 terminal cells.
25
+ Small panes lose punctuation detail, and Braille dot spacing depends on the font;
26
+ physically tiny panes cannot retain a recognizable rabbit. No image protocol,
27
+ image file, graphics probe or runtime image library is used.
28
+
29
+ No games or replays are computed until the animation pane first receives focus
30
+ via Ctrl+T/Ctrl+X. That enables games for the console lifetime. Ctrl+G cycles
31
+ games while focused; leaving the pane keeps the existing automatic animation,
32
+ not the startup rabbit. Resizing, modal input priority and the mission draft
33
+ retain their existing behavior. Monochrome terminals keep the same geometry.
34
+
17
35
  [← Home](Home.md)
@@ -720,6 +720,7 @@ module PWN
720
720
  @menu = nil
721
721
  panes = @header_pane_height ? %i[mission session animation] : %i[mission session]
722
722
  @focus = panes[(panes.index(@focus).to_i + 1) % panes.length]
723
+ @banner_activated = true if @focus == :animation
723
724
  @banner_game&.controls
724
725
  @banner_game_tick = nil
725
726
  return
@@ -1227,6 +1228,9 @@ module PWN
1227
1228
  # Artwork has no worker, IO, or request-progress meaning. The existing
1228
1229
  # event-loop repaint supplies its clock, including while requests run.
1229
1230
  def banner_frame(size, cells: false)
1231
+ # Startup is artwork only. Once focused, retain the existing seeded
1232
+ # automatic behavior on leaving the game; never return to the splash.
1233
+ @banner_activated ||= @focus == :animation
1230
1234
  now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
1231
1235
  session = @pry.config.pwn_ai_session_id.to_s
1232
1236
  if @banner_session != session
@@ -1236,6 +1240,11 @@ module PWN
1236
1240
  @banner_seed = session.bytes.reduce(PWN::Banner::MINI_SEED) { |seed, byte| ((seed * 33) ^ byte) & 0xffffffff }
1237
1241
  @banner_started = now
1238
1242
  end
1243
+ unless @banner_activated
1244
+ art = rabbit_cells(size)
1245
+ return cells ? art : art.map { |row| row.map { |cell| cell[:glyph] }.join }
1246
+ end
1247
+
1239
1248
  cadence = cells ? @banner_frame_seconds : PWN::Banner::MINI_FRAME_SECONDS
1240
1249
  frame = ((now - @banner_started) / cadence).floor % PWN::Banner::MINI_FRAME_COUNT
1241
1250
  return playable_banner(size, now) if cells && @focus == :animation
@@ -1249,6 +1258,97 @@ module PWN
1249
1258
  canvas
1250
1259
  end
1251
1260
 
1261
+ # Rasterize the actual banner's punctuation, not a replacement rabbit.
1262
+ # These 3x6 character strokes retain the usual 1:2 terminal-cell aspect.
1263
+ # Only the dedication and wordmark are omitted; the full banner API is
1264
+ # unchanged. At native size use its original characters verbatim.
1265
+ RABBIT_STROKES = {
1266
+ ' ' => [0, 0, 0, 0, 0, 0], '_' => [0, 0, 0, 0, 0, 7],
1267
+ '/' => [1, 1, 2, 2, 4, 4], '\\' => [4, 4, 2, 2, 1, 1],
1268
+ '{' => [1, 2, 2, 4, 2, 1], '}' => [4, 2, 2, 1, 2, 4],
1269
+ 'Y' => [5, 5, 2, 2, 2, 2], '.' => [0, 0, 0, 0, 0, 2],
1270
+ '-' => [0, 0, 0, 7, 0, 0], '"' => [5, 5, 0, 0, 0, 0],
1271
+ '`' => [4, 2, 0, 0, 0, 0], ',' => [0, 0, 0, 0, 2, 4],
1272
+ ';' => [0, 2, 0, 0, 2, 4], '|' => [2, 2, 2, 2, 2, 2]
1273
+ }.transform_values(&:freeze).freeze
1274
+ RABBIT_DOTS = [[0, 0, 1], [0, 1, 2], [0, 2, 4], [1, 0, 8], [1, 1, 16], [1, 2, 32], [0, 3, 64], [1, 3, 128]].map(&:freeze).freeze
1275
+
1276
+ def rabbit_source
1277
+ @rabbit_source ||= begin
1278
+ rows = PWN::Banner::WhiteRabbit.get.uncolorize.lines.map { |line| line.sub('R.I.P. Houdini', '').sub('pwn', '').rstrip }
1279
+ rows.shift while rows.first&.strip == ''
1280
+ rows.pop while rows.last&.strip == ''
1281
+ left = rows.reject { |row| row.strip.empty? }.map { |row| row.index(/\S/) }.min
1282
+ rows.map { |row| row[left..].to_s.freeze }.freeze
1283
+ end
1284
+ end
1285
+
1286
+ def rabbit_cells(size)
1287
+ size = [size.to_i, 0].max
1288
+ return [] if size.zero?
1289
+ return @rabbit_cells if @rabbit_size == size
1290
+
1291
+ @rabbit_size = size
1292
+ rows = rabbit_source
1293
+ columns = rows.map(&:length).max
1294
+ if size >= [columns, rows.length].max
1295
+ left = (size - columns) / 2
1296
+ top = (size - rows.length) / 2
1297
+ return @rabbit_cells = Array.new(size) do |y|
1298
+ Array.new(size) do |x|
1299
+ glyph = y.between?(top, top + rows.length - 1) && x.between?(left, left + columns - 1) ? rows[y - top][x - left] : nil
1300
+ { glyph: glyph || ' ', foreground: :white, background: :black }.freeze
1301
+ end.freeze
1302
+ end.freeze
1303
+ end
1304
+
1305
+ source_width = columns * 3
1306
+ source = rows.flat_map do |row|
1307
+ 6.times.map do |y|
1308
+ row.ljust(columns).chars.reduce(0) { |bits, char| (bits << 3) | RABBIT_STROKES.fetch(char)[y] }
1309
+ end
1310
+ end
1311
+ # Square Braille dots contain the same physical aspect as the ASCII
1312
+ # source. Fractional area sampling keeps thin punctuation on shrink.
1313
+ scale = [size * 2.0 / source_width, size * 4.0 / source.length].min
1314
+ width = [(source_width * scale).round, 1].max
1315
+ height = [(source.length * scale).round, 1].max
1316
+ left = ((size * 2) - width) / 2
1317
+ top = ((size * 4) - height) / 2
1318
+ threshold = size < 5 ? 0.02 : 0.12
1319
+ @rabbit_cells = Array.new(size) do |y|
1320
+ Array.new(size) do |x|
1321
+ mask = RABBIT_DOTS.sum do |dx, dy, bit|
1322
+ px = (x * 2) + dx - left
1323
+ py = (y * 4) + dy - top
1324
+ next 0 unless px.between?(0, width - 1) && py.between?(0, height - 1)
1325
+
1326
+ coverage = rabbit_coverage(source, source_width, [px.fdiv(width), py.fdiv(height), (px + 1).fdiv(width), (py + 1).fdiv(height)])
1327
+ coverage >= threshold ? bit : 0
1328
+ end
1329
+ { glyph: mask.zero? ? ' ' : (0x2800 + mask).chr(Encoding::UTF_8), foreground: :white, background: :black }.freeze
1330
+ end.freeze
1331
+ end.freeze
1332
+ end
1333
+
1334
+ # Fractional box-area coverage avoids nearest-neighbour holes.
1335
+ def rabbit_coverage(source, source_width, bounds)
1336
+ left, top, right, bottom = bounds
1337
+ left *= source_width
1338
+ right *= source_width
1339
+ top *= source.length
1340
+ bottom *= source.length
1341
+ ink = (top.floor...bottom.ceil).sum do |y|
1342
+ overlap_y = [y + 1, bottom].min - [y, top].max
1343
+ (left.floor...right.ceil).sum do |x|
1344
+ next 0 unless source[y][source_width - 1 - x] == 1
1345
+
1346
+ ([x + 1, right].min - [x, left].max) * overlap_y
1347
+ end
1348
+ end
1349
+ ink.fdiv((right - left) * (bottom - top))
1350
+ end
1351
+
1252
1352
  def automatic_banner(size, now)
1253
1353
  key = [@banner_session, @banner_name, size, @banner_seed]
1254
1354
  if @banner_demo_key != key
@@ -1296,7 +1396,7 @@ module PWN
1296
1396
  row.first(size).each_with_index do |cell, x|
1297
1397
  index = PALETTE.index(cell[:foreground].to_s)
1298
1398
  background = PALETTE.index(cell[:background].to_s)
1299
- colors = @banner_colors && (@banner_name != :falling_blocks || @banner_two_colors)
1399
+ colors = @banner_colors && (!@banner_activated || @banner_name != :falling_blocks || @banner_two_colors)
1300
1400
  pair = if colors && index && background && (background == 7 || @banner_two_colors)
1301
1401
  background == 7 ? 9 + index : 17 + (background * 8) + index
1302
1402
  end
data/lib/pwn/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PWN
4
- VERSION = '0.5.762'
4
+ VERSION = '0.5.763'
5
5
  end
@@ -515,6 +515,7 @@ describe 'pwn-ai curses launch' do # rubocop:disable Metrics/BlockLength -- publ
515
515
  pry = Pry.new
516
516
  pry.config.pwn_ai_session_id = 'banner-one'
517
517
  console = PWN::Plugins::REPL::AIConsole::Console.new(pry: pry, input: StringIO.new, curses: nil, getch: nil)
518
+ console.instance_variable_set(:@banner_activated, true)
518
519
  choices = PWN::Banner.mini_names.first(2)
519
520
  allow(PWN::Banner).to receive(:mini_names).and_return(choices)
520
521
  allow(choices).to receive(:sample).and_return(*choices)
@@ -536,6 +537,7 @@ describe 'pwn-ai curses launch' do # rubocop:disable Metrics/BlockLength -- publ
536
537
 
537
538
  it 'centers capped retro artwork inside a larger cell-square canvas' do
538
539
  console = PWN::Plugins::REPL::AIConsole::Console.new(pry: Pry.new, input: StringIO.new, curses: nil, getch: nil)
540
+ console.instance_variable_set(:@banner_activated, true)
539
541
  allow(PWN::Banner).to receive(:mini_names).and_return([:pacman])
540
542
  allow(Process).to receive(:clock_gettime).with(Process::CLOCK_MONOTONIC).and_return(100.0)
541
543
  size = 24
@@ -551,6 +553,7 @@ describe 'pwn-ai curses launch' do # rubocop:disable Metrics/BlockLength -- publ
551
553
 
552
554
  it 'paints falling blocks against the actual pane floor even beyond sixteen cells' do
553
555
  console = PWN::Plugins::REPL::AIConsole::Console.new(pry: Pry.new, input: StringIO.new, curses: nil, getch: nil)
556
+ console.instance_variable_set(:@banner_activated, true)
554
557
  allow(PWN::Banner).to receive(:mini_names).and_return([:falling_blocks])
555
558
  now = 100.0
556
559
  allow(Process).to receive(:clock_gettime).with(Process::CLOCK_MONOTONIC) { now }
@@ -594,7 +597,8 @@ describe 'pwn-ai curses launch' do # rubocop:disable Metrics/BlockLength -- publ
594
597
  expect(painted.select { |y, x, _text, _color| y < 10 && x < 10 }.map { |_, _, text| text }.join).not_to match(/[PWN]/)
595
598
  expect(painted.select { |y, x, text, _color| y.between?(1, 8) && x == 1 && text == ' ' * 8 }.length).to eq(8)
596
599
  expect(painted.select { |y, _x, _text, _color| y < 10 }).to all(satisfy { |y, x, text, _color| y >= 0 && x >= 0 && x + console.width(text) <= 120 })
597
- expect(PWN::Banner).to have_received(:mini_cells).with(hash_including(width: 8, height: 8)).once
600
+ expect(PWN::Banner).not_to have_received(:mini_cells)
601
+ expect(console.banner_frame(8, cells: true).flatten.map { |cell| cell[:foreground] }.uniq).to eq([:white])
598
602
  [[80, 36], [120, 24]].each do |columns, rows|
599
603
  allow(curses).to receive_messages(cols: columns, lines: rows)
600
604
  rectangles.clear
@@ -607,7 +611,7 @@ describe 'pwn-ai curses launch' do # rubocop:disable Metrics/BlockLength -- publ
607
611
  console.draw
608
612
  expect(console.instance_variable_get(:@header_text_column)).to eq(2)
609
613
  expect(console.header_lines('ollama', 'fixture', 115).length).to be > 16
610
- expect(PWN::Banner).to have_received(:mini_cells).once
614
+ expect(PWN::Banner).not_to have_received(:mini_cells)
611
615
  expect([console.instance_variable_get(:@editor).text, console.instance_variable_get(:@editor).cursor]).to eq(['retained draft', 4])
612
616
  end
613
617
 
@@ -641,6 +645,7 @@ describe 'pwn-ai curses launch' do # rubocop:disable Metrics/BlockLength -- publ
641
645
  console = PWN::Plugins::REPL::AIConsole::Console.new(pry: Pry.new, input: StringIO.new, curses: nil, getch: nil)
642
646
  console.instance_variable_set(:@width, 120)
643
647
  console.instance_variable_set(:@height, 40)
648
+ console.instance_variable_set(:@banner_activated, true)
644
649
  allow(console).to receive(:put)
645
650
  allow(console).to receive(:box)
646
651
  allow(PWN::Banner).to receive(:mini_cells).and_call_original
@@ -710,6 +715,7 @@ describe 'pwn-ai curses launch' do # rubocop:disable Metrics/BlockLength -- publ
710
715
  pry = Pry.new
711
716
  pry.config.pwn_ai_session_id = 'colored-session'
712
717
  console = PWN::Plugins::REPL::AIConsole::Console.new(pry: pry, input: StringIO.new, curses: nil, getch: nil)
718
+ console.instance_variable_set(:@banner_activated, true)
713
719
  names = [:snake]
714
720
  allow(PWN::Banner).to receive(:mini_names).and_return(names)
715
721
  expect(names).to receive(:sample).twice.and_return(:snake)
@@ -734,6 +740,7 @@ describe 'pwn-ai curses launch' do # rubocop:disable Metrics/BlockLength -- publ
734
740
  pry = Pry.new
735
741
  pry.config.pwn_ai_session_id = 'fast-colored-session'
736
742
  console = PWN::Plugins::REPL::AIConsole::Console.new(pry: pry, input: StringIO.new, curses: nil, getch: nil)
743
+ console.instance_variable_set(:@banner_activated, true)
737
744
  names = PWN::Banner.mini_names
738
745
  allow(PWN::Banner).to receive(:mini_names).and_return(names)
739
746
  expect(names).to receive(:sample).once.and_return(name)
@@ -1120,6 +1127,144 @@ describe 'pwn-ai curses launch' do # rubocop:disable Metrics/BlockLength -- publ
1120
1127
  expect(console.instance_variable_get(:@focus)).to eq(:mission)
1121
1128
  end
1122
1129
 
1130
+ it 'shows only the white rabbit before the first animation focus without computing games' do
1131
+ console = PWN::Plugins::REPL::AIConsole::Console.new(pry: Pry.new, input: StringIO.new, curses: nil, getch: nil)
1132
+ expect(PWN::Banner).not_to receive(:mini_cells)
1133
+ expect(PWN::Banner).not_to receive(:mini_frame)
1134
+ expect(PWN::Banner::MiniGame).not_to receive(:new)
1135
+ first = console.banner_frame(8, cells: true)
1136
+ expect(first.flatten.map { |cell| cell[:foreground] }.uniq).to eq([:white])
1137
+ expect(first.flatten.count { |cell| cell[:glyph] != ' ' }).to be > 16
1138
+ console.instance_variable_set(:@header_pane_height, 10)
1139
+ console.handle("\u0014")
1140
+ expect(console.banner_frame(8, cells: true)).to eq(first)
1141
+ expect(console.banner_frame(8).join).not_to match(/[PWN]/)
1142
+ end
1143
+
1144
+ it 'uses fine two by four text dots without terminal image output' do
1145
+ console = PWN::Plugins::REPL::AIConsole::Console.new(pry: Pry.new, input: StringIO.new, curses: nil, getch: nil)
1146
+ [8, 12, 16, 18].each do |size|
1147
+ glyphs = console.banner_frame(size, cells: true).flatten.map { |cell| cell[:glyph] }
1148
+ expect(glyphs).to all(match(/\A[ \u2801-\u28ff]\z/))
1149
+ expect(glyphs.uniq.length).to be > 4
1150
+ end
1151
+ source = File.read(File.expand_path('../../../../../lib/pwn/plugins/repl/ai_console.rb', __dir__))
1152
+ expect(source).not_to include('graphics_response?', '\\e_G')
1153
+ end
1154
+
1155
+ it 'area-filters fractional source pixels instead of losing a bend to point sampling' do
1156
+ console = PWN::Plugins::REPL::AIConsole::Console.new(pry: Pry.new, input: StringIO.new, curses: nil, getch: nil)
1157
+ expect(console.rabbit_coverage([0b10, 0b01], 2, [0, 0, 1, 1])).to eq(0.5)
1158
+ expect(console.rabbit_coverage([0b10, 0b01], 2, [0.25, 0, 0.75, 0.5])).to eq(0.5)
1159
+ expect(console.rabbit_coverage([0b10, 0b01], 2, [0, 0, 0.5, 0.5])).to eq(1.0)
1160
+ expect(console.rabbit_coverage([0b10, 0b01], 2, [0.5, 0, 1, 0.5])).to eq(0.0)
1161
+ end
1162
+
1163
+ it 'derives every source row from WhiteRabbit.get and preserves the native art without labels' do
1164
+ console = PWN::Plugins::REPL::AIConsole::Console.new(pry: Pry.new, input: StringIO.new, curses: nil, getch: nil)
1165
+ source = PWN::Banner::WhiteRabbit.get.uncolorize
1166
+ expect(PWN::Banner::WhiteRabbit).to receive(:get).once.and_return(source)
1167
+ rows = source.lines.map { |row| row.sub('R.I.P. Houdini', '').sub('pwn', '').rstrip }.reject(&:empty?).map { |row| row[8..] }
1168
+ expect(console.rabbit_source).to eq(rows)
1169
+ expect(rows.join.chars.uniq - PWN::Plugins::REPL::AIConsole::Console::RABBIT_STROKES.keys).to be_empty
1170
+ [22, 34, 64].each do |size|
1171
+ art = console.banner_frame(size)
1172
+ top = (size - rows.length) / 2
1173
+ left = (size - rows.map(&:length).max) / 2
1174
+ rows.each_with_index { |row, y| expect(art[top + y][left, row.length]).to eq(row) }
1175
+ expect(art.join).not_to match(/pwn|Houdini|\e/)
1176
+ end
1177
+ end
1178
+
1179
+ it 'contains the rabbit with centered aspect-correct padding across tiny resizes' do
1180
+ console = PWN::Plugins::REPL::AIConsole::Console.new(pry: Pry.new, input: StringIO.new, curses: nil, getch: nil)
1181
+ expect(PWN::Banner::MiniGame).not_to receive(:new)
1182
+ expect(console.banner_frame(0, cells: true)).to eq([])
1183
+ [1, 2, 3, 5, 8, 12, 13, 14, 16, 18, 8].each do |size|
1184
+ cells = console.banner_frame(size, cells: true)
1185
+ expect(cells.length).to eq(size)
1186
+ expect(cells.map(&:length)).to all(eq(size))
1187
+ expect(cells.flatten.map { |cell| [cell[:foreground], cell[:background]] }.uniq).to eq([%i[white black]])
1188
+ points = []
1189
+ cells.each_with_index do |row, y|
1190
+ row.each_with_index do |cell, x|
1191
+ next if cell[:glyph] == ' '
1192
+
1193
+ mask = cell[:glyph] == ' ' ? 0 : cell[:glyph].ord - 0x2800
1194
+ expect(mask).not_to be_nil
1195
+ [[0, 0, 1], [0, 1, 2], [0, 2, 4], [0, 3, 64], [1, 0, 8], [1, 1, 16], [1, 2, 32], [1, 3, 128]].each do |dx, dy, bit|
1196
+ points << [(x * 2) + dx, (y * 4) + dy] if mask.anybits?(bit)
1197
+ end
1198
+ end
1199
+ end
1200
+ expect(points).not_to be_empty
1201
+ next if size < 8
1202
+
1203
+ xs, ys = points.transpose
1204
+ ratio = console.rabbit_source.map(&:length).max.fdiv(console.rabbit_source.length * 2)
1205
+ expect((xs.max - xs.min + 1).fdiv(ys.max - ys.min + 1)).to be_within(0.1).of(ratio)
1206
+ expect((xs.min - ((size * 2) - 1 - xs.max)).abs).to be <= 2
1207
+ expect((ys.min - ((size * 4) - 1 - ys.max)).abs).to be <= 2
1208
+ expect(points.length).to be > size * 3
1209
+ expect(cells.flatten.map { |cell| cell[:glyph] }.uniq.length).to be > 4
1210
+ expect(console.banner_frame(size, cells: true)).to equal(cells)
1211
+ end
1212
+ end
1213
+
1214
+ it 'paints only white with a 64-pair palette and keeps rabbit geometry in monochrome' do
1215
+ [false, true].each do |no_color|
1216
+ curses = double(has_colors?: true, start_color: nil, use_default_colors: nil, init_pair: nil, color_pairs: 64)
1217
+ console = PWN::Plugins::REPL::AIConsole::Console.new(pry: Pry.new, input: StringIO.new, curses: curses, getch: nil)
1218
+ allow(ENV).to receive(:key?).with('NO_COLOR').and_return(no_color)
1219
+ allow(PWN::Banner).to receive(:mini_names).and_return([:falling_blocks])
1220
+ console.setup_colors
1221
+ painted = []
1222
+ allow(console).to receive(:put) { |*args| painted << args }
1223
+ console.draw_banner(8)
1224
+ ink = painted.reject { |_, _, glyph, _| glyph.strip.empty? }
1225
+ expect(ink).not_to be_empty
1226
+ white = 9 + PWN::Plugins::REPL::AIConsole::Console::PALETTE.index('white')
1227
+ expect(ink.map(&:last).uniq).to eq([no_color ? nil : white])
1228
+ expect(ink.map { |_, _, glyph, _| glyph }).to all(match(/[\u2801-\u28ff]/))
1229
+ end
1230
+ end
1231
+
1232
+ it 'activates once on actual focus, cycles games and never restores the splash on return' do
1233
+ console = PWN::Plugins::REPL::AIConsole::Console.new(pry: Pry.new, input: StringIO.new, curses: nil, getch: nil)
1234
+ allow(PWN::Banner).to receive(:mini_names).and_return(%i[snake pacman])
1235
+ allow(Process).to receive(:clock_gettime).and_return(10.0)
1236
+ splash = console.banner_frame(8, cells: true)
1237
+ console.instance_variable_set(:@header_pane_height, 10)
1238
+ 2.times { console.handle("\u0014") }
1239
+ console.banner_frame(8, cells: true)
1240
+ game = console.instance_variable_get(:@banner_game)
1241
+ expect(game).to be_a(PWN::Banner::MiniGame)
1242
+ console.handle("\e")
1243
+ expect(console.banner_frame(8, cells: true)).not_to eq(splash)
1244
+ 2.times { console.handle("\u0018") }
1245
+ console.banner_frame(8, cells: true)
1246
+ expect(console.instance_variable_get(:@banner_game)).to equal(game)
1247
+ name = console.instance_variable_get(:@banner_name)
1248
+ console.handle("\u0007")
1249
+ console.banner_frame(8, cells: true)
1250
+ expect(console.instance_variable_get(:@banner_name)).not_to eq(name)
1251
+ expect(console.instance_variable_get(:@banner_game)).not_to equal(game)
1252
+ console.handle("\e")
1253
+ expect(console.banner_frame(12, cells: true)).not_to eq(console.rabbit_cells(12))
1254
+ end
1255
+
1256
+ it 'does not activate games when the pane is hidden or focus keys belong to a modal' do
1257
+ console = PWN::Plugins::REPL::AIConsole::Console.new(pry: Pry.new, input: StringIO.new, curses: nil, getch: nil)
1258
+ expect(PWN::Banner::MiniGame).not_to receive(:new)
1259
+ 4.times { console.handle("\u0014") }
1260
+ expect(console.instance_variable_get(:@banner_activated)).to be_nil
1261
+ console.instance_variable_set(:@header_pane_height, 10)
1262
+ console.instance_variable_set(:@details, 0)
1263
+ expect(console).to receive(:handle_details).with("\u0018")
1264
+ console.handle("\u0018")
1265
+ expect(console.banner_frame(8, cells: true)).to eq(console.rabbit_cells(8))
1266
+ end
1267
+
1123
1268
  it 'cycles mission, session and visible animation with both shortcuts without changing the draft' do
1124
1269
  console = PWN::Plugins::REPL::AIConsole::Console.new(pry: Pry.new, input: StringIO.new, curses: nil, getch: nil)
1125
1270
  editor = console.instance_variable_get(:@editor)
@@ -1249,6 +1394,7 @@ describe 'pwn-ai curses launch' do # rubocop:disable Metrics/BlockLength -- publ
1249
1394
  it 'returns to the unchanged automatic animation when focus leaves the game' do
1250
1395
  console = PWN::Plugins::REPL::AIConsole::Console.new(pry: Pry.new, input: StringIO.new, curses: nil, getch: nil)
1251
1396
  allow(Process).to receive(:clock_gettime).and_return(10.0)
1397
+ console.instance_variable_set(:@banner_activated, true)
1252
1398
  automatic = console.banner_frame(8, cells: true)
1253
1399
  console.instance_variable_set(:@header_pane_height, 10)
1254
1400
  2.times { console.handle("\u0014") }
@@ -1263,6 +1409,7 @@ describe 'pwn-ai curses launch' do # rubocop:disable Metrics/BlockLength -- publ
1263
1409
  it 'streams the new arcade demos on the UI owner without a synchronous full replay build' do
1264
1410
  %i[pacman galaga frogger].each do |name|
1265
1411
  console = PWN::Plugins::REPL::AIConsole::Console.new(pry: Pry.new, input: StringIO.new, curses: nil, getch: nil)
1412
+ console.instance_variable_set(:@banner_activated, true)
1266
1413
  allow(PWN::Banner).to receive(:mini_names).and_return([name])
1267
1414
  allow(Process).to receive(:clock_gettime).and_return(10.0)
1268
1415
  expect(PWN::Banner).not_to receive(:mini_cells)
@@ -3405,6 +3405,9 @@
3405
3405
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.prepare_swarm_mission Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.prepare_swarm_mission`: "}]}
3406
3406
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.put Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.put`: "}]}
3407
3407
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.put_spans Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.put_spans`: "}]}
3408
+ {"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.rabbit_cells Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.rabbit_cells`: "}]}
3409
+ {"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.rabbit_coverage Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.rabbit_coverage`: "}]}
3410
+ {"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.rabbit_source Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.rabbit_source`: "}]}
3408
3411
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.refresh_completion Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.refresh_completion`: "}]}
3409
3412
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.refresh_swarm Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.refresh_swarm`: "}]}
3410
3413
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.restore_streams Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.restore_streams`: "}]}
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pwn
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.762
4
+ version: 0.5.763
5
5
  platform: ruby
6
6
  authors:
7
7
  - 0day Inc.
@@ -897,14 +897,14 @@ dependencies:
897
897
  requirements:
898
898
  - - '='
899
899
  - !ruby/object:Gem::Version
900
- version: 7.1.6
900
+ version: 7.1.7
901
901
  type: :development
902
902
  prerelease: false
903
903
  version_requirements: !ruby/object:Gem::Requirement
904
904
  requirements:
905
905
  - - '='
906
906
  - !ruby/object:Gem::Version
907
- version: 7.1.6
907
+ version: 7.1.7
908
908
  - !ruby/object:Gem::Dependency
909
909
  name: rqrcode
910
910
  requirement: !ruby/object:Gem::Requirement