Compare commits

..

11 Commits

8 changed files with 234 additions and 155 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
.idea
venv

3
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

8
.idea/kls.iml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="jdk" jdkName="Python 3.11 (kls)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

7
.idea/misc.xml Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.11" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11 (kls)" project-jdk-type="Python SDK" />
</project>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/kls.iml" filepath="$PROJECT_DIR$/.idea/kls.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

355
kls
View File

@ -2,207 +2,246 @@
import curses import curses
import subprocess import subprocess
stdscr = None screen = None
# в curses зачем-то сделали задержку на срабатывание Escape, уменьшаем её до 1 милисекунды (до 0 нельзя)
curses.set_escdelay(1)
running = True
# состояния меню
SELECTED_WITHOUT_SEARCH = 1
SELECTED_WITH_SEARCH = 2
NOT_SELECTED_WITHOUT_SEARCH = 3
NOT_SELECTED_WITH_SEARCH = 4
def filter_words(words, start):
return list(filter(lambda s: s.startswith(start), words))
# я не знаю, что делается в этой функции. # я не знаю, что делается в этой функции.
def init_screen(): def init_screen():
global stdscr global screen
stdscr = curses.initscr() screen = curses.initscr()
stdscr.refresh() screen.refresh()
curses.noecho() curses.noecho()
curses.cbreak() curses.cbreak()
stdscr.keypad(True) screen.keypad(True)
curses.curs_set(0) curses.curs_set(0)
init_screen() init_screen()
class Menu: class Menu:
def __init__(self, name, rows, begin_x): def __init__(self, name, rows, begin_x, state):
self.state = state
self.name = name # заголовок окна self.name = name # заголовок окна
self.rows = rows # строки окна self.rows = rows # строки окна
self.begin_x = begin_x # где начинается окно по х? self.begin_x = begin_x # где начинается окно по х?
self.win = curses.newwin(curses.LINES, curses.COLS // 3, 0, begin_x) # окно с высотой во весь экран, шириной экран / 3, и началом по х в точке begin_x self.win = curses.newwin(curses.LINES, curses.COLS // 3, 0,
begin_x) # окно с высотой во весь экран, шириной экран / 3, и началом по х в точке begin_x
self.win.box() # ? self.win.box() # ?
self.win.addstr(1, 2, self.name) # рисуем заголовок
for index, row in enumerate(self.rows): # рисуем строки
self.win.addstr(index + 3, 2, row) # + 3 потому что я хочу чтобы строки оборажались ниже заголовка на три строки
self.row = 0 # выбранная строка self.row = 0 # выбранная строка
self.filter = ""
# рисуем первое меню # рисуем первое меню
## готовим контент ## готовим контент
bytes_list = subprocess.check_output("kubectl get ns --no-headers -o template='{{range .items}}{{.metadata.name}} {{end}}'", shell=True).split() bytes_list = subprocess.check_output(
"kubectl get ns --no-headers -o template='{{range .items}}{{.metadata.name}} {{end}}'", shell=True).split()
namespaces = [bytes_list[i].decode('utf-8') for i in range(len(bytes_list))] namespaces = [bytes_list[i].decode('utf-8') for i in range(len(bytes_list))]
## отрисовываем меню ## отрисовываем меню
menu1 = Menu("Namespaces", namespaces, 0) menu1 = Menu("Namespaces", namespaces, 0, SELECTED_WITHOUT_SEARCH)
# рисуем второе меню # рисуем второе меню
## готовим контент ## готовим контент
api_resources = ["pods", "services", "deployments", "ingresses"] api_resources = ["pods", "services", "deployments", "statefulsets", "ingresses", "configmaps", "secrets"]
## отрисовываем меню ## отрисовываем меню
menu2 = Menu("API resources", api_resources, 0 + curses.COLS // 3) menu2 = Menu("API resources", api_resources, 0 + curses.COLS // 3, NOT_SELECTED_WITHOUT_SEARCH)
# рисуем третье меню # рисуем третье меню
## готовим контент ## готовим контент
bytes_list = subprocess.check_output("kubectl get pods -n kube-system --no-headers -o template='{{range .items}}{{.metadata.name}} {{end}}'", shell=True).split() bytes_list = subprocess.check_output(
"kubectl get pods -n kube-system --no-headers -o template='{{range .items}}{{.metadata.name}} {{end}}'",
shell=True).split()
pods = [bytes_list[i].decode('utf-8') for i in range(len(bytes_list))] pods = [bytes_list[i].decode('utf-8') for i in range(len(bytes_list))]
## отрисовываем меню ## отрисовываем меню
menu3 = Menu("Resources", pods, 0 + curses.COLS // 3 * 2) menu3 = Menu("Resources", pods, 0 + curses.COLS // 3 * 2, NOT_SELECTED_WITHOUT_SEARCH)
menus = [menu1, menu2, menu3] menus = [menu1, menu2, menu3]
def run_command(command, current_menu, rows=None):
namespace = menus[0].rows[menus[0].row] def run_command(command, namespace, api_resource, resource):
api_resource = menus[1].rows[menus[1].row] curses.def_shell_mode()
resource = menus[2].rows[menus[2].row] curses.endwin()
subprocess.call(eval(command), shell=True) subprocess.call(eval(command), shell=True)
init_screen() curses.reset_shell_mode()
for menu in menus:
menu.win.clear()
menu.win.box() def update_menu3():
menu1_filtered_rows = list(filter(lambda x: (x.startswith(menu1.filter)), menu1.rows)) # фильтруем строки
menu2_filtered_rows = list(filter(lambda x: (x.startswith(menu2.filter)), menu2.rows)) # фильтруем строки
if not menu1_filtered_rows or not menu2_filtered_rows:
resources = [f"No resources matched criteria.", ]
else:
namespace = menu1_filtered_rows[menu1.row]
api_resource = menu2_filtered_rows[menu2.row]
command = "f'kubectl get {api_resource} -n {namespace} --no-headers -o template=\"{{{{range .items}}}}{{{{.metadata.name}}}} {{{{end}}}}\"'"
bytes_list = subprocess.check_output(eval(command), shell=True).split()
resources = [bytes_list[i].decode('utf-8') for i in range(len(bytes_list))]
if not resources:
resources = [f"No resources found in {namespace} namespace.", ]
menu3.rows = resources
menu3.row = 0
def draw_menu(menu):
draw_header(menu) # рисуем заголовок
draw_rows(menu) # рисуем строки меню
draw_search_box(menu) # рисуем строку поиска
def draw_header(menu):
if menu.state in [1, 2]:
menu.win.addstr(1, 2, menu.name, curses.A_REVERSE | curses.A_ITALIC)
else:
menu.win.addstr(1, 2, menu.name) menu.win.addstr(1, 2, menu.name)
if menu.name == "Namespaces": menu.win.refresh() # обновляем окно
menu.win.addstr(curses.LINES - 2, 2, "Press / for search")
if rows and menu.name == "Resources":
menu.rows = rows def draw_rows(menu):
menu.row = 0 filtered_rows = list(filter(lambda x: (x.startswith(menu.filter)), menu.rows)) # фильтруем строки
for index, row in enumerate(menu.rows): for index, row in enumerate(filtered_rows): # рисуем то, что отфильтровали
menu.win.addstr(index + 3, 2, row) menu.win.addstr(index + 3, 2, row)
menu.win.addstr(menu.row + 3, 2, menu.rows[menu.row], curses.A_REVERSE | curses.A_ITALIC) if filtered_rows:
menus[current_menu].win.addstr(1, 2, menus[current_menu].name, curses.A_REVERSE | curses.A_ITALIC) menu.win.addstr(3, 2, filtered_rows[0], curses.A_REVERSE | curses.A_ITALIC) # выделяем первую строку
def navigate_horizontally(direction, current_menu):
increment = {"right": 1, "left": -1}
menus[current_menu].win.addstr(1, 2, menus[current_menu].name) # удаляем выделение с текущего меню
current_menu = (current_menu + increment[direction]) % 3 # переходим к предыдущему/следующему меню
menus[current_menu].win.addstr(1, 2, menus[current_menu].name, curses.A_REVERSE | curses.A_ITALIC) # и выделяем его
return current_menu
def navigate_vertically(direction, current_menu):
increment = {"down": 1, "up": -1}
if current_menu == 2 and menus[2].rows[menus[2].row].startswith("No resources"):
return
menu = menus[current_menu]
menu.win.addstr(menu.row + 3, 2, menu.rows[menu.row]) # удаляем выделение с текущей строки
menu.row = (menu.row + increment[direction]) % len(menu.rows) # переходим к предыдущей/следующей строке
menu.win.addstr(menu.row + 3, 2, menu.rows[menu.row], curses.A_REVERSE | curses.A_ITALIC) # и выделяем её
if current_menu != 2: # если изменился выбор нэймспейса или апи ресурса
namespace = menus[0].rows[menus[0].row]
api_resource = menus[1].rows[menus[1].row]
command = "f'kubectl get {api_resource} -n {namespace} --no-headers -o template=\"{{{{range .items}}}}{{{{.metadata.name}}}} {{{{end}}}}\"'"
bytes_list = subprocess.check_output(eval(command), shell=True).split()
resources = [bytes_list[i].decode('utf-8') for i in range(len(bytes_list))]
if not resources:
resources = [f"No resources found in {namespace} namespace.",]
run_command(command, current_menu, rows=resources)
def main(stdscr):
search_mode = False
search_string = ""
stdscr.refresh()
running = True
current_menu = 0
menus[current_menu].win.addstr(1, 2, menus[current_menu].name, curses.A_REVERSE | curses.A_ITALIC)
for menu in menus:
menu.win.addstr(3, 2, menu.rows[menu.row], curses.A_REVERSE | curses.A_ITALIC)
if menu.name == "Namespaces":
menu.win.addstr(curses.LINES - 2, 2, "Press / for search")
while running:
[menu.win.refresh() for menu in menus] # refresh all menus
key_pressed = stdscr.getkey()
if current_menu == 0:
if key_pressed == "/":
search_mode = True
if search_mode:
match key_pressed:
case '\t' | "KEY_RIGHT":
current_menu = navigate_horizontally("right", current_menu)
case "KEY_BTAB" | "KEY_LEFT":
current_menu = navigate_horizontally("left", current_menu)
case "KEY_DOWN":
navigate_vertically("down", current_menu)
case "KEY_UP":
navigate_vertically("up", current_menu)
case "KEY_BACKSPACE":
if search_string:
search_string = search_string[:-1]
else:
search_mode = False
if key_pressed.isalpha() or key_pressed == "-":
search_string += key_pressed
elif key_pressed == "/" and search_string == "":
pass
else:
continue
init_screen()
for menu in menus:
menu.win.clear() # очищаем окно
menu.win.box() menu.win.box()
menu.win.addstr(1, 2, menu.name) # добавляем заголовок окна menu.win.refresh()
if menu.name == "Namespaces":
menu.rows = list(filter(lambda x: (x.startswith(search_string)), namespaces)) # меняем строки у окна Namespaces
menu.row = 0
if search_mode:
menu.win.addstr(curses.LINES - 2, 2, f"/{search_string}")
else:
menu.win.addstr(curses.LINES - 2, 2, "Press / for search")
elif menu.name == "Resources":
if menus[0].rows:
namespace = menus[0].rows[menus[0].row]
api_resource = menus[1].rows[menus[1].row]
command = "f'kubectl get {api_resource} -n {namespace} --no-headers -o template=\"{{{{range .items}}}}{{{{.metadata.name}}}} {{{{end}}}}\"'"
bytes_list = subprocess.check_output(eval(command), shell=True).split()
resources = [bytes_list[i].decode('utf-8') for i in range(len(bytes_list))]
if not resources:
resources = [f"No resources found in {namespace} namespace.",]
else:
resources = ["No namespace selected",]
menu.rows = resources
for index, row in enumerate(menu.rows):
menu.win.addstr(index + 3, 2, row)
if menu.rows:
menu.win.addstr(3, 2, menu.rows[menu.row], curses.A_REVERSE | curses.A_ITALIC) # выделяем первую строку
menus[current_menu].win.addstr(1, 2, menus[current_menu].name, curses.A_REVERSE | curses.A_ITALIC) # помечаем выбранное меню
continue
match key_pressed: def draw_search_box(menu):
case 'q': # рисуем строку поиска
if menu.state in [2, 4]:
content = f"/{menu.filter}"
else:
content = "Press / for search"
menu.win.addstr(curses.LINES - 2, 2, content) # рисуем контент
menu.win.clrtoeol() # очищаем остальную часть строки
menu.win.box() # рисуем рамку
menu.win.refresh() # обновляем окно
def catch_input(menu):
global running
key_pressed = screen.getkey()
if key_pressed == "/":
menu.state = SELECTED_WITH_SEARCH
elif key_pressed == "q" and menu.state == SELECTED_WITHOUT_SEARCH:
running = False running = False
case '/': elif key_pressed == "\x1b" and menu.state == SELECTED_WITH_SEARCH: # Escape disables search mode
search_mode = True menu.filter = ""
case 'g': menu.win.clear()
if current_menu == 2 and not menus[2].rows[menus[2].row].startswith("No resources"): draw_rows(menu)
run_command("f'kubectl -n {namespace} get {api_resource} {resource} -o yaml | batcat -l yaml --paging always --style numbers'", current_menu) menu.state = SELECTED_WITHOUT_SEARCH
case 'd': elif key_pressed == "KEY_BACKSPACE" and menu.filter:
if current_menu == 2 and not menus[2].rows[menus[2].row].startswith("No resources"): menu.filter = menu.filter[:-1] # удаляем символ из строки поиска
run_command("f'kubectl -n {namespace} describe {api_resource} {resource} | batcat -l yaml --paging always --style numbers'", current_menu) menu.win.clear()
case 'l': draw_menu(menu)
if current_menu == 2 and not menus[2].rows[menus[2].row].startswith("No resources") and menus[1].rows[menus[1].row] == "pods": elif key_pressed == "KEY_BACKSPACE" and not menu.filter:
run_command("f'kubectl -n {namespace} logs {resource} | batcat -l log --paging always --style numbers'", current_menu) menu.state = SELECTED_WITHOUT_SEARCH
case 'e': elif key_pressed == '\t' or key_pressed == "KEY_RIGHT":
if current_menu == 2 and not menus[2].rows[menus[2].row].startswith("No resources"): navigate_horizontally("right", menu)
run_command("f'kubectl edit {api_resource} -n {namespace} {resource}'", current_menu) elif key_pressed == "KEY_BTAB" or key_pressed == "KEY_LEFT":
case '\t' | "KEY_RIGHT": navigate_horizontally("left", menu)
current_menu = navigate_horizontally("right", current_menu) elif key_pressed == "KEY_DOWN":
case "KEY_BTAB" | "KEY_LEFT": navigate_vertically("down", menu)
current_menu = navigate_horizontally("left", current_menu) elif key_pressed == "KEY_UP":
case "KEY_DOWN": navigate_vertically("up", menu)
navigate_vertically("down", current_menu) elif key_pressed in "gdle" and menu3.state in [1, 2] and menu3.rows and not menu3.rows[menu3.row].startswith("No resources"):
case "KEY_UP": menu3_filtered_rows = list(filter(lambda x: (x.startswith(menu3.filter)), menu3.rows)) # фильтруем строки меню 3
navigate_vertically("up", current_menu) if not menu3_filtered_rows:
main(stdscr) return
menu1_filtered_rows = list(filter(lambda x: (x.startswith(menu1.filter)), menu1.rows)) # фильтруем строки
menu2_filtered_rows = list(filter(lambda x: (x.startswith(menu2.filter)), menu2.rows)) # фильтруем строки
namespace = menu1_filtered_rows[menu1.row]
api_resource = menu2_filtered_rows[menu2.row]
resource = menu3.rows[menu3.row]
if key_pressed == 'g':
command = f"f'kubectl -n {namespace} get {api_resource} {resource} -o yaml | batcat -l yaml --paging always --style numbers'"
elif key_pressed == 'd':
command = f"f'kubectl -n {namespace} describe {api_resource} {resource} | batcat -l yaml --paging always --style numbers'"
elif key_pressed == 'l' and api_resource != "pods":
return
elif key_pressed == 'l' and api_resource == "pods":
command = f"f'kubectl -n {namespace} logs {resource} | batcat -l log --paging always --style numbers'"
if key_pressed == 'e':
command = f"f'kubectl edit {api_resource} -n {namespace} {resource}'"
# raise ValueError(str(menu.state) + ' ' + eval(command))
run_command(command, namespace, api_resource, resource)
elif (key_pressed.isalpha() or key_pressed == "-") and menu.state == SELECTED_WITH_SEARCH: # объекты в кубе не могут иметь иных символов кроме a-z и -
menu.filter += key_pressed
menu.win.clear()
menu.row = 0
draw_menu(menu)
if menu != menu3 and not (key_pressed in ["KEY_RIGHT", "KEY_LEFT", "\t", "KEY_BTAB"] ):
menu3.win.clear()
update_menu3()
draw_menu(menu3)
def navigate_horizontally(direction, menu):
increment = {"right": 1, "left": -1}
# чтобы понять, какой порядковый номер у следующего/предыдущего меню, нужно сперва определить номер текущего меню
menu_index = {menu1: 0, menu2: 1, menu3: 2}
next_menu = menus[(menu_index[menu] + increment[direction]) % 3]
if menu.filter:
menu.state = NOT_SELECTED_WITH_SEARCH
else:
menu.state = NOT_SELECTED_WITHOUT_SEARCH
draw_header(menu) # убираем выделение с текущего меню
if next_menu.filter:
next_menu.state = SELECTED_WITH_SEARCH
else:
next_menu.state = SELECTED_WITHOUT_SEARCH
def navigate_vertically(direction, menu):
increment = {"down": 1, "up": -1}
filtered_rows = list(filter(lambda x: (x.startswith(menu.filter)), menu.rows)) # фильтруем строки
if not filtered_rows or len(filtered_rows) == 1:
return
elif filtered_rows[menu.row].startswith("No resources"): # это касается только третьего меню
return
menu.win.addstr(menu.row + 3, 2, filtered_rows[menu.row]) # удаляем выделение с текущей строки
menu.row = (menu.row + increment[direction]) % len(filtered_rows) # переходим к предыдущей/следующей строке
menu.win.addstr(menu.row + 3, 2, filtered_rows[menu.row], curses.A_REVERSE | curses.A_ITALIC) # и выделяем её
def main(screen):
global running
screen.refresh()
# начальный экран
for menu in menus:
draw_menu(menu)
while running:
### выбрано первое меню ###
if menu1.state in [1, 2]:
draw_header(menu1) # рисуем заголовок
draw_search_box(menu1) # рисуем строку поиска
catch_input(menu1) # перехватываем нажатия клавиш
### выбрано второе меню ###
elif menu2.state in [1, 2]:
draw_header(menu2) # рисуем заголовок
draw_search_box(menu2) # рисуем строку поиска
catch_input(menu2) # перехватываем нажатия клавиш
### выбрано третье меню ###
elif menu3.state in [1, 2]:
draw_header(menu3) # рисуем заголовок
draw_search_box(menu3) # рисуем строку поиска
catch_input(menu3) # перехватываем нажатия клавиш
main(screen)
# curses.wrapper(main)
curses.nocbreak() curses.nocbreak()
stdscr.keypad(False) screen.keypad(False)
curses.echo() curses.echo()
curses.endwin() curses.endwin()
subprocess.call(["clear"]) subprocess.call(["clear"])