New keybindings, height/width vars
This commit is contained in:
		
							parent
							
								
									56eb336ac9
								
							
						
					
					
						commit
						67ce50dbb8
					
				
							
								
								
									
										12
									
								
								README.md
								
								
								
								
							
							
						
						
									
										12
									
								
								README.md
								
								
								
								
							|  | @ -1,11 +1,13 @@ | |||
| # KLS | ||||
| 
 | ||||
| ## Description | ||||
| `kls` is a cli tool for managing kubernetes cluster resources. Inspired by `lf` and `ranger` file managers.  | ||||
| `kls` is a cli tool based on `kubectl` for managing kubernetes cluster resources.  | ||||
| Inspired by `lf` and `ranger` file managers.  | ||||
| It is lightweight and easy to customize. Supports mouse navigation as well as keyboard navigation. | ||||
| 
 | ||||
| ## Key bindings for kubectl | ||||
| - `1` - get yaml of resource | ||||
| ## Key bindings | ||||
| For kubectl (You can customize these bindings or add extra bindings in `KEY_BINDINGS` variable of `kls` in a row #4): | ||||
| - `1` or `Enter` - get yaml of resource | ||||
| - `2` - describe resource | ||||
| - `3` - edit resource  | ||||
| - `4` - logs of pod | ||||
|  | @ -13,7 +15,9 @@ It is lightweight and easy to customize. Supports mouse navigation as well as ke | |||
| - `6` - network debug of pod (with nicolaka/netshoot container attached) | ||||
| - `delete` - delete resource | ||||
| 
 | ||||
| You can customize these bindings or add extra bindings in `KEY_BINDINGS` variable of `kls` (in a row #4). | ||||
| Other: | ||||
| - `Escape` - exit filter mode or `kls` itself | ||||
| - `TAB`, arrow keys - navigation | ||||
| 
 | ||||
|  | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										61
									
								
								kls → kls.py
								
								
								
								
							
							
						
						
									
										61
									
								
								kls → kls.py
								
								
								
								
							|  | @ -3,6 +3,7 @@ import subprocess, curses, time | |||
| 
 | ||||
| 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 | ||||
|     "2": 'kubectl -n {namespace} describe {api_resource} {resource} | batcat -l yaml --paging always --style numbers', | ||||
|     "3": 'kubectl -n {namespace} edit {api_resource} {resource}', | ||||
|     "4": 'kubectl -n {namespace} logs {resource} | batcat -l log --paging always --style numbers', | ||||
|  | @ -12,11 +13,17 @@ 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"] | ||||
|                      "deployments", "statefulsets", "daemonsets",  "storageclasses", "all"] | ||||
| 
 | ||||
| HELP_TEXT = "Esc: exit filter mode or exit kls, 1: get yaml, 2: describe, 3: edit, 4: pod logs, arrows/TAB: navigation" | ||||
| HELP_TEXT = "Esc: exit filter mode or exit kls, 1/Enter: get yaml, 2: describe, 3: edit, 4: logs, 5: exec, 6: debug, arrows/TAB: navigation" | ||||
| 
 | ||||
| MOUSE = True | ||||
| MOUSE_ENABLED = True | ||||
| 
 | ||||
| SCREEN = curses.initscr()  # screen initialization, needed for ROWS_HEIGHT working | ||||
| HEADER_HEIGHT = 4  # in rows | ||||
| FOOTER_HEIGHT = 3 | ||||
| ROWS_HEIGHT = curses.LINES - HEADER_HEIGHT - FOOTER_HEIGHT - 3   # maximum number of visible rows indices | ||||
| WIDTH = curses.COLS | ||||
| 
 | ||||
| 
 | ||||
| class Menu: | ||||
|  | @ -29,14 +36,14 @@ class Menu: | |||
|         # __start_index - starting from which row we will select rows from filtered_rows()? Usually from the first row, | ||||
|         # but if the size of filtered_rows is greater than HEIGHT and filtered_row_index exceeds the menu HEIGHT, | ||||
|         # we shift __start_index to the right by filtered_row_index - HEIGHT. This way we implement menu scrolling | ||||
|         self.__start_index = lambda: 0 if self.filtered_row_index < HEIGHT else self.filtered_row_index - HEIGHT + 1 | ||||
|         self.visible_rows = lambda: self.filtered_rows()[self.__start_index():][:HEIGHT]  # visible rows | ||||
|         self.__start_index = lambda: 0 if self.filtered_row_index < ROWS_HEIGHT else self.filtered_row_index - ROWS_HEIGHT + 1 | ||||
|         self.visible_rows = lambda: self.filtered_rows()[self.__start_index():][:ROWS_HEIGHT]  # visible rows | ||||
|         self.__visible_row_index = lambda: self.filtered_row_index - self.__start_index()  # index of the selected visible row | ||||
|         # selected row from visible rows | ||||
|         self.selected_row = lambda: self.visible_rows()[self.__visible_row_index()] if self.visible_rows() else None | ||||
|         self.width = width | ||||
|         self.begin_x = begin_x | ||||
|         self.win = curses.newwin(curses.LINES - 3, width, 0, begin_x) | ||||
|         self.win = curses.newwin(curses.LINES - FOOTER_HEIGHT, width, 0, begin_x) | ||||
| 
 | ||||
| 
 | ||||
| def draw_row(window: curses.window, text: str, y: int, x: int, selected: bool = False): | ||||
|  | @ -47,14 +54,14 @@ def draw_row(window: curses.window, text: str, y: int, x: int, selected: bool = | |||
| 
 | ||||
| def draw_rows(menu: Menu): | ||||
|     for index, row in enumerate(menu.visible_rows()): | ||||
|         draw_row(menu.win, row, index + 3, 2, selected=True if row == menu.selected_row() else False) | ||||
|         draw_row(menu.win, row, index + HEADER_HEIGHT, 2, selected=True if row == menu.selected_row() else False) | ||||
| 
 | ||||
| 
 | ||||
| def draw_menu(menu: Menu): | ||||
|     menu.win.clear()  # clear menu window | ||||
|     draw_row(menu.win, menu.title, 1, 2, selected=True if menu == SELECTED_MENU else False)  # draw title | ||||
|     draw_rows(menu)  # draw menu rows | ||||
|     draw_row(menu.win, f"/{menu.filter}" if menu.filter else "", curses.LINES - 5, 2)  # draw filter row | ||||
|     draw_row(menu.win, f"/{menu.filter}" if menu.filter else "", curses.LINES - FOOTER_HEIGHT - 2, 2)  # draw filter row | ||||
| 
 | ||||
| 
 | ||||
| def refresh_third_menu(): | ||||
|  | @ -64,16 +71,20 @@ def refresh_third_menu(): | |||
|     draw_menu(MENUS[2]) | ||||
| 
 | ||||
| 
 | ||||
| def run_command(key: str): | ||||
|     if not (key in ("4","5","6") and api_resource() != "pods"): | ||||
|         curses.def_prog_mode()  # save the previous terminal state | ||||
|         curses.endwin()  # without this, there are problems after exiting vim | ||||
|         command = KEY_BINDINGS[key].format(namespace=namespace(), api_resource=api_resource(), resource=resource()) | ||||
|         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 | ||||
| def run_command(key: str, api_resource: str, resource: str): | ||||
|     if key in ("4", "5", "6"): | ||||
|         if api_resource not in ["pods", "all"] or (api_resource == "all" and not resource.startswith("pod/")): | ||||
|             return | ||||
|     curses.def_prog_mode()  # save the previous terminal state | ||||
|     curses.endwin()  # without this, there are problems after exiting vim | ||||
|     command = KEY_BINDINGS[key].format(namespace=namespace(), api_resource=api_resource, resource=resource) | ||||
|     if api_resource == "all": | ||||
|         command = command.replace(" all", "") | ||||
|     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 | ||||
| 
 | ||||
| 
 | ||||
| def handle_filter_state(key: str, menu: Menu): | ||||
|  | @ -92,7 +103,7 @@ def handle_filter_state(key: str, menu: Menu): | |||
| 
 | ||||
| 
 | ||||
| def handle_mouse(mouse_info: tuple, menu: Menu): | ||||
|     row_number = mouse_info[2] - 3 | ||||
|     row_number = mouse_info[2] - HEADER_HEIGHT | ||||
|     column_number = mouse_info[1] | ||||
|     next_menu = None | ||||
|     if column_number > (menu.begin_x + menu.width): | ||||
|  | @ -140,11 +151,11 @@ def catch_input(menu: Menu): | |||
|         draw_rows(menu)  # this will change selected row in menu | ||||
|         if menu != MENUS[2]: | ||||
|             MENUS[2].filtered_row_index = 0  # reset the selected row index of third menu before redrawing | ||||
|     elif key == "KEY_MOUSE" and MOUSE: | ||||
|     elif key == "KEY_MOUSE" and MOUSE_ENABLED: | ||||
|         mouse_info = curses.getmouse() | ||||
|         handle_mouse(mouse_info, menu) | ||||
|     elif key in KEY_BINDINGS.keys() and MENUS[2].selected_row(): | ||||
|         run_command(key) | ||||
|         run_command(key, api_resource(), resource()) | ||||
|     elif key == "\x1b" and not menu.filter: | ||||
|         globals().update(SELECTED_MENU=None)  # exit | ||||
|     else: | ||||
|  | @ -155,15 +166,13 @@ def kubectl(command: str) -> list: | |||
|     return subprocess.check_output(f"kubectl {command}", shell=True).decode().strip().split("\n") | ||||
| 
 | ||||
| 
 | ||||
| SCREEN = curses.initscr()  # screen initialization | ||||
| api_resources_kubectl = [x.split()[0] for x in kubectl("api-resources --no-headers --verbs=get")] | ||||
| api_resources = list(dict.fromkeys(TOP_API_RESOURCES + api_resources_kubectl))  # so top api resources are at the top | ||||
| width_unit = curses.COLS // 8 | ||||
| width_unit = WIDTH // 8 | ||||
| MENUS = [Menu("Namespaces", kubectl("get ns --no-headers -o custom-columns=NAME:.metadata.name"), 0, width_unit), | ||||
|          Menu("API resources", api_resources, width_unit, width_unit * 2), | ||||
|          Menu("Resources", [], width_unit * 3, curses.COLS - width_unit * 3)] | ||||
|          Menu("Resources", [], width_unit * 3, WIDTH - width_unit * 3)] | ||||
| SELECTED_MENU = MENUS[0] | ||||
| HEIGHT = curses.LINES - 9  # maximum number of visible row indices | ||||
| namespace = MENUS[0].selected_row  # method alias | ||||
| api_resource = MENUS[1].selected_row | ||||
| resource = lambda: MENUS[2].selected_row().split()[0] | ||||
|  | @ -181,7 +190,7 @@ def main(screen): | |||
|     print('\033[?1003h') # enable mouse tracking with the XTERM API. That's the magic | ||||
|     for menu in MENUS:  # draw the main windows | ||||
|         draw_menu(menu) | ||||
|     draw_row(curses.newwin(3, curses.COLS, curses.LINES - 3, 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 | ||||
|     while SELECTED_MENU: | ||||
|         catch_input(SELECTED_MENU)  # if a menu is selected, catch user input | ||||
| 
 | ||||
		Loading…
	
		Reference in New Issue