From 03bc2bc96deb9319485df698af1b92ad906d8f24 Mon Sep 17 00:00:00 2001 From: Digital Studium Date: Mon, 6 May 2024 13:26:08 +0300 Subject: [PATCH] Fix index out of range for third menu --- kls | 71 +++++++++++++++++++++++++++++++++++---------------------- test.py | 42 +++++++++++++++++++++++++++++----- 2 files changed, 80 insertions(+), 33 deletions(-) diff --git a/kls b/kls index bb6d884..124d7d6 100755 --- a/kls +++ b/kls @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import subprocess, curses, time +# constants KEY_BINDINGS = { # can be extended "1": 'kubectl -n {namespace} get {api_resource} {resource} -o yaml | batcat -l yaml --paging always --style numbers', "\n": 'kubectl -n {namespace} get {api_resource} {resource} -o yaml | batcat -l yaml --paging always --style numbers', # Enter key @@ -14,11 +15,8 @@ KEY_BINDINGS = { # can be extended # which api resources are on the top of menu? TOP_API_RESOURCES = ["pods", "services", "configmaps", "secrets", "persistentvolumeclaims", "ingresses", "nodes", "deployments", "statefulsets", "daemonsets", "storageclasses", "all"] - HELP_TEXT = "letters: filter mode, Esc: exit filter mode or exit kls, 1/Enter: get yaml, 2: describe, 3: edit, 4: logs, 5: exec, 6: debug, arrows/TAB/PgUp/PgDn: navigation" - MOUSE_ENABLED = True - SCREEN = curses.initscr() # screen initialization, needed for ROWS_HEIGHT working HEADER_HEIGHT = 4 # in rows FOOTER_HEIGHT = 3 @@ -26,6 +24,7 @@ ROWS_HEIGHT = curses.LINES - HEADER_HEIGHT - FOOTER_HEIGHT - 3 # maximum numbe WIDTH = curses.COLS +# classes class CircularList: def __init__(self, elements): self.elements = elements @@ -56,6 +55,7 @@ class Menu: self.win = curses.newwin(curses.LINES - FOOTER_HEIGHT, width, 0, begin_x) +# helper functions def draw_row(window: curses.window, text: str, y: int, x: int, selected: bool = False): window.addstr(y, x, text, curses.A_REVERSE | curses.A_BOLD if selected else curses.A_NORMAL) window.clrtoeol() @@ -87,7 +87,9 @@ def refresh_third_menu(): draw_menu(menu) -def run_command(key: str, api_resource: str, resource: str): +def handle_key_bindings(key: str, api_resource: str, resource: str): + if not menus[2].selected_row(): + return if key in ("4", "5", "6"): if api_resource not in ["pods", "all"] or (api_resource == "all" and not resource.startswith("pod/")): return @@ -99,12 +101,15 @@ def run_command(key: str, api_resource: str, resource: str): subprocess.call(command, shell=True) curses.reset_prog_mode() # restore the previous terminal state SCREEN.refresh() - curses.mousemask(curses.REPORT_MOUSE_POSITION) # mouse tracking - print('\033[?1003h') # enable mouse tracking with the XTERM API. That's the magic + enable_mouse_support() def handle_filter_state(key: str, menu: Menu): - if key == "\x1b": + if key in ["KEY_BACKSPACE", "\x08"] and not menu.filter: + return + elif key == "\x1b" and not menu.filter: + globals().update(selected_menu=None) # exit + elif key == "\x1b": menu.filter = "" # Escape key exits filter mode elif key in ["KEY_BACKSPACE", "\x08"]: menu.filter = menu.filter[:-1] # Backspace key deletes a character (\x08 is also Backspace) @@ -118,6 +123,8 @@ def handle_filter_state(key: str, menu: Menu): def handle_mouse(menu: Menu): + if not MOUSE_ENABLED: + return try: mouse_info = curses.getmouse() except curses.error: # this fixes scrolling error @@ -153,21 +160,29 @@ def handle_mouse(menu: Menu): def handle_vertical_navigation(key: str, menu: Menu): if len(menu.visible_rows()) <= 1: return - shifts = {"KEY_DOWN": 1, "KEY_UP": -1, "KEY_NPAGE": 1, "KEY_PPAGE": -1, 'KEY_HOME': 0, 'KEY_END': -1} + keys_numbers = {"KEY_DOWN": 1, "KEY_UP": -1, "KEY_NPAGE": 1, "KEY_PPAGE": -1, 'KEY_HOME': 0, 'KEY_END': -1} if key in ["KEY_DOWN", "KEY_UP"]: if menu.filtered_rows.size > menu.rows_height: - menu.filtered_rows.shift(shifts[key]) + menu.filtered_rows.shift(keys_numbers[key]) else: - menu.visible_row_index = (menu.visible_row_index + shifts[key]) % menu.filtered_rows.size # index of the selected visible row + menu.visible_row_index = (menu.visible_row_index + keys_numbers[key]) % menu.filtered_rows.size # index of the selected visible row elif key in ["KEY_NPAGE", "KEY_PPAGE"]: - menu.filtered_rows.shift(shifts[key] * len(menu.visible_rows())) + menu.filtered_rows.shift(keys_numbers[key] * len(menu.visible_rows())) elif key in ['KEY_HOME','KEY_END']: - menu.visible_row_index = shifts[key] + menu.visible_row_index = keys_numbers[key] draw_rows(menu) if menu != menus[2]: menus[2].visible_row_index = 0 +def handle_horizontal_navigation(key, menu): + increment = {"KEY_RIGHT": 1, "\t": 1, "KEY_LEFT": -1, "KEY_BTAB": -1}[key] + next_menu = menus[(menus.index(menu) + increment) % 3] + draw_row(menu.win, menu.title, 1, 2, selected=False) # remove selection from the current menu title + draw_row(next_menu.win, next_menu.title, 1, 2, selected=True) # and select the new menu title + globals().update(selected_menu=next_menu) + + def catch_input(menu: Menu): while True: # refresh third menu until key pressed try: @@ -177,20 +192,14 @@ def catch_input(menu: Menu): refresh_third_menu() time.sleep(0.1) if key in ["\t", "KEY_RIGHT", "KEY_BTAB", "KEY_LEFT"]: - increment = {"KEY_RIGHT": 1, "\t": 1, "KEY_LEFT": -1, "KEY_BTAB": -1}[key] - next_menu = menus[(menus.index(menu) + increment) % 3] - draw_row(menu.win, menu.title, 1, 2, selected=False) # remove selection from the current menu title - draw_row(next_menu.win, next_menu.title, 1, 2, selected=True) # and select the new menu title - globals().update(selected_menu=next_menu) + handle_horizontal_navigation(key, menu) elif key in ["KEY_UP", "KEY_DOWN", "KEY_NPAGE", "KEY_PPAGE", "KEY_HOME", "KEY_END"]: handle_vertical_navigation(key, menu) - elif key == "KEY_MOUSE" and MOUSE_ENABLED: + elif key == "KEY_MOUSE": handle_mouse(menu) - elif key in KEY_BINDINGS.keys() and menus[2].selected_row(): - run_command(key, api_resource(), resource()) - elif key == "\x1b" and not menu.filter: - globals().update(selected_menu=None) # exit - elif (key in ["\x1b", "KEY_BACKSPACE", "\x08"] and menu.filter) or key.isalpha() or key == "-": + elif key in KEY_BINDINGS.keys(): + handle_key_bindings(key, api_resource(), resource()) + elif key in ["\x1b", "KEY_BACKSPACE", "\x08"] or key.isalpha() or key == "-": # \x1b - escape, \x08 - backspace handle_filter_state(key, menu) @@ -198,7 +207,12 @@ def kubectl(command: str) -> list: return subprocess.check_output(f"kubectl {command}", shell=True).decode().strip().split("\n") -def main(screen): +def enable_mouse_support(): + curses.mousemask(curses.REPORT_MOUSE_POSITION) # mouse tracking + print('\033[?1003h') # enable mouse tracking with the XTERM API. That's the magic + + +def init_menus(): global menus, selected_menu, namespace, api_resource, resource api_resources_kubectl = [x.split()[0] for x in kubectl("api-resources --no-headers --verbs=get")] api_resources = list( @@ -210,7 +224,7 @@ def main(screen): selected_menu = menus[0] namespace = menus[0].selected_row # method alias api_resource = menus[1].selected_row - resource = lambda: menus[2].selected_row().split()[0] + resource = lambda: menus[2].selected_row().split()[0] if menus[2].selected_row() else None SCREEN.refresh() # I don't know why this is needed but it doesn't work without it SCREEN.nodelay(True) # don't block while waiting for input SCREEN.keypad(True) # needed for arrow keys @@ -218,8 +232,11 @@ def main(screen): curses.curs_set(0) # make the cursor invisible curses.use_default_colors() # don't change the terminal color curses.noecho() # don't output characters at the top - curses.mousemask(curses.REPORT_MOUSE_POSITION) # mouse tracking - print('\033[?1003h') # enable mouse tracking with the XTERM API. That's the magic + enable_mouse_support() + + +def main(screen): + init_menus() for menu in menus: # draw the main windows draw_menu(menu) draw_row(curses.newwin(3, curses.COLS, curses.LINES - FOOTER_HEIGHT, 0), HELP_TEXT, 1, 2) # and the help window diff --git a/test.py b/test.py index 8781f7a..bb614f5 100644 --- a/test.py +++ b/test.py @@ -4,12 +4,12 @@ import os os.system("ln -s kls kls.py") -from kls import * # Import the functions and classes from kls script +import kls class TestCircularList(unittest.TestCase): def setUp(self): - self.circular_list = CircularList(['a', 'b', 'c']) + self.circular_list = kls.CircularList(['a', 'b', 'c']) def test_forward(self): self.circular_list.shift(1) @@ -24,14 +24,16 @@ class TestScriptFunctions(unittest.TestCase): @patch('kls.subprocess.check_output') def test_kubectl(self, mock_check_output): mock_check_output.return_value = b'pod1\npod2\npod3' - result = kubectl('get pods') + result = kls.kubectl('get pods') self.assertEqual(result, ['pod1', 'pod2', 'pod3']) class TestMenu(unittest.TestCase): def setUp(self): self.rows = ['a', 'b', 'c'] - self.menu = Menu('Test', self.rows, 0, 10, 2) + self.second_menu = kls.Menu("Test Menu 2", ["option1", "option2", "option3"], 0, 10, 2) + self.third_menu = kls.Menu("Test Menu 3", ["option1", "option2", "option3"], 0, 10, 2) + self.menu = kls.Menu('Test', self.rows, 0, 10, 2) os.system("ln -s kls kls.py") def test_menu(self): @@ -43,15 +45,43 @@ class TestMenu(unittest.TestCase): def test_filter_rows_with_filter(self): # Apply a filter and test self.menu.filter = 'a' - self.menu.filtered_rows = CircularList([x for x in self.menu.rows if self.menu.filter in x]) + self.menu.filtered_rows = kls.CircularList([x for x in self.menu.rows if self.menu.filter in x]) self.assertEqual(self.menu.filtered_rows.elements, ['a']) def test_filter_rows_with_nonexistent_filter(self): # Apply a filter that matches no rows self.menu.filter = 'nonexistent' - self.menu.filtered_rows = CircularList([x for x in self.menu.rows if self.menu.filter in x]) + self.menu.filtered_rows = kls.CircularList([x for x in self.menu.rows if self.menu.filter in x]) self.assertEqual(self.menu.filtered_rows.elements, []) + def test_vertical_navigation(self): + kls.selected_menu = self.menu + # global menus + kls.menus = [self.menu, self.second_menu, self.third_menu] # Add the menu to the list of menus + # Test moving down one row + kls.handle_vertical_navigation("KEY_DOWN", self.menu) + self.assertEqual(self.menu.visible_row_index, 0) + + # Test moving up one row + kls.handle_vertical_navigation("KEY_UP", self.menu) + self.assertEqual(self.menu.visible_row_index, 0) + + # Test moving to the next page + kls.handle_vertical_navigation("KEY_NPAGE", self.menu) + self.assertEqual(self.menu.visible_row_index, 0) + + # Test moving to the previous page + kls.handle_vertical_navigation("KEY_PPAGE", self.menu) + self.assertEqual(self.menu.visible_row_index, 0) + + # Test moving to the first row + kls.handle_vertical_navigation("KEY_HOME", self.menu) + self.assertEqual(self.menu.visible_row_index, 0) + + # Test moving to the last row + kls.handle_vertical_navigation("KEY_END", self.menu) + self.assertEqual(self.menu.visible_row_index, -1) + def tearDown(self): # Remove the symlink after each test os.unlink('kls.py')