Reformat again
This commit is contained in:
parent
4db4959369
commit
fad8d1d04b
161
kls
161
kls
|
@ -79,7 +79,7 @@ FOOTER_HEIGHT: int = 3
|
||||||
ROWS_HEIGHT: int = curses.LINES - HEADER_HEIGHT - FOOTER_HEIGHT - 3
|
ROWS_HEIGHT: int = curses.LINES - HEADER_HEIGHT - FOOTER_HEIGHT - 3
|
||||||
# Generate HELP_TEXT from KEY_BINDINGS
|
# Generate HELP_TEXT from KEY_BINDINGS
|
||||||
HELP_TEXT: str = ", ".join(f"{key}: {binding['description']}" for key, binding in KEY_BINDINGS.items())
|
HELP_TEXT: str = ", ".join(f"{key}: {binding['description']}" for key, binding in KEY_BINDINGS.items())
|
||||||
HELP_TEXT += ", /: filter mode, Esc: exit filter mode or kls, arrows/TAB: navigation"
|
HELP_TEXT += ", /: filter mode, Esc: exit filter mode, arrows/TAB: navigation, q: exit kls"
|
||||||
|
|
||||||
|
|
||||||
# **************************** #
|
# **************************** #
|
||||||
|
@ -103,8 +103,8 @@ class CircularList:
|
||||||
|
|
||||||
class MenuState(Enum):
|
class MenuState(Enum):
|
||||||
NORMAL = auto()
|
NORMAL = auto()
|
||||||
EMPTY_FILTER = auto()
|
FILTER_MODE = auto()
|
||||||
FILLED_FILTER = auto()
|
FILTER_MODE_WITH_FILTER = auto()
|
||||||
|
|
||||||
|
|
||||||
class Menu:
|
class Menu:
|
||||||
|
@ -113,12 +113,13 @@ class Menu:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
title: str,
|
title: str,
|
||||||
rows: list[str],
|
rows_function,
|
||||||
begin_x: int,
|
begin_x: int,
|
||||||
width: int,
|
width: int,
|
||||||
):
|
):
|
||||||
self.title: str = title
|
self.title: str = title
|
||||||
self.rows: list[str] = rows
|
self.rows: list[str] = []
|
||||||
|
self.rows_function = rows_function
|
||||||
self.filter: str = ""
|
self.filter: str = ""
|
||||||
self.state: MenuState = MenuState.NORMAL
|
self.state: MenuState = MenuState.NORMAL
|
||||||
self.filtered_rows: CircularList = CircularList([x for x in self.rows if self.filter in x])
|
self.filtered_rows: CircularList = CircularList([x for x in self.rows if self.filter in x])
|
||||||
|
@ -132,6 +133,9 @@ class Menu:
|
||||||
self.win: curses.window = curses.newwin(curses.LINES - FOOTER_HEIGHT, self.width, 0, self.begin_x)
|
self.win: curses.window = curses.newwin(curses.LINES - FOOTER_HEIGHT, self.width, 0, self.begin_x)
|
||||||
self.dependent_menus: list[Self] = []
|
self.dependent_menus: list[Self] = []
|
||||||
|
|
||||||
|
async def refresh_rows(self):
|
||||||
|
self.rows = await self.rows_function()
|
||||||
|
|
||||||
def refresh_filtered_rows(self):
|
def refresh_filtered_rows(self):
|
||||||
self.filtered_rows = CircularList([x for x in self.rows if self.filter in x])
|
self.filtered_rows = CircularList([x for x in self.rows if self.filter in x])
|
||||||
|
|
||||||
|
@ -141,14 +145,12 @@ class Menu:
|
||||||
match self.state:
|
match self.state:
|
||||||
case MenuState.NORMAL:
|
case MenuState.NORMAL:
|
||||||
self.filter = ""
|
self.filter = ""
|
||||||
self.draw_menu_or_footer("")
|
await self.draw_menu_or_footer("")
|
||||||
await self.refresh_dependent_menus()
|
case MenuState.FILTER_MODE:
|
||||||
case MenuState.EMPTY_FILTER:
|
|
||||||
self.filter = ""
|
self.filter = ""
|
||||||
self.draw_menu_or_footer("/")
|
await self.draw_menu_or_footer("/")
|
||||||
await self.refresh_dependent_menus()
|
case MenuState.FILTER_MODE_WITH_FILTER:
|
||||||
case MenuState.FILLED_FILTER:
|
await self.draw_menu_or_footer(f"/{self.filter}") # if redrawing whole menu is not needed
|
||||||
self.draw_menu_or_footer(f"/{self.filter}") # if redrawing whole menu is not needed
|
|
||||||
await self.refresh_dependent_menus()
|
await self.refresh_dependent_menus()
|
||||||
|
|
||||||
def draw_rows(self) -> None:
|
def draw_rows(self) -> None:
|
||||||
|
@ -161,31 +163,36 @@ class Menu:
|
||||||
self.draw_rows()
|
self.draw_rows()
|
||||||
draw_row(
|
draw_row(
|
||||||
self.win,
|
self.win,
|
||||||
f"/{self.filter}" if self.state in [MenuState.EMPTY_FILTER, MenuState.FILLED_FILTER] else "",
|
f"/{self.filter}" if self.state in [MenuState.FILTER_MODE, MenuState.FILTER_MODE_WITH_FILTER] else "",
|
||||||
curses.LINES - FOOTER_HEIGHT - 2,
|
curses.LINES - FOOTER_HEIGHT - 2,
|
||||||
2,
|
2,
|
||||||
)
|
)
|
||||||
|
|
||||||
def draw_menu_or_footer(self, footer_text: str) -> None:
|
async def draw_menu_or_footer(self, footer_text: str) -> None:
|
||||||
previous_visible_rows = self.visible_rows()
|
previous_visible_rows = self.visible_rows()
|
||||||
self.refresh_filtered_rows()
|
self.refresh_filtered_rows()
|
||||||
if self.visible_rows() != previous_visible_rows: # draw whole menu
|
if self.visible_rows() != previous_visible_rows: # draw whole menu
|
||||||
self.visible_row_index = 0
|
self.visible_row_index = 0
|
||||||
self.draw_menu_with_footer()
|
self.draw_menu_with_footer()
|
||||||
self.refresh_dependent_menus()
|
if self == MENUS[0]:
|
||||||
|
await switch_context(self.selected_row())
|
||||||
|
await self.refresh_dependent_menus()
|
||||||
else: # draw footer only
|
else: # draw footer only
|
||||||
draw_row(self.win, footer_text, curses.LINES - FOOTER_HEIGHT - 2, 2)
|
draw_row(self.win, footer_text, curses.LINES - FOOTER_HEIGHT - 2, 2)
|
||||||
|
|
||||||
def set_dependent_menus(self, menus: list[Self]) -> None:
|
|
||||||
self.dependent_menus = menus
|
|
||||||
|
|
||||||
async def refresh_dependent_menus(self):
|
async def refresh_dependent_menus(self):
|
||||||
for menu in self.dependent_menus:
|
for menu in self.dependent_menus:
|
||||||
await refresh_menu(menu)
|
await menu.refresh_menu()
|
||||||
|
|
||||||
|
async def refresh_menu(self) -> None:
|
||||||
|
await self.refresh_rows()
|
||||||
|
self.refresh_filtered_rows()
|
||||||
|
if self.visible_row_index >= len(self.visible_rows()):
|
||||||
|
self.visible_row_index = 0 # reset selected row only if number of lines changed
|
||||||
|
self.draw_menu_with_footer()
|
||||||
|
|
||||||
|
|
||||||
# Global variables
|
# Global variables
|
||||||
FOURTH_MENU_LOCK: asyncio.Lock = asyncio.Lock()
|
|
||||||
FOURTH_MENU_TASK: Optional[asyncio.Task] = None
|
FOURTH_MENU_TASK: Optional[asyncio.Task] = None
|
||||||
MENUS: list[Menu] = []
|
MENUS: list[Menu] = []
|
||||||
|
|
||||||
|
@ -196,39 +203,13 @@ def draw_row(window: curses.window, text: str, y: int, x: int, selected: bool =
|
||||||
window.refresh()
|
window.refresh()
|
||||||
|
|
||||||
|
|
||||||
async def refresh_menu(menu: Menu) -> None:
|
async def switch_context(context: str) -> None:
|
||||||
if menu == MENUS[1]:
|
if not context:
|
||||||
menu.rows = await get_namespaces()
|
return
|
||||||
elif menu == MENUS[2]:
|
|
||||||
menu.rows = await get_api_resources()
|
|
||||||
menu.refresh_filtered_rows()
|
|
||||||
menu.visible_row_index = 0
|
|
||||||
menu.draw_menu_with_footer()
|
|
||||||
|
|
||||||
|
|
||||||
async def refresh_resources_menu(namespace: Optional[str], api_resource: Optional[str]) -> None:
|
|
||||||
try:
|
try:
|
||||||
async with FOURTH_MENU_LOCK:
|
await kubectl_async(f"config use-context {context}")
|
||||||
menu = MENUS[3]
|
|
||||||
previous_menu_rows = menu.rows
|
|
||||||
if api_resource and namespace:
|
|
||||||
try:
|
|
||||||
menu.rows = await kubectl_async(
|
|
||||||
f"-n {namespace} get {api_resource} --no-headers --ignore-not-found --sort-by='{{.metadata.name}}'"
|
|
||||||
)
|
|
||||||
except subprocess.CalledProcessError:
|
except subprocess.CalledProcessError:
|
||||||
menu.rows = []
|
pass
|
||||||
else:
|
|
||||||
menu.rows = []
|
|
||||||
index_before_update = menu.filtered_rows.index
|
|
||||||
menu.refresh_filtered_rows()
|
|
||||||
menu.filtered_rows.index = index_before_update
|
|
||||||
if menu.visible_row_index >= len(menu.visible_rows()):
|
|
||||||
menu.visible_row_index = 0
|
|
||||||
if previous_menu_rows != menu.rows:
|
|
||||||
menu.draw_menu_with_footer()
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
raise
|
|
||||||
|
|
||||||
|
|
||||||
async def get_contexts() -> list[str]:
|
async def get_contexts() -> list[str]:
|
||||||
|
@ -242,15 +223,6 @@ async def get_contexts() -> list[str]:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
async def switch_context(context: str) -> None:
|
|
||||||
if not context:
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
await kubectl_async(f"config use-context {context}")
|
|
||||||
except subprocess.CalledProcessError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
async def get_namespaces() -> list[str]:
|
async def get_namespaces() -> list[str]:
|
||||||
namespaces: list[str] = []
|
namespaces: list[str] = []
|
||||||
context = MENUS[0].selected_row() and MENUS[0].selected_row().split()[0]
|
context = MENUS[0].selected_row() and MENUS[0].selected_row().split()[0]
|
||||||
|
@ -282,18 +254,31 @@ async def get_api_resources() -> list[str]:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
async def get_resources() -> list[str]:
|
||||||
|
api_resource = MENUS[2].selected_row()
|
||||||
|
namespace = MENUS[1].selected_row()
|
||||||
|
if not (api_resource and namespace):
|
||||||
|
return []
|
||||||
|
try:
|
||||||
|
resources = await kubectl_async(
|
||||||
|
f"-n {namespace} get {api_resource} --no-headers --ignore-not-found --sort-by='{{.metadata.name}}'"
|
||||||
|
)
|
||||||
|
return resources
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
async def handle_key_bindings(key: str) -> None:
|
async def handle_key_bindings(key: str) -> None:
|
||||||
api_resource = MENUS[2].selected_row()
|
api_resource = MENUS[2].selected_row()
|
||||||
|
if key == "KEY_DC":
|
||||||
|
key = "Delete"
|
||||||
if KEY_BINDINGS[key]["kind"] != api_resource and KEY_BINDINGS[key]["kind"] != "all":
|
if KEY_BINDINGS[key]["kind"] != api_resource and KEY_BINDINGS[key]["kind"] != "all":
|
||||||
return
|
return
|
||||||
resource = MENUS[3].selected_row() and MENUS[3].selected_row().split()[0]
|
resource = MENUS[3].selected_row() and MENUS[3].selected_row().split()[0]
|
||||||
if not resource:
|
if not resource:
|
||||||
return
|
return
|
||||||
namespace = MENUS[1].selected_row()
|
namespace = MENUS[1].selected_row()
|
||||||
if key == "KEY_DC":
|
|
||||||
key = "Delete"
|
|
||||||
await cancel_resources_refreshing()
|
await cancel_resources_refreshing()
|
||||||
async with FOURTH_MENU_LOCK:
|
|
||||||
curses.def_prog_mode()
|
curses.def_prog_mode()
|
||||||
curses.endwin()
|
curses.endwin()
|
||||||
command = KEY_BINDINGS[key]["command"].format(namespace=namespace, api_resource=api_resource, resource=resource)
|
command = KEY_BINDINGS[key]["command"].format(namespace=namespace, api_resource=api_resource, resource=resource)
|
||||||
|
@ -438,19 +423,16 @@ def enable_mouse_support() -> None:
|
||||||
|
|
||||||
|
|
||||||
async def init_menus() -> list[Menu]:
|
async def init_menus() -> list[Menu]:
|
||||||
MENUS.append(Menu("Contexts", await get_contexts(), 0, CONTEXTS_WIDTH))
|
MENUS.append(Menu("Contexts", get_contexts, 0, CONTEXTS_WIDTH))
|
||||||
MENUS.append(Menu("Namespaces", await get_namespaces(), CONTEXTS_WIDTH, NAMESPACES_WIDTH))
|
MENUS.append(Menu("Namespaces", get_namespaces, CONTEXTS_WIDTH, NAMESPACES_WIDTH))
|
||||||
|
MENUS.append(Menu("API resources", get_api_resources, CONTEXTS_WIDTH + NAMESPACES_WIDTH, API_RESOURCES_WIDTH))
|
||||||
MENUS.append(
|
MENUS.append(
|
||||||
Menu("API resources", await get_api_resources(), CONTEXTS_WIDTH + NAMESPACES_WIDTH, API_RESOURCES_WIDTH)
|
Menu("Resources", get_resources, CONTEXTS_WIDTH + NAMESPACES_WIDTH + API_RESOURCES_WIDTH, RESOURCES_WIDTH)
|
||||||
)
|
)
|
||||||
MENUS.append(Menu("Resources", [], CONTEXTS_WIDTH + NAMESPACES_WIDTH + API_RESOURCES_WIDTH, RESOURCES_WIDTH))
|
|
||||||
return MENUS
|
return MENUS
|
||||||
|
|
||||||
|
|
||||||
async def main_async() -> None:
|
async def setup_curses() -> None:
|
||||||
global MENUS, FOURTH_MENU_TASK
|
|
||||||
MENUS = await init_menus()
|
|
||||||
Menu.selected = MENUS[0]
|
|
||||||
SCREEN.refresh()
|
SCREEN.refresh()
|
||||||
SCREEN.nodelay(True)
|
SCREEN.nodelay(True)
|
||||||
SCREEN.keypad(True)
|
SCREEN.keypad(True)
|
||||||
|
@ -459,52 +441,65 @@ async def main_async() -> None:
|
||||||
curses.use_default_colors()
|
curses.use_default_colors()
|
||||||
curses.noecho()
|
curses.noecho()
|
||||||
enable_mouse_support()
|
enable_mouse_support()
|
||||||
|
|
||||||
|
|
||||||
|
async def initialize_interface() -> None:
|
||||||
|
global MENUS
|
||||||
|
MENUS = await init_menus()
|
||||||
|
Menu.selected = MENUS[0]
|
||||||
|
await setup_curses()
|
||||||
|
|
||||||
for index, menu in enumerate(MENUS):
|
for index, menu in enumerate(MENUS):
|
||||||
|
await menu.refresh_rows()
|
||||||
|
menu.refresh_filtered_rows()
|
||||||
menu.draw_menu_with_footer()
|
menu.draw_menu_with_footer()
|
||||||
menu.set_dependent_menus(MENUS[index + 1 :])
|
menu.dependent_menus = MENUS[index + 1 :] # all other menu to the right
|
||||||
draw_row(curses.newwin(3, curses.COLS, curses.LINES - FOOTER_HEIGHT, 0), HELP_TEXT, 1, 2)
|
draw_row(curses.newwin(3, curses.COLS, curses.LINES - FOOTER_HEIGHT, 0), HELP_TEXT, 1, 2)
|
||||||
|
|
||||||
|
|
||||||
|
async def main_async() -> None:
|
||||||
|
global MENUS, FOURTH_MENU_TASK
|
||||||
|
await initialize_interface()
|
||||||
while True:
|
while True:
|
||||||
menu = Menu.selected
|
menu = Menu.selected
|
||||||
try:
|
try:
|
||||||
key = SCREEN.getkey()
|
key = SCREEN.getkey()
|
||||||
except curses.error:
|
except curses.error:
|
||||||
if FOURTH_MENU_TASK is None or FOURTH_MENU_TASK.done():
|
if FOURTH_MENU_TASK is None or FOURTH_MENU_TASK.done():
|
||||||
FOURTH_MENU_TASK = asyncio.create_task(
|
FOURTH_MENU_TASK = asyncio.create_task(MENUS[3].refresh_menu())
|
||||||
refresh_resources_menu(MENUS[1].selected_row(), MENUS[2].selected_row())
|
|
||||||
)
|
|
||||||
await asyncio.sleep(0.01)
|
await asyncio.sleep(0.01)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# handle state-dependent keys
|
# handle state-dependent keys
|
||||||
match menu.state:
|
match menu.state:
|
||||||
case MenuState.NORMAL:
|
case MenuState.NORMAL:
|
||||||
if key == "\x1b": # E (Escape)
|
if key == "q": # Q (Quit)
|
||||||
break # Exit
|
break # Exit
|
||||||
elif key == "/": # S (Slash)
|
elif key == "/": # S (Slash)
|
||||||
await menu.set_state(MenuState.EMPTY_FILTER) # Transition to EmptyFilter state
|
await menu.set_state(MenuState.FILTER_MODE) # Transition to EmptyFilter state
|
||||||
continue
|
continue
|
||||||
case MenuState.EMPTY_FILTER:
|
case MenuState.FILTER_MODE:
|
||||||
if key == "\x1b": # E (Escape)
|
if key == "\x1b": # E (Escape)
|
||||||
await menu.set_state(MenuState.NORMAL) # Transition to Normal state
|
await menu.set_state(MenuState.NORMAL) # Transition to Normal state
|
||||||
continue
|
continue
|
||||||
elif key.isalnum() or key == "-": # A (Type text)
|
elif key.isalnum() or key == "-": # A (Type text)
|
||||||
menu.filter += key.lower()
|
menu.filter += key.lower()
|
||||||
await menu.set_state(MenuState.FILLED_FILTER) # Transition to FilledFilter state
|
await menu.set_state(MenuState.FILTER_MODE_WITH_FILTER) # Transition to FilledFilter state
|
||||||
continue
|
continue
|
||||||
case MenuState.FILLED_FILTER: # FilledFilter state
|
case MenuState.FILTER_MODE_WITH_FILTER: # FilledFilter state
|
||||||
if key == "\x1b": # E (Escape)
|
if key == "\x1b": # E (Escape)
|
||||||
await menu.set_state(MenuState.NORMAL) # Transition to Normal state
|
await menu.set_state(MenuState.NORMAL) # Transition to Normal state
|
||||||
continue
|
continue
|
||||||
elif key in ["KEY_BACKSPACE", "\x08"]: # B (Backspace)
|
elif key in ["KEY_BACKSPACE", "\x08"]: # B (Backspace)
|
||||||
if len(menu.filter) == 1:
|
if len(menu.filter) == 1:
|
||||||
await menu.set_state(MenuState.EMPTY_FILTER) # Transition to EmptyFilter state
|
await menu.set_state(MenuState.FILTER_MODE) # Transition to EmptyFilter state
|
||||||
continue
|
continue
|
||||||
menu.filter = menu.filter[:-1]
|
menu.filter = menu.filter[:-1]
|
||||||
menu.draw_menu_or_footer(f"/{menu.filter}")
|
await menu.draw_menu_or_footer(f"/{menu.filter}")
|
||||||
continue
|
continue
|
||||||
elif key.isalnum() or key == "-": # A (Type text)
|
elif key.isalnum() or key == "-": # A (Type text)
|
||||||
menu.filter += key.lower() # Stay in FilledFilter state
|
menu.filter += key.lower() # Stay in FilledFilter state
|
||||||
menu.draw_menu_or_footer(f"/{menu.filter}")
|
await menu.draw_menu_or_footer(f"/{menu.filter}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# handle state-independent keys (Vertical/Horizontal navigation etc. available in all states)
|
# handle state-independent keys (Vertical/Horizontal navigation etc. available in all states)
|
||||||
|
|
Loading…
Reference in New Issue