kls/1

122 lines
9.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
import curses, subprocess
SELECTED, SELECTED_WITH_FILTER, NOT_SELECTED, NOT_SELECTED_WITH_FILTER = 1, 2, 3, 4 # состояния меню
INCREMENT = {"KEY_RIGHT": 1, "\t": 1, "KEY_DOWN": 1, "KEY_LEFT": -1, "KEY_BTAB": -1, "KEY_UP": -1} # при нажатии этих кнопок прибавляем или отнимаем позицию?
HELP_TEXT = "q: exit, /: enter filter mode, Esc: exit filter mode, 1: get yaml, 2: describe, 3: edit, 4: pod logs, arrows/TAB: navigation"
SCREEN = curses.initscr() # инициализация экрана
class Menu:
def __init__(self, name, rows, begin_x, width, state):
self.state = state # состояние меню
self.name = name # заголовок меню
self.rows = rows # строки меню
self.filter = "" # фильтр строк меню
self.filtered_rows = lambda: [x for x in self.rows if self.filter in x] # отфильтрованные строки меню
self.selected_row_index = 0 # индекс выбранной строки меню
self.selected_row = lambda: self.filtered_rows()[self.selected_row_index] # выбранная строка меню
self.begin_x = begin_x # где начинается меню по х?
self.win = curses.newwin(curses.LINES - 3, width, 0, begin_x) # окно с высотой во весь экран, шириной width, и началом по х в точке begin_x
self.rows_number = curses.LINES - 9 # максимальное число видимых строк меню, начиная с 0
execute_cmd = lambda command: subprocess.check_output(command, shell=True).decode().strip().split("\n") # вывод команды преобразуем в list
api_resources_top = ["pods", "services", "deployments", "statefulsets", "daemonsets", "ingresses", "configmaps", "secrets", "persistentvolumes", "persistentvolumeclaims", "nodes", "storageclasses"]
api_resources_kubectl = execute_cmd("kubectl api-resources --no-headers --verbs=get | awk '{print $1}'")
menu0 = Menu("Namespaces", execute_cmd("kubectl get ns --no-headers | awk '{print $1}'"), 0, curses.COLS // 10 * 2, 1)
menu1 = Menu("API resources", api_resources_top + sorted(list(set(api_resources_kubectl) - set(api_resources_top))), curses.COLS // 10 * 2, curses.COLS // 10 * 3, 3)
menu2 = Menu("Resources", execute_cmd(f"kubectl -n {menu0.selected_row()} get {menu1.selected_row()} --no-headers | awk '{{print $1}}'"), curses.COLS // 10 * 5, curses.COLS - curses.COLS // 10 * 5, 3)
def update_menu3():
if not menu0.filtered_rows() or not menu1.filtered_rows(): menu2.rows = ["No namespace or API resource selected"]
elif menu1.selected_row() == "pods": menu2.rows = execute_cmd(f"kubectl -n {menu0.selected_row()} get pods --no-headers | awk '{{ print $1, $3 }}'")
else: menu2.rows = execute_cmd(f"kubectl -n {menu0.selected_row()} get {menu1.selected_row()} --no-headers | awk '{{print $1}}'")
if menu0.selected_row() == "kube-public": raise ValueError(menu2.rows)
if not menu2.rows: menu2.rows = [f"No {menu1.selected_row()} found in {menu0.selected_row()} namespace."]
menu2.selected_row_index = 0 # перед перерисовкой сбрасываем выбранную строку на 0
draw_menu(menu2)
def draw_row(window, text, y, x, selected=False):
window.addstr(y, x, text, curses.A_REVERSE | curses.A_BOLD if selected else curses.A_NORMAL)
window.refresh()
def draw_menu(menu):
menu.win.clear() # очищаем окно меню
draw_row(menu.win, menu.name, 1, 2, selected=True if menu.state in [1, 2] else False) # рисуем заголовок
if menu.filtered_rows(): # рисуем строки меню. Если строк нет, рисовать нечего
first_row_index = 0 if menu.selected_row_index < menu.rows_number else menu.selected_row_index - menu.rows_number + 1
selected_rows = menu.filtered_rows()[first_row_index:][:menu.rows_number] # выбираем, от/до какой cтроки списка будет меню
selected_row_index = menu.selected_row_index - first_row_index # индекс выбранной строки в выбранных строках
for index, row in enumerate(selected_rows): # рисуем то, что отфильтровали
draw_row(menu.win, row, index + 3, 2, selected=True if index == selected_row_index else False)
draw_row(menu.win, f"/{menu.filter}" if menu.state in [2, 4] else "", curses.LINES - 5, 2) # рисуем строку поиска
if menu != menu2: update_menu3() # перерисовываем третье меню, если мы перерисовали первое или второе меню
def run_command(key_pressed):
if not menu2.filtered_rows() or menu2.filtered_rows()[0].startswith("No ") or (key_pressed == "4" and menu1.selected_row() != "pods"): return
commands = {"1": f'get {menu1.selected_row()} {menu2.selected_row().split()[0]} -o yaml | batcat -l yaml --paging always --style numbers',
"2": f'describe {menu1.selected_row()} {menu2.selected_row().split()[0]} | batcat -l yaml --paging always --style numbers',
"3": f'edit {menu1.selected_row()} {menu2.selected_row().split()[0]}',
"4": f'logs {menu2.selected_row().split()[0]} | batcat -l log --paging always --style numbers'}
curses.def_prog_mode() # сохраняем преыдущее состояние терминала
curses.endwin() # без этого после выхода из vim начинаются проблемы
subprocess.call(f"kubectl -n {menu0.selected_row()} " + commands[key_pressed], shell=True)
curses.reset_prog_mode() # восстанавливаем преыдущее состояние терминала
SCREEN.refresh()
def navigate_horizontally(key_pressed, menu):
next_menu = eval("menu" + str(([menu0, menu1, menu2].index(menu) + INCREMENT[key_pressed]) % 3))
menu.state = NOT_SELECTED_WITH_FILTER if menu.state == SELECTED_WITH_FILTER else NOT_SELECTED
next_menu.state = SELECTED_WITH_FILTER if next_menu.state == NOT_SELECTED_WITH_FILTER else SELECTED
draw_row(menu.win, menu.name, 1, 2, selected=False) # убираем выделение с заголовка текущего меню
draw_row(next_menu.win, next_menu.name, 1, 2, selected=True) # выделяем заголовок следующего/предыдущего меню
def handle_selected_with_filter_state(key_pressed, menu):
match key_pressed:
case "\x1b": menu.filter = "" # Нажатие Escape выходит из режима поиска
case "KEY_BACKSPACE" | "\x08": menu.filter = menu.filter[:-1] # Нажатие Backspace удаляет символ (\x08 это тоже Backspace)
case key_pressed if key_pressed.isalpha() or key_pressed == "-": menu.filter += key_pressed.lower()
case _: return
menu.state = SELECTED if not menu.filter else menu.state
menu.selected_row_index = 0
draw_menu(menu)
def catch_input(menu):
key_pressed = SCREEN.getkey()
match key_pressed: # сначала обрабатываем нажатия кнопок, не зависящие от состояния меню
case "\t"| "KEY_RIGHT" | "KEY_BTAB" | "KEY_LEFT": navigate_horizontally(key_pressed, menu)
case "KEY_DOWN" | "KEY_UP" if len(menu.filtered_rows()) > 1:
menu.selected_row_index = (menu.selected_row_index + INCREMENT[key_pressed]) % len(menu.filtered_rows()) # учитываем, сколько строк в меню
draw_menu(menu) # перерисовываем меню
case "1" | "2" | "3" | "4": run_command(key_pressed)
case "q" | "Q" if menu.state == SELECTED: menu.state = NOT_SELECTED # выход
case "/" if menu.state == SELECTED:
menu.state = SELECTED_WITH_FILTER
draw_row(menu.win, "/", curses.LINES - 5, 2) # рисуем строку поиска
case _ if menu.state == SELECTED_WITH_FILTER: handle_selected_with_filter_state(key_pressed, menu)
def main():
SCREEN.refresh() # не знаю зачем это нужно но без этого не работает
SCREEN.keypad(True) # нужно для работы со стрелками
curses.set_escdelay(1) # в curses зачем-то сделали задержку на срабатывание Escape, уменьшаем её до 1 милисекунды (до 0 нельзя)
curses.curs_set(0) # делаем курсор невидимым
curses.noecho() # не выводим символы вверху
for menu in [menu0, menu1, menu2]: draw_menu(menu) # рисуем основные окна
draw_row(curses.newwin(3, curses.COLS, curses.LINES - 3, 0), HELP_TEXT, 1, 2) # и окно помощи
while any(menu.state in [1, 2] for menu in [menu0, menu1, menu2]): # пока выбрано хоть одно меню, продолжаем работу
for menu in [menu0, menu1, menu2]:
if menu.state in [1, 2]: catch_input(menu) # если меню выбрано, перехватываем ввод пользователя
try: main()
finally: curses.endwin() # нужно для нормальной работы терминала после выхода