psiemu 0.0.0__tar.gz

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.
psiemu-0.0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Jason Morley
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
psiemu-0.0.0/PKG-INFO ADDED
@@ -0,0 +1,38 @@
1
+ Metadata-Version: 2.4
2
+ Name: psiemu
3
+ Version: 0.0.0
4
+ Author-email: Jason Morley <hello@jbmorley.co.uk>
5
+ License-Expression: MIT
6
+ Description-Content-Type: text/markdown
7
+ License-File: LICENSE
8
+ Dynamic: license-file
9
+
10
+ # PsiEmu
11
+
12
+ Lightweight TUI launcher for Psion MAME emulators.
13
+
14
+ ![](images/preview.gif)
15
+
16
+ ## Usage
17
+
18
+ ```sh
19
+ git clone ssh://git@codeberg.org/psion/psiemu.git
20
+ cd psiemu
21
+
22
+ export PSIEMU_ROM_PATH=~/path/to/roms
23
+ ./psiemu
24
+ ```
25
+
26
+ ## Development
27
+
28
+ Print specific details of MAME systems using `-listroms` and `-listbios`. For exmaple,
29
+
30
+ ```sh
31
+ $ mame psion3mx_fr -listbios Chii
32
+ BIOS options for system Series 3mx (French) (psion3mx_fr):
33
+ 620f V6.20F/FRE
34
+ $ mame psion3mx_fr -listroms Chii
35
+ ROMs required for driver "psion3mx_fr".
36
+ Name Size Checksum
37
+ maple_v6.20f_fre.bin 2097152 CRC(b4fc57f4) SHA1(26588937d811adf08b973a0188927707d1f6a6e4)
38
+ ```
psiemu-0.0.0/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # PsiEmu
2
+
3
+ Lightweight TUI launcher for Psion MAME emulators.
4
+
5
+ ![](images/preview.gif)
6
+
7
+ ## Usage
8
+
9
+ ```sh
10
+ git clone ssh://git@codeberg.org/psion/psiemu.git
11
+ cd psiemu
12
+
13
+ export PSIEMU_ROM_PATH=~/path/to/roms
14
+ ./psiemu
15
+ ```
16
+
17
+ ## Development
18
+
19
+ Print specific details of MAME systems using `-listroms` and `-listbios`. For exmaple,
20
+
21
+ ```sh
22
+ $ mame psion3mx_fr -listbios Chii
23
+ BIOS options for system Series 3mx (French) (psion3mx_fr):
24
+ 620f V6.20F/FRE
25
+ $ mame psion3mx_fr -listroms Chii
26
+ ROMs required for driver "psion3mx_fr".
27
+ Name Size Checksum
28
+ maple_v6.20f_fre.bin 2097152 CRC(b4fc57f4) SHA1(26588937d811adf08b973a0188927707d1f6a6e4)
29
+ ```
@@ -0,0 +1,16 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta:__legacy__"
4
+
5
+ [project]
6
+ name = "psiemu"
7
+ readme = "README.md"
8
+ license = "MIT"
9
+ authors = [
10
+ {name = "Jason Morley", email = "hello@jbmorley.co.uk"}
11
+ ]
12
+ dependencies = []
13
+ dynamic = ["version"]
14
+
15
+ [project.scripts]
16
+ psiemu = "src.psiemu:main"
psiemu-0.0.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env python3
2
+
3
+ # Copyright (c) 2025 Jason Morley
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ from . import *
@@ -0,0 +1,33 @@
1
+ # Copyright (c) 2025 Jason Morley
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in all
11
+ # copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ # SOFTWARE.
20
+
21
+ import os
22
+ import sys
23
+
24
+ if not __package__:
25
+ # Make CLI runnable from source tree with
26
+ # python src/package
27
+ package_source_path = os.path.dirname(os.path.dirname(__file__))
28
+ sys.path.insert(0, package_source_path)
29
+
30
+
31
+ if __name__ == "__main__":
32
+ from src.psiemu import main
33
+ main()
@@ -0,0 +1,38 @@
1
+ Metadata-Version: 2.4
2
+ Name: psiemu
3
+ Version: 0.0.0
4
+ Author-email: Jason Morley <hello@jbmorley.co.uk>
5
+ License-Expression: MIT
6
+ Description-Content-Type: text/markdown
7
+ License-File: LICENSE
8
+ Dynamic: license-file
9
+
10
+ # PsiEmu
11
+
12
+ Lightweight TUI launcher for Psion MAME emulators.
13
+
14
+ ![](images/preview.gif)
15
+
16
+ ## Usage
17
+
18
+ ```sh
19
+ git clone ssh://git@codeberg.org/psion/psiemu.git
20
+ cd psiemu
21
+
22
+ export PSIEMU_ROM_PATH=~/path/to/roms
23
+ ./psiemu
24
+ ```
25
+
26
+ ## Development
27
+
28
+ Print specific details of MAME systems using `-listroms` and `-listbios`. For exmaple,
29
+
30
+ ```sh
31
+ $ mame psion3mx_fr -listbios Chii
32
+ BIOS options for system Series 3mx (French) (psion3mx_fr):
33
+ 620f V6.20F/FRE
34
+ $ mame psion3mx_fr -listroms Chii
35
+ ROMs required for driver "psion3mx_fr".
36
+ Name Size Checksum
37
+ maple_v6.20f_fre.bin 2097152 CRC(b4fc57f4) SHA1(26588937d811adf08b973a0188927707d1f6a6e4)
38
+ ```
@@ -0,0 +1,11 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/__init__.py
5
+ src/__main__.py
6
+ src/psiemu.py
7
+ src/psiemu.egg-info/PKG-INFO
8
+ src/psiemu.egg-info/SOURCES.txt
9
+ src/psiemu.egg-info/dependency_links.txt
10
+ src/psiemu.egg-info/entry_points.txt
11
+ src/psiemu.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ psiemu = src.psiemu:main
@@ -0,0 +1,3 @@
1
+ __init__
2
+ __main__
3
+ psiemu
@@ -0,0 +1,512 @@
1
+ #!/usr/bin/env python3
2
+
3
+ # Copyright (c) 2025 Jason Morley
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ import argparse
24
+ import curses
25
+ import os
26
+ import subprocess
27
+
28
+ from dataclasses import dataclass
29
+
30
+
31
+ PROFILES = [
32
+
33
+ {
34
+ "name": "Psion",
35
+ "devices": [
36
+
37
+ {
38
+ "id": "psion3",
39
+ "title": "Series 3",
40
+ "resolution": (240, 80),
41
+ "scale": 2,
42
+ "variants": [
43
+ {
44
+ "name": "V1.91F",
45
+ "bios": "191f",
46
+ "languages": [
47
+ "en-GB",
48
+ "fr-FR",
49
+ "de-DE",
50
+ "es-ES",
51
+ "it-IT",
52
+ ]
53
+ },
54
+ {
55
+ "name": "V1.80F",
56
+ "bios": "180f",
57
+ "languages": [
58
+ "en-GB",
59
+ "fr-FR",
60
+ "de-DE",
61
+ "it-IT",
62
+ ],
63
+ },
64
+ ]
65
+ },
66
+
67
+ {
68
+ "id": "psion3s",
69
+ "title": "Series 3s",
70
+ "description": "Series 3 variant with built-in Sheet program",
71
+ "resolution": (240, 80),
72
+ "scale": 2,
73
+ "variants": [
74
+ {
75
+ "name": "V1.91F",
76
+ "bios": "191f",
77
+ "languages": [
78
+ "en-GB",
79
+ ],
80
+ },
81
+ ],
82
+ },
83
+
84
+ {
85
+ "id": "psion3a",
86
+ "title": "Series 3a (1 MB ROM)",
87
+ "description": "1MB ROM variant of the Series 3a",
88
+ "resolution": (480, 160),
89
+ "variants": [
90
+ {
91
+ "name": "V3.22F",
92
+ "bios": "322f",
93
+ "languages": [
94
+ "en-GB",
95
+ ],
96
+ },
97
+ ],
98
+ },
99
+
100
+ {
101
+ "id": "psion3a",
102
+ "title": "Series 3a (2 MB ROM)",
103
+ "description": "2MB ROM variant of the Series 3a with built-in Spell and Patience programs",
104
+ "resolution": (480, 160),
105
+ "variants": [
106
+ {
107
+ "name": "V3.40F",
108
+ "id": "psion3a2",
109
+ "bios": "340f",
110
+ "languages": [
111
+ "en-GB",
112
+ ],
113
+ },
114
+ {
115
+ "name": "V3.40F",
116
+ "id": "psion3a2_it",
117
+ "bios": "340f",
118
+ "languages": [
119
+ "it-IT",
120
+ ],
121
+ },
122
+ {
123
+ "name": "V3.40F",
124
+ "id": "psion3a2_us",
125
+ "bios": "340f",
126
+ "languages": [
127
+ "en-US",
128
+ ],
129
+ },
130
+ {
131
+ "name": "V3.41F",
132
+ "id": "psion3a2_de",
133
+ "bios": "341f",
134
+ "languages": [
135
+ "de-DE",
136
+ ],
137
+ },
138
+ {
139
+ "name": "V3.43F",
140
+ "id": "psion3a2_ru",
141
+ "bios": "343f",
142
+ "languages": [
143
+ "ru-RU",
144
+ ],
145
+ },
146
+ ],
147
+ },
148
+
149
+ {
150
+ "id": "psion3c",
151
+ "title": "Series 3c",
152
+ "resolution": (480, 160),
153
+ "variants": [
154
+ {
155
+ "name": "V5.20F",
156
+ "bios": "520f",
157
+ "languages": [
158
+ "en-GB",
159
+ ],
160
+ },
161
+ ],
162
+ },
163
+
164
+ {
165
+ "title": "Series 3mx",
166
+ "resolution": (480, 160),
167
+ "variants": [
168
+ {
169
+ "name": "V6.16F",
170
+ "id": "psion3mx",
171
+ "bios": "616f",
172
+ "languages": [
173
+ "en-GB",
174
+ ],
175
+ },
176
+ {
177
+ "name": "V6.17F",
178
+ "id": "psion3mx_nl",
179
+ "bios": "617f",
180
+ "languages": [
181
+ "nl-NL",
182
+ ],
183
+ },
184
+ {
185
+ "name": "V6.20F",
186
+ "id": "psion3mx_fr",
187
+ "bios": "620f",
188
+ "languages": [
189
+ "fr-FR",
190
+ ],
191
+ },
192
+ ]
193
+ },
194
+
195
+ {
196
+ "title": "Siena",
197
+ "resolution": (240, 160),
198
+ "variants": [
199
+ {
200
+ "name": "V4.20F",
201
+ "id": "siena",
202
+ "bios": "420f",
203
+ "languages": [
204
+ "en-GB",
205
+ ],
206
+ },
207
+ {
208
+ "name": "V4.21F",
209
+ "id": "siena_fr",
210
+ "bios": "421f",
211
+ "languages": [
212
+ "fr-FR",
213
+ ],
214
+ },
215
+ ]
216
+ },
217
+
218
+ {
219
+ "id": "psionwa",
220
+ "title": "Workabout",
221
+ "resolution": (240, 100),
222
+ "variants": [
223
+ {
224
+ "name": "V2.40F",
225
+ "bios": "240f",
226
+ "languages": [
227
+ "en-GB",
228
+ ],
229
+ },
230
+ {
231
+ "name": "V1.00F",
232
+ "bios": "100f",
233
+ "languages": [
234
+ "en-GB",
235
+ ],
236
+ },
237
+ {
238
+ "name": "V0.24B",
239
+ "bios": "024b",
240
+ "languages": [
241
+ "en-GB",
242
+ ],
243
+ },
244
+ ],
245
+ },
246
+
247
+ {
248
+ "id": "psionwamx",
249
+ "title": "Workabout MX",
250
+ "resolution": (240, 100),
251
+ "variants": [
252
+ {
253
+ "name": "V7.20F",
254
+ "bios": "720f",
255
+ "languages": [
256
+ "en-GB",
257
+ ],
258
+ },
259
+ ],
260
+ },
261
+ ],
262
+ },
263
+
264
+ {
265
+ "name": "Acorn",
266
+ "devices": [
267
+
268
+ {
269
+ "id": "pocketbk",
270
+ "title": "Acorn Pocket Book",
271
+ "resolution": (240, 80),
272
+ "scale": 2,
273
+ "variants": [
274
+ {
275
+ "name": "V1.91F",
276
+ "bios": "191f",
277
+ "languages": [
278
+ "en-GB",
279
+ ],
280
+ }
281
+ ]
282
+ },
283
+
284
+ {
285
+ "id": "pocketbk2",
286
+ "title": "Acorn Pocket Book II",
287
+ "resolution": (480, 160),
288
+ "variants": [
289
+ {
290
+ "name": "V1.30F",
291
+ "bios": "130f",
292
+ "languages": [
293
+ "en-GB",
294
+ ],
295
+ }
296
+ ]
297
+ },
298
+
299
+ ],
300
+ },
301
+
302
+ ]
303
+
304
+ LANGUAGES = {
305
+ "de-DE": {"name": "German", "symbol": "de"},
306
+ "en-GB": {"name": "British English", "symbol": "en"},
307
+ "en-US": {"name": "American English", "symbol": "us"},
308
+ "es-ES": {"name": "Spanish", "symbol": "es"},
309
+ "fr-FR": {"name": "French", "symbol": "fr"},
310
+ "it-IT": {"name": "Italian", "symbol": "it"},
311
+ "nl-NL": {"name": "Dutch", "symbol": "nl"},
312
+ "ru-RU": {"name": "Russian", "symbol": "ru"},
313
+ }
314
+
315
+ NOTES = """Special keys:
316
+
317
+ - Menu -> F11 (Shift + F11 on macOS)
318
+ - Psion -> Alt
319
+ - Help -> F10
320
+
321
+ Silkscreen buttons:
322
+
323
+ - System -> F1
324
+ - Data -> F2
325
+ - Word -> F3
326
+ - Agenda -> F4
327
+ - Time -> F5
328
+ - World -> F6
329
+ - Calc -> F7
330
+ - Sheet -> F8
331
+
332
+ """
333
+
334
+
335
+ @dataclass
336
+ class Selection:
337
+
338
+ vendor: int
339
+ device: int
340
+ variant: int
341
+
342
+
343
+ def mame_command(profile):
344
+
345
+ command = [
346
+ "mame",
347
+ "-window",
348
+ "-nomaximize",
349
+ "-skip_gameinfo",
350
+ "-rompath", os.environ["PSIEMU_ROM_PATH"],
351
+ profile["id"],
352
+ ]
353
+
354
+ if "resolution" in profile:
355
+ device_scale = profile["scale"] if "scale" in profile else 1
356
+ scale = 2 * device_scale # TODO: Detect display scale.
357
+ (width, height) = profile['resolution']
358
+ command.extend([
359
+ "-prescale", "%s" % (scale, ),
360
+ "-resolution", "%dx%d" % (width * scale, height * scale),
361
+ ])
362
+
363
+ if "bios" in profile:
364
+ command.extend([
365
+ "-bios", profile["bios"],
366
+ ])
367
+
368
+ return command
369
+
370
+
371
+ def run_mame(profile):
372
+ subprocess.Popen(mame_command(profile),
373
+ start_new_session=True,
374
+ stdout=subprocess.DEVNULL,
375
+ stderr=subprocess.DEVNULL)
376
+
377
+
378
+ def language_symbol(variant):
379
+ languages = variant["languages"]
380
+ if len(languages) > 1:
381
+ return "+ "
382
+ elif len(languages) == 1:
383
+ return LANGUAGES[languages[0]]["symbol"]
384
+ else:
385
+ return " "
386
+
387
+
388
+ def language_description(variant):
389
+ return ", " .join([LANGUAGES[language]["name"] for language in variant["languages"]])
390
+
391
+
392
+ def device_picker(stdscr):
393
+
394
+ def render_device_section(devices, is_section_active, y_pos, selection):
395
+
396
+ for device_index, profile in enumerate(devices):
397
+ title = profile["title"].ljust(22)
398
+ variants = profile["variants"]
399
+
400
+ for variant_index, variant in enumerate(variants):
401
+ name = variant["name"]
402
+ languages = language_symbol(variant)
403
+ # languages = "".join([LANGUAGES[language] for language in variant["languages"]])
404
+ if is_section_active and device_index == selection.device and variant_index == selection.variant:
405
+ name = "-> " + name
406
+ else:
407
+ name = " " + name
408
+ # While it might seem like a good idea to use `ljust` here to ensure these are consistent lengths
409
+ # we're instead relying on `name` being of equal length to avoid bumping into Python's terrible
410
+ # handling of multi-codepoint emoji.
411
+ # In the future, we might want to use 'grapheme' which provides suport for this.
412
+ title += f"{name} {languages} "
413
+
414
+ stdscr.addstr(y_pos + device_index, 0, " " + title)
415
+
416
+ return y_pos + len(devices)
417
+
418
+ curses.use_default_colors()
419
+ curses.curs_set(0)
420
+
421
+ selection = Selection(0, 0, 0)
422
+
423
+ while True:
424
+
425
+ (height, width) = stdscr.getmaxyx()
426
+ stdscr.clear()
427
+
428
+ offset = 0
429
+ for vendor_index, vendor in enumerate(PROFILES):
430
+ stdscr.addstr(offset, 0, vendor["name"])
431
+ offset += render_device_section(devices=vendor["devices"],
432
+ is_section_active=vendor_index == selection.vendor,
433
+ y_pos=offset + 2,
434
+ selection=selection)
435
+ offset += 1
436
+
437
+ # Current selection.
438
+ vendor = PROFILES[selection.vendor]
439
+ devices = vendor["devices"]
440
+ profile = vendor["devices"][selection.device]
441
+ variant = profile["variants"][selection.variant]
442
+
443
+ # Fixup the profile.
444
+ profile["bios"] = variant["bios"]
445
+ if "id" in variant:
446
+ profile["id"] = variant["id"]
447
+
448
+ # Get the command.
449
+ command = mame_command(profile)
450
+
451
+ # Display the description for the current selection.
452
+ stdscr.hline(height - 5, 0, curses.ACS_HLINE, width)
453
+ if "description" in profile:
454
+ stdscr.addstr(height-4, 0, profile["description"])
455
+ stdscr.addstr(height-3, 0, language_description(variant))
456
+ stdscr.addstr(height-2, 0, " ".join(command))
457
+
458
+ # Get and handle input.
459
+ key = stdscr.getch()
460
+ if key == curses.KEY_UP:
461
+
462
+ if selection.device > 0:
463
+ selection.device -= 1
464
+ selection.variant = 0
465
+ elif selection.vendor > 0:
466
+ selection.vendor -= 1
467
+ selection.device = len(PROFILES[selection.vendor]["devices"]) - 1
468
+ selection.variant = 0
469
+
470
+ elif key == curses.KEY_DOWN:
471
+
472
+ if selection.device < len(devices) - 1:
473
+ selection.device += 1
474
+ selection.variant = 0
475
+ elif selection.vendor < len(PROFILES) - 1:
476
+ selection.vendor += 1
477
+ selection.device = 0
478
+ selection.variant = 0
479
+
480
+ elif key == curses.KEY_LEFT:
481
+
482
+ if selection.variant > 0:
483
+ selection.variant -= 1
484
+
485
+ elif key == curses.KEY_RIGHT:
486
+
487
+ variants = profile["variants"]
488
+ if selection.variant < len(variants) - 1:
489
+ selection.variant += 1
490
+
491
+ elif key == ord('\n'):
492
+
493
+ profile["bios"] = variant["bios"]
494
+ if "id" in variant:
495
+ profile["id"] = variant["id"]
496
+ run_mame(profile)
497
+
498
+ elif key == 27 or key == ord('q'):
499
+
500
+ return None
501
+
502
+
503
+ def main():
504
+ parser = argparse.ArgumentParser()
505
+ options = parser.parse_args()
506
+
507
+ os.environ.setdefault('ESCDELAY', '25')
508
+ curses.wrapper(lambda stdscr: device_picker(stdscr))
509
+
510
+
511
+ if __name__ == "__main__":
512
+ main()