import curses
import os
from pathlib import Path
from fontTools.ttLib import TTFont
KITTY_CONF = Path.home() / ".config/kitty/kitty.conf"
BACKUP_CONF = Path.home() / ".config/kitty/kitty-old.conf"
def get_installed_fonts():
font_dirs = [
os.path.expanduser("~/.local/share/fonts"),
"/usr/share/fonts",
"/usr/local/share/fonts",
]
font_files = []
for font_dir in font_dirs:
if os.path.isdir(font_dir):
for root, _, files in os.walk(font_dir):
for f in files:
if f.lower().endswith((".ttf", ".otf")):
font_files.append(os.path.join(root, f))
families = set()
for font_file in font_files:
try:
font = TTFont(font_file)
name = font["name"]
for record in name.names:
if record.nameID == 1:
fam = record.toUnicode()
if fam:
families.add(fam)
break
font.close()
except Exception:
pass
return sorted(families)
FONT_FAMILIES = get_installed_fonts()
if not FONT_FAMILIES:
FONT_FAMILIES = ["monospace"]
def load_external_config(path):
path = Path(path).expanduser()
if not path.exists():
return None
opacity = 9
blur = 0
hide_decor = False
font_size = 12
font_family = FONT_FAMILIES[0]
with path.open(encoding="utf-8") as f:
for line in f:
line = line.strip()
if line.startswith("background_opacity"):
try:
opacity = int(float(line.split()[1]) * 10)
except:
pass
elif line.startswith("background_blur"):
try:
blur = int(line.split()[1])
except:
pass
elif line.startswith("hide_window_decorations"):
hide_decor = (line.split()[1].lower() == "yes")
elif line.startswith("font_size"):
try:
font_size = int(line.split()[1])
except:
pass
elif line.startswith("font_family"):
fam = line.split(maxsplit=1)[1].strip()
if fam in FONT_FAMILIES:
font_family = fam
return opacity, blur, hide_decor, font_size, font_family
def load_kitty_conf():
return load_external_config(KITTY_CONF)
def write_new_kitty_conf(opacity, blur, hide_decor, font_size, font_family):
if KITTY_CONF.exists():
if BACKUP_CONF.exists():
BACKUP_CONF.unlink()
KITTY_CONF.rename(BACKUP_CONF)
lines = [
f"background_opacity {opacity:.1f}",
f"background_blur {blur}",
f"hide_window_decorations {'yes' if hide_decor else 'no'}",
f"font_size {font_size}",
f"font_family {font_family}",
]
KITTY_CONF.write_text("\n".join(lines) + "\n")
def draw_slider(stdscr, label, value, max_value, y, x, selected):
stdscr.addstr(y, x, label)
bar_length = 20
slider = "["
scaled_value = int((value / max_value) * bar_length)
for i in range(bar_length):
if i < scaled_value:
slider += "="
elif i == scaled_value:
slider += "|"
else:
slider += " "
slider += f"] {value}/{max_value}"
if selected:
stdscr.addstr(y, x + len(label) + 1, slider, curses.A_REVERSE)
else:
stdscr.addstr(y, x + len(label) + 1, slider)
def draw_toggle(stdscr, label, value, y, x, selected):
val_str = "yes" if value else "no"
text = f"{label}: {val_str}"
if selected:
stdscr.addstr(y, x, text, curses.A_REVERSE)
else:
stdscr.addstr(y, x, text)
def draw_select(stdscr, label, options, current_index, y, x, selected):
text = f"{label}: {options[current_index]}"
if selected:
stdscr.addstr(y, x, text, curses.A_REVERSE)
else:
stdscr.addstr(y, x, text)
def main(stdscr):
curses.curs_set(0)
stdscr.keypad(True)
height, width = stdscr.getmaxyx()
opacity_value, blur_value, hide_window_decor, font_size, font_family = load_kitty_conf()
font_size = max(8, min(font_size, 40))
try:
font_family_index = FONT_FAMILIES.index(font_family)
except ValueError:
font_family_index = 0
selected_control = 0
import_path = ""
editing_import = False
total_controls = 6 # 5 settings + 1 import input
while True:
stdscr.clear()
stdscr.addstr(1, (width - len("Kitty Config TUI")) // 2, "Kitty Config TUI", curses.A_BOLD)
stdscr.addstr(3, 4, "Appearance", curses.A_UNDERLINE)
draw_slider(stdscr, "Opacity ", opacity_value, 10, y=5, x=4, selected=(selected_control == 0 and not editing_import))
draw_slider(stdscr, "Background Blur", blur_value, 10, y=7, x=4, selected=(selected_control == 1 and not editing_import))
draw_toggle(stdscr, "Hide Decorations", hide_window_decor, y=9, x=4, selected=(selected_control == 2 and not editing_import))
draw_slider(stdscr, "Font Size ", font_size, 40, y=11, x=4, selected=(selected_control == 3 and not editing_import))
draw_select(stdscr, "Font Family ", FONT_FAMILIES, font_family_index, y=13, x=4, selected=(selected_control == 4 and not editing_import))
# Import config line with editable input
import_label = "Import Config : "
if selected_control == 5:
stdscr.addstr(15, 4, import_label, curses.A_REVERSE)
if editing_import:
curses.curs_set(1)
stdscr.addstr(15, 4 + len(import_label), import_path, curses.A_REVERSE)
stdscr.move(15, 4 + len(import_label) + len(import_path))
else:
curses.curs_set(0)
stdscr.addstr(15, 4 + len(import_label), import_path)
else:
curses.curs_set(0)
stdscr.addstr(15, 4, import_label + import_path)
stdscr.refresh()
ch = stdscr.getch()
if editing_import:
if ch in (curses.KEY_ENTER, 10, 13):
# Try to load config file from import_path
result = load_external_config(import_path)
if result:
opacity_value, blur_value, hide_window_decor, font_size, font_family = result
font_family_index = FONT_FAMILIES.index(font_family) if font_family in FONT_FAMILIES else 0
editing_import = False
import_path = ""
curses.curs_set(0)
elif ch in (27, ord(' ')): # ESC or space to cancel editing
editing_import = False
import_path = ""
curses.curs_set(0)
elif ch in (curses.KEY_BACKSPACE, 127, 8):
import_path = import_path[:-1]
else:
try:
import_path += chr(ch)
except:
pass
else:
if ch == curses.KEY_UP:
selected_control = (selected_control - 1) % total_controls
elif ch == curses.KEY_DOWN:
selected_control = (selected_control + 1) % total_controls
elif ch == curses.KEY_LEFT:
if selected_control == 0 and opacity_value > 0:
opacity_value -= 1
elif selected_control == 1 and blur_value > 0:
blur_value -= 1
elif selected_control == 3 and font_size > 8:
font_size -= 1
elif selected_control == 4:
font_family_index = (font_family_index - 1) % len(FONT_FAMILIES)
elif ch == curses.KEY_RIGHT:
if selected_control == 0 and opacity_value < 10:
opacity_value += 1
elif selected_control == 1 and blur_value < 10:
blur_value += 1
elif selected_control == 3 and font_size < 40:
font_size += 1
elif selected_control == 4:
font_family_index = (font_family_index + 1) % len(FONT_FAMILIES)
elif ch == ord(' '):
if selected_control == 2:
hide_window_decor = not hide_window_decor
elif ch == ord('\n') or ch == curses.KEY_ENTER:
if selected_control == 5:
editing_import = True
elif ch == ord('q'):
break
elif ch == ord('w'):
write_new_kitty_conf(opacity_value / 10, blur_value, hide_window_decor, font_size, FONT_FAMILIES[font_family_index])
elif ch == ord('W'):
write_new_kitty_conf(opacity_value / 10, blur_value, hide_window_decor, font_size, FONT_FAMILIES[font_family_index])
break
if __name__ == "__main__":
curses.wrapper(main)