badline 0.16.1 → 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 (256) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +213 -0
  3. data/CLAUDE.md +11 -7
  4. data/CONTRIBUTING.md +7 -1
  5. data/README.md +265 -26
  6. data/doc/media.md +5 -2
  7. data/doc/pinned-behaviour.md +189 -0
  8. data/doc/snapshot-format.md +616 -74
  9. data/doc/snapshots.md +192 -24
  10. data/exe/badline-ruby +5 -0
  11. data/lib/badline/audio/terminal.rb +34 -2
  12. data/lib/badline/c128/keyed_state.rb +2 -0
  13. data/lib/badline/c128.rb +1 -0
  14. data/lib/badline/c64/keyed_state.rb +2 -0
  15. data/lib/badline/c64.rb +1 -0
  16. data/lib/badline/cbm2/acia.rb +10 -0
  17. data/lib/badline/cbm2/bus.rb +263 -0
  18. data/lib/badline/cbm2/cpu.rb +58 -0
  19. data/lib/badline/cbm2/crtc_video.rb +133 -0
  20. data/lib/badline/cbm2/ieee_drive.rb +18 -0
  21. data/lib/badline/cbm2/ieee_layout.rb +69 -0
  22. data/lib/badline/cbm2/ieee_ports.rb +104 -0
  23. data/lib/badline/cbm2/ieee_traps.rb +81 -0
  24. data/lib/badline/cbm2/keyboard_ports.rb +60 -0
  25. data/lib/badline/cbm2/keyed_modules.rb +67 -0
  26. data/lib/badline/cbm2/keyed_state.rb +95 -0
  27. data/lib/badline/cbm2/model.rb +44 -0
  28. data/lib/badline/cbm2/p500/control_ports.rb +80 -0
  29. data/lib/badline/cbm2/p500/ieee_layout.rb +41 -0
  30. data/lib/badline/cbm2/p500/keyed_state.rb +85 -0
  31. data/lib/badline/cbm2/p500/model.rb +39 -0
  32. data/lib/badline/cbm2/p500/pages.rb +57 -0
  33. data/lib/badline/cbm2/p500.rb +264 -0
  34. data/lib/badline/cbm2/sid_page.rb +19 -0
  35. data/lib/badline/cbm2.rb +270 -0
  36. data/lib/badline/{plus4 → chips}/acia/frames.rb +1 -1
  37. data/lib/badline/chips/acia/keyed_state.rb +47 -0
  38. data/lib/badline/chips/acia.rb +307 -0
  39. data/lib/badline/chips/cia/timer.rb +2 -2
  40. data/lib/badline/chips/cpu/instruction_set.rb +1 -1
  41. data/lib/badline/chips/crtc.rb +3 -2
  42. data/lib/badline/chips/m6809/arithmetic.rb +140 -0
  43. data/lib/badline/chips/m6809/bus_steps.rb +223 -0
  44. data/lib/badline/chips/m6809/core.rb +149 -0
  45. data/lib/badline/chips/m6809/indexed.rb +82 -0
  46. data/lib/badline/chips/m6809/interrupts.rb +159 -0
  47. data/lib/badline/chips/m6809/keyed_state.rb +147 -0
  48. data/lib/badline/chips/m6809/opcodes.rb +159 -0
  49. data/lib/badline/chips/m6809/operations.rb +258 -0
  50. data/lib/badline/chips/m6809/plans.rb +143 -0
  51. data/lib/badline/chips/m6809/registers.rb +82 -0
  52. data/lib/badline/chips/m6809/stack.rb +140 -0
  53. data/lib/badline/chips/m6809.rb +13 -0
  54. data/lib/badline/chips/riot/fast_forward.rb +72 -0
  55. data/lib/badline/chips/riot/keyed_parts.rb +69 -0
  56. data/lib/badline/chips/riot/timer.rb +91 -0
  57. data/lib/badline/chips/riot.rb +162 -0
  58. data/lib/badline/chips/rriot/keyed_state.rb +30 -0
  59. data/lib/badline/chips/rriot.rb +134 -0
  60. data/lib/badline/chips/sid/filter.rb +4 -4
  61. data/lib/badline/chips/sid/waveform.rb +6 -6
  62. data/lib/badline/chips/tpi/keyed_state.rb +38 -0
  63. data/lib/badline/chips/tpi.rb +233 -0
  64. data/lib/badline/chips/vic/bank.rb +60 -5
  65. data/lib/badline/chips/vic/sprite.rb +4 -3
  66. data/lib/badline/chips/vic_i/keyed_parts.rb +7 -2
  67. data/lib/badline/chips/vic_i.rb +84 -21
  68. data/lib/badline/data_path.rb +3 -3
  69. data/lib/badline/drives/datasette/keyed_state.rb +9 -1
  70. data/lib/badline/drives/datasette.rb +27 -3
  71. data/lib/badline/drives/ieee_models.rb +54 -0
  72. data/lib/badline/drives/model1001.rb +37 -0
  73. data/lib/badline/drives/model2031.rb +6 -0
  74. data/lib/badline/drives/model4040/disk.rb +62 -0
  75. data/lib/badline/drives/model4040/ip_bus.rb +46 -0
  76. data/lib/badline/drives/model4040/keyed_state.rb +57 -0
  77. data/lib/badline/drives/model4040/mechanism.rb +192 -0
  78. data/lib/badline/drives/model4040/parts.rb +20 -0
  79. data/lib/badline/drives/model4040.rb +179 -0
  80. data/lib/badline/drives/model8050/board.rb +77 -0
  81. data/lib/badline/drives/model8050/disk.rb +163 -0
  82. data/lib/badline/drives/model8050/dos_board.rb +91 -0
  83. data/lib/badline/drives/model8050/fdc_bus.rb +102 -0
  84. data/lib/badline/drives/model8050/idle.rb +181 -0
  85. data/lib/badline/drives/model8050/ieee_port.rb +147 -0
  86. data/lib/badline/drives/model8050/ip_bus.rb +93 -0
  87. data/lib/badline/drives/model8050/keyed_state.rb +163 -0
  88. data/lib/badline/drives/model8050/mechanism.rb +230 -0
  89. data/lib/badline/drives/model8050/parts.rb +37 -0
  90. data/lib/badline/drives/model8050.rb +131 -0
  91. data/lib/badline/drives/model9060/idle.rb +149 -0
  92. data/lib/badline/drives/model9060/keyed_state.rb +123 -0
  93. data/lib/badline/drives/model9060/parts.rb +98 -0
  94. data/lib/badline/drives/model9060/sasi_bus.rb +88 -0
  95. data/lib/badline/drives/model9060/sasi_commands.rb +168 -0
  96. data/lib/badline/drives/model9060/sasi_target.rb +161 -0
  97. data/lib/badline/drives/model9060.rb +195 -0
  98. data/lib/badline/frontend/app.rb +18 -3
  99. data/lib/badline/frontend/boot.rb +4 -3
  100. data/lib/badline/frontend/controls.rb +107 -15
  101. data/lib/badline/frontend/gamepads.rb +3 -3
  102. data/lib/badline/frontend/keys.rb +97 -2
  103. data/lib/badline/frontend/kim1_panel.rb +208 -0
  104. data/lib/badline/frontend/media_slots.rb +8 -1
  105. data/lib/badline/frontend/menu/drive_types.rb +56 -0
  106. data/lib/badline/frontend/menu/menu_dialogs.rb +61 -14
  107. data/lib/badline/frontend/menu/menu_media.rb +47 -14
  108. data/lib/badline/frontend/menu/menu_pages.rb +113 -4
  109. data/lib/badline/frontend/menu/menu_ports.rb +77 -4
  110. data/lib/badline/frontend/menu/pause_menu.rb +6 -2
  111. data/lib/badline/frontend/pet_keyboard.rb +45 -7
  112. data/lib/badline/frontend/screen.rb +1 -1
  113. data/lib/badline/frontend/screens.rb +40 -2
  114. data/lib/badline/frontend/teletype_pane.rb +152 -0
  115. data/lib/badline/frontend/timeline.rb +49 -11
  116. data/lib/badline/frontend.rb +3 -0
  117. data/lib/badline/kernal_trap/dos/disk_commands.rb +80 -0
  118. data/lib/badline/kernal_trap/dos/status.rb +3 -0
  119. data/lib/badline/kernal_trap/dos.rb +18 -5
  120. data/lib/badline/kernal_trap/ieee.rb +4 -2
  121. data/lib/badline/kernal_trap/ieee_layout.rb +95 -43
  122. data/lib/badline/kim1/bus.rb +117 -0
  123. data/lib/badline/kim1/cpu.rb +25 -0
  124. data/lib/badline/kim1/host_terminal.rb +74 -0
  125. data/lib/badline/kim1/key_entry.rb +74 -0
  126. data/lib/badline/kim1/keyed_modules.rb +53 -0
  127. data/lib/badline/kim1/keyed_state.rb +133 -0
  128. data/lib/badline/kim1/panel.rb +176 -0
  129. data/lib/badline/kim1/rriots.rb +28 -0
  130. data/lib/badline/kim1/sound.rb +29 -0
  131. data/lib/badline/kim1/tape.rb +116 -0
  132. data/lib/badline/kim1/teletype.rb +193 -0
  133. data/lib/badline/kim1.rb +190 -0
  134. data/lib/badline/machine/ieee_drives.rb +171 -0
  135. data/lib/badline/machine/keyboard_buffer.rb +20 -3
  136. data/lib/badline/machine/line_sound.rb +122 -0
  137. data/lib/badline/machine/settling.rb +4 -0
  138. data/lib/badline/machine.rb +62 -14
  139. data/lib/badline/media/cbm2_media.rb +29 -0
  140. data/lib/badline/media/extensions.rb +8 -4
  141. data/lib/badline/media/kim1_media.rb +150 -0
  142. data/lib/badline/media/pet_media.rb +3 -1
  143. data/lib/badline/media/program.rb +6 -3
  144. data/lib/badline/media/true_drive.rb +51 -25
  145. data/lib/badline/media.rb +23 -8
  146. data/lib/badline/options/family.rb +108 -6
  147. data/lib/badline/options/help.rb +58 -12
  148. data/lib/badline/options/table.rb +60 -14
  149. data/lib/badline/options/validation.rb +71 -11
  150. data/lib/badline/options.rb +46 -13
  151. data/lib/badline/pet/bus.rb +16 -2
  152. data/lib/badline/pet/cassettes.rb +22 -1
  153. data/lib/badline/pet/ieee_drive.rb +5 -64
  154. data/lib/badline/pet/ieee_ports.rb +7 -0
  155. data/lib/badline/pet/keyboard_ports.rb +9 -2
  156. data/lib/badline/pet/keyed_state.rb +4 -0
  157. data/lib/badline/pet/model.rb +11 -2
  158. data/lib/badline/pet/rom_sets.rb +1 -0
  159. data/lib/badline/pet/sound.rb +5 -105
  160. data/lib/badline/pet/superpet/acia.rb +12 -0
  161. data/lib/badline/pet/superpet/board.rb +221 -0
  162. data/lib/badline/pet/superpet/dongle.rb +81 -0
  163. data/lib/badline/pet/superpet/keyed_modules.rb +63 -0
  164. data/lib/badline/pet/superpet/keyed_parts.rb +76 -0
  165. data/lib/badline/pet/superpet/m6809.rb +32 -0
  166. data/lib/badline/pet/superpet/waterloo_traps.rb +199 -0
  167. data/lib/badline/pet/superpet.rb +216 -0
  168. data/lib/badline/pet.rb +33 -6
  169. data/lib/badline/plus4/acia.rb +4 -298
  170. data/lib/badline/plus4/keyed_parts.rb +0 -44
  171. data/lib/badline/plus4/keyed_state.rb +2 -0
  172. data/lib/badline/plus4.rb +2 -0
  173. data/lib/badline/roms/1001/fdc-251257-02a.rom +0 -0
  174. data/lib/badline/roms/4040/dos-1.2.rom +0 -0
  175. data/lib/badline/roms/4040/dos-2.1-rev.rom +0 -0
  176. data/lib/badline/roms/4040/dos-2.1.rom +0 -0
  177. data/lib/badline/roms/4040/fdc-901466-02.rom +0 -0
  178. data/lib/badline/roms/4040/fdc-901466-04.rom +0 -0
  179. data/lib/badline/roms/8050/dos-2.7.rom +0 -0
  180. data/lib/badline/roms/8050/fdc-901869-01.rom +0 -0
  181. data/lib/badline/roms/8050/fdc-901885-04.rom +0 -0
  182. data/lib/badline/roms/9060/dos-3.0.rom +0 -0
  183. data/lib/badline/roms/9060/sasi-300515-revb.rom +0 -0
  184. data/lib/badline/roms/README +277 -0
  185. data/lib/badline/roms/cbm2/basic128-02b.rom +0 -0
  186. data/lib/badline/roms/cbm2/basic128.rom +0 -0
  187. data/lib/badline/roms/cbm2/basic256.rom +0 -0
  188. data/lib/badline/roms/cbm2/characters-600.rom +0 -0
  189. data/lib/badline/roms/cbm2/characters-700.rom +0 -0
  190. data/lib/badline/roms/cbm2/kernal-03b.rom +0 -0
  191. data/lib/badline/roms/cbm2/kernal-04a.rom +0 -0
  192. data/lib/badline/roms/cbm2/p500-basic.rom +0 -0
  193. data/lib/badline/roms/cbm2/p500-kernal.rom +0 -0
  194. data/lib/badline/roms/kim1/6530-002.rom +0 -0
  195. data/lib/badline/roms/kim1/6530-003.rom +0 -0
  196. data/lib/badline/roms/pet/characters-superpet.rom +0 -0
  197. data/lib/badline/roms/pet/superpet-a000.rom +0 -0
  198. data/lib/badline/roms/pet/superpet-c000.rom +0 -0
  199. data/lib/badline/roms/pet/superpet-e000-50hz.rom +0 -0
  200. data/lib/badline/roms/pet/superpet-e000.rom +0 -0
  201. data/lib/badline/snapshot/cbm2_setup.rb +53 -0
  202. data/lib/badline/snapshot/container.rb +9 -2
  203. data/lib/badline/snapshot/field_doc/markdown.rb +4 -2
  204. data/lib/badline/snapshot/field_doc.rb +59 -3
  205. data/lib/badline/snapshot/image.rb +65 -9
  206. data/lib/badline/snapshot/kim1_setup.rb +28 -0
  207. data/lib/badline/snapshot/pet_setup.rb +27 -11
  208. data/lib/badline/snapshot/storage_setup.rb +4 -1
  209. data/lib/badline/snapshot/vic20_setup.rb +11 -7
  210. data/lib/badline/snapshot/vice/cbm2.rb +93 -0
  211. data/lib/badline/snapshot/vice/cbm2_acia.rb +75 -0
  212. data/lib/badline/snapshot/vice/cbm2_crtc.rb +127 -0
  213. data/lib/badline/snapshot/vice/cbm2_mains.rb +49 -0
  214. data/lib/badline/snapshot/vice/cbm2_mem.rb +75 -0
  215. data/lib/badline/snapshot/vice/cbm2_restore.rb +204 -0
  216. data/lib/badline/snapshot/vice/cbm2_tpi.rb +58 -0
  217. data/lib/badline/snapshot/vice/cias.rb +13 -9
  218. data/lib/badline/snapshot/vice/p500.rb +96 -0
  219. data/lib/badline/snapshot/vice/p500_restore.rb +130 -0
  220. data/lib/badline/snapshot/vice/p500_vicii.rb +166 -0
  221. data/lib/badline/snapshot/vice/pet.rb +20 -10
  222. data/lib/badline/snapshot/vice/pet_cpu.rb +42 -5
  223. data/lib/badline/snapshot/vice/pet_crtc.rb +25 -17
  224. data/lib/badline/snapshot/vice/pet_drive.rb +180 -0
  225. data/lib/badline/snapshot/vice/pet_drive8050.rb +336 -0
  226. data/lib/badline/snapshot/vice/pet_mem.rb +49 -5
  227. data/lib/badline/snapshot/vice/pet_restore.rb +100 -7
  228. data/lib/badline/snapshot/vice/pet_via.rb +6 -5
  229. data/lib/badline/snapshot/vice/sid_registers.rb +2 -1
  230. data/lib/badline/snapshot/vice/vicii_import.rb +10 -6
  231. data/lib/badline/snapshot/vice.rb +3 -0
  232. data/lib/badline/snapshot.rb +15 -3
  233. data/lib/badline/status.rb +30 -30
  234. data/lib/badline/storage/crt_file.rb +7 -1
  235. data/lib/badline/storage/d64_image.rb +7 -0
  236. data/lib/badline/storage/d67_image.rb +55 -0
  237. data/lib/badline/storage/d80_image.rb +17 -1
  238. data/lib/badline/storage/d81_image.rb +14 -0
  239. data/lib/badline/storage/d90_image.rb +180 -0
  240. data/lib/badline/storage/disk_image/bam.rb +18 -3
  241. data/lib/badline/storage/disk_image/formatting.rb +69 -0
  242. data/lib/badline/storage/disk_image/writing.rb +37 -2
  243. data/lib/badline/storage/disk_image.rb +2 -0
  244. data/lib/badline/storage/kim_tape.rb +254 -0
  245. data/lib/badline/storage/papertape.rb +90 -0
  246. data/lib/badline/storage.rb +5 -0
  247. data/lib/badline/support/cbm2_checkpoint.rb +48 -0
  248. data/lib/badline/support/kim1_checkpoint.rb +41 -0
  249. data/lib/badline/support/p500_checkpoint.rb +35 -0
  250. data/lib/badline/support/superpet_checkpoint.rb +35 -0
  251. data/lib/badline/version.rb +1 -1
  252. data/lib/badline/vic20/keyed_modules.rb +6 -3
  253. data/lib/badline/vic20/keyed_state.rb +6 -4
  254. data/lib/badline/vic20.rb +26 -12
  255. data/lib/badline.rb +2 -0
  256. metadata +148 -2
@@ -12,16 +12,28 @@ module Badline
12
12
  # With either Alt down, a key that types a character presses the PET
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
+ #
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
+ #
22
+ # The CBM-II's keyboard goes the same way (Keys.cbm2_key), and there
23
+ # Alt with a key presses one of the keys the host has none for
24
+ # (Keys.cbm2_alt_key).
15
25
  class PETKeyboard
16
26
  LSHIFT = 225
17
27
  RSHIFT = 229
18
28
  LALT = 226
19
29
  RALT = 230
20
30
 
21
- # +layout+ is the PET's keyboard, :graphics or :business.
31
+ # +layout+ is the PET's keyboard, :graphics or :business, or :cbm2
32
+ # for the CBM-II's.
22
33
  def initialize(keyboard, layout)
23
34
  @keyboard = keyboard
24
35
  @layout = layout
36
+ @shift = layout == :cbm2 ? :shift : :lshift
25
37
  @held = {}
26
38
  @left_shift = false
27
39
  @right_shift = false
@@ -29,8 +41,12 @@ module Badline
29
41
  @right_alt = false
30
42
  @forced = -1
31
43
  @forced_shift = false
44
+ @positional = false
32
45
  end
33
46
 
47
+ # Whether the keys go by position, for the SuperPET's APL characters.
48
+ attr_writer :positional
49
+
34
50
  def key(scancode, down)
35
51
  if [LSHIFT, RSHIFT].include?(scancode)
36
52
  shift_key(scancode, down)
@@ -43,6 +59,11 @@ module Badline
43
59
  end
44
60
  end
45
61
 
62
+ # Whether a key the scancode pressed is still down.
63
+ def holds?(scancode) = @held.key?(scancode)
64
+
65
+ def alt? = @left_alt || @right_alt
66
+
46
67
  # Lets go of every key, SHIFT among them.
47
68
  def release_all
48
69
  @held.each_value { |key| @keyboard.release(key) }
@@ -59,7 +80,10 @@ module Badline
59
80
 
60
81
  def key_down(scancode)
61
82
  key_up(scancode) if @held.key?(scancode)
62
- key, shift = alt? ? shifted_key(scancode) : Keys.pet_key(scancode, shifted?, @layout)
83
+ key, shift = if alt? then shifted_key(scancode)
84
+ elsif @positional then positional_key(scancode)
85
+ else layout_key(scancode, shifted?)
86
+ end
63
87
  return if key == :none
64
88
 
65
89
  unless shift == :host
@@ -99,10 +123,17 @@ module Badline
99
123
  end
100
124
  end
101
125
 
126
+ def layout_key(scancode, shift)
127
+ @layout == :cbm2 ? Keys.cbm2_key(scancode, shift) : Keys.pet_key(scancode, shift, @layout)
128
+ end
129
+
102
130
  # The PET key for the scancode with Alt down: for a letter or a key
103
131
  # that types a character, the key its unshifted character is on, with
104
- # SHIFT, and for any other key the one it presses without Alt.
132
+ # SHIFT, and for any other key the one it presses without Alt. The
133
+ # CBM-II's is the one Keys.cbm2_alt_key names.
105
134
  def shifted_key(scancode)
135
+ return Keys.cbm2_alt_key(scancode) if @layout == :cbm2
136
+
106
137
  key, shift = Keys.pet_key(scancode, false, @layout)
107
138
  return [key, shift] if key == :none
108
139
  return [key, shift] unless scancode.between?(4, 29) || Keys::US_CHARACTERS.key?(scancode)
@@ -110,14 +141,21 @@ module Badline
110
141
  [key, :on]
111
142
  end
112
143
 
113
- def shifted? = @left_shift || @right_shift
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)
114
149
 
115
- def alt? = @left_alt || @right_alt
150
+ [key, :host]
151
+ end
152
+
153
+ def shifted? = @left_shift || @right_shift
116
154
 
117
155
  def sync_shift
118
156
  shift = @forced == -1 ? shifted? : @forced_shift
119
- @keyboard.release(:lshift)
120
- @keyboard.press(:lshift) if shift
157
+ @keyboard.release(@shift)
158
+ @keyboard.press(@shift) if shift
121
159
  end
122
160
  end
123
161
  end
@@ -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
@@ -25,7 +25,8 @@ module Badline
25
25
  LOCKS = %w[caps_lock forty_eighty].freeze
26
26
 
27
27
  # What's wrong with the events for the family's machine, the first
28
- # key or menu page that names nothing, or an empty string.
28
+ # key or menu page that names nothing, or an empty string. The
29
+ # CBM-II's P500 is family :p500 here, for its joysticks.
29
30
  def self.error(events, family)
30
31
  event = events.find { |candidate| candidate.action == "key" && !key?(candidate.argument, family) }
31
32
  return "no such key: #{event.argument}" unless event.nil?
@@ -38,23 +39,28 @@ module Badline
38
39
  # sound. An empty one is its first.
39
40
  def self.page?(name) = name.empty? || PauseMenu::PAGES.any? { |page| page.to_s == name }
40
41
 
41
- # Whether the family's machine has a key of the name. The Plus/4 has
42
- # no RESTORE, and the PET neither that nor joysticks.
42
+ # Whether the family's machine has a key of the name, the CBM-II's
43
+ # P500's as family :p500. The Plus/4 has no RESTORE, the P500 none
44
+ # either, and the PET and the CBM-II's B series neither that nor
45
+ # joysticks.
43
46
  def self.key?(name, family)
44
- pet = family == :pet
45
- (name == "restore" && !pet && family != :plus4) || (family == :c128 && LOCKS.include?(name)) ||
47
+ return !kim1_key(name).empty? if family == :kim1
48
+
49
+ pet = %i[pet cbm2].include?(family)
50
+ (name == "restore" && !pet && !%i[plus4 p500].include?(family)) || (family == :c128 && LOCKS.include?(name)) ||
46
51
  !keyboard_key(name, family).nil? || (!pet && !joystick_direction(name).nil?)
47
52
  end
48
53
 
49
54
  # The family's Keyboard key for a name such as space, return or a,
50
55
  # on the C128 esc or keypad1, on the Plus/4 help or crsr_up, or on
51
- # the PET rvs or kp1, of either of its keyboards. The shifted cursor
52
- # keys, such as cursor_up, are the C64's and the C128's.
56
+ # the PET rvs or kp1, of either of its keyboards, or on the CBM-II
57
+ # escape, norm_graph or kp00. The shifted cursor keys, such as
58
+ # cursor_up, are the C64's and the C128's.
53
59
  def self.keyboard_key(name, family)
54
60
  return nil if name == "none"
55
61
 
56
62
  key = keyboard_matrix(family).flatten.find { |candidate| candidate.to_s == name }
57
- return key unless key.nil? && !%i[plus4 pet].include?(family)
63
+ return key unless key.nil? && !%i[plus4 pet cbm2 p500].include?(family)
58
64
 
59
65
  Input::Keyboard::COMBINATIONS.keys.find { |candidate| candidate.to_s == name }
60
66
  end
@@ -64,10 +70,18 @@ module Badline
64
70
  when :c128 then C128::KEYBOARD_MATRIX
65
71
  when :plus4 then Plus4::KeyboardPort::MATRIX
66
72
  when :pet then PET::KeyboardPorts::GRAPHICS_MATRIX + PET::KeyboardPorts::BUSINESS_MATRIX
73
+ when :cbm2, :p500 then CBM2::KeyboardPorts::MATRIX
67
74
  else Input::Keyboard::C64_MATRIX
68
75
  end
69
76
  end
70
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
+
71
85
  # The direction of joy1-NAME or joy2-NAME, such as up or fire.
72
86
  def self.joystick_direction(name)
73
87
  return nil unless name.start_with?("joy1-") || name.start_with?("joy2-")
@@ -145,7 +159,7 @@ module Badline
145
159
  def perform(computer, event)
146
160
  case event.action
147
161
  when "key" then press(computer, event)
148
- when "type" then computer.type_text(event.argument.gsub("\\n", "\r").gsub("\\r", "\r"))
162
+ when "type" then type(computer, event.argument)
149
163
  when "insert" then insert(computer, event.argument)
150
164
  when "eject" then eject(computer, event.argument)
151
165
  when "reset" then computer.reset!
@@ -153,8 +167,15 @@ module Badline
153
167
  end
154
168
  end
155
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
+
156
176
  def press(computer, event)
157
177
  name = event.argument
178
+ return press_kim1(computer, event) if computer.family == :kim1
158
179
  return lock(computer, name) if LOCKS.include?(name)
159
180
 
160
181
  if name == "restore"
@@ -171,6 +192,17 @@ module Badline
171
192
  hold(event)
172
193
  end
173
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
+
174
206
  # CAPS LOCK and 40/80 DISPLAY lock: a push puts one down if it was up,
175
207
  # and up if it was down.
176
208
  def lock(computer, name)
@@ -194,6 +226,8 @@ module Badline
194
226
  def release(computer, event)
195
227
  return computer.release_cartridge_button if event.action == "freeze" && computer.family == :c64
196
228
 
229
+ return computer.release_key if computer.family == :kim1
230
+
197
231
  name = event.argument
198
232
  return computer.release_restore if name == "restore"
199
233
 
@@ -228,9 +262,13 @@ module Badline
228
262
  end
229
263
 
230
264
  # Whether there's one in to eject. A VIC-20 has no cartridge to take
231
- # out yet, and a PET no expansion port.
265
+ # out yet, a PET no expansion port, and a CBM-II no datasette, and
266
+ # its cartridge stays in.
232
267
  def ejectable?(computer, what)
233
- (what != "cartridge" || !%i[vic20 pet].include?(computer.family)) && inserted?(computer, what)
268
+ family = computer.family
269
+ return false if family == :kim1 || (family == :cbm2 && what != "disk")
270
+
271
+ (what != "cartridge" || !%i[vic20 pet].include?(family)) && inserted?(computer, what)
234
272
  end
235
273
 
236
274
  def inserted?(computer, what)
@@ -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"
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module KernalTrap
5
+ class DOS
6
+ # N, R and C: the commands that format a disk, rename a file and copy
7
+ # files. Each takes its names after a colon. A name can't hold a
8
+ # wildcard, and R and C take their new name before an "=". The DOS
9
+ # here has one drive, so a file on drive 1 is never found, and C
10
+ # without a colon, which copies a whole disk between the drives of a
11
+ # dual drive, and D, which duplicates one, are invalid commands, as on
12
+ # a single drive.
13
+ module DiskCommands
14
+ WILDCARD = /[*?]/
15
+ OTHER_DRIVE = /\A[1-9]\d*:/
16
+ DRIVE_ZERO = /\A0+:/
17
+
18
+ private
19
+
20
+ # N with an ID after a comma formats the disk, and without one only
21
+ # clears it. The ID is the two bytes after the comma, which takes the
22
+ # carriage return PRINT# ends the command with when the ID is one
23
+ # character. A comma with nothing after it gives no ID.
24
+ def new_disk(text)
25
+ colon = text.index(":")
26
+ return report(NO_FILE_GIVEN) unless colon
27
+
28
+ command = text.chomp("\r")
29
+ name, id = command[(colon + 1)..].split(",", 2)
30
+ return report(SYNTAX_ERROR) if name.to_s.match?(/[*?=]/)
31
+ return report(WRITE_PROTECT_ON) unless @storage.respond_to?(:format)
32
+
33
+ id = id.to_s.empty? ? nil : text.bytes[(command.index(",") + 1), 2]
34
+ writing { @storage.format(name.to_s, id && (id + [Storage::DiskImage::NAME_PADDING]).first(2)) }
35
+ end
36
+
37
+ # R takes the new name, then the file's.
38
+ def rename_file(text)
39
+ new_name, old_names = file_names(text, NO_FILE_GIVEN)
40
+ return unless new_name
41
+ return report(SYNTAX_ERROR) unless old_names.one?
42
+ return report(WRITE_PROTECT_ON) unless @storage.respond_to?(:rename)
43
+ return report(FILE_NOT_FOUND) if old_names.first.match?(OTHER_DRIVE)
44
+
45
+ writing { @storage.rename(Storage.strip_drive_prefix(old_names.first), new_name) }
46
+ end
47
+
48
+ # C takes the new name, then the files it joins, separated by
49
+ # commas. A first file named with drive 0 that is the new file
50
+ # itself has the others added to its end.
51
+ def copy_file(text)
52
+ new_name, old_names = file_names(text, INVALID_COMMAND)
53
+ return unless new_name
54
+ return report(WRITE_PROTECT_ON) unless @storage.respond_to?(:copy)
55
+ return report(FILE_NOT_FOUND) if old_names.any? { |name| name.match?(OTHER_DRIVE) }
56
+
57
+ names = old_names.map { |name| Storage.strip_drive_prefix(name) }
58
+ in_place = old_names.first.match?(DRIVE_ZERO) && names.first == new_name
59
+ writing { in_place ? @storage.concat(new_name, names.drop(1)) : @storage.copy(new_name, names) }
60
+ end
61
+
62
+ # The new name and the old ones of "new=old,...", or nil once it
63
+ # has reported what's wrong.
64
+ def file_names(text, missing_colon)
65
+ colon = text.index(":")
66
+ return report(missing_colon) unless colon
67
+
68
+ new_name, rest = text[(colon + 1)..].split("=", 2)
69
+ new_name = new_name.to_s
70
+ return report(SYNTAX_ERROR) if rest.nil? || new_name.include?(",")
71
+
72
+ old_names = rest.empty? ? [""] : rest.split(",", -1)
73
+ return report(SYNTAX_ERROR) if [new_name, *old_names].any? { |name| name.match?(WILDCARD) }
74
+
75
+ [new_name, old_names]
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -15,8 +15,11 @@ module Badline
15
15
  28 => "WRITE ERROR",
16
16
  29 => "DISK ID MISMATCH",
17
17
  30 => "SYNTAX ERROR",
18
+ 31 => "SYNTAX ERROR",
19
+ 34 => "SYNTAX ERROR",
18
20
  62 => "FILE NOT FOUND",
19
21
  63 => "FILE EXISTS",
22
+ 64 => "FILE TYPE MISMATCH",
20
23
  65 => "NO BLOCK",
21
24
  66 => "ILLEGAL TRACK OR SECTOR",
22
25
  70 => "NO CHANNEL",
@@ -3,6 +3,7 @@
3
3
  require "badline/kernal_trap/dos/block_commands"
4
4
  require "badline/kernal_trap/dos/channel"
5
5
  require "badline/kernal_trap/dos/channels"
6
+ require "badline/kernal_trap/dos/disk_commands"
6
7
  require "badline/kernal_trap/dos/memory"
7
8
  require "badline/kernal_trap/dos/parameters"
8
9
  require "badline/kernal_trap/dos/status"
@@ -15,10 +16,12 @@ module Badline
15
16
  # command channel and its status message. `U1` reads a raw block into a
16
17
  # buffer channel, which is how block-access loaders bypass the KERNAL's
17
18
  # LOAD. A disk image whose host file takes writes takes files, blocks
18
- # and BAM changes too. Other disks are write-protected, though storage
19
- # that can write files still takes a SAVE handed over whole.
19
+ # and BAM changes too, and N, R and C format it, rename and copy files.
20
+ # Other disks are write-protected, though storage that can write files
21
+ # still takes a SAVE handed over whole.
20
22
  class DOS
21
23
  include BlockCommands
24
+ include DiskCommands
22
25
  include Writes
23
26
 
24
27
  COMMAND_CHANNEL = 15
@@ -28,6 +31,8 @@ module Badline
28
31
  FILES_SCRATCHED = 1
29
32
  WRITE_PROTECT_ON = 26
30
33
  SYNTAX_ERROR = 30
34
+ INVALID_COMMAND = 31
35
+ NO_FILE_GIVEN = 34
31
36
  FILE_NOT_FOUND = 62
32
37
  FILE_EXISTS = 63
33
38
  NO_BLOCK = 65
@@ -60,11 +65,15 @@ module Badline
60
65
  /\AV/i => :initialized,
61
66
  /\AB-A\s*:?\s*(.*)/i => :block_allocate,
62
67
  /\AB-F\s*:?\s*(.*)/i => :block_free,
63
- /\AS[^:]*:(.*)/im => :scratch
68
+ /\AS[^:]*:(.*)/im => :scratch,
69
+ /\AN(.*)/im => :new_disk,
70
+ /\AR(.*)/im => :rename_file,
71
+ /\AC(.*)/im => :copy_file,
72
+ /\AD/i => :invalid
64
73
  }.freeze
65
74
 
66
75
  # Commands that take their argument as text rather than numbers.
67
- TEXT_COMMANDS = %i[scratch].freeze
76
+ TEXT_COMMANDS = %i[scratch new_disk rename_file copy_file].freeze
68
77
 
69
78
  # A drive powers on reporting its DOS version, as a reset does. It
70
79
  # can take the RAM of a drive it replaces, and initializes its disk.
@@ -197,7 +206,7 @@ module Badline
197
206
  return report(SYNTAX_ERROR) unless action
198
207
 
199
208
  action = USER_TABLE[(command.getbyte(1) - 1) & 0x0f] if action == :user
200
- argument = pattern.match(command)[1]
209
+ argument = pattern.match(action == :new_disk ? text.lstrip : command)[1]
201
210
  run_command(action, TEXT_COMMANDS.include?(action) ? argument : Parameters.parse(argument))
202
211
  end
203
212
 
@@ -218,6 +227,10 @@ module Badline
218
227
  when :block_allocate then block_allocate(arguments)
219
228
  when :block_free then block_free(arguments)
220
229
  when :scratch then scratch(arguments)
230
+ when :new_disk then new_disk(arguments)
231
+ when :rename_file then rename_file(arguments)
232
+ when :copy_file then copy_file(arguments)
233
+ when :invalid then report(INVALID_COMMAND)
221
234
  when :cold_reset then reset(cold: true)
222
235
  when :warm_reset then reset(cold: false)
223
236
  when :memory_write then memory_write(arguments)