New keybindings, height/width vars

This commit is contained in:
Digital Studium 2024-04-26 07:40:54 +03:00
parent 56eb336ac9
commit 67ce50dbb8
2 changed files with 43 additions and 30 deletions

View File

@ -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
![kls in action](./images/kls.gif)

View File

@ -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,11 +71,15 @@ def refresh_third_menu():
draw_menu(MENUS[2])
def run_command(key: str):
if not (key in ("4","5","6") and api_resource() != "pods"):
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())
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()
@ -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