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 | # KLS | ||||||
| 
 | 
 | ||||||
| ## Description | ## 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. | It is lightweight and easy to customize. Supports mouse navigation as well as keyboard navigation. | ||||||
| 
 | 
 | ||||||
| ## Key bindings for kubectl | ## Key bindings | ||||||
| - `1` - get yaml of resource | 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 | - `2` - describe resource | ||||||
| - `3` - edit resource  | - `3` - edit resource  | ||||||
| - `4` - logs of pod | - `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) | - `6` - network debug of pod (with nicolaka/netshoot container attached) | ||||||
| - `delete` - delete resource | - `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 | 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 | ||||||
|     "2": 'kubectl -n {namespace} describe {api_resource} {resource} | batcat -l yaml --paging always --style numbers', |     "2": 'kubectl -n {namespace} describe {api_resource} {resource} | batcat -l yaml --paging always --style numbers', | ||||||
|     "3": 'kubectl -n {namespace} edit {api_resource} {resource}', |     "3": 'kubectl -n {namespace} edit {api_resource} {resource}', | ||||||
|     "4": 'kubectl -n {namespace} logs {resource} | batcat -l log --paging always --style numbers', |     "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? | # 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"] |                      "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: | 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, |         # __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, |         # 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 |         # 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.__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():][:HEIGHT]  # visible rows |         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 |         self.__visible_row_index = lambda: self.filtered_row_index - self.__start_index()  # index of the selected visible row | ||||||
|         # selected row from visible rows |         # selected row from visible rows | ||||||
|         self.selected_row = lambda: self.visible_rows()[self.__visible_row_index()] if self.visible_rows() else None |         self.selected_row = lambda: self.visible_rows()[self.__visible_row_index()] if self.visible_rows() else None | ||||||
|         self.width = width |         self.width = width | ||||||
|         self.begin_x = begin_x |         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): | 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): | def draw_rows(menu: Menu): | ||||||
|     for index, row in enumerate(menu.visible_rows()): |     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): | def draw_menu(menu: Menu): | ||||||
|     menu.win.clear()  # clear menu window |     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_row(menu.win, menu.title, 1, 2, selected=True if menu == SELECTED_MENU else False)  # draw title | ||||||
|     draw_rows(menu)  # draw menu rows |     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(): | def refresh_third_menu(): | ||||||
|  | @ -64,16 +71,20 @@ def refresh_third_menu(): | ||||||
|     draw_menu(MENUS[2]) |     draw_menu(MENUS[2]) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def run_command(key: str): | def run_command(key: str, api_resource: str, resource: str): | ||||||
|     if not (key in ("4","5","6") and api_resource() != "pods"): |     if key in ("4", "5", "6"): | ||||||
|         curses.def_prog_mode()  # save the previous terminal state |         if api_resource not in ["pods", "all"] or (api_resource == "all" and not resource.startswith("pod/")): | ||||||
|         curses.endwin()  # without this, there are problems after exiting vim |             return | ||||||
|         command = KEY_BINDINGS[key].format(namespace=namespace(), api_resource=api_resource(), resource=resource()) |     curses.def_prog_mode()  # save the previous terminal state | ||||||
|         subprocess.call(command, shell=True) |     curses.endwin()  # without this, there are problems after exiting vim | ||||||
|         curses.reset_prog_mode()  # restore the previous terminal state |     command = KEY_BINDINGS[key].format(namespace=namespace(), api_resource=api_resource, resource=resource) | ||||||
|         SCREEN.refresh() |     if api_resource == "all": | ||||||
|         curses.mousemask(curses.REPORT_MOUSE_POSITION)  # mouse tracking |         command = command.replace(" all", "") | ||||||
|         print('\033[?1003h') # enable mouse tracking with the XTERM API. That's the magic |     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): | 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): | 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] |     column_number = mouse_info[1] | ||||||
|     next_menu = None |     next_menu = None | ||||||
|     if column_number > (menu.begin_x + menu.width): |     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 |         draw_rows(menu)  # this will change selected row in menu | ||||||
|         if menu != MENUS[2]: |         if menu != MENUS[2]: | ||||||
|             MENUS[2].filtered_row_index = 0  # reset the selected row index of third menu before redrawing |             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() |         mouse_info = curses.getmouse() | ||||||
|         handle_mouse(mouse_info, menu) |         handle_mouse(mouse_info, menu) | ||||||
|     elif key in KEY_BINDINGS.keys() and MENUS[2].selected_row(): |     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: |     elif key == "\x1b" and not menu.filter: | ||||||
|         globals().update(SELECTED_MENU=None)  # exit |         globals().update(SELECTED_MENU=None)  # exit | ||||||
|     else: |     else: | ||||||
|  | @ -155,15 +166,13 @@ 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") | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| SCREEN = curses.initscr()  # screen initialization |  | ||||||
| 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(dict.fromkeys(TOP_API_RESOURCES + api_resources_kubectl))  # so top api resources are at the top | 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), | 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("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] | SELECTED_MENU = MENUS[0] | ||||||
| HEIGHT = curses.LINES - 9  # maximum number of visible row indices |  | ||||||
| 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] | ||||||
|  | @ -181,7 +190,7 @@ def main(screen): | ||||||
|     print('\033[?1003h') # enable mouse tracking with the XTERM API. That's the magic |     print('\033[?1003h') # enable mouse tracking with the XTERM API. That's the magic | ||||||
|     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 - 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: |     while SELECTED_MENU: | ||||||
|         catch_input(SELECTED_MENU)  # if a menu is selected, catch user input |         catch_input(SELECTED_MENU)  # if a menu is selected, catch user input | ||||||
| 
 | 
 | ||||||
		Loading…
	
		Reference in New Issue