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