Skip to content

Commit 737b63a

Browse files
committed
Add console UI for menu selection
1 parent c250922 commit 737b63a

File tree

3 files changed

+191
-0
lines changed

3 files changed

+191
-0
lines changed

lglpy/ui/__init__.py

Whitespace-only changes.

lglpy/ui/console.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# SPDX-License-Identifier: MIT
2+
# -----------------------------------------------------------------------------
3+
# Copyright (c) 2019-2025 Arm Limited
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
7+
# deal in the Software without restriction, including without limitation the
8+
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
9+
# sell 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
13+
# all 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+
24+
'''
25+
This module implements a simple interactive command line menu that can
26+
present a list of options and prompt the user to select one.
27+
'''
28+
29+
import math
30+
from typing import Optional
31+
32+
33+
def get_input(text: str) -> str:
34+
'''
35+
Wrapper around input() so that it can be mocked for testing.
36+
37+
Args:
38+
text: The text to display as a prompt.
39+
'''
40+
return input(text)
41+
42+
43+
def select_from_menu(title: str, options: list[str]) -> Optional[int]:
44+
'''
45+
Prompt user to select from an on-screen menu.
46+
47+
If the option list contains only a single option it will be auto-selected.
48+
49+
Args:
50+
title: The title string.
51+
options: The list of options to present.
52+
53+
Returns:
54+
The selected list index, or None if no selection made.
55+
'''
56+
assert len(options) > 0, 'No menu options provided'
57+
58+
if len(options) == 1:
59+
print(f'\nSelect a {title}')
60+
print(f' Auto-selected {options[0]}')
61+
return 0
62+
63+
selection = None
64+
while True:
65+
try:
66+
# Print the menu
67+
print(f'\nSelect a {title}')
68+
chars = int(math.log10(len(options))) + 1
69+
for i, entry in enumerate(options):
70+
print(f' {i+1:{chars}}) {entry}')
71+
72+
print(f' {0:{chars}}) Exit menu')
73+
74+
# Process the response
75+
response = int(get_input('\n Select entry: '))
76+
if response == 0:
77+
return None
78+
79+
if 0 < response <= len(options):
80+
selection = response - 1
81+
break
82+
83+
raise ValueError()
84+
85+
except ValueError:
86+
print(f'\n Please enter a value between 0 and {len(options)}')
87+
88+
print(f'\n Selected {options[selection]}')
89+
return selection

lglpy/ui/test.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
2+
# SPDX-License-Identifier: MIT
3+
# -----------------------------------------------------------------------------
4+
# Copyright (c) 2025 Arm Limited
5+
#
6+
# Permission is hereby granted, free of charge, to any person obtaining a copy
7+
# of this software and associated documentation files (the 'Software'), to
8+
# deal in the Software without restriction, including without limitation the
9+
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
10+
# sell copies of the Software, and to permit persons to whom the Software is
11+
# furnished to do so, subject to the following conditions:
12+
#
13+
# The above copyright notice and this permission notice shall be included in
14+
# all copies or substantial portions of the Software.
15+
#
16+
# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
# SOFTWARE.
23+
# -----------------------------------------------------------------------------
24+
25+
'''
26+
This module implements tests for the lglpy.ui package.
27+
'''
28+
29+
import sys
30+
import unittest
31+
from unittest import mock
32+
33+
from . import console
34+
35+
36+
class ConsoleTestMenu(unittest.TestCase):
37+
'''
38+
Tests for the console UI for simple list item menu selection.
39+
'''
40+
41+
@staticmethod
42+
def make_options(count: int) -> list[str]:
43+
'''
44+
Make a list of options ...
45+
'''
46+
return [f'Option {i + 1}' for i in range(0, count)]
47+
48+
@mock.patch('lglpy.ui.console.get_input', side_effect='0')
49+
def test_menu_cancel(self, mock_get_input):
50+
'''
51+
Test the user cancelling an option in the menu.
52+
'''
53+
del mock_get_input
54+
options = self.make_options(3)
55+
selected_option = console.select_from_menu('Title', options)
56+
self.assertEqual(selected_option, None)
57+
58+
@mock.patch('lglpy.ui.console.get_input', side_effect=['1'])
59+
def test_menu_select_1(self, mock_get_input):
60+
'''
61+
Test the user entering a valid value in the menu.
62+
'''
63+
del mock_get_input
64+
options = self.make_options(3)
65+
selected_option = console.select_from_menu('Title', options)
66+
self.assertEqual(selected_option, 0)
67+
68+
@mock.patch('lglpy.ui.console.get_input', side_effect=['4', '2'])
69+
def test_menu_select_bad_range(self, mock_get_input):
70+
'''
71+
Test the user entering an out-of-bounds value in the menu.
72+
'''
73+
options = self.make_options(3)
74+
selected_option = console.select_from_menu('Title', options)
75+
76+
self.assertEqual(mock_get_input.call_count, 2)
77+
self.assertEqual(selected_option, 1)
78+
79+
@mock.patch('lglpy.ui.console.get_input', side_effect=['fox', '3'])
80+
def test_menu_select_bad_formant(self, mock_get_input):
81+
'''
82+
Test the user entering an out-of-bounds value in the menu.
83+
'''
84+
options = self.make_options(3)
85+
selected_option = console.select_from_menu('Title', options)
86+
self.assertEqual(mock_get_input.call_count, 2)
87+
self.assertEqual(selected_option, 2)
88+
89+
90+
def main():
91+
'''
92+
The main function.
93+
94+
Returns:
95+
int: The process return code.
96+
'''
97+
results = unittest.main(exit=False)
98+
return 0 if results.result.wasSuccessful() else 1
99+
100+
101+
if __name__ == '__main__':
102+
sys.exit(main())

0 commit comments

Comments
 (0)