badline 0.17.0 → 0.18.0

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 (166) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +153 -0
  3. data/CLAUDE.md +11 -7
  4. data/CONTRIBUTING.md +7 -1
  5. data/README.md +169 -20
  6. data/doc/pinned-behaviour.md +128 -0
  7. data/doc/snapshot-format.md +253 -18
  8. data/doc/snapshots.md +57 -23
  9. data/exe/badline-ruby +5 -0
  10. data/lib/badline/audio/terminal.rb +34 -2
  11. data/lib/badline/c128/keyed_state.rb +2 -0
  12. data/lib/badline/c128.rb +1 -0
  13. data/lib/badline/c64/keyed_state.rb +2 -0
  14. data/lib/badline/c64.rb +1 -0
  15. data/lib/badline/cbm2/keyed_state.rb +4 -0
  16. data/lib/badline/cbm2/p500.rb +5 -3
  17. data/lib/badline/cbm2.rb +6 -3
  18. data/lib/badline/chips/cia/timer.rb +2 -2
  19. data/lib/badline/chips/cpu/instruction_set.rb +1 -1
  20. data/lib/badline/chips/m6809/arithmetic.rb +140 -0
  21. data/lib/badline/chips/m6809/bus_steps.rb +223 -0
  22. data/lib/badline/chips/m6809/core.rb +149 -0
  23. data/lib/badline/chips/m6809/indexed.rb +82 -0
  24. data/lib/badline/chips/m6809/interrupts.rb +159 -0
  25. data/lib/badline/chips/m6809/keyed_state.rb +147 -0
  26. data/lib/badline/chips/m6809/opcodes.rb +159 -0
  27. data/lib/badline/chips/m6809/operations.rb +258 -0
  28. data/lib/badline/chips/m6809/plans.rb +143 -0
  29. data/lib/badline/chips/m6809/registers.rb +82 -0
  30. data/lib/badline/chips/m6809/stack.rb +140 -0
  31. data/lib/badline/chips/m6809.rb +13 -0
  32. data/lib/badline/chips/riot/timer.rb +14 -3
  33. data/lib/badline/chips/rriot.rb +4 -2
  34. data/lib/badline/chips/sid/filter.rb +4 -4
  35. data/lib/badline/chips/sid/waveform.rb +6 -6
  36. data/lib/badline/chips/vic_i/keyed_parts.rb +7 -2
  37. data/lib/badline/chips/vic_i.rb +84 -21
  38. data/lib/badline/data_path.rb +3 -3
  39. data/lib/badline/drives/ieee_models.rb +54 -0
  40. data/lib/badline/drives/model1001.rb +37 -0
  41. data/lib/badline/drives/model2031.rb +6 -0
  42. data/lib/badline/drives/model4040/disk.rb +62 -0
  43. data/lib/badline/drives/model4040/ip_bus.rb +46 -0
  44. data/lib/badline/drives/model4040/keyed_state.rb +57 -0
  45. data/lib/badline/drives/model4040/mechanism.rb +192 -0
  46. data/lib/badline/drives/model4040/parts.rb +20 -0
  47. data/lib/badline/drives/model4040.rb +179 -0
  48. data/lib/badline/drives/model8050/board.rb +77 -0
  49. data/lib/badline/drives/model8050/dos_board.rb +91 -0
  50. data/lib/badline/drives/model8050/idle.rb +11 -7
  51. data/lib/badline/drives/model8050/keyed_state.rb +5 -2
  52. data/lib/badline/drives/model8050.rb +18 -130
  53. data/lib/badline/drives/model9060/idle.rb +149 -0
  54. data/lib/badline/drives/model9060/keyed_state.rb +123 -0
  55. data/lib/badline/drives/model9060/parts.rb +98 -0
  56. data/lib/badline/drives/model9060/sasi_bus.rb +88 -0
  57. data/lib/badline/drives/model9060/sasi_commands.rb +168 -0
  58. data/lib/badline/drives/model9060/sasi_target.rb +161 -0
  59. data/lib/badline/drives/model9060.rb +195 -0
  60. data/lib/badline/frontend/app.rb +8 -0
  61. data/lib/badline/frontend/boot.rb +3 -2
  62. data/lib/badline/frontend/controls.rb +62 -6
  63. data/lib/badline/frontend/keys.rb +36 -0
  64. data/lib/badline/frontend/kim1_panel.rb +208 -0
  65. data/lib/badline/frontend/media_slots.rb +8 -1
  66. data/lib/badline/frontend/menu/drive_types.rb +56 -0
  67. data/lib/badline/frontend/menu/menu_dialogs.rb +61 -14
  68. data/lib/badline/frontend/menu/menu_media.rb +42 -12
  69. data/lib/badline/frontend/menu/menu_pages.rb +97 -2
  70. data/lib/badline/frontend/menu/menu_ports.rb +38 -1
  71. data/lib/badline/frontend/menu/pause_menu.rb +5 -1
  72. data/lib/badline/frontend/pet_keyboard.rb +23 -1
  73. data/lib/badline/frontend/screen.rb +1 -1
  74. data/lib/badline/frontend/screens.rb +40 -2
  75. data/lib/badline/frontend/teletype_pane.rb +152 -0
  76. data/lib/badline/frontend/timeline.rb +31 -2
  77. data/lib/badline/frontend.rb +3 -0
  78. data/lib/badline/kim1/bus.rb +117 -0
  79. data/lib/badline/kim1/cpu.rb +25 -0
  80. data/lib/badline/kim1/host_terminal.rb +74 -0
  81. data/lib/badline/kim1/key_entry.rb +74 -0
  82. data/lib/badline/kim1/keyed_modules.rb +53 -0
  83. data/lib/badline/kim1/keyed_state.rb +133 -0
  84. data/lib/badline/kim1/panel.rb +176 -0
  85. data/lib/badline/kim1/rriots.rb +28 -0
  86. data/lib/badline/kim1/sound.rb +29 -0
  87. data/lib/badline/kim1/tape.rb +116 -0
  88. data/lib/badline/kim1/teletype.rb +193 -0
  89. data/lib/badline/kim1.rb +190 -0
  90. data/lib/badline/machine/ieee_drives.rb +94 -117
  91. data/lib/badline/machine/keyboard_buffer.rb +20 -3
  92. data/lib/badline/machine/line_sound.rb +122 -0
  93. data/lib/badline/machine.rb +46 -15
  94. data/lib/badline/media/extensions.rb +8 -4
  95. data/lib/badline/media/kim1_media.rb +150 -0
  96. data/lib/badline/media/pet_media.rb +3 -1
  97. data/lib/badline/media/true_drive.rb +42 -45
  98. data/lib/badline/media.rb +12 -6
  99. data/lib/badline/options/family.rb +98 -8
  100. data/lib/badline/options/help.rb +44 -12
  101. data/lib/badline/options/table.rb +43 -11
  102. data/lib/badline/options/validation.rb +44 -8
  103. data/lib/badline/options.rb +27 -8
  104. data/lib/badline/pet/bus.rb +16 -2
  105. data/lib/badline/pet/keyboard_ports.rb +9 -2
  106. data/lib/badline/pet/keyed_state.rb +4 -0
  107. data/lib/badline/pet/model.rb +11 -2
  108. data/lib/badline/pet/rom_sets.rb +1 -0
  109. data/lib/badline/pet/sound.rb +5 -105
  110. data/lib/badline/pet/superpet/acia.rb +12 -0
  111. data/lib/badline/pet/superpet/board.rb +221 -0
  112. data/lib/badline/pet/superpet/dongle.rb +81 -0
  113. data/lib/badline/pet/superpet/keyed_modules.rb +63 -0
  114. data/lib/badline/pet/superpet/keyed_parts.rb +76 -0
  115. data/lib/badline/pet/superpet/m6809.rb +32 -0
  116. data/lib/badline/pet/superpet/waterloo_traps.rb +199 -0
  117. data/lib/badline/pet/superpet.rb +216 -0
  118. data/lib/badline/pet.rb +19 -3
  119. data/lib/badline/plus4/keyed_state.rb +2 -0
  120. data/lib/badline/plus4.rb +1 -0
  121. data/lib/badline/roms/1001/fdc-251257-02a.rom +0 -0
  122. data/lib/badline/roms/4040/dos-1.2.rom +0 -0
  123. data/lib/badline/roms/4040/dos-2.1-rev.rom +0 -0
  124. data/lib/badline/roms/4040/dos-2.1.rom +0 -0
  125. data/lib/badline/roms/4040/fdc-901466-02.rom +0 -0
  126. data/lib/badline/roms/4040/fdc-901466-04.rom +0 -0
  127. data/lib/badline/roms/9060/dos-3.0.rom +0 -0
  128. data/lib/badline/roms/9060/sasi-300515-revb.rom +0 -0
  129. data/lib/badline/roms/README +166 -0
  130. data/lib/badline/roms/kim1/6530-002.rom +0 -0
  131. data/lib/badline/roms/kim1/6530-003.rom +0 -0
  132. data/lib/badline/roms/pet/characters-superpet.rom +0 -0
  133. data/lib/badline/roms/pet/superpet-a000.rom +0 -0
  134. data/lib/badline/roms/pet/superpet-c000.rom +0 -0
  135. data/lib/badline/roms/pet/superpet-e000-50hz.rom +0 -0
  136. data/lib/badline/roms/pet/superpet-e000.rom +0 -0
  137. data/lib/badline/snapshot/container.rb +2 -0
  138. data/lib/badline/snapshot/field_doc/markdown.rb +1 -1
  139. data/lib/badline/snapshot/field_doc.rb +27 -6
  140. data/lib/badline/snapshot/image.rb +4 -1
  141. data/lib/badline/snapshot/kim1_setup.rb +28 -0
  142. data/lib/badline/snapshot/pet_setup.rb +27 -11
  143. data/lib/badline/snapshot/storage_setup.rb +4 -1
  144. data/lib/badline/snapshot/vic20_setup.rb +11 -7
  145. data/lib/badline/snapshot/vice/pet.rb +13 -7
  146. data/lib/badline/snapshot/vice/pet_cpu.rb +39 -5
  147. data/lib/badline/snapshot/vice/pet_crtc.rb +15 -2
  148. data/lib/badline/snapshot/vice/pet_drive8050.rb +188 -68
  149. data/lib/badline/snapshot/vice/pet_mem.rb +49 -5
  150. data/lib/badline/snapshot/vice/pet_restore.rb +40 -11
  151. data/lib/badline/snapshot.rb +4 -0
  152. data/lib/badline/status.rb +30 -30
  153. data/lib/badline/storage/d67_image.rb +55 -0
  154. data/lib/badline/storage/d90_image.rb +180 -0
  155. data/lib/badline/storage/disk_image/writing.rb +2 -2
  156. data/lib/badline/storage/kim_tape.rb +254 -0
  157. data/lib/badline/storage/papertape.rb +90 -0
  158. data/lib/badline/storage.rb +4 -0
  159. data/lib/badline/support/kim1_checkpoint.rb +41 -0
  160. data/lib/badline/support/superpet_checkpoint.rb +35 -0
  161. data/lib/badline/version.rb +1 -1
  162. data/lib/badline/vic20/keyed_modules.rb +6 -3
  163. data/lib/badline/vic20/keyed_state.rb +6 -4
  164. data/lib/badline/vic20.rb +26 -12
  165. data/lib/badline.rb +1 -0
  166. metadata +77 -1
@@ -13,8 +13,20 @@ module Badline
13
13
  COLUMNS = 25
14
14
  ROW_WIDTH = 200
15
15
 
16
+ # The KIM-1's settings on its PANEL page (MenuPorts).
17
+ KIM1_SETTINGS = %i[sst_off sst_on keypad teletype irq_open irq_wired pb7_out pa0_speaker].freeze
18
+
19
+ # The KIM-1's deck on its TAPE page: the keys down, and the rest.
20
+ KIM1_DECK = [%w[STOP PLAY RECORD], %i[kim1_stop kim1_play kim1_record]].freeze
21
+ KIM1_TAPE = (KIM1_DECK.last + %i[kim1_rewind kim1_eject]).freeze
22
+
16
23
  RECENT = %i[recent0 recent1 recent2 recent3 recent4 recent5 recent6 recent7].freeze
17
24
 
25
+ # The SuperPET's switches: SW2 picks the CPU and SW1 protects the
26
+ # bank RAM, each at PROG left to the system latch.
27
+ CPU_SWITCH = [%w[6502 6809 PROG], %i[cpu6502 cpu6809 cpu_prog]].freeze
28
+ RAM_SWITCH = [%w[R/W READ PROG], %i[bank_rw bank_read bank_prog]].freeze
29
+
18
30
  SID_MODELS = %i[mos6581 mos8580].freeze
19
31
  RAM_NAMES = {
20
32
  unexpanded: "", "3k": "3K", "8k": "8K", "16k": "16K", "24k": "24K", "32k": "32K",
@@ -76,8 +88,12 @@ module Badline
76
88
  when :swap then @controls.swap_ports
77
89
  when :sound_on, :mute then @sound.toggle_mute unless @sound.muted? == (action == :mute)
78
90
  when :sid6581, :sid8580 then @computer.sound_source.model = action == :sid8580 ? :mos8580 : :mos6581
91
+ when :cpu6502, :cpu6809, :cpu_prog then switch_cpu(action)
92
+ when :bank_rw, :bank_read, :bank_prog then switch_bank_ram(action)
79
93
  when :reset then @computer.reset!
80
94
  when :power_cycle then @computer.power_cycle!
95
+ when *KIM1_SETTINGS then kim1_setting(action)
96
+ when *KIM1_TAPE then kim1_tape(action)
81
97
  else pick_port(action)
82
98
  end
83
99
  end
@@ -92,10 +108,17 @@ module Badline
92
108
  # too.
93
109
  def cbm2? = @computer.family == :cbm2
94
110
 
111
+ # The model of the true drive on device 8, or an empty one for the
112
+ # traps.
113
+ def drive_model = MediaSlots.drive_model(@computer)
114
+
95
115
  # Whether the machine is the CBM-II's B series, whose ports page is
96
116
  # its keyboard's alone.
97
117
  def b_series? = cbm2? && !Controls.p500?(@computer)
98
118
 
119
+ # Whether the machine is a KIM-1, whose ports page is its panel's.
120
+ def kim1? = @computer.family == :kim1
121
+
99
122
  private
100
123
 
101
124
  def vic20? = @computer.family == :vic20
@@ -103,6 +126,8 @@ module Badline
103
126
  def plus4? = @computer.family == :plus4
104
127
 
105
128
  def draw_drive
129
+ return info("DRIVE", "") if kim1?
130
+
106
131
  media = @media
107
132
  inserted = media.inserted?
108
133
  set = inserted ? media.disk_set : []
@@ -118,11 +143,13 @@ module Badline
118
143
  skip
119
144
  drive = @computer.true_drive
120
145
  info("DEVICE", drive.nil? ? "FAST LOADING" : "#{drive.model_name}, TRUE DRIVE")
146
+ row("DRIVE TYPE...", :drive_type, drive_model) if pet? || cbm2?
121
147
  end
122
148
 
123
- # The CBM-II has no datasette.
149
+ # The CBM-II has no datasette, and the KIM-1 a deck of its own.
124
150
  def draw_datasette
125
151
  return info("TAPE", "") if cbm2?
152
+ return draw_kim1_tape if kim1?
126
153
 
127
154
  datasette = @computer.datasette
128
155
  tape = datasette.tape
@@ -135,9 +162,55 @@ module Badline
135
162
  info("MOTOR", datasette.motor? ? "ON" : "OFF")
136
163
  end
137
164
 
165
+ # The KIM-1's deck, with its tape's name and how far into it the deck
166
+ # is, in minutes and seconds.
167
+ def draw_kim1_tape
168
+ deck = @computer.tape
169
+ tape = deck.tape
170
+ info("TAPE", File.basename(tape.path))
171
+ row("INSERT...", :insert_tape)
172
+ row("EJECT", :kim1_eject)
173
+ down = 0
174
+ down = 1 if deck.playing?
175
+ down = 2 if deck.recording?
176
+ toggle("DECK", :kim1_deck, [KIM1_DECK.first, KIM1_DECK.last, down])
177
+ row("REWIND", :kim1_rewind)
178
+ skip
179
+ info("POSITION", "#{clock(deck.position)} OF #{clock(tape.duration)}")
180
+ end
181
+
182
+ def clock(microseconds)
183
+ seconds = microseconds / 1_000_000
184
+ "#{seconds / 60}:#{(seconds % 60).to_s.rjust(2, '0')}"
185
+ end
186
+
187
+ # Presses the KIM-1 deck's keys. RECORD with no file to record to
188
+ # puts a new tape in first, in the tapes folder (Badline.data_folder).
189
+ def kim1_tape(action)
190
+ deck = @computer.tape
191
+ case action
192
+ when :kim1_stop then deck.stop
193
+ when :kim1_play then deck.play
194
+ when :kim1_record
195
+ deck.insert_tape(Storage::KIMTape.create(new_tape_path)) if deck.tape.path.empty?
196
+ deck.record
197
+ when :kim1_rewind then deck.rewind
198
+ else deck.eject
199
+ end
200
+ end
201
+
202
+ # The first of kim1-1.wav, kim1-2.wav and on that isn't taken.
203
+ def new_tape_path
204
+ folder = Badline.data_folder("tapes")
205
+ number = 1
206
+ number += 1 while File.exist?(File.join(folder, "kim1-#{number}.wav"))
207
+ File.join(folder, "kim1-#{number}.wav")
208
+ end
209
+
138
210
  def draw_expansion
139
211
  return draw_pet_memory if pet?
140
212
  return info("RAM", "#{@computer.model.ram_kb}K") if cbm2?
213
+ return info("RAM", "1K") if kim1?
141
214
 
142
215
  info("CARTRIDGE", @media.cartridge_name)
143
216
  return info("RAM EXPANSION", RAM_NAMES.fetch(@computer.ram_configuration)) if vic20?
@@ -175,6 +248,8 @@ module Badline
175
248
  return draw_8296_memory if model.cbm8296?
176
249
 
177
250
  info("RAM", "#{@computer.ram_kb}K")
251
+ return info("BANK RAM", "64K, BANK #{@computer.board.bank} AT $9000") if model.superpet?
252
+
178
253
  info("EXPANSION RAM", model.expansion ? "64K BOARD" : "")
179
254
  end
180
255
 
@@ -204,8 +279,9 @@ module Badline
204
279
  # 901244 or the P500's 901234.
205
280
  def draw_power
206
281
  info("MACHINE", "#{@computer.model.name.upcase}, #{@computer.mode.to_s.upcase} MODE") if c128?
207
- info("MACHINE", @computer.model_label.upcase) if pet? || cbm2?
282
+ info("MACHINE", @computer.model_label.upcase) if pet? || cbm2? || kim1?
208
283
  info("KERNAL", "#{Controls.p500?(@computer) ? 901_234 : 901_244}-#{@computer.kernal.upcase}") if cbm2?
284
+ draw_superpet_switches if pet? && @computer.superpet?
209
285
  if plus4?
210
286
  info("MACHINE", @computer.model_label)
211
287
  expansion = @computer.ram_expansion_label
@@ -217,6 +293,25 @@ module Badline
217
293
  row("QUIT", :quit)
218
294
  end
219
295
 
296
+ # The SuperPET's CPU switch, which a move applies with a reset, and its
297
+ # bank RAM switch, which applies at once, with the CPU that has the
298
+ # bus.
299
+ def draw_superpet_switches
300
+ board = @computer.board
301
+ toggle("CPU SWITCH", :cpu_switch, [*CPU_SWITCH, PET::SuperPET::Board::CPU_SWITCH.index(board.cpu_switch)])
302
+ toggle("RAM SWITCH", :ram_switch, [*RAM_SWITCH, PET::SuperPET::Board::RAM_SWITCH.index(board.ram_switch)])
303
+ info("RUNNING", @computer.m6809_running? ? "6809, WATERLOO" : "6502, BASIC 4")
304
+ end
305
+
306
+ def switch_cpu(action)
307
+ position = PET::SuperPET::Board::CPU_SWITCH[CPU_SWITCH[1].index(action)]
308
+ @computer.switch_cpu(position) unless position == @computer.board.cpu_switch
309
+ end
310
+
311
+ def switch_bank_ram(action)
312
+ @computer.switch_bank_ram(PET::SuperPET::Board::RAM_SWITCH[RAM_SWITCH[1].index(action)])
313
+ end
314
+
220
315
  # A label, and its value under it, or NONE in the dim colour when the
221
316
  # value is empty.
222
317
  def info(label, value)
@@ -8,7 +8,11 @@ module Badline
8
8
  # joystick's, and the Plus/4 two joysticks and no pot devices. The PET
9
9
  # and the CBM-II's B series have no ports, and the page, KEYBOARD
10
10
  # there, says where their keys are instead. The P500 has two joysticks,
11
- # and the page says where its keys are while they type.
11
+ # and the page says where its keys are while they type. The KIM-1's,
12
+ # PANEL there, sets its SST switch and TTY/KB jumper, which presses RS
13
+ # for the monitor to start again on the keypad or the teletype, and what its
14
+ # application connector wires: U3's PB7 to IRQ, and a speaker on U3's
15
+ # PA0 in place of the audio output on U2's PB7.
12
16
  module MenuPorts
13
17
  PORT_DEVICES = %i[joystick mouse paddles].freeze
14
18
  PORT_NAMES = %w[JOY MOUSE PADDLES].freeze
@@ -35,6 +39,7 @@ module Badline
35
39
  return draw_pet_keys if @computer.family == :pet
36
40
  return draw_p500_ports if Controls.p500?(@computer)
37
41
  return draw_cbm2_keys if @computer.family == :cbm2
42
+ return draw_kim1_panel if @computer.family == :kim1
38
43
 
39
44
  draw_pot_ports
40
45
  return unless draw_keys(@computer.family == :c128 ? "C128" : "C64")
@@ -68,15 +73,47 @@ module Badline
68
73
 
69
74
  # The PET's keyboard, whose keys type the characters on the host's
70
75
  # (PETKeyboard), and the keys that stand for the rest of its keys.
76
+ # The SuperPET's 6809 shows whether its keys type ASCII, or go by
77
+ # position for the APL symbols on its keycaps.
71
78
  def draw_pet_keys
72
79
  business = @computer.model.keyboard == :business
73
80
  info("KEYBOARD", business ? "BUSINESS" : "GRAPHICS")
81
+ draw_superpet_characters if @computer.superpet?
74
82
  (PET_KEYS + (business ? PET_BUSINESS_KEYS : PET_GRAPHICS_KEYS)).each do |line|
75
83
  @painter.text(@left, @top, line, MenuTheme::TEXT)
76
84
  @top += MenuPages::LINE
77
85
  end
78
86
  end
79
87
 
88
+ def draw_superpet_characters
89
+ apl = @computer.apl_characters?
90
+ return unless apl || @computer.m6809_running?
91
+
92
+ info("CHARACTERS", apl ? "APL, KEYS BY POSITION" : "ASCII")
93
+ end
94
+
95
+ def draw_kim1_panel
96
+ kim = @computer
97
+ toggle("SST", :sst, [%w[OFF ON], %i[sst_off sst_on], kim.sst ? 1 : 0])
98
+ toggle("TTY/KB", :tty, [%w[KEYPAD TTY], %i[keypad teletype], kim.tty? ? 1 : 0])
99
+ skip
100
+ info("RAM", "#{kim.ram_kb}K")
101
+ toggle("U3 PB7", :irq, [%w[OPEN IRQ], %i[irq_open irq_wired], kim.irq_wired ? 1 : 0])
102
+ toggle("SOUND", :speaker, [["U2 PB7", "U3 PA0"], %i[pb7_out pa0_speaker], kim.speaker? ? 1 : 0])
103
+ end
104
+
105
+ def kim1_setting(action)
106
+ kim = @computer
107
+ case action
108
+ when :sst_off, :sst_on then kim.sst = action == :sst_on
109
+ when :keypad, :teletype
110
+ kim.tty = action == :teletype
111
+ kim.reset!
112
+ when :irq_open, :irq_wired then kim.irq_wired = action == :irq_wired
113
+ else kim.speaker = action == :pa0_speaker
114
+ end
115
+ end
116
+
80
117
  # What each control port has plugged in, a joystick, a mouse or
81
118
  # paddles.
82
119
  def draw_pot_ports
@@ -88,7 +88,7 @@ module Badline
88
88
  # any: :resume, :quit, :freeze or :swap.
89
89
  def click(left, top)
90
90
  action = @buttons.action_at(left, top)
91
- return @dialogs.click(action, top) if %i[browse confirm cancel].include?(action)
91
+ return @dialogs.click(action, top) if %i[browse confirm cancel].include?(action) || picks_drive?(action)
92
92
 
93
93
  choose(action)
94
94
  end
@@ -159,6 +159,7 @@ module Badline
159
159
  when :insert_cartridge then @dialogs.browse(:cartridge)
160
160
  when :quick_open then @dialogs.browse(:program)
161
161
  when :remove_cartridge then @dialogs.ask_remove
162
+ when :drive_type then @dialogs.pick_drive(@pages.drive_model)
162
163
  else @pages.perform(action)
163
164
  end
164
165
  nil
@@ -235,10 +236,13 @@ module Badline
235
236
  repeat.zero? || Keys::DIRECTIONS.key?(scancode) ? key(scancode) : nil
236
237
  end
237
238
 
239
+ def picks_drive?(action) = DriveTypes::ACTIONS.include?(action) && @dialogs.open?
240
+
238
241
  def draw_sections
239
242
  top = @top + 32
240
243
  SECTIONS.each_with_index do |name, index|
241
244
  name = "KEYBOARD" if PAGES[index] == :ports && (@pages.pet? || @pages.b_series?)
245
+ name = "PANEL" if PAGES[index] == :ports && @pages.kim1?
242
246
  @buttons.plain(@left + 6, top, name.ljust(14), PAGES[index], on: index == @section)
243
247
  top += Buttons::HEIGHT + 2
244
248
  end
@@ -13,6 +13,12 @@ module Badline
13
13
  # key its unshifted character is on with the PET's SHIFT, so the
14
14
  # graphics on the graphics keyboard's symbol keys can be typed.
15
15
  #
16
+ # While the SuperPET shows its APL characters the keys go by position
17
+ # instead (#positional=): a key that types a character presses the PET
18
+ # key its unshifted character is on, and the PET's SHIFT follows the
19
+ # host's, so a key types the APL symbol on the SuperPET's keycap in the
20
+ # same place, shifted or not.
21
+ #
16
22
  # The CBM-II's keyboard goes the same way (Keys.cbm2_key), and there
17
23
  # Alt with a key presses one of the keys the host has none for
18
24
  # (Keys.cbm2_alt_key).
@@ -35,8 +41,12 @@ module Badline
35
41
  @right_alt = false
36
42
  @forced = -1
37
43
  @forced_shift = false
44
+ @positional = false
38
45
  end
39
46
 
47
+ # Whether the keys go by position, for the SuperPET's APL characters.
48
+ attr_writer :positional
49
+
40
50
  def key(scancode, down)
41
51
  if [LSHIFT, RSHIFT].include?(scancode)
42
52
  shift_key(scancode, down)
@@ -70,7 +80,10 @@ module Badline
70
80
 
71
81
  def key_down(scancode)
72
82
  key_up(scancode) if @held.key?(scancode)
73
- key, shift = alt? ? shifted_key(scancode) : layout_key(scancode, shifted?)
83
+ key, shift = if alt? then shifted_key(scancode)
84
+ elsif @positional then positional_key(scancode)
85
+ else layout_key(scancode, shifted?)
86
+ end
74
87
  return if key == :none
75
88
 
76
89
  unless shift == :host
@@ -128,6 +141,15 @@ module Badline
128
141
  [key, :on]
129
142
  end
130
143
 
144
+ # The PET key for the scancode by position: the one its unshifted
145
+ # character is on, with the host's SHIFT.
146
+ def positional_key(scancode)
147
+ key, shift = Keys.pet_key(scancode, false, @layout)
148
+ return [key, shift] if key == :none || !Keys::US_CHARACTERS.key?(scancode)
149
+
150
+ [key, :host]
151
+ end
152
+
131
153
  def shifted? = @left_shift || @right_shift
132
154
 
133
155
  def sync_shift
@@ -15,7 +15,7 @@ module Badline
15
15
  # least MIN_WIDTH window pixels wide, and at least as high as PAL's C64
16
16
  # crop, 272 lines, so a C64 shows in the same window whatever its
17
17
  # region. A crop with fewer lines or pixels sits in the middle of the
18
- # texture, between black bands. The VIC-20's 284 by 284 crop and the
18
+ # texture, between black bands. The PAL VIC-20's 284 by 284 crop and the
19
19
  # TED's 384 by 288 fill their own.
20
20
  #
21
21
  # With `frame_lines`, the crop's lines run on past the frame's last
@@ -10,6 +10,10 @@ module Badline
10
10
  # On the C128, #follow switches to the screen the machine prints to
11
11
  # (C128#active_screen) each time it moves there, and #show_vdc's choice
12
12
  # holds until the next move.
13
+ #
14
+ # The KIM-1 has no video chip, and shows its panel (KIM1Panel), or with
15
+ # its TTY/KB jumper fitted its teletype's page (TeletypePane), which
16
+ # #follow brings up to the machine each frame.
13
17
  class Screens
14
18
  # The frames a move holds before #follow takes it. The C128 KERNAL's
15
19
  # reset clears the 80 column screen with the editor on it for over a
@@ -18,8 +22,17 @@ module Badline
18
22
 
19
23
  attr_reader :screen
20
24
 
25
+ # The KIM-1's panel, the picture a KIM-1 shows.
26
+ attr_reader :kim1_panel
27
+
28
+ # The KIM-1's teletype's page, which it shows with the jumper fitted.
29
+ attr_reader :teletype_pane
30
+
21
31
  def initialize(computer)
22
32
  @computer = computer
33
+ @kim1_panel = KIM1Panel.new
34
+ @teletype_pane = TeletypePane.new
35
+ @built_tty = false
23
36
  @vdc_shown = false
24
37
  @vdc_shape = []
25
38
  forget_screen
@@ -31,6 +44,8 @@ module Badline
31
44
  # Shows another machine's video chip, once #build builds its screen.
32
45
  def computer=(computer)
33
46
  @computer = computer
47
+ @kim1_panel = KIM1Panel.new
48
+ @teletype_pane = TeletypePane.new
34
49
  @vdc_shown = false
35
50
  forget_screen
36
51
  end
@@ -38,6 +53,7 @@ module Badline
38
53
  # Shows the screen a C128 has moved its output to once the move has
39
54
  # held for FOLLOW_FRAMES calls, one a frame. #build then builds it.
40
55
  def follow
56
+ return follow_kim1 if @computer.family == :kim1
41
57
  return unless @computer.family == :c128
42
58
 
43
59
  vdc = @computer.active_screen == :vdc
@@ -60,11 +76,17 @@ module Badline
60
76
 
61
77
  # Whether the screen was built for the other chip, or the VDC's
62
78
  # display has changed size or crop since the screen was built for it.
63
- def stale? = @built_vdc != @vdc_shown || (@vdc_shown && vdc_shape(@computer.vdc) != @vdc_shape)
79
+ def stale?
80
+ return @built_tty != @computer.tty? if @computer.family == :kim1
81
+
82
+ @built_vdc != @vdc_shown || (@vdc_shown && vdc_shape(@computer.vdc) != @vdc_shape)
83
+ end
64
84
 
65
85
  def build
66
86
  @built_vdc = @vdc_shown
67
- if @vdc_shown
87
+ if @computer.family == :kim1
88
+ build_kim1
89
+ elsif @vdc_shown
68
90
  vdc = @computer.vdc
69
91
  @vdc_shape = vdc_shape(vdc)
70
92
  @screen = Screen.new(vdc, vdc.crop, dots: 2)
@@ -78,6 +100,22 @@ module Badline
78
100
 
79
101
  private
80
102
 
103
+ # The panel and the teletype's page both follow the machine, the
104
+ # LEDs' brightness and the page's lines going on behind the other.
105
+ def follow_kim1
106
+ @kim1_panel.follow(@computer)
107
+ @teletype_pane.follow(@computer)
108
+ end
109
+
110
+ def build_kim1
111
+ @built_tty = @computer.tty?
112
+ @screen = if @built_tty
113
+ Screen.new(@teletype_pane, @teletype_pane.crop)
114
+ else
115
+ Screen.new(@kim1_panel, @kim1_panel.crop)
116
+ end
117
+ end
118
+
81
119
  # The machine's output counts as on the VIC-IIe's screen, the one
82
120
  # shown.
83
121
  def forget_screen
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Frontend
5
+ # The KIM-1's teletype as the window shows it with the TTY/KB jumper
6
+ # fitted: a page of COLUMNS by ROWS characters in the character ROM's
7
+ # font, printed as the teletype prints (KIM1::Teletype#output), with a
8
+ # line of the host keys that type what the window's keys don't. RETURN
9
+ # goes back to the line's start, LINE FEED down a line, the page
10
+ # scrolling up from the last, and a line full at COLUMNS goes on to the
11
+ # next. It answers what a Screen reads of a video chip, as KIM1Panel
12
+ # does.
13
+ class TeletypePane
14
+ COLUMNS = 72
15
+ ROWS = 24
16
+ LINE = 10
17
+ MARGIN = 8
18
+ TOP = 22
19
+
20
+ WIDTH = (COLUMNS * 8) + (2 * MARGIN)
21
+ HEIGHT = 272
22
+
23
+ PAPER = 0
24
+ INK = 1
25
+ HINT = 2
26
+ CURSOR = 3
27
+ PALETTE = [0xf2ecd8, 0x202020, 0x6c6858, 0x9c9480].freeze
28
+
29
+ HINTS = "BACKSPACE: RUBOUT DOWN: LINE FEED F9: MENU"
30
+
31
+ attr_reader :display, :dirty_lines
32
+
33
+ def initialize
34
+ @display = Array.new(WIDTH * HEIGHT, PAPER)
35
+ @dirty_lines = Array.new(HEIGHT, true)
36
+ @font = ROM.read("character.rom")
37
+ @page = Array.new(ROWS) { Array.new(COLUMNS, 0x20) }
38
+ @column = 0
39
+ @row = 0
40
+ @printed = 0
41
+ @baud = 0
42
+ draw_cursor(CURSOR)
43
+ end
44
+
45
+ def width = WIDTH
46
+
47
+ def height = HEIGHT
48
+
49
+ def palette = PALETTE
50
+
51
+ def crop = [0, 0, WIDTH, HEIGHT]
52
+
53
+ def clear_dirty_lines!
54
+ @dirty_lines.map! { false }
55
+ end
56
+
57
+ # The characters on the page, a line each, without trailing spaces.
58
+ def lines = @page.map { |row| row.map(&:chr).join.rstrip }
59
+
60
+ # Prints what the KIM-1 `computer`'s teletype has printed since the
61
+ # last call.
62
+ def follow(computer)
63
+ teletype = computer.teletype
64
+ title(teletype.baud) if teletype.baud != @baud
65
+ output = teletype.output
66
+ return if output.size == @printed
67
+
68
+ draw_cursor(PAPER)
69
+ output[@printed, output.size - @printed].bytes.each { |byte| print_byte(byte) }
70
+ @printed = output.size
71
+ draw_cursor(CURSOR)
72
+ end
73
+
74
+ private
75
+
76
+ def title(baud)
77
+ @baud = baud
78
+ box(0, 0, WIDTH, TOP, PAPER)
79
+ text(MARGIN, 6, "KIM-1 TELETYPE, #{baud} BAUD", HINT)
80
+ text(WIDTH - MARGIN - (HINTS.size * 8), 6, HINTS, HINT)
81
+ end
82
+
83
+ def print_byte(byte)
84
+ case byte
85
+ when 0x0d then @column = 0
86
+ when 0x0a then line_feed
87
+ when 0x20..0x7e then print_character(byte)
88
+ end
89
+ end
90
+
91
+ def print_character(byte)
92
+ if @column == COLUMNS
93
+ @column = 0
94
+ line_feed
95
+ end
96
+ @page[@row][@column] = byte
97
+ glyph(MARGIN + (@column * 8), TOP + (@row * LINE), byte, INK)
98
+ @column += 1
99
+ end
100
+
101
+ def line_feed
102
+ if @row < ROWS - 1
103
+ @row += 1
104
+ return
105
+ end
106
+
107
+ @page.shift
108
+ @page << Array.new(COLUMNS, 0x20)
109
+ redraw_page
110
+ end
111
+
112
+ def redraw_page
113
+ box(0, TOP, WIDTH, ROWS * LINE, PAPER)
114
+ @page.each_with_index do |characters, row|
115
+ characters.each_with_index do |byte, column|
116
+ glyph(MARGIN + (column * 8), TOP + (row * LINE), byte, INK) unless byte == 0x20
117
+ end
118
+ end
119
+ end
120
+
121
+ def draw_cursor(color)
122
+ column = [@column, COLUMNS - 1].min
123
+ box(MARGIN + (column * 8), TOP + (@row * LINE) + 8, 8, 1, color)
124
+ end
125
+
126
+ def box(left, top, width, height, color)
127
+ height.times do |row|
128
+ line = top + row
129
+ @display.fill(color, (line * WIDTH) + left, width)
130
+ @dirty_lines[line] = true
131
+ end
132
+ end
133
+
134
+ def text(left, top, string, color)
135
+ string.bytes.each_with_index { |byte, index| glyph(left + (index * 8), top, byte, color) }
136
+ end
137
+
138
+ # Draws the glyph for the ASCII `byte` over a cleared cell.
139
+ def glyph(left, top, byte, color)
140
+ code = Painter::SCREEN_CODES[byte & 0x7f]
141
+ 8.times do |row|
142
+ bits = @font[0x800 + (code * 8) + row]
143
+ line = top + row
144
+ 8.times do |dot|
145
+ @display[(line * WIDTH) + left + dot] = bits.anybits?(0x80 >> dot) ? color : PAPER
146
+ end
147
+ @dirty_lines[line] = true
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
@@ -44,6 +44,8 @@ module Badline
44
44
  # either, and the PET and the CBM-II's B series neither that nor
45
45
  # joysticks.
46
46
  def self.key?(name, family)
47
+ return !kim1_key(name).empty? if family == :kim1
48
+
47
49
  pet = %i[pet cbm2].include?(family)
48
50
  (name == "restore" && !pet && !%i[plus4 p500].include?(family)) || (family == :c128 && LOCKS.include?(name)) ||
49
51
  !keyboard_key(name, family).nil? || (!pet && !joystick_direction(name).nil?)
@@ -73,6 +75,13 @@ module Badline
73
75
  end
74
76
  end
75
77
 
78
+ # The KIM-1's key for a name such as go, a or plus, as
79
+ # KIM1::Panel::KEYS names it, with st and rs, or an empty string.
80
+ def self.kim1_key(name)
81
+ key = name == "plus" ? "+" : name.upcase
82
+ KIM1::Panel::KEYS.include?(key) || %w[ST RS].include?(key) ? key : ""
83
+ end
84
+
76
85
  # The direction of joy1-NAME or joy2-NAME, such as up or fire.
77
86
  def self.joystick_direction(name)
78
87
  return nil unless name.start_with?("joy1-") || name.start_with?("joy2-")
@@ -150,7 +159,7 @@ module Badline
150
159
  def perform(computer, event)
151
160
  case event.action
152
161
  when "key" then press(computer, event)
153
- when "type" then computer.type_text(event.argument.gsub("\\n", "\r").gsub("\\r", "\r"))
162
+ when "type" then type(computer, event.argument)
154
163
  when "insert" then insert(computer, event.argument)
155
164
  when "eject" then eject(computer, event.argument)
156
165
  when "reset" then computer.reset!
@@ -158,8 +167,15 @@ module Badline
158
167
  end
159
168
  end
160
169
 
170
+ # Types the text, which on the KIM-1 goes to its teletype, or with
171
+ # none presses the keys it names (KIM1#type_text).
172
+ def type(computer, text)
173
+ computer.type_text(text.gsub("\\n", "\r").gsub("\\r", "\r"))
174
+ end
175
+
161
176
  def press(computer, event)
162
177
  name = event.argument
178
+ return press_kim1(computer, event) if computer.family == :kim1
163
179
  return lock(computer, name) if LOCKS.include?(name)
164
180
 
165
181
  if name == "restore"
@@ -176,6 +192,17 @@ module Badline
176
192
  hold(event)
177
193
  end
178
194
 
195
+ # ST and RS act as they go down, and the KIM-1's other keys are held.
196
+ def press_kim1(computer, event)
197
+ case (key = Timeline.kim1_key(event.argument))
198
+ when "ST" then computer.press_stop
199
+ when "RS" then computer.press_reset
200
+ else
201
+ computer.press_key(key)
202
+ hold(event)
203
+ end
204
+ end
205
+
179
206
  # CAPS LOCK and 40/80 DISPLAY lock: a push puts one down if it was up,
180
207
  # and up if it was down.
181
208
  def lock(computer, name)
@@ -199,6 +226,8 @@ module Badline
199
226
  def release(computer, event)
200
227
  return computer.release_cartridge_button if event.action == "freeze" && computer.family == :c64
201
228
 
229
+ return computer.release_key if computer.family == :kim1
230
+
202
231
  name = event.argument
203
232
  return computer.release_restore if name == "restore"
204
233
 
@@ -237,7 +266,7 @@ module Badline
237
266
  # its cartridge stays in.
238
267
  def ejectable?(computer, what)
239
268
  family = computer.family
240
- return false if family == :cbm2 && what != "disk"
269
+ return false if family == :kim1 || (family == :cbm2 && what != "disk")
241
270
 
242
271
  (what != "cartridge" || !%i[vic20 pet].include?(family)) && inserted?(computer, what)
243
272
  end
@@ -4,6 +4,8 @@ require "badline/sdl"
4
4
  require "badline/frontend/boot"
5
5
  require "badline/frontend/frame_rate"
6
6
  require "badline/frontend/screen"
7
+ require "badline/frontend/kim1_panel"
8
+ require "badline/frontend/teletype_pane"
7
9
  require "badline/frontend/screens"
8
10
  require "badline/frontend/drive_led"
9
11
  require "badline/frontend/keys"
@@ -42,6 +44,7 @@ require "badline/frontend/menu/pause_menu"
42
44
  require "badline/frontend/menu/file_browser"
43
45
  require "badline/frontend/menu/confirmation"
44
46
  require "badline/frontend/menu/name_field"
47
+ require "badline/frontend/menu/drive_types"
45
48
  require "badline/frontend/menu/menu_dialogs"
46
49
  require "badline/frontend/menu/menu_media"
47
50
  require "badline/frontend/menu/menu_ports"