Remove the old curses-based TUI code (#4497)
The new Textual-based TUI shipped in the April .iso.
This commit is contained in:
parent
468e33d54f
commit
3223ae4212
|
|
@ -5,7 +5,6 @@ from typing import Any, Self, override
|
||||||
from archinstall.lib.menu.helpers import Selection
|
from archinstall.lib.menu.helpers import Selection
|
||||||
from archinstall.lib.output import error
|
from archinstall.lib.output import error
|
||||||
from archinstall.lib.translationhandler import tr
|
from archinstall.lib.translationhandler import tr
|
||||||
from archinstall.tui.types import Chars
|
|
||||||
from archinstall.tui.ui.components import InstanceRunnable
|
from archinstall.tui.ui.components import InstanceRunnable
|
||||||
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
|
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
|
||||||
from archinstall.tui.ui.result import ResultType
|
from archinstall.tui.ui.result import ResultType
|
||||||
|
|
@ -154,7 +153,7 @@ class AbstractSubMenu[ValueT](AbstractMenu[ValueT]):
|
||||||
auto_cursor: bool = True,
|
auto_cursor: bool = True,
|
||||||
allow_reset: bool = False,
|
allow_reset: bool = False,
|
||||||
):
|
):
|
||||||
back_text = f'{Chars.Right_arrow} ' + tr('Back')
|
back_text = '← ' + tr('Back')
|
||||||
item_group.add_item(MenuItem(text=back_text))
|
item_group.add_item(MenuItem(text=back_text))
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,109 +0,0 @@
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from enum import Enum
|
|
||||||
|
|
||||||
from archinstall.lib.translationhandler import tr
|
|
||||||
|
|
||||||
|
|
||||||
class HelpTextGroupId(Enum):
|
|
||||||
GENERAL = 'General'
|
|
||||||
NAVIGATION = 'Navigation'
|
|
||||||
SELECTION = 'Selection'
|
|
||||||
SEARCH = 'Search'
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class HelpText:
|
|
||||||
description: str
|
|
||||||
keys: list[str] = field(default_factory=list)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class HelpGroup:
|
|
||||||
group_id: HelpTextGroupId
|
|
||||||
group_entries: list[HelpText]
|
|
||||||
|
|
||||||
def get_desc_width(self) -> int:
|
|
||||||
return max([len(e.description) for e in self.group_entries])
|
|
||||||
|
|
||||||
def get_key_width(self) -> int:
|
|
||||||
return max([len(', '.join(e.keys)) for e in self.group_entries])
|
|
||||||
|
|
||||||
|
|
||||||
class Help:
|
|
||||||
# the groups needs to be classmethods not static methods
|
|
||||||
# because they rely on the DeferredTranslation setup first;
|
|
||||||
# if they are static methods, they will be called before the
|
|
||||||
# translation setup is done
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def general() -> HelpGroup:
|
|
||||||
return HelpGroup(
|
|
||||||
group_id=HelpTextGroupId.GENERAL,
|
|
||||||
group_entries=[
|
|
||||||
HelpText(tr('Show help'), ['Ctrl+h']),
|
|
||||||
HelpText(tr('Exit help'), ['Esc']),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def navigation() -> HelpGroup:
|
|
||||||
return HelpGroup(
|
|
||||||
group_id=HelpTextGroupId.NAVIGATION,
|
|
||||||
group_entries=[
|
|
||||||
HelpText(tr('Preview scroll up'), ['PgUp']),
|
|
||||||
HelpText(tr('Preview scroll down'), ['PgDown']),
|
|
||||||
HelpText(tr('Move up'), ['k', '↑']),
|
|
||||||
HelpText(tr('Move down'), ['j', '↓']),
|
|
||||||
HelpText(tr('Move right'), ['l', '→']),
|
|
||||||
HelpText(tr('Move left'), ['h', '←']),
|
|
||||||
HelpText(tr('Jump to entry'), ['1..9']),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def selection() -> HelpGroup:
|
|
||||||
return HelpGroup(
|
|
||||||
group_id=HelpTextGroupId.SELECTION,
|
|
||||||
group_entries=[
|
|
||||||
HelpText(tr('Skip selection (if available)'), ['Esc']),
|
|
||||||
HelpText(tr('Reset selection (if available)'), ['Ctrl+c']),
|
|
||||||
HelpText(tr('Select on single select'), ['Enter']),
|
|
||||||
HelpText(tr('Select on multi select'), ['Space', 'Tab']),
|
|
||||||
HelpText(tr('Reset'), ['Ctrl-C']),
|
|
||||||
HelpText(tr('Skip selection menu'), ['Esc']),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def search() -> HelpGroup:
|
|
||||||
return HelpGroup(
|
|
||||||
group_id=HelpTextGroupId.SEARCH,
|
|
||||||
group_entries=[
|
|
||||||
HelpText(tr('Start search mode'), ['/']),
|
|
||||||
HelpText(tr('Exit search mode'), ['Esc']),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_help_text() -> str:
|
|
||||||
help_output = ''
|
|
||||||
help_texts = [
|
|
||||||
Help.general(),
|
|
||||||
Help.navigation(),
|
|
||||||
Help.selection(),
|
|
||||||
Help.search(),
|
|
||||||
]
|
|
||||||
max_desc_width = max([help.get_desc_width() for help in help_texts]) + 2
|
|
||||||
max_key_width = max([help.get_key_width() for help in help_texts])
|
|
||||||
|
|
||||||
for help_group in help_texts:
|
|
||||||
help_output += f'{help_group.group_id.value}\n'
|
|
||||||
divider_len = max_desc_width + max_key_width
|
|
||||||
help_output += '-' * divider_len + '\n'
|
|
||||||
|
|
||||||
for entry in help_group.group_entries:
|
|
||||||
help_output += entry.description.ljust(max_desc_width, ' ') + ', '.join(entry.keys) + '\n'
|
|
||||||
|
|
||||||
help_output += '\n'
|
|
||||||
|
|
||||||
return help_output
|
|
||||||
|
|
@ -1,515 +0,0 @@
|
||||||
from collections.abc import Callable
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from enum import Enum
|
|
||||||
from functools import cached_property
|
|
||||||
from typing import Any, ClassVar, Self, override
|
|
||||||
|
|
||||||
from archinstall.lib.translationhandler import tr
|
|
||||||
from archinstall.lib.utils.encoding import unicode_ljust
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class MenuItem:
|
|
||||||
text: str
|
|
||||||
value: Any | None = None
|
|
||||||
action: Callable[[Any], Any] | None = None
|
|
||||||
enabled: bool = True
|
|
||||||
read_only: bool = False
|
|
||||||
mandatory: bool = False
|
|
||||||
dependencies: list[str | Callable[[], bool]] = field(default_factory=list)
|
|
||||||
dependencies_not: list[str] = field(default_factory=list)
|
|
||||||
display_action: Callable[[Any], str] | None = None
|
|
||||||
preview_action: Callable[[Self], str | None] | None = None
|
|
||||||
key: str | None = None
|
|
||||||
|
|
||||||
_id: str = ''
|
|
||||||
_yes: ClassVar[Self | None] = None
|
|
||||||
_no: ClassVar[Self | None] = None
|
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
|
||||||
if self.key is not None:
|
|
||||||
self._id = self.key
|
|
||||||
else:
|
|
||||||
self._id = str(id(self))
|
|
||||||
|
|
||||||
@override
|
|
||||||
def __hash__(self) -> int:
|
|
||||||
return hash(self._id)
|
|
||||||
|
|
||||||
def get_id(self) -> str:
|
|
||||||
return self._id
|
|
||||||
|
|
||||||
def get_value(self) -> Any:
|
|
||||||
assert self.value is not None
|
|
||||||
return self.value
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def yes(cls, action: Callable[[Any], Any] | None = None) -> Self:
|
|
||||||
if cls._yes is None:
|
|
||||||
cls._yes = cls(tr('Yes'), value=True, key='yes', action=action)
|
|
||||||
|
|
||||||
return cls._yes
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def no(cls, action: Callable[[Any], Any] | None = None) -> Self:
|
|
||||||
if cls._no is None:
|
|
||||||
cls._no = cls(tr('No'), value=False, key='no', action=action)
|
|
||||||
|
|
||||||
return cls._no
|
|
||||||
|
|
||||||
def is_empty(self) -> bool:
|
|
||||||
return self.text == '' or self.text is None
|
|
||||||
|
|
||||||
def has_value(self) -> bool:
|
|
||||||
if self.value is None:
|
|
||||||
return False
|
|
||||||
elif isinstance(self.value, list) and len(self.value) == 0:
|
|
||||||
return False
|
|
||||||
elif isinstance(self.value, dict) and len(self.value) == 0:
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
return True
|
|
||||||
|
|
||||||
def get_display_value(self) -> str | None:
|
|
||||||
if self.display_action is not None:
|
|
||||||
return self.display_action(self.value)
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class MenuItemGroup:
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
menu_items: list[MenuItem],
|
|
||||||
focus_item: MenuItem | None = None,
|
|
||||||
default_item: MenuItem | None = None,
|
|
||||||
sort_items: bool = False,
|
|
||||||
sort_case_sensitive: bool = True,
|
|
||||||
checkmarks: bool = False,
|
|
||||||
) -> None:
|
|
||||||
if len(menu_items) < 1:
|
|
||||||
raise ValueError('Menu must have at least one item')
|
|
||||||
|
|
||||||
if sort_items:
|
|
||||||
if sort_case_sensitive:
|
|
||||||
menu_items = sorted(menu_items, key=lambda x: x.text)
|
|
||||||
else:
|
|
||||||
menu_items = sorted(menu_items, key=lambda x: x.text.lower())
|
|
||||||
|
|
||||||
self._filter_pattern: str = ''
|
|
||||||
self._checkmarks: bool = checkmarks
|
|
||||||
|
|
||||||
self._menu_items: list[MenuItem] = menu_items
|
|
||||||
self.focus_item: MenuItem | None = focus_item
|
|
||||||
self.selected_items: list[MenuItem] = []
|
|
||||||
self.default_item: MenuItem | None = default_item
|
|
||||||
|
|
||||||
if not focus_item:
|
|
||||||
self.focus_first()
|
|
||||||
|
|
||||||
if self.focus_item not in self.items:
|
|
||||||
raise ValueError(f'Selected item not in menu: {self.focus_item}')
|
|
||||||
|
|
||||||
def add_item(self, item: MenuItem) -> None:
|
|
||||||
self._menu_items.append(item)
|
|
||||||
del self.items # resetting the cache
|
|
||||||
|
|
||||||
def find_by_id(self, item_id: str) -> MenuItem:
|
|
||||||
for item in self._menu_items:
|
|
||||||
if item.get_id() == item_id:
|
|
||||||
return item
|
|
||||||
|
|
||||||
raise ValueError(f'No item found for id: {item_id}')
|
|
||||||
|
|
||||||
def find_by_key(self, key: str) -> MenuItem:
|
|
||||||
for item in self._menu_items:
|
|
||||||
if item.key == key:
|
|
||||||
return item
|
|
||||||
|
|
||||||
raise ValueError(f'No item found for key: {key}')
|
|
||||||
|
|
||||||
def get_enabled_items(self) -> list[MenuItem]:
|
|
||||||
return [it for it in self.items if self.is_enabled(it)]
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def yes_no(cls) -> Self:
|
|
||||||
return cls(
|
|
||||||
[MenuItem.yes(), MenuItem.no()],
|
|
||||||
sort_items=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_enum(
|
|
||||||
cls,
|
|
||||||
enum_cls: type[Enum],
|
|
||||||
sort_items: bool = False,
|
|
||||||
preset: Enum | None = None,
|
|
||||||
) -> Self:
|
|
||||||
items = [MenuItem(elem.value, value=elem) for elem in enum_cls]
|
|
||||||
group = cls(items, sort_items=sort_items)
|
|
||||||
|
|
||||||
if preset is not None:
|
|
||||||
group.set_selected_by_value(preset)
|
|
||||||
|
|
||||||
return group
|
|
||||||
|
|
||||||
def set_preview_for_all(self, action: Callable[[Any], str | None]) -> None:
|
|
||||||
for item in self.items:
|
|
||||||
item.preview_action = action
|
|
||||||
|
|
||||||
def set_focus_by_value(self, value: Any) -> None:
|
|
||||||
for item in self._menu_items:
|
|
||||||
if item.value == value:
|
|
||||||
self.focus_item = item
|
|
||||||
break
|
|
||||||
|
|
||||||
def set_default_by_value(self, value: Any) -> None:
|
|
||||||
for item in self._menu_items:
|
|
||||||
if item.value == value:
|
|
||||||
self.default_item = item
|
|
||||||
break
|
|
||||||
|
|
||||||
def set_selected_by_value(self, values: Any | list[Any] | None) -> None:
|
|
||||||
if values is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
if not isinstance(values, list):
|
|
||||||
values = [values]
|
|
||||||
|
|
||||||
for item in self._menu_items:
|
|
||||||
if item.value in values:
|
|
||||||
self.selected_items.append(item)
|
|
||||||
|
|
||||||
if values:
|
|
||||||
self.set_focus_by_value(values[0])
|
|
||||||
|
|
||||||
def get_focused_index(self) -> int | None:
|
|
||||||
items = self.get_enabled_items()
|
|
||||||
|
|
||||||
if self.focus_item and items:
|
|
||||||
try:
|
|
||||||
return items.index(self.focus_item)
|
|
||||||
except ValueError:
|
|
||||||
# on large menus (15k+) when filtering very quickly
|
|
||||||
# the index search is too slow while the items are reduced
|
|
||||||
# by the filter and it will blow up as it cannot find the
|
|
||||||
# focus item
|
|
||||||
pass
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def index_focus(self) -> int | None:
|
|
||||||
if self.focus_item and self.items:
|
|
||||||
try:
|
|
||||||
return self.items.index(self.focus_item)
|
|
||||||
except ValueError:
|
|
||||||
# on large menus (15k+) when filtering very quickly
|
|
||||||
# the index search is too slow while the items are reduced
|
|
||||||
# by the filter and it will blow up as it cannot find the
|
|
||||||
# focus item
|
|
||||||
pass
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def size(self) -> int:
|
|
||||||
return len(self.items)
|
|
||||||
|
|
||||||
def get_max_width(self) -> int:
|
|
||||||
# use the menu_items not the items here otherwise the preview
|
|
||||||
# will get resized all the time when a filter is applied
|
|
||||||
return max([len(self.get_item_text(item)) for item in self._menu_items])
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def _max_items_text_width(self) -> int:
|
|
||||||
return max([len(item.text) for item in self._menu_items])
|
|
||||||
|
|
||||||
def get_item_text(self, item: MenuItem) -> str:
|
|
||||||
if item.is_empty():
|
|
||||||
return ''
|
|
||||||
|
|
||||||
max_width = self._max_items_text_width
|
|
||||||
display_text = item.get_display_value()
|
|
||||||
|
|
||||||
default_text = self._default_suffix(item)
|
|
||||||
|
|
||||||
text = unicode_ljust(str(item.text), max_width, ' ')
|
|
||||||
spacing = ' ' * 4
|
|
||||||
|
|
||||||
if display_text:
|
|
||||||
text = f'{text}{spacing}{display_text}'
|
|
||||||
elif self._checkmarks:
|
|
||||||
from archinstall.tui.types import Chars
|
|
||||||
|
|
||||||
if item.has_value():
|
|
||||||
if item.get_value() is not False:
|
|
||||||
text = f'{text}{spacing}{Chars.Check}'
|
|
||||||
else:
|
|
||||||
text = item.text
|
|
||||||
|
|
||||||
if default_text:
|
|
||||||
text = f'{text} {default_text}'
|
|
||||||
|
|
||||||
return text.rstrip(' ')
|
|
||||||
|
|
||||||
def _default_suffix(self, item: MenuItem) -> str:
|
|
||||||
if self.default_item == item:
|
|
||||||
return tr(' (default)')
|
|
||||||
return ''
|
|
||||||
|
|
||||||
def set_action_for_all(self, action: Callable[[Any], Any]) -> None:
|
|
||||||
for item in self.items:
|
|
||||||
item.action = action
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def items(self) -> list[MenuItem]:
|
|
||||||
pattern = self._filter_pattern.lower()
|
|
||||||
items = filter(lambda item: item.is_empty() or pattern in item.text.lower(), self._menu_items)
|
|
||||||
l_items = sorted(items, key=self._items_score)
|
|
||||||
return l_items
|
|
||||||
|
|
||||||
def _items_score(self, item: MenuItem) -> int:
|
|
||||||
pattern = self._filter_pattern.lower()
|
|
||||||
if item.text.lower().startswith(pattern):
|
|
||||||
return 0
|
|
||||||
return 1
|
|
||||||
|
|
||||||
@property
|
|
||||||
def filter_pattern(self) -> str:
|
|
||||||
return self._filter_pattern
|
|
||||||
|
|
||||||
def has_filter(self) -> bool:
|
|
||||||
return self._filter_pattern != ''
|
|
||||||
|
|
||||||
def set_filter_pattern(self, pattern: str) -> None:
|
|
||||||
self._filter_pattern = pattern
|
|
||||||
del self.items # resetting the cache
|
|
||||||
self.focus_first()
|
|
||||||
|
|
||||||
def append_filter(self, pattern: str) -> None:
|
|
||||||
self._filter_pattern += pattern
|
|
||||||
del self.items # resetting the cache
|
|
||||||
self.focus_first()
|
|
||||||
|
|
||||||
def reduce_filter(self) -> None:
|
|
||||||
self._filter_pattern = self._filter_pattern[:-1]
|
|
||||||
del self.items # resetting the cache
|
|
||||||
self.focus_first()
|
|
||||||
|
|
||||||
def _reload_focus_item(self) -> None:
|
|
||||||
if len(self.items) > 0:
|
|
||||||
if self.focus_item not in self.items:
|
|
||||||
self.focus_first()
|
|
||||||
else:
|
|
||||||
self.focus_item = None
|
|
||||||
|
|
||||||
def is_item_selected(self, item: MenuItem) -> bool:
|
|
||||||
return item in self.selected_items
|
|
||||||
|
|
||||||
def select_current_item(self) -> None:
|
|
||||||
if self.focus_item:
|
|
||||||
if self.focus_item in self.selected_items:
|
|
||||||
self.selected_items.remove(self.focus_item)
|
|
||||||
else:
|
|
||||||
self.selected_items.append(self.focus_item)
|
|
||||||
|
|
||||||
def focus_index(self, index: int) -> None:
|
|
||||||
enabled = self.get_enabled_items()
|
|
||||||
self.focus_item = enabled[index]
|
|
||||||
|
|
||||||
def focus_first(self) -> None:
|
|
||||||
if len(self.items) == 0:
|
|
||||||
return
|
|
||||||
|
|
||||||
first_item: MenuItem | None = self.items[0]
|
|
||||||
|
|
||||||
if first_item and not self._is_selectable(first_item):
|
|
||||||
first_item = self._find_next_selectable_item(self.items, first_item, 1)
|
|
||||||
|
|
||||||
if first_item is not None:
|
|
||||||
self.focus_item = first_item
|
|
||||||
|
|
||||||
def focus_last(self) -> None:
|
|
||||||
if len(self.items) == 0:
|
|
||||||
return
|
|
||||||
|
|
||||||
last_item: MenuItem | None = self.items[-1]
|
|
||||||
|
|
||||||
if last_item and not self._is_selectable(last_item):
|
|
||||||
last_item = self._find_next_selectable_item(self.items, last_item, -1)
|
|
||||||
|
|
||||||
if last_item is not None:
|
|
||||||
self.focus_item = last_item
|
|
||||||
|
|
||||||
def focus_prev(self, skip_empty: bool = True) -> None:
|
|
||||||
# e.g. when filter shows no items
|
|
||||||
if self.focus_item is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
item = self._find_next_selectable_item(self.items, self.focus_item, -1)
|
|
||||||
|
|
||||||
if item is not None:
|
|
||||||
self.focus_item = item
|
|
||||||
|
|
||||||
def focus_next(self, skip_not_enabled: bool = True) -> None:
|
|
||||||
# e.g. when filter shows no items
|
|
||||||
if self.focus_item is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
item = self._find_next_selectable_item(self.items, self.focus_item, 1)
|
|
||||||
|
|
||||||
if item is not None:
|
|
||||||
self.focus_item = item
|
|
||||||
|
|
||||||
def _find_next_selectable_item(
|
|
||||||
self,
|
|
||||||
items: list[MenuItem],
|
|
||||||
start_item: MenuItem,
|
|
||||||
direction: int,
|
|
||||||
) -> MenuItem | None:
|
|
||||||
start_index = self.items.index(start_item)
|
|
||||||
n = len(items)
|
|
||||||
|
|
||||||
current_index = start_index
|
|
||||||
for _ in range(n):
|
|
||||||
current_index = (current_index + direction) % n
|
|
||||||
|
|
||||||
if self._is_selectable(items[current_index]):
|
|
||||||
return items[current_index]
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def is_mandatory_fulfilled(self) -> bool:
|
|
||||||
for item in self._menu_items:
|
|
||||||
if item.mandatory and not item.value:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def max_item_width(self) -> int:
|
|
||||||
spaces = [len(str(it.text)) for it in self.items]
|
|
||||||
if spaces:
|
|
||||||
return max(spaces)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def _is_selectable(self, item: MenuItem) -> bool:
|
|
||||||
if item.is_empty():
|
|
||||||
return False
|
|
||||||
elif item.read_only:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return self.is_enabled(item)
|
|
||||||
|
|
||||||
def is_enabled(self, item: MenuItem) -> bool:
|
|
||||||
if not item.enabled:
|
|
||||||
return False
|
|
||||||
|
|
||||||
for dep in item.dependencies:
|
|
||||||
if isinstance(dep, str):
|
|
||||||
item = self.find_by_key(dep)
|
|
||||||
if not item.value or not self.is_enabled(item):
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
return dep()
|
|
||||||
|
|
||||||
for dep_not in item.dependencies_not:
|
|
||||||
item = self.find_by_key(dep_not)
|
|
||||||
if item.value is not None:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class MenuItemsState:
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
item_group: MenuItemGroup,
|
|
||||||
total_cols: int,
|
|
||||||
total_rows: int,
|
|
||||||
with_frame: bool,
|
|
||||||
) -> None:
|
|
||||||
self._item_group = item_group
|
|
||||||
self._total_cols = total_cols
|
|
||||||
self._total_rows = total_rows - 2 if with_frame else total_rows
|
|
||||||
|
|
||||||
self._prev_row_idx: int = -1
|
|
||||||
self._prev_visible_rows: list[int] = []
|
|
||||||
self._view_items: list[list[MenuItem]] = []
|
|
||||||
|
|
||||||
def _determine_focus_row(self) -> int | None:
|
|
||||||
focus_index = self._item_group.index_focus()
|
|
||||||
|
|
||||||
if focus_index is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
row_index = focus_index // self._total_cols
|
|
||||||
return row_index
|
|
||||||
|
|
||||||
def get_view_items(self) -> list[list[MenuItem]]:
|
|
||||||
enabled_items = self._item_group.get_enabled_items()
|
|
||||||
focus_row_idx = self._determine_focus_row()
|
|
||||||
|
|
||||||
if focus_row_idx is None:
|
|
||||||
return []
|
|
||||||
|
|
||||||
start, end = 0, 0
|
|
||||||
|
|
||||||
if len(self._view_items) == 0 or self._prev_row_idx == -1 or focus_row_idx == 0: # initial setup
|
|
||||||
if focus_row_idx < self._total_rows:
|
|
||||||
start = 0
|
|
||||||
end = self._total_rows
|
|
||||||
elif focus_row_idx > len(enabled_items) - self._total_rows:
|
|
||||||
start = len(enabled_items) - self._total_rows
|
|
||||||
end = len(enabled_items)
|
|
||||||
else:
|
|
||||||
start = focus_row_idx
|
|
||||||
end = focus_row_idx + self._total_rows
|
|
||||||
elif len(enabled_items) <= self._total_rows: # the view can handle all items
|
|
||||||
start = 0
|
|
||||||
end = self._total_rows
|
|
||||||
elif not self._item_group.has_filter() and focus_row_idx in self._prev_visible_rows: # focus is in the same view
|
|
||||||
self._prev_row_idx = focus_row_idx
|
|
||||||
return self._view_items
|
|
||||||
else:
|
|
||||||
if self._item_group.has_filter():
|
|
||||||
start = focus_row_idx
|
|
||||||
end = focus_row_idx + self._total_rows
|
|
||||||
else:
|
|
||||||
delta = focus_row_idx - self._prev_row_idx
|
|
||||||
|
|
||||||
if delta > 0: # cursor is on the bottom most row
|
|
||||||
start = focus_row_idx - self._total_rows + 1
|
|
||||||
end = focus_row_idx + 1
|
|
||||||
else: # focus is on the top most row
|
|
||||||
start = focus_row_idx
|
|
||||||
end = focus_row_idx + self._total_rows
|
|
||||||
|
|
||||||
self._view_items = self._get_view_items(enabled_items, start, end)
|
|
||||||
self._prev_visible_rows = list(range(start, end))
|
|
||||||
self._prev_row_idx = focus_row_idx
|
|
||||||
|
|
||||||
return self._view_items
|
|
||||||
|
|
||||||
def _get_view_items(
|
|
||||||
self,
|
|
||||||
items: list[MenuItem],
|
|
||||||
start_row: int,
|
|
||||||
total_rows: int,
|
|
||||||
) -> list[list[MenuItem]]:
|
|
||||||
groups: list[list[MenuItem]] = []
|
|
||||||
nr_items = self._total_cols * min(total_rows, len(items))
|
|
||||||
|
|
||||||
for x in range(start_row, nr_items, self._total_cols):
|
|
||||||
groups.append(
|
|
||||||
items[x : x + self._total_cols],
|
|
||||||
)
|
|
||||||
|
|
||||||
return groups
|
|
||||||
|
|
||||||
def _max_visible_items(self) -> int:
|
|
||||||
return self._total_cols * self._total_rows
|
|
||||||
|
|
||||||
def _remaining_next_spots(self) -> int:
|
|
||||||
return self._max_visible_items() - self._prev_row_idx
|
|
||||||
|
|
||||||
def _remaining_prev_spots(self) -> int:
|
|
||||||
return self._max_visible_items() - self._remaining_next_spots()
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
from dataclasses import dataclass
|
|
||||||
from enum import Enum, auto
|
|
||||||
|
|
||||||
from archinstall.tui.menu_item import MenuItem
|
|
||||||
|
|
||||||
|
|
||||||
class ResultType(Enum):
|
|
||||||
Selection = auto()
|
|
||||||
Skip = auto()
|
|
||||||
Reset = auto()
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Result[ValueT]:
|
|
||||||
type_: ResultType
|
|
||||||
_item: MenuItem | list[MenuItem] | str | None
|
|
||||||
|
|
||||||
def has_item(self) -> bool:
|
|
||||||
return self._item is not None
|
|
||||||
|
|
||||||
def get_value(self) -> ValueT:
|
|
||||||
return self.item().get_value() # type: ignore[no-any-return]
|
|
||||||
|
|
||||||
def get_values(self) -> list[ValueT]:
|
|
||||||
return [i.get_value() for i in self.items()]
|
|
||||||
|
|
||||||
def item(self) -> MenuItem:
|
|
||||||
assert self._item is not None and isinstance(self._item, MenuItem)
|
|
||||||
return self._item
|
|
||||||
|
|
||||||
def items(self) -> list[MenuItem]:
|
|
||||||
assert self._item is not None and isinstance(self._item, list)
|
|
||||||
return self._item
|
|
||||||
|
|
||||||
def text(self) -> str:
|
|
||||||
assert self._item is not None and isinstance(self._item, str)
|
|
||||||
return self._item
|
|
||||||
|
|
@ -1,144 +0,0 @@
|
||||||
import curses
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from enum import Enum, auto
|
|
||||||
from typing import Self
|
|
||||||
|
|
||||||
SCROLL_INTERVAL = 10
|
|
||||||
|
|
||||||
|
|
||||||
class STYLE(Enum):
|
|
||||||
NORMAL = 1
|
|
||||||
CURSOR_STYLE = 2
|
|
||||||
MENU_STYLE = 3
|
|
||||||
HELP = 4
|
|
||||||
ERROR = 5
|
|
||||||
|
|
||||||
|
|
||||||
class MenuKeys(Enum):
|
|
||||||
# latin keys
|
|
||||||
STD_KEYS = frozenset(range(32, 127))
|
|
||||||
# numbers
|
|
||||||
NUM_KEYS = frozenset(range(49, 58))
|
|
||||||
# Menu up: up, k
|
|
||||||
MENU_UP = frozenset({259, 107})
|
|
||||||
# Menu down: down, j
|
|
||||||
MENU_DOWN = frozenset({258, 106})
|
|
||||||
# Menu left: left, h
|
|
||||||
MENU_LEFT = frozenset({260, 104})
|
|
||||||
# Menu right: right, l
|
|
||||||
MENU_RIGHT = frozenset({261, 108})
|
|
||||||
# Menu start: home CTRL-a
|
|
||||||
MENU_START = frozenset({262, 1})
|
|
||||||
# Menu end: end CTRL-e
|
|
||||||
MENU_END = frozenset({360, 5})
|
|
||||||
# Enter
|
|
||||||
ACCEPT = frozenset({10})
|
|
||||||
# Selection: space, tab
|
|
||||||
MULTI_SELECT = frozenset({32, 9})
|
|
||||||
# Search: /
|
|
||||||
ENABLE_SEARCH = frozenset({47})
|
|
||||||
# ESC
|
|
||||||
ESC = frozenset({27})
|
|
||||||
# BACKSPACE (search)
|
|
||||||
BACKSPACE = frozenset({127, 263})
|
|
||||||
# Help view: ctrl+h
|
|
||||||
HELP = frozenset({8})
|
|
||||||
# Scroll up: PGUP
|
|
||||||
SCROLL_UP = frozenset({339})
|
|
||||||
# Scroll down: PGDOWN
|
|
||||||
SCROLL_DOWN = frozenset({338})
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_ord(cls, key: int) -> list[Self]:
|
|
||||||
matches: list[Self] = []
|
|
||||||
|
|
||||||
for group in cls:
|
|
||||||
if key in group.value:
|
|
||||||
matches.append(group)
|
|
||||||
|
|
||||||
return matches
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def decode(cls, key: int) -> str:
|
|
||||||
byte_str = curses.keyname(key)
|
|
||||||
return byte_str.decode('utf-8')
|
|
||||||
|
|
||||||
|
|
||||||
class FrameStyle(Enum):
|
|
||||||
MAX = auto()
|
|
||||||
MIN = auto()
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class FrameProperties:
|
|
||||||
header: str
|
|
||||||
w_frame_style: FrameStyle = FrameStyle.MAX
|
|
||||||
h_frame_style: FrameStyle = FrameStyle.MAX
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def max(cls, header: str) -> Self:
|
|
||||||
return cls(
|
|
||||||
header,
|
|
||||||
FrameStyle.MAX,
|
|
||||||
FrameStyle.MAX,
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def min(cls, header: str) -> Self:
|
|
||||||
return cls(
|
|
||||||
header,
|
|
||||||
FrameStyle.MIN,
|
|
||||||
FrameStyle.MIN,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Orientation(Enum):
|
|
||||||
VERTICAL = auto()
|
|
||||||
HORIZONTAL = auto()
|
|
||||||
|
|
||||||
|
|
||||||
class PreviewStyle(Enum):
|
|
||||||
NONE = auto()
|
|
||||||
BOTTOM = auto()
|
|
||||||
RIGHT = auto()
|
|
||||||
TOP = auto()
|
|
||||||
|
|
||||||
|
|
||||||
# https://www.compart.com/en/unicode/search?q=box+drawings#characters
|
|
||||||
# https://en.wikipedia.org/wiki/Box-drawing_characters
|
|
||||||
class Chars:
|
|
||||||
Horizontal = '─'
|
|
||||||
Vertical = '│'
|
|
||||||
Upper_left = '┌'
|
|
||||||
Upper_right = '┐'
|
|
||||||
Lower_left = '└'
|
|
||||||
Lower_right = '┘'
|
|
||||||
Block = '█'
|
|
||||||
Triangle_up = '▲'
|
|
||||||
Triangle_down = '▼'
|
|
||||||
Check = '+'
|
|
||||||
Cross = 'x'
|
|
||||||
Right_arrow = '←'
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ViewportEntry:
|
|
||||||
text: str
|
|
||||||
row: int
|
|
||||||
col: int
|
|
||||||
style: STYLE
|
|
||||||
|
|
||||||
|
|
||||||
class Alignment(Enum):
|
|
||||||
LEFT = auto()
|
|
||||||
CENTER = auto()
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class FrameDim:
|
|
||||||
x_start: int
|
|
||||||
x_end: int
|
|
||||||
height: int
|
|
||||||
|
|
||||||
def x_delta(self) -> int:
|
|
||||||
return self.x_end - self.x_start
|
|
||||||
Loading…
Reference in New Issue