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
|
||||||
|
|
||||||
![kls in action](./images/kls.gif)
|
![kls in action](./images/kls.gif)
|
||||||
|
|
||||||
|
|
47
kls → kls.py
47
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,11 +71,15 @@ 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"):
|
||||||
|
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.def_prog_mode() # save the previous terminal state
|
||||||
curses.endwin() # without this, there are problems after exiting vim
|
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)
|
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()
|
||||||
|
@ -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