Fix index out of range for third menu
This commit is contained in:
parent
25dd5420d1
commit
03bc2bc96d
71
kls
71
kls
|
@ -1,6 +1,7 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import subprocess, curses, time
|
import subprocess, curses, time
|
||||||
|
|
||||||
|
# constants
|
||||||
KEY_BINDINGS = { # can be extended
|
KEY_BINDINGS = { # can be extended
|
||||||
"1": 'kubectl -n {namespace} get {api_resource} {resource} -o yaml | batcat -l yaml --paging always --style numbers',
|
"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
|
"\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?
|
# which api resources are on the top of menu?
|
||||||
TOP_API_RESOURCES = ["pods", "services", "configmaps", "secrets", "persistentvolumeclaims", "ingresses", "nodes",
|
TOP_API_RESOURCES = ["pods", "services", "configmaps", "secrets", "persistentvolumeclaims", "ingresses", "nodes",
|
||||||
"deployments", "statefulsets", "daemonsets", "storageclasses", "all"]
|
"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"
|
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
|
MOUSE_ENABLED = True
|
||||||
|
|
||||||
SCREEN = curses.initscr() # screen initialization, needed for ROWS_HEIGHT working
|
SCREEN = curses.initscr() # screen initialization, needed for ROWS_HEIGHT working
|
||||||
HEADER_HEIGHT = 4 # in rows
|
HEADER_HEIGHT = 4 # in rows
|
||||||
FOOTER_HEIGHT = 3
|
FOOTER_HEIGHT = 3
|
||||||
|
@ -26,6 +24,7 @@ ROWS_HEIGHT = curses.LINES - HEADER_HEIGHT - FOOTER_HEIGHT - 3 # maximum numbe
|
||||||
WIDTH = curses.COLS
|
WIDTH = curses.COLS
|
||||||
|
|
||||||
|
|
||||||
|
# classes
|
||||||
class CircularList:
|
class CircularList:
|
||||||
def __init__(self, elements):
|
def __init__(self, elements):
|
||||||
self.elements = elements
|
self.elements = elements
|
||||||
|
@ -56,6 +55,7 @@ class Menu:
|
||||||
self.win = curses.newwin(curses.LINES - FOOTER_HEIGHT, width, 0, begin_x)
|
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):
|
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.addstr(y, x, text, curses.A_REVERSE | curses.A_BOLD if selected else curses.A_NORMAL)
|
||||||
window.clrtoeol()
|
window.clrtoeol()
|
||||||
|
@ -87,7 +87,9 @@ def refresh_third_menu():
|
||||||
draw_menu(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 key in ("4", "5", "6"):
|
||||||
if api_resource not in ["pods", "all"] or (api_resource == "all" and not resource.startswith("pod/")):
|
if api_resource not in ["pods", "all"] or (api_resource == "all" and not resource.startswith("pod/")):
|
||||||
return
|
return
|
||||||
|
@ -99,12 +101,15 @@ def run_command(key: str, api_resource: str, resource: str):
|
||||||
subprocess.call(command, shell=True)
|
subprocess.call(command, shell=True)
|
||||||
curses.reset_prog_mode() # restore the previous terminal state
|
curses.reset_prog_mode() # restore the previous terminal state
|
||||||
SCREEN.refresh()
|
SCREEN.refresh()
|
||||||
curses.mousemask(curses.REPORT_MOUSE_POSITION) # mouse tracking
|
enable_mouse_support()
|
||||||
print('\033[?1003h') # enable mouse tracking with the XTERM API. That's the magic
|
|
||||||
|
|
||||||
|
|
||||||
def handle_filter_state(key: str, menu: Menu):
|
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
|
menu.filter = "" # Escape key exits filter mode
|
||||||
elif key in ["KEY_BACKSPACE", "\x08"]:
|
elif key in ["KEY_BACKSPACE", "\x08"]:
|
||||||
menu.filter = menu.filter[:-1] # Backspace key deletes a character (\x08 is also Backspace)
|
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):
|
def handle_mouse(menu: Menu):
|
||||||
|
if not MOUSE_ENABLED:
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
mouse_info = curses.getmouse()
|
mouse_info = curses.getmouse()
|
||||||
except curses.error: # this fixes scrolling error
|
except curses.error: # this fixes scrolling error
|
||||||
|
@ -153,21 +160,29 @@ def handle_mouse(menu: Menu):
|
||||||
def handle_vertical_navigation(key: str, menu: Menu):
|
def handle_vertical_navigation(key: str, menu: Menu):
|
||||||
if len(menu.visible_rows()) <= 1:
|
if len(menu.visible_rows()) <= 1:
|
||||||
return
|
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 key in ["KEY_DOWN", "KEY_UP"]:
|
||||||
if menu.filtered_rows.size > menu.rows_height:
|
if menu.filtered_rows.size > menu.rows_height:
|
||||||
menu.filtered_rows.shift(shifts[key])
|
menu.filtered_rows.shift(keys_numbers[key])
|
||||||
else:
|
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"]:
|
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']:
|
elif key in ['KEY_HOME','KEY_END']:
|
||||||
menu.visible_row_index = shifts[key]
|
menu.visible_row_index = keys_numbers[key]
|
||||||
draw_rows(menu)
|
draw_rows(menu)
|
||||||
if menu != menus[2]:
|
if menu != menus[2]:
|
||||||
menus[2].visible_row_index = 0
|
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):
|
def catch_input(menu: Menu):
|
||||||
while True: # refresh third menu until key pressed
|
while True: # refresh third menu until key pressed
|
||||||
try:
|
try:
|
||||||
|
@ -177,20 +192,14 @@ def catch_input(menu: Menu):
|
||||||
refresh_third_menu()
|
refresh_third_menu()
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
if key in ["\t", "KEY_RIGHT", "KEY_BTAB", "KEY_LEFT"]:
|
if key in ["\t", "KEY_RIGHT", "KEY_BTAB", "KEY_LEFT"]:
|
||||||
increment = {"KEY_RIGHT": 1, "\t": 1, "KEY_LEFT": -1, "KEY_BTAB": -1}[key]
|
handle_horizontal_navigation(key, menu)
|
||||||
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)
|
|
||||||
elif key in ["KEY_UP", "KEY_DOWN", "KEY_NPAGE", "KEY_PPAGE", "KEY_HOME", "KEY_END"]:
|
elif key in ["KEY_UP", "KEY_DOWN", "KEY_NPAGE", "KEY_PPAGE", "KEY_HOME", "KEY_END"]:
|
||||||
handle_vertical_navigation(key, menu)
|
handle_vertical_navigation(key, menu)
|
||||||
elif key == "KEY_MOUSE" and MOUSE_ENABLED:
|
elif key == "KEY_MOUSE":
|
||||||
handle_mouse(menu)
|
handle_mouse(menu)
|
||||||
elif key in KEY_BINDINGS.keys() and menus[2].selected_row():
|
elif key in KEY_BINDINGS.keys():
|
||||||
run_command(key, api_resource(), resource())
|
handle_key_bindings(key, api_resource(), resource())
|
||||||
elif key == "\x1b" and not menu.filter:
|
elif key in ["\x1b", "KEY_BACKSPACE", "\x08"] or key.isalpha() or key == "-": # \x1b - escape, \x08 - backspace
|
||||||
globals().update(selected_menu=None) # exit
|
|
||||||
elif (key in ["\x1b", "KEY_BACKSPACE", "\x08"] and menu.filter) or key.isalpha() or key == "-":
|
|
||||||
handle_filter_state(key, menu)
|
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")
|
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
|
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_kubectl = [x.split()[0] for x in kubectl("api-resources --no-headers --verbs=get")]
|
||||||
api_resources = list(
|
api_resources = list(
|
||||||
|
@ -210,7 +224,7 @@ def main(screen):
|
||||||
selected_menu = menus[0]
|
selected_menu = menus[0]
|
||||||
namespace = menus[0].selected_row # method alias
|
namespace = menus[0].selected_row # method alias
|
||||||
api_resource = menus[1].selected_row
|
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.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.nodelay(True) # don't block while waiting for input
|
||||||
SCREEN.keypad(True) # needed for arrow keys
|
SCREEN.keypad(True) # needed for arrow keys
|
||||||
|
@ -218,8 +232,11 @@ def main(screen):
|
||||||
curses.curs_set(0) # make the cursor invisible
|
curses.curs_set(0) # make the cursor invisible
|
||||||
curses.use_default_colors() # don't change the terminal color
|
curses.use_default_colors() # don't change the terminal color
|
||||||
curses.noecho() # don't output characters at the top
|
curses.noecho() # don't output characters at the top
|
||||||
curses.mousemask(curses.REPORT_MOUSE_POSITION) # mouse tracking
|
enable_mouse_support()
|
||||||
print('\033[?1003h') # enable mouse tracking with the XTERM API. That's the magic
|
|
||||||
|
|
||||||
|
def main(screen):
|
||||||
|
init_menus()
|
||||||
for menu in menus: # draw the main windows
|
for menu in menus: # draw the main windows
|
||||||
draw_menu(menu)
|
draw_menu(menu)
|
||||||
draw_row(curses.newwin(3, curses.COLS, curses.LINES - FOOTER_HEIGHT, 0), HELP_TEXT, 1, 2) # and the help window
|
draw_row(curses.newwin(3, curses.COLS, curses.LINES - FOOTER_HEIGHT, 0), HELP_TEXT, 1, 2) # and the help window
|
||||||
|
|
42
test.py
42
test.py
|
@ -4,12 +4,12 @@ import os
|
||||||
|
|
||||||
os.system("ln -s kls kls.py")
|
os.system("ln -s kls kls.py")
|
||||||
|
|
||||||
from kls import * # Import the functions and classes from kls script
|
import kls
|
||||||
|
|
||||||
|
|
||||||
class TestCircularList(unittest.TestCase):
|
class TestCircularList(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.circular_list = CircularList(['a', 'b', 'c'])
|
self.circular_list = kls.CircularList(['a', 'b', 'c'])
|
||||||
|
|
||||||
def test_forward(self):
|
def test_forward(self):
|
||||||
self.circular_list.shift(1)
|
self.circular_list.shift(1)
|
||||||
|
@ -24,14 +24,16 @@ class TestScriptFunctions(unittest.TestCase):
|
||||||
@patch('kls.subprocess.check_output')
|
@patch('kls.subprocess.check_output')
|
||||||
def test_kubectl(self, mock_check_output):
|
def test_kubectl(self, mock_check_output):
|
||||||
mock_check_output.return_value = b'pod1\npod2\npod3'
|
mock_check_output.return_value = b'pod1\npod2\npod3'
|
||||||
result = kubectl('get pods')
|
result = kls.kubectl('get pods')
|
||||||
self.assertEqual(result, ['pod1', 'pod2', 'pod3'])
|
self.assertEqual(result, ['pod1', 'pod2', 'pod3'])
|
||||||
|
|
||||||
|
|
||||||
class TestMenu(unittest.TestCase):
|
class TestMenu(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.rows = ['a', 'b', 'c']
|
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")
|
os.system("ln -s kls kls.py")
|
||||||
|
|
||||||
def test_menu(self):
|
def test_menu(self):
|
||||||
|
@ -43,15 +45,43 @@ class TestMenu(unittest.TestCase):
|
||||||
def test_filter_rows_with_filter(self):
|
def test_filter_rows_with_filter(self):
|
||||||
# Apply a filter and test
|
# Apply a filter and test
|
||||||
self.menu.filter = 'a'
|
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'])
|
self.assertEqual(self.menu.filtered_rows.elements, ['a'])
|
||||||
|
|
||||||
def test_filter_rows_with_nonexistent_filter(self):
|
def test_filter_rows_with_nonexistent_filter(self):
|
||||||
# Apply a filter that matches no rows
|
# Apply a filter that matches no rows
|
||||||
self.menu.filter = 'nonexistent'
|
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, [])
|
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):
|
def tearDown(self):
|
||||||
# Remove the symlink after each test
|
# Remove the symlink after each test
|
||||||
os.unlink('kls.py')
|
os.unlink('kls.py')
|
||||||
|
|
Loading…
Reference in New Issue