badline 0.11.1 → 0.12.1

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 (175) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -0
  3. data/CLAUDE.md +8 -7
  4. data/CONTRIBUTING.md +7 -6
  5. data/README.md +12 -5
  6. data/doc/media.md +28 -1
  7. data/doc/pinned-behaviour.md +31 -1
  8. data/exe/badline-ruby +1 -1
  9. data/lib/badline/address_bus/saved_state.rb +4 -10
  10. data/lib/badline/address_bus.rb +8 -27
  11. data/lib/badline/audio/bare_player.rb +1 -1
  12. data/lib/badline/c128/bus.rb +17 -52
  13. data/lib/badline/c128/bus_state.rb +5 -5
  14. data/lib/badline/c128/drives.rb +35 -22
  15. data/lib/badline/c128/fast_serial.rb +1 -0
  16. data/lib/badline/c128/io_pages.rb +35 -0
  17. data/lib/badline/c128/mmu.rb +9 -7
  18. data/lib/badline/c128/modes.rb +17 -4
  19. data/lib/badline/c128/saved_state.rb +22 -0
  20. data/lib/badline/c128/z80.rb +49 -0
  21. data/lib/badline/c128/z80_pages.rb +118 -0
  22. data/lib/badline/c128/z80_turns.rb +106 -0
  23. data/lib/badline/c128.rb +21 -83
  24. data/lib/badline/cartridge/action_replay.rb +2 -2
  25. data/lib/badline/cartridge/comal80.rb +1 -7
  26. data/lib/badline/cartridge/dinamic.rb +0 -8
  27. data/lib/badline/cartridge/final_cartridge3.rb +6 -6
  28. data/lib/badline/cartridge/fun_play.rb +0 -8
  29. data/lib/badline/cartridge/game_system.rb +0 -8
  30. data/lib/badline/cartridge/kcs_power.rb +10 -7
  31. data/lib/badline/cartridge/mach5.rb +2 -4
  32. data/lib/badline/cartridge/magic_desk.rb +0 -8
  33. data/lib/badline/cartridge/ocean.rb +0 -8
  34. data/lib/badline/cartridge/pagefox.rb +2 -2
  35. data/lib/badline/cartridge/retro_replay.rb +7 -6
  36. data/lib/badline/cartridge/rex_utility.rb +2 -6
  37. data/lib/badline/cartridge/rgcd.rb +2 -2
  38. data/lib/badline/cartridge/simons_basic.rb +0 -5
  39. data/lib/badline/cartridge/super_games.rb +4 -2
  40. data/lib/badline/cartridge/westermann.rb +2 -6
  41. data/lib/badline/cartridge.rb +37 -9
  42. data/lib/badline/cia/fast_forward.rb +21 -3
  43. data/lib/badline/cia.rb +8 -5
  44. data/lib/badline/computer/attachments.rb +14 -95
  45. data/lib/badline/computer/common.rb +34 -0
  46. data/lib/badline/computer/kernal_traps.rb +65 -5
  47. data/lib/badline/computer/true_drives.rb +69 -0
  48. data/lib/badline/computer.rb +9 -68
  49. data/lib/badline/core.rb +4 -2
  50. data/lib/badline/cpu_port.rb +112 -0
  51. data/lib/badline/drive/core.rb +8 -33
  52. data/lib/badline/drive/fast_serial.rb +73 -0
  53. data/lib/badline/drive/idle.rb +1 -11
  54. data/lib/badline/drive/orbit.rb +2 -5
  55. data/lib/badline/drive/sleep.rb +0 -2
  56. data/lib/badline/drive/vias.rb +84 -0
  57. data/lib/badline/drive.rb +2 -0
  58. data/lib/badline/drive1541/disk.rb +14 -17
  59. data/lib/badline/drive1541/disk_state.rb +7 -15
  60. data/lib/badline/drive1541.rb +1 -0
  61. data/lib/badline/drive1571/wd1770.rb +94 -8
  62. data/lib/badline/drive1571.rb +8 -58
  63. data/lib/badline/drive1581/bus.rb +137 -0
  64. data/lib/badline/drive1581/cpu.rb +11 -0
  65. data/lib/badline/drive1581/disk.rb +135 -0
  66. data/lib/badline/drive1581/disk_state.rb +87 -0
  67. data/lib/badline/drive1581/mechanism.rb +117 -0
  68. data/lib/badline/drive1581/serial_port.rb +52 -0
  69. data/lib/badline/drive1581/slot.rb +78 -0
  70. data/lib/badline/drive1581/track.rb +100 -0
  71. data/lib/badline/drive1581/track_bytes.rb +75 -0
  72. data/lib/badline/drive1581/wd1772/registers.rb +89 -0
  73. data/lib/badline/drive1581/wd1772/sector_writes.rb +73 -0
  74. data/lib/badline/drive1581/wd1772/sectors.rb +150 -0
  75. data/lib/badline/drive1581/wd1772/state.rb +66 -0
  76. data/lib/badline/drive1581/wd1772/stepping.rb +109 -0
  77. data/lib/badline/drive1581/wd1772/tracks.rb +125 -0
  78. data/lib/badline/drive1581/wd1772/turn.rb +73 -0
  79. data/lib/badline/drive1581/wd1772.rb +216 -0
  80. data/lib/badline/drive1581.rb +233 -0
  81. data/lib/badline/frontend/app.rb +1 -3
  82. data/lib/badline/frontend/audio_device.rb +67 -0
  83. data/lib/badline/frontend/audio_sink.rb +16 -50
  84. data/lib/badline/frontend/boot.rb +24 -67
  85. data/lib/badline/frontend/buttons.rb +7 -7
  86. data/lib/badline/frontend/keys.rb +33 -8
  87. data/lib/badline/frontend/media_slots.rb +42 -0
  88. data/lib/badline/frontend/{confirmation.rb → menu/confirmation.rb} +4 -4
  89. data/lib/badline/frontend/{file_browser.rb → menu/file_browser.rb} +18 -34
  90. data/lib/badline/frontend/{menu_dialogs.rb → menu/menu_dialogs.rb} +3 -7
  91. data/lib/badline/frontend/{menu_media.rb → menu/menu_media.rb} +26 -51
  92. data/lib/badline/frontend/{menu_pages.rb → menu/menu_pages.rb} +5 -8
  93. data/lib/badline/frontend/{name_field.rb → menu/name_field.rb} +11 -24
  94. data/lib/badline/frontend/{pause_menu.rb → menu/pause_menu.rb} +13 -28
  95. data/lib/badline/frontend/menu/theme.rb +18 -0
  96. data/lib/badline/frontend/{filter_view.rb → player/filter_view.rb} +4 -4
  97. data/lib/badline/frontend/{info_view.rb → player/info_view.rb} +3 -3
  98. data/lib/badline/frontend/{player_window.rb → player/player_controller.rb} +17 -25
  99. data/lib/badline/frontend/{player_footer.rb → player/player_footer.rb} +8 -8
  100. data/lib/badline/frontend/{player_header.rb → player/player_header.rb} +5 -5
  101. data/lib/badline/frontend/{player_screen.rb → player/player_screen.rb} +4 -5
  102. data/lib/badline/frontend/{scope.rb → player/scope.rb} +1 -1
  103. data/lib/badline/frontend/{sid_view.rb → player/sid_view.rb} +16 -20
  104. data/lib/badline/frontend/player/theme.rb +18 -0
  105. data/lib/badline/frontend/{visualizer_view.rb → player/visualizer_view.rb} +8 -8
  106. data/lib/badline/frontend/sound.rb +19 -38
  107. data/lib/badline/frontend/timeline.rb +3 -14
  108. data/lib/badline/frontend.rb +24 -20
  109. data/lib/badline/iec_bus.rb +14 -2
  110. data/lib/badline/kernal_trap/{file.rb → file_routine.rb} +1 -1
  111. data/lib/badline/kernal_trap/load.rb +1 -1
  112. data/lib/badline/kernal_trap/save.rb +1 -1
  113. data/lib/badline/kernal_trap.rb +1 -1
  114. data/lib/badline/machine/clocking.rb +50 -0
  115. data/lib/badline/machine.rb +63 -6
  116. data/lib/badline/media/boot_disk.rb +50 -0
  117. data/lib/badline/media/disk_list.rb +3 -3
  118. data/lib/badline/media/disk_set.rb +1 -1
  119. data/lib/badline/media/extensions.rb +33 -0
  120. data/lib/badline/media/not_disk.rb +8 -0
  121. data/lib/badline/media/program.rb +28 -0
  122. data/lib/badline/media/true_drive.rb +38 -17
  123. data/lib/badline/media/tune.rb +35 -0
  124. data/lib/badline/media/vic20_cartridge.rb +1 -7
  125. data/lib/badline/media/vic20_media.rb +38 -80
  126. data/lib/badline/media.rb +90 -81
  127. data/lib/badline/options/event.rb +4 -2
  128. data/lib/badline/options/family.rb +3 -9
  129. data/lib/badline/options/help.rb +2 -1
  130. data/lib/badline/roms/README +9 -0
  131. data/lib/badline/roms/dos1581.rom +0 -0
  132. data/lib/badline/sdl.rb +37 -20
  133. data/lib/badline/sid.rb +1 -1
  134. data/lib/badline/snapshot/machine_state.rb +9 -1
  135. data/lib/badline/snapshot/state.rb +6 -4
  136. data/lib/badline/snapshot.rb +11 -9
  137. data/lib/badline/storage/d64_image.rb +15 -7
  138. data/lib/badline/storage/disk_image/writing.rb +2 -2
  139. data/lib/badline/storage/disk_image.rb +1 -1
  140. data/lib/badline/storage/host_directory.rb +1 -1
  141. data/lib/badline/storage/p00.rb +8 -0
  142. data/lib/badline/storage/t64.rb +1 -1
  143. data/lib/badline/time_of_day.rb +3 -3
  144. data/lib/badline/version.rb +1 -1
  145. data/lib/badline/vic/bank.rb +1 -1
  146. data/lib/badline/vic/border_mask.rb +1 -1
  147. data/lib/badline/vic/collisions.rb +1 -1
  148. data/lib/badline/vic/color_patches.rb +1 -1
  149. data/lib/badline/vic/display_state.rb +1 -1
  150. data/lib/badline/vic/graphics_mode.rb +1 -1
  151. data/lib/badline/vic/graphics_shifter.rb +1 -1
  152. data/lib/badline/vic/register_log.rb +1 -1
  153. data/lib/badline/vic/registers.rb +1 -1
  154. data/lib/badline/vic/sequencer.rb +1 -1
  155. data/lib/badline/vic/sequencer_output.rb +1 -1
  156. data/lib/badline/vic/sequencer_state.rb +1 -1
  157. data/lib/badline/vic/sprite/internal_bus.rb +1 -1
  158. data/lib/badline/vic/sprite/saved_state.rb +1 -1
  159. data/lib/badline/vic/sprite/shifter.rb +1 -1
  160. data/lib/badline/vic/sprite/timing.rb +1 -1
  161. data/lib/badline/vic/sprite.rb +1 -1
  162. data/lib/badline/vic/sprites.rb +1 -1
  163. data/lib/badline/vic/sprites_state.rb +1 -1
  164. data/lib/badline/vic.rb +6 -5
  165. data/lib/badline/vic20/attachments.rb +15 -71
  166. data/lib/badline/vic20/saved_state.rb +2 -30
  167. data/lib/badline/vic20.rb +4 -37
  168. data/lib/badline/z80/core.rb +2 -0
  169. data/lib/badline/z80/saved_state.rb +69 -0
  170. metadata +61 -23
  171. data/lib/badline/cycleable.rb +0 -38
  172. /data/lib/badline/frontend/{menu_ports.rb → menu/menu_ports.rb} +0 -0
  173. /data/lib/badline/frontend/{player_commands.rb → player/player_commands.rb} +0 -0
  174. /data/lib/badline/frontend/{player_state.rb → player/player_state.rb} +0 -0
  175. /data/lib/badline/frontend/{sid_history.rb → player/sid_history.rb} +0 -0
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Frontend
5
+ # An SDL audio device on SDL's queue: signed 16-bit samples, `channels`
6
+ # of them interleaved per frame, go in at the device's rate and SDL
7
+ # plays them out behind us. When #open fails the device stays closed,
8
+ # with the reason in SDL_GetError for the caller to report.
9
+ class AudioDevice
10
+ attr_reader :rate
11
+
12
+ def initialize(channels:, rate:, samples:)
13
+ @channels = channels
14
+ @rate = rate
15
+ @samples = samples
16
+ @device = 0
17
+ @buffer = IO::Buffer.new(4096)
18
+ end
19
+
20
+ def open? = @device != 0
21
+
22
+ # With `exact_rate` false SDL may pick the device's own rate instead,
23
+ # which #rate then reports.
24
+ def open(exact_rate)
25
+ return unless SDL.SDL_InitSubSystem(SDL::INIT_AUDIO).zero?
26
+
27
+ SDL.spec_freq(SDL.wanted, @rate)
28
+ SDL.spec_format(SDL.wanted, SDL::AUDIO_S16LSB)
29
+ SDL.spec_channels(SDL.wanted, @channels)
30
+ SDL.spec_samples(SDL.wanted, @samples)
31
+ allowed_changes = exact_rate ? 0 : SDL::ALLOW_FREQUENCY_CHANGE
32
+ @device = SDL.SDL_OpenAudioDevice(nil, 0, SDL.wanted, SDL.obtained, allowed_changes)
33
+ @rate = SDL.read_i32(SDL.obtained) if open?
34
+ end
35
+
36
+ # Returns SDL_QueueAudio's result, negative on failure.
37
+ def queue(samples)
38
+ count = samples.size
39
+ bytes = count * 2
40
+ @buffer.resize(bytes) if bytes > @buffer.size
41
+ buffer = @buffer
42
+ i = 0
43
+ while i < count
44
+ buffer.set_value(:s16, i * 2, samples[i])
45
+ i += 1
46
+ end
47
+ SDL.SDL_QueueAudio(@device, buffer, bytes)
48
+ end
49
+
50
+ def queued_seconds = SDL.SDL_GetQueuedAudioSize(@device) / (2.0 * @channels * @rate)
51
+
52
+ def start = SDL.SDL_PauseAudioDevice(@device, 0)
53
+
54
+ def pause = SDL.SDL_PauseAudioDevice(@device, 1)
55
+
56
+ def clear = SDL.SDL_ClearQueuedAudio(@device)
57
+
58
+ def close
59
+ return unless open?
60
+
61
+ SDL.SDL_CloseAudioDevice(@device)
62
+ SDL.SDL_QuitSubSystem(SDL::INIT_AUDIO)
63
+ @device = 0
64
+ end
65
+ end
66
+ end
67
+ end
@@ -2,72 +2,38 @@
2
2
 
3
3
  module Badline
4
4
  module Frontend
5
- # The host's audio device for --headless and the SID player, fed through
6
- # SDL's queue: stereo signed 16-bit samples, the left and right
7
- # interleaved, go in at the device's rate and SDL plays them out behind
8
- # us. It answers what Audio::Playback asks of a sink.
5
+ # The host's audio device for --headless and the SID player: a stereo
6
+ # AudioDevice, the left and right samples interleaved, that raises on
7
+ # SDL's failures. It answers what Audio::Playback asks of a sink.
9
8
  class AudioSink
10
9
  class Error < Audio::Playback::DeviceError; end
11
10
 
12
- attr_reader :rate
13
-
14
11
  # With `exact_rate` false SDL may pick the device's own rate instead,
15
12
  # which #rate then reports.
16
13
  def initialize(rate:, exact_rate: false, buffer: 1024)
17
- @device = 0
18
- @rate = rate
19
- @buffer = IO::Buffer.new(4096)
20
- check(SDL.SDL_InitSubSystem(SDL::INIT_AUDIO))
21
- open_device(rate, exact_rate ? 0 : SDL::ALLOW_FREQUENCY_CHANGE, buffer)
22
- end
23
-
24
- def queue(samples)
25
- count = samples.size
26
- bytes = count * 2
27
- @buffer.resize(bytes) if bytes > @buffer.size
28
- buffer = @buffer
29
- i = 0
30
- while i < count
31
- buffer.set_value(:s16, i * 2, samples[i])
32
- i += 1
33
- end
34
- check(SDL.SDL_QueueAudio(@device, buffer, bytes))
14
+ @device = AudioDevice.new(channels: 2, rate:, samples: buffer)
15
+ @device.open(exact_rate)
16
+ raise Error, SDL.SDL_GetError unless @device.open?
35
17
  end
36
18
 
37
- def queued_seconds = SDL.SDL_GetQueuedAudioSize(@device) / (4.0 * @rate)
38
-
39
- def start = SDL.SDL_PauseAudioDevice(@device, 0)
40
-
41
- def pause = SDL.SDL_PauseAudioDevice(@device, 1)
42
-
43
- def clear = SDL.SDL_ClearQueuedAudio(@device)
19
+ def rate = @device.rate
44
20
 
45
- def close
46
- return if @device.zero?
21
+ def queue(samples)
22
+ result = @device.queue(samples)
23
+ raise Error, SDL.SDL_GetError if result.negative?
47
24
 
48
- SDL.SDL_CloseAudioDevice(@device)
49
- SDL.SDL_QuitSubSystem(SDL::INIT_AUDIO)
50
- @device = 0
25
+ result
51
26
  end
52
27
 
53
- private
28
+ def queued_seconds = @device.queued_seconds
54
29
 
55
- def open_device(rate, allowed_changes, buffer)
56
- SDL.spec_freq(SDL.wanted, rate)
57
- SDL.spec_format(SDL.wanted, SDL::AUDIO_S16LSB)
58
- SDL.spec_channels(SDL.wanted, 2)
59
- SDL.spec_samples(SDL.wanted, buffer)
60
- @device = SDL.SDL_OpenAudioDevice(nil, 0, SDL.wanted, SDL.obtained, allowed_changes)
61
- raise Error, SDL.SDL_GetError if @device.zero?
30
+ def start = @device.start
62
31
 
63
- @rate = SDL.read_i32(SDL.obtained)
64
- end
32
+ def pause = @device.pause
65
33
 
66
- def check(result)
67
- raise Error, SDL.SDL_GetError if result.negative?
34
+ def clear = @device.clear
68
35
 
69
- result
70
- end
36
+ def close = @device.close
71
37
  end
72
38
  end
73
39
  end
@@ -16,13 +16,10 @@ module Badline
16
16
  return 1
17
17
  end
18
18
 
19
- begin
20
- computer = boot(options)
21
- rescue Media::TrueDrive::Error, Storage::SIDFile::FormatError, Storage::T64::FormatError,
22
- Storage::TAP::FormatError, Storage::CRTFile::FormatError, Storage::G64Image::FormatError,
23
- Cartridge::UnsupportedTypeError, Snapshot::FormatError, Media::DiskList::Error,
24
- Datasette::Missing, Unsupported => e
25
- warn "#{options.program}: #{options.media_path}: #{e.message}"
19
+ computer = nil
20
+ problem = media_problem { computer = boot(options) }
21
+ unless problem.empty?
22
+ warn "#{options.program}: #{options.media_path}: #{problem}"
26
23
  return 1
27
24
  end
28
25
  timeline = Timeline.new(options)
@@ -30,6 +27,19 @@ module Badline
30
27
  timeline.failed? ? 1 : 0
31
28
  end
32
29
 
30
+ # Runs the block and returns the message of the error it raised over
31
+ # media that won't go in or start, or an empty string. Other errors,
32
+ # a bare ArgumentError among them, go through.
33
+ def self.media_problem
34
+ yield
35
+ ""
36
+ rescue Machine::Unsupported, Media::TrueDrive::Error, Media::DiskList::Error, Media::NotDisk, Datasette::Missing,
37
+ SystemCallError, Storage::SIDFile::FormatError, Storage::T64::FormatError, Storage::TAP::FormatError,
38
+ Storage::CRTFile::FormatError, Storage::G64Image::FormatError, Cartridge::UnsupportedTypeError,
39
+ Snapshot::FormatError => e
40
+ e.message
41
+ end
42
+
33
43
  # The machine a .vsf snapshot holds, built as the saved one was, or a
34
44
  # new one built from the options with the media attached.
35
45
  def self.boot(options)
@@ -43,11 +53,10 @@ module Badline
43
53
  start(options, media, options.writable?)
44
54
  end
45
55
 
46
- # A new machine built from the options, with `media`, if not nil,
47
- # attached and started as the command line does, its disk writable if
48
- # `writable`. The family and model give the machine, and a .sid tune
49
- # or --sid the SID. The VIC-20 raises Unsupported for a .sid
50
- # tune.
56
+ # A new machine built from the options (Machine.for_options), with
57
+ # `media`, if not nil, attached and started as the command line does,
58
+ # its disk writable if `writable`. The VIC-20 raises
59
+ # Machine::Unsupported for a .sid tune.
51
60
  def self.start(options, media, writable)
52
61
  computer = build(options, media)
53
62
  Media::TrueDrive.plug(computer) if options.true_drive?
@@ -58,62 +67,10 @@ module Badline
58
67
  computer
59
68
  end
60
69
 
70
+ # The machine the options name, built for `media`, which says the RAM
71
+ # expansion a VIC-20 gets for it.
61
72
  def self.build(options, media)
62
- case options.family
63
- when :vic20 then vic20(options, media)
64
- when :c128 then c128(options, media)
65
- else c64(options, media)
66
- end
67
- end
68
-
69
- def self.c64(options, media)
70
- sid_model = options.sid_model || Media.sid_model(media, otherwise: nil)
71
- Machine.build(:c64, model: options.model, sid_model:, reu: options.reu)
73
+ Machine.for_options(options, media) { |line| puts line }
72
74
  end
73
-
74
- # A C128 in C128 mode. With --c64, or media only C64 mode runs, a .sid
75
- # tune or a program that loads at the C64's BASIC start, C= is held
76
- # through the reset, so the C128 KERNAL starts C64 mode.
77
- def self.c128(options, media)
78
- sid_model = options.sid_model || Media.sid_model(media, otherwise: nil)
79
- machine = C128.new(model: options.model, sid_model:, mode: :c128)
80
- machine.hold_commodore_key if options.c64_mode? || c64_media?(media)
81
- machine
82
- end
83
-
84
- def self.c64_media?(media)
85
- return false if media.nil? || !File.file?(media)
86
-
87
- extension = File.extname(media).downcase
88
- return true if extension == ".sid"
89
- return false unless %w[.prg .p00].include?(extension)
90
-
91
- bytes = File.binread(media).bytes
92
- bytes = Storage::P00.data(bytes) if Storage::P00.wraps?(bytes)
93
- bytes.length >= 2 && [Media::BASIC_START, Media::BASIC_START - 1].include?(bytes[0] | (bytes[1] << 8))
94
- end
95
-
96
- # A VIC-20 with the RAM --ram names, or else the RAM `media` needs
97
- # (Media::Vic20Media.ram_for), which it says. Raises Unsupported for
98
- # media a VIC-20 doesn't take.
99
- def self.vic20(options, media)
100
- ram = options.ram
101
- unless media.nil?
102
- unsupported = Media::Vic20Media::UNSUPPORTED.include?(File.extname(media).downcase)
103
- raise Unsupported, "doesn't go in a VIC-20" if unsupported
104
-
105
- ram ||= vic20_ram(media)
106
- end
107
- Machine.build(:vic20, model: options.model, ram:)
108
- end
109
-
110
- def self.vic20_ram(media)
111
- ram = Media::Vic20Media.ram_for(media)
112
- puts "RAM expansion: #{ram}"
113
- ram
114
- end
115
-
116
- # Media the machine the options name doesn't take.
117
- class Unsupported < ArgumentError; end
118
75
  end
119
76
  end
@@ -2,17 +2,17 @@
2
2
 
3
3
  module Badline
4
4
  module Frontend
5
- # The SID player's buttons: each is drawn where it goes on every frame,
6
- # and remembers its place, so a click finds the action under it. The
7
- # one under the pointer lights up, and one that's on is drawn reversed.
8
- # They take the player's colours unless given others: the text, the text
9
- # under the pointer, the background and, if any, a fill behind each
10
- # button.
5
+ # The buttons of the SID player and the pause menu: each is drawn where
6
+ # it goes on every frame, and remembers its place, so a click finds the
7
+ # action under it. The one under the pointer lights up, and one that's
8
+ # on is drawn reversed. They take their colours from the caller: the
9
+ # text, the text under the pointer, the background and, if any, a fill
10
+ # behind each button.
11
11
  class Buttons
12
12
  PAD = 2
13
13
  HEIGHT = Painter::GLYPH + (PAD * 2)
14
14
 
15
- def initialize(painter, colors = [SIDView::TEXT, SIDView::BRIGHT, PlayerScreen::BACKGROUND])
15
+ def initialize(painter, colors)
16
16
  @painter = painter
17
17
  @text = colors[0]
18
18
  @bright = colors[1]
@@ -6,7 +6,27 @@ module Badline
6
6
  # keys, the C128's own keys and, in joystick mode, the joystick
7
7
  # directions.
8
8
  module Keys
9
+ RETURN = 40
10
+ ESCAPE = 41
11
+ BACKSPACE = 42
9
12
  TAB = 43
13
+ SPACE = 44
14
+ F8 = 65
15
+ F9 = 66
16
+ F10 = 67
17
+ F11 = 68
18
+ F12 = 69
19
+ HOME = 74
20
+ PAGE_UP = 75
21
+ END_KEY = 77
22
+ PAGE_DOWN = 78
23
+ RIGHT = 79
24
+ LEFT = 80
25
+ DOWN = 81
26
+ UP = 82
27
+
28
+ # The arrow keys, which move through the menus.
29
+ DIRECTIONS = { RIGHT => :right, LEFT => :left, DOWN => :down, UP => :up }.freeze
10
30
 
11
31
  LETTERS = %i[a b c d e f g h i j k l m n o p q r s t u v w x y z].freeze
12
32
  DIGITS = %i[1 2 3 4 5 6 7 8 9 0].freeze
@@ -36,14 +56,6 @@ module Badline
36
56
  63 => :forty_eighty, 72 => :forty_eighty, 78 => :run_stop
37
57
  }.freeze
38
58
 
39
- ESCAPE = 41
40
-
41
- F8 = 65
42
- F9 = 66
43
- F10 = 67
44
- F11 = 68
45
- F12 = 69
46
-
47
59
  ARROWS = { 44 => :fire, 228 => :fire, 79 => :right, 80 => :left, 81 => :down, 82 => :up }.freeze
48
60
  WASD = { 225 => :fire, 7 => :right, 4 => :left, 22 => :down, 26 => :up }.freeze
49
61
 
@@ -65,6 +77,19 @@ module Badline
65
77
  C128_OTHERS[scancode] || c64_key(scancode)
66
78
  end
67
79
 
80
+ # The character the key types on a US layout: a letter, a capital
81
+ # with `shift`, or a digit, and "" for any other key.
82
+ def self.character(scancode, shift)
83
+ if scancode.between?(4, 29)
84
+ letter = (97 + scancode - 4).chr
85
+ shift ? letter.upcase : letter
86
+ elsif scancode.between?(30, 39)
87
+ "1234567890"[scancode - 30]
88
+ else
89
+ ""
90
+ end
91
+ end
92
+
68
93
  def self.arrows(scancode) = ARROWS[scancode] || :none
69
94
 
70
95
  def self.wasd(scancode) = WASD[scancode] || :none
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Frontend
5
+ # The machine's media slots as the pause menu and the --at events
6
+ # both take media out of them: device 8, a true drive there or else
7
+ # the KERNAL traps, and the expansion port.
8
+ module MediaSlots
9
+ class << self
10
+ # Whether device 8 has a disk in.
11
+ def disk?(computer)
12
+ drive = Media::TrueDrive.drive(computer)
13
+ drive.nil? ? computer.mounted? : !drive.disk.nil?
14
+ end
15
+
16
+ # The expanded path of the disk in device 8, however it went in, or
17
+ # an empty one.
18
+ def disk_path(computer)
19
+ drive = Media::TrueDrive.drive(computer)
20
+ unless drive.nil?
21
+ disk = drive.disk
22
+ return disk.nil? ? "" : disk.path.to_s
23
+ end
24
+
25
+ path = computer.mounted_path
26
+ path.empty? ? path : File.expand_path(path)
27
+ end
28
+
29
+ def eject_disk(computer)
30
+ drive = Media::TrueDrive.drive(computer)
31
+ drive.nil? ? computer.unmount : drive.insert(nil)
32
+ end
33
+
34
+ # Takes the cartridge out with the power off.
35
+ def remove_cartridge(computer)
36
+ computer.address_bus.detach_cartridge
37
+ computer.power_cycle!
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -43,11 +43,11 @@ module Badline
43
43
 
44
44
  def draw(left, top)
45
45
  painter = @painter
46
- painter.text(left, top, TITLES.fetch(@kind), PauseMenu::BRIGHT)
47
- painter.text(left, top + 20, Painter.fit(File.basename(@path), 40), PauseMenu::TEXT) unless @path.empty?
46
+ painter.text(left, top, TITLES.fetch(@kind), MenuTheme::BRIGHT)
47
+ painter.text(left, top + 20, Painter.fit(File.basename(@path), 40), MenuTheme::TEXT) unless @path.empty?
48
48
  warning = WARNINGS.fetch(@kind)
49
- painter.text(left, top + 44, warning[0], PauseMenu::TEXT)
50
- painter.text(left, top + 56, warning[1], PauseMenu::TEXT) unless warning[1].empty?
49
+ painter.text(left, top + 44, warning[0], MenuTheme::TEXT)
50
+ painter.text(left, top + 56, warning[1], MenuTheme::TEXT) unless warning[1].empty?
51
51
  @buttons.row([left, top + 80, MenuPages::ROW_WIDTH], "CANCEL", :cancel)
52
52
  @buttons.row([left, top + 80 + Buttons::HEIGHT + 2, MenuPages::ROW_WIDTH], ANSWERS.fetch(@kind), :confirm)
53
53
  end
@@ -12,19 +12,6 @@ module Badline
12
12
  ROWS = 12
13
13
  COLUMNS = 40
14
14
 
15
- RETURN = 40
16
- ESCAPE = 41
17
- BACKSPACE = 42
18
- HOME = 74
19
- PAGE_UP = 75
20
- END_KEY = 77
21
- PAGE_DOWN = 78
22
- RIGHT = 79
23
- LEFT = 80
24
- DOWN = 81
25
- UP = 82
26
- DIGITS = "1234567890"
27
-
28
15
  attr_reader :directory
29
16
 
30
17
  def initialize(painter, buttons)
@@ -60,9 +47,9 @@ module Badline
60
47
  # Handles a key, and returns the path of a file picked, :cancel when
61
48
  # Esc leaves the browser, or nil.
62
49
  def key(scancode)
63
- return :cancel if scancode == ESCAPE
64
- return pick if [RETURN, RIGHT].include?(scancode)
65
- return up if [LEFT, BACKSPACE].include?(scancode)
50
+ return :cancel if scancode == Keys::ESCAPE
51
+ return pick if [Keys::RETURN, Keys::RIGHT].include?(scancode)
52
+ return up if [Keys::LEFT, Keys::BACKSPACE].include?(scancode)
66
53
 
67
54
  select(@selected + step(scancode)) unless step(scancode).zero?
68
55
  jump(scancode)
@@ -83,14 +70,14 @@ module Badline
83
70
  end
84
71
 
85
72
  def draw(left, top, width, title)
86
- @painter.text(left, top, title, PauseMenu::BRIGHT)
87
- @painter.text(left, top + ROW + 2, shown_directory, PauseMenu::DIM)
73
+ @painter.text(left, top, title, MenuTheme::BRIGHT)
74
+ @painter.text(left, top + ROW + 2, shown_directory, MenuTheme::DIM)
88
75
  @list_top = top + (ROW * 2) + 8
89
76
  @buttons.area([left, @list_top, width, ROW * ROWS], :browse)
90
77
  draw_rows(left, width)
91
78
  draw_bar(left + width - 2) if @names.size > ROWS
92
79
  @painter.text(left, @list_top + (ROW * ROWS) + 6, "RETURN: OPEN LEFT: BACK ESC: CANCEL",
93
- PauseMenu::DIM)
80
+ MenuTheme::DIM)
94
81
  end
95
82
 
96
83
  private
@@ -109,10 +96,10 @@ module Badline
109
96
  name = label(index)
110
97
  top = @list_top + (row * ROW)
111
98
  if index == @selected
112
- @painter.box(left - 2, top - 2, width - 4, ROW, PauseMenu::TEXT)
113
- @painter.text(left, top, name, PauseMenu::PANEL)
99
+ @painter.box(left - 2, top - 2, width - 4, ROW, MenuTheme::TEXT)
100
+ @painter.text(left, top, name, MenuTheme::PANEL)
114
101
  else
115
- @painter.text(left, top, name, index < @folders ? PauseMenu::BRIGHT : PauseMenu::TEXT)
102
+ @painter.text(left, top, name, index < @folders ? MenuTheme::BRIGHT : MenuTheme::TEXT)
116
103
  end
117
104
  end
118
105
  end
@@ -121,8 +108,8 @@ module Badline
121
108
  height = ROW * ROWS
122
109
  size = [(height * ROWS) / @names.size, 4].max
123
110
  offset = ((height - size) * @top) / [@names.size - ROWS, 1].max
124
- @painter.box(left, @list_top - 2, 2, height, PauseMenu::EDGE)
125
- @painter.box(left, @list_top - 2 + offset, 2, size, PauseMenu::TEXT)
111
+ @painter.box(left, @list_top - 2, 2, height, MenuTheme::EDGE)
112
+ @painter.box(left, @list_top - 2 + offset, 2, size, MenuTheme::TEXT)
126
113
  end
127
114
 
128
115
  def label(index)
@@ -133,12 +120,12 @@ module Badline
133
120
 
134
121
  def step(scancode)
135
122
  case scancode
136
- when UP then -1
137
- when DOWN then 1
138
- when PAGE_UP then -ROWS
139
- when PAGE_DOWN then ROWS
140
- when HOME then -@names.size
141
- when END_KEY then @names.size
123
+ when Keys::UP then -1
124
+ when Keys::DOWN then 1
125
+ when Keys::PAGE_UP then -ROWS
126
+ when Keys::PAGE_DOWN then ROWS
127
+ when Keys::HOME then -@names.size
128
+ when Keys::END_KEY then @names.size
142
129
  else 0
143
130
  end
144
131
  end
@@ -154,10 +141,7 @@ module Badline
154
141
  # Selects the next name after the one selected that starts with the
155
142
  # key's letter or digit.
156
143
  def jump(scancode)
157
- char = if scancode.between?(4, 29) then (97 + scancode - 4).chr
158
- elsif scancode.between?(30, 39) then DIGITS[scancode - 30]
159
- else ""
160
- end
144
+ char = Keys.character(scancode, false)
161
145
  return if char.empty? || @names.empty?
162
146
 
163
147
  @names.size.times do |offset|
@@ -14,10 +14,6 @@ module Badline
14
14
  TITLES = { disk: "INSERT A DISK", tape: "INSERT A TAPE", cartridge: "INSERT A CARTRIDGE",
15
15
  program: "QUICK OPEN", snapshot: "LOAD A SAVE" }.freeze
16
16
 
17
- ESCAPE = 41
18
- RETURN = 40
19
- SPACE = 44
20
-
21
17
  # The machine Quick open started or a save loaded.
22
18
  attr_reader :computer
23
19
 
@@ -78,10 +74,10 @@ module Badline
78
74
  if @name_field.open? && !@confirmation.open?
79
75
  return named(@name_field.key(scancode, SDL.event_mod(SDL.event).anybits?(SDL::KMOD_SHIFT)))
80
76
  end
81
- return answer(:cancel) if scancode == ESCAPE
82
- return answer(@buttons.focus) if [RETURN, SPACE].include?(scancode)
77
+ return answer(:cancel) if scancode == Keys::ESCAPE
78
+ return answer(@buttons.focus) if [Keys::RETURN, Keys::SPACE].include?(scancode)
83
79
 
84
- direction = PauseMenu::ARROWS[scancode]
80
+ direction = Keys::DIRECTIONS[scancode]
85
81
  @buttons.shift(direction == :up ? -1 : 1, 1) if %i[up down].include?(direction)
86
82
  nil
87
83
  end
@@ -25,40 +25,22 @@ module Badline
25
25
 
26
26
  def writable? = @writable
27
27
 
28
- # The expanded path of the disk in device 8, however it went in, or
29
- # an empty one.
30
- def disk_path
31
- drive = @computer.true_drive
32
- unless drive.nil?
33
- disk = drive.disk
34
- return disk.nil? ? "" : disk.path.to_s
35
- end
36
-
37
- path = @computer.mounted_path
38
- path.empty? ? path : File.expand_path(path)
39
- end
28
+ def disk_path = MediaSlots.disk_path(@computer)
40
29
 
41
- def inserted?
42
- drive = @computer.true_drive
43
- drive.nil? ? @computer.mounted? : !drive.disk.nil?
44
- end
30
+ def inserted? = MediaSlots.disk?(@computer)
45
31
 
46
32
  # Puts a disk in device 8 or a tape in the datasette, `kind` :disk or
47
33
  # :tape, and says whether it went in. A list puts its first disk in.
48
34
  def insert(kind, path)
49
- @problem = ""
50
- if kind == :disk
51
- disk = Media::DiskList.disk(path)
52
- Media.insert_disk(@computer, disk, read_only: !@writable)
53
- list(path)
54
- else
55
- @computer.datasette.insert(Storage::TAP.new(path))
35
+ succeeds? do
36
+ if kind == :disk
37
+ disk = Media::DiskList.disk(path)
38
+ Media.insert_disk(@computer, disk, read_only: !@writable)
39
+ list(path)
40
+ else
41
+ @computer.datasette.insert(Storage::TAP.new(path))
42
+ end
56
43
  end
57
- true
58
- rescue ArgumentError, SystemCallError, Storage::TAP::FormatError, Storage::T64::FormatError,
59
- Storage::G64Image::FormatError => e
60
- @problem = e.message
61
- false
62
44
  end
63
45
 
64
46
  # Does what the Drive page's :eject_disk, :previous_disk, :next_disk,
@@ -72,10 +54,7 @@ module Badline
72
54
  end
73
55
  end
74
56
 
75
- def eject_disk
76
- drive = @computer.true_drive
77
- drive.nil? ? @computer.unmount : drive.insert(nil)
78
- end
57
+ def eject_disk = MediaSlots.eject_disk(@computer)
79
58
 
80
59
  # Sets whether disks are writable, for the disk in the drive too,
81
60
  # which goes in again as the notch now says.
@@ -111,12 +90,7 @@ module Badline
111
90
  # Swaps the cartridge for the one at `path`, which power cycles the
112
91
  # machine, and says whether it went in.
113
92
  def attach_cartridge(path)
114
- @problem = ""
115
- Media.attach(@computer, path)
116
- true
117
- rescue ArgumentError, SystemCallError, Storage::CRTFile::FormatError, Cartridge::UnsupportedTypeError => e
118
- @problem = e.message
119
- false
93
+ succeeds? { Media.attach(@computer, path) }
120
94
  end
121
95
 
122
96
  # What a save of the machine is named after: the cartridge, the disk,
@@ -147,27 +121,28 @@ module Badline
147
121
  end
148
122
 
149
123
  # Takes the cartridge out with the power off.
150
- def remove_cartridge
151
- @computer.address_bus.detach_cartridge
152
- @computer.power_cycle!
153
- end
124
+ def remove_cartridge = MediaSlots.remove_cartridge(@computer)
154
125
 
155
126
  # A new machine started on `path`, as the command line would, or nil
156
127
  # when it won't start.
157
128
  def start(options, path)
158
- @problem = ""
159
- computer = Frontend.start(options, path, @writable)
160
- list(path)
161
- computer
162
- rescue ArgumentError, SystemCallError, Media::TrueDrive::Error, Storage::SIDFile::FormatError,
163
- Storage::T64::FormatError, Storage::TAP::FormatError, Storage::CRTFile::FormatError,
164
- Storage::G64Image::FormatError, Cartridge::UnsupportedTypeError => e
165
- @problem = e.message
166
- nil
129
+ computer = nil
130
+ started = succeeds? do
131
+ computer = Frontend.start(options, path, @writable)
132
+ list(path)
133
+ end
134
+ started ? computer : nil
167
135
  end
168
136
 
169
137
  private
170
138
 
139
+ # Runs the block, keeps the media error it raised as the problem, and
140
+ # says whether there was none.
141
+ def succeeds?(&)
142
+ @problem = Frontend.media_problem(&)
143
+ @problem.empty?
144
+ end
145
+
171
146
  # Keeps `path` as the list the set comes from, if it's a list.
172
147
  def list(path)
173
148
  return unless Media::DiskList.list?(path)