Skip to content

Commit 86f86d2

Browse files
committed
ADD: FunctionalMenu
UPDATE: Refactor Was written on YouTube stream: https://www.youtube.com/watch?v=ppZoCcmPhpc&t=1179s
1 parent d12a112 commit 86f86d2

3 files changed

Lines changed: 112 additions & 85 deletions

File tree

pymenu/menu.py

Lines changed: 76 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,124 +1,109 @@
11
import curses
2+
from abc import ABC
23
from curses import wrapper, use_default_colors, curs_set
3-
from typing import List
4-
from pymenu.misc import Item, Keyboard
4+
from typing import List, Callable
5+
from pymenu.misc import Item, Keyboard, FunctionalItem
56

67

7-
class SelectorMenu:
8+
class __BaseMenu(ABC):
89

9-
def __init__(self, options: List[str], title: str = '', default_index: int = 0, indicator: str = "->"):
10+
def __init__(self, options: list, title: str = '', default_index: int = 0, indicator: str = "->"):
1011
if len(options) == 0:
1112
raise Exception('must contains 1 element')
1213
if 0 <= default_index >= len(options):
1314
raise ValueError('default_index should be less than the length of options')
14-
15-
self._options = options
15+
if not len(indicator):
16+
raise Exception
1617
self._title = title
18+
self._options = options
1719
self._index = default_index
1820
self._indicator = indicator
1921
self._scroll_top = 0
2022

21-
def get_selected(self) -> Item:
22-
return Item(name=self._options[self._index], index=self._index)
23-
24-
def get_option_lines(self):
25-
lines = []
26-
for index, option in enumerate(self._options):
27-
if index == self._index:
28-
prefix = self._indicator
29-
else:
30-
prefix = len(self._indicator) * ' '
31-
lines.append(f'{prefix} {option}')
32-
return lines
23+
self._config = {
24+
Keyboard.UP: self.go_up,
25+
Keyboard.DOWN: self.go_down,
26+
}
27+
self._returnable_config = {
28+
Keyboard.ENTER: self.get_selected
29+
}
30+
self._titles = self._get_titles()
3331

34-
def get_lines(self):
35-
title_lines = self._title.split('\n')
36-
current_line_y = self._index + len(title_lines) + 1
37-
lines = title_lines + self.get_option_lines()
38-
return lines, current_line_y
32+
# region Ideal
3933

4034
def go_up(self):
4135
self._index = (self._index - 1) % len(self._options)
4236

4337
def go_down(self):
4438
self._index = (self._index + 1) % len(self._options)
4539

40+
def input(self):
41+
return wrapper(self.run_loop)
42+
43+
def _get_titles(self):
44+
return self._options
45+
46+
def get_selected(self):
47+
raise NotImplementedError
48+
49+
def run_loop(self, screen):
50+
use_default_colors()
51+
curs_set(0)
52+
while True:
53+
self.draw(screen)
54+
key = screen.getch()
55+
for keys in self._config.keys():
56+
if key in keys:
57+
self._config[keys]()
58+
break
59+
for keys in self._returnable_config.keys():
60+
if key in keys:
61+
return self._returnable_config[keys]()
62+
63+
# endregion
64+
4665
def draw(self, screen) -> None:
4766
screen.clear()
4867

4968
max_y, max_x = screen.getmaxyx()
5069

51-
lines, current_line = self.get_lines()
70+
y = 0
5271

53-
if current_line <= self._scroll_top:
54-
self._scroll_top = 0
55-
elif current_line - self._scroll_top > max_y:
56-
self._scroll_top = current_line - max_y
57-
58-
lines_to_draw = lines[self._scroll_top:self._scroll_top + max_y]
72+
for line in self._title.split('\n'):
73+
screen.addnstr(y, 0, line, max_x)
74+
y += 1
5975

60-
for y, line in enumerate(lines_to_draw):
61-
screen.addnstr(y, 0, line, max_x - 2)
76+
for local_y, line in enumerate(self._title):
77+
if local_y == self._index:
78+
line = f'{self._indicator}{line}'
79+
else:
80+
line = f'{" " * len(self._indicator)}{line}'
81+
screen.addnstr(y, 0, line, max_x)
6282
y += 1
6383

6484
screen.refresh()
6585

66-
def run_loop(self, screen):
67-
config = {
68-
Keyboard.UP: self.go_up,
69-
Keyboard.DOWN: self.go_down,
70-
}
71-
returnable_config = {
72-
Keyboard.ENTER: self.get_selected,
73-
}
74-
while True:
75-
self.draw(screen)
76-
key = screen.getch()
77-
for keys in config.keys():
78-
if key in keys:
79-
config[keys]()
80-
break
81-
for keys in returnable_config.keys():
82-
if key in keys:
83-
return returnable_config[keys]()
8486

85-
def __start(self, screen):
86-
use_default_colors()
87-
curs_set(0)
88-
return self.run_loop(screen)
87+
class SelectorMenu(__BaseMenu):
8988

90-
def input(self):
91-
return wrapper(self.__start)
89+
def __init__(self, options: List[str], title: str = '', default_index: int = 0, indicator: str = "->"):
90+
super().__init__(options, title, default_index, indicator)
9291

92+
def get_selected(self) -> Item:
93+
return Item(name=self._options[self._index], index=self._index)
9394

94-
class MultiSelectorMenu(SelectorMenu):
95+
96+
class MultiSelectorMenu(__BaseMenu):
9597

9698
def __init__(self, options: List[str], count: int, title: str = '', default_index: int = 0, indicator: str = "->"):
9799
if 1 <= count > len(options):
98100
raise Exception
99101
self.count = count
100102
self.selected: list[int] = []
101103
super().__init__(options, title, default_index, indicator)
102-
103-
def run_loop(self, screen):
104-
config = {
105-
Keyboard.UP: self.go_up,
106-
Keyboard.DOWN: self.go_down,
104+
self._config.update({
107105
Keyboard.SELECT: self.select
108-
}
109-
returnable_config = {
110-
Keyboard.ENTER: self.get_selected,
111-
}
112-
while True:
113-
self.draw(screen)
114-
key = screen.getch()
115-
for keys in config.keys():
116-
if key in keys:
117-
config[keys]()
118-
break
119-
for keys in returnable_config.keys():
120-
if key in keys:
121-
return returnable_config[keys]()
106+
})
122107

123108
def select(self):
124109
if self._index in self.selected:
@@ -138,14 +123,14 @@ def draw(self, screen) -> None:
138123

139124
max_y, max_x = screen.getmaxyx()
140125

141-
lines, current_line = self.get_lines()
126+
current_line = len(self._title.split('\n'))
142127

143128
if current_line <= self._scroll_top:
144129
self._scroll_top = 0
145130
elif current_line - self._scroll_top > max_y:
146131
self._scroll_top = current_line - max_y
147132

148-
lines_to_draw = lines[self._scroll_top:self._scroll_top + max_y]
133+
lines_to_draw = self._titles[self._scroll_top:self._scroll_top + max_y]
149134

150135
for y, line in enumerate(lines_to_draw):
151136
if y - len(self._title.split('\n')) in self.selected:
@@ -154,4 +139,16 @@ def draw(self, screen) -> None:
154139
screen.addnstr(y, 0, line, max_x - 2)
155140
y += 1
156141

157-
screen.refresh()
142+
screen.refresh()
143+
144+
145+
class FunctionalMenu(__BaseMenu):
146+
147+
def __init__(self, options: List[FunctionalItem], title: str = '', default_index: int = 0, indicator: str = "->"):
148+
super().__init__(options, title, default_index, indicator)
149+
150+
def get_selected(self) -> Callable:
151+
return self._options[self._index].func
152+
153+
def _get_titles(self):
154+
return [option.name for option in self._options]

pymenu/misc.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
from abc import ABC
22
from curses import KEY_UP, KEY_DOWN, KEY_ENTER
3-
from typing import NamedTuple, Final
3+
from typing import NamedTuple, Final, Callable
44

55

66
class Item(NamedTuple):
77
name: str
88
index: int
99

1010

11+
class FunctionalItem(NamedTuple):
12+
name: str
13+
func: Callable
14+
15+
1116
class Keyboard(ABC):
1217
UP: Final = (KEY_UP, ord('w'))
1318
DOWN: Final = (KEY_DOWN, ord('s'))

run.py

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,42 @@
1-
from pymenu.menu import SelectorMenu, MultiSelectorMenu
1+
from pymenu.menu import SelectorMenu, MultiSelectorMenu, FunctionalMenu
2+
from pymenu.misc import FunctionalItem
23

34

4-
def main() -> None:
5-
# MultiSelector
6-
menu = MultiSelectorMenu(['С++', 'Python', 'Pascal'], count=2, title='Выбери ЯП мечты')
5+
def multi_selector():
6+
menu = MultiSelectorMenu(['С++', 'Python', 'Pascal', 'C#'], count=3, title='Выбери ЯП мечты')
77
ans = menu.input()
88
print(ans)
99

10-
# Selector
10+
11+
def selector():
1112
menu = SelectorMenu(['С++', 'Python', 'Pascal'], title='Выбери ЯП мечты')
1213
ans = menu.input()
1314
print(ans)
1415

1516

17+
def func1():
18+
print('Я попугай')
19+
20+
21+
def func2():
22+
print('Я чебурашка')
23+
24+
25+
def functional():
26+
data = [
27+
FunctionalItem('Чебурашка', func1),
28+
FunctionalItem('Попугай', func2),
29+
]
30+
menu = FunctionalMenu(data, title='Выбери ЯП мечты')
31+
ans = menu.input()
32+
ans()
33+
34+
35+
def main() -> None:
36+
selector()
37+
# multi_selector()
38+
# functional()
39+
40+
1641
if __name__ == "__main__":
1742
main()

0 commit comments

Comments
 (0)