Files
alacritty-config/alacritty_config_gui.py

532 lines
18 KiB
Python

#!/usr/bin/env python3
import os
import platform
import sys
import yaml
from PyQt5 import QtWidgets, QtGui, QtCore
ALACRITTY_CONFIG = os.path.expanduser("~/.config/alacritty/alacritty.yml")
NAME = 'Alacritty Config'
class ColorSelect(QtWidgets.QPushButton):
def __init__(self, value):
super().__init__()
self.set_value(value.replace('0x', '#'))
self.clicked.connect(self.handle_clicked)
def handle_clicked(self):
color = QtWidgets.QColorDialog.getColor()
if color.isValid():
self.set_value(color.name())
def set_value(self, value):
self._value = value
color = QtGui.QColor(self._value)
self.setFlat(True)
self.setAutoFillBackground(True)
self.setStyleSheet("""
QPushButton {{
color: {0};
background-color: {0};
border-style: outset;
}}
QPushButton:checked{{
color: {0};
background-color: {0};
border-style: outset;
}}
QPushButton:hover{{
background-color: {0};
border-style: outset;
}}
""".format(color.name()))
self.update()
def value(self):
return self._value.replace('#', '0x')
class FontSelect(QtWidgets.QPushButton):
def __init__(self, family, style):
super().__init__()
self.set_value(family, style)
self.clicked.connect(self.handle_clicked)
def handle_clicked(self):
font, valid = QtWidgets.QFontDialog.getFont()
if valid:
self.set_value(font.family(), font.styleName())
def set_value(self, family, style):
self.family = family
self.style = style
self.setStyleSheet("""
QPushButton {{
background-color: white;
font-family: {0};
}}
QPushButton:checked{{
background-color: white;
}}
QPushButton:hover{{
background-color: white;
}}
""".format(family))
self.setText(self.family)
self.update()
def value(self):
return {
'family': self.family,
'style': self.style
}
class Spoiler(QtWidgets.QWidget):
def __init__(self, title):
super().__init__()
self.toggle = QtWidgets.QToolButton()
self.toggle.setStyleSheet("QToolButton { border: none; }")
self.toggle.setToolButtonStyle(QtCore.Qt.ToolButtonTextBesideIcon)
self.toggle.setArrowType(QtCore.Qt.ArrowType.RightArrow)
self.toggle.setText(title)
self.toggle.setCheckable(True)
self.toggle.setChecked(False)
self.header = QtWidgets.QFrame()
self.header.setFrameShape(QtWidgets.QFrame.HLine)
self.header.setFrameShadow(QtWidgets.QFrame.Sunken)
self.header.setSizePolicy(
QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Maximum
)
self.content = QtWidgets.QScrollArea()
self.content.setStyleSheet("""
QScrollArea { border: none; background-color: transparent; }
""")
self.content.setSizePolicy(
QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed
)
self.content.setMaximumHeight(0)
self.content.setMinimumHeight(0)
self.animation = QtCore.QParallelAnimationGroup()
self.animation.addAnimation(
QtCore.QPropertyAnimation(self, b"minimumHeight")
)
self.animation.addAnimation(
QtCore.QPropertyAnimation(self, b"maximumHeight")
)
self.animation.addAnimation(
QtCore.QPropertyAnimation(self.content, b"maximumHeight")
)
self.layout = QtWidgets.QGridLayout()
self.layout.addWidget(self.toggle, 0, 0, 1, 1, QtCore.Qt.AlignLeft)
self.layout.addWidget(self.header, 0, 2, 1, 1)
self.layout.addWidget(self.content, 1, 0, 1, 3)
self.layout.setContentsMargins(0, 0, 0, 0)
self.setLayout(self.layout)
self.toggle.clicked.connect(self.update_state)
def update_state(self, checked):
self.toggle.setArrowType(
QtCore.Qt.ArrowType.DownArrow if checked
else QtCore.Qt.ArrowType.RightArrow
)
self.animation.setDirection(
QtCore.QAbstractAnimation.Forward if checked
else QtCore.QAbstractAnimation.Backward
)
self.animation.start()
def set_layout(self, layout):
self.content.setLayout(layout)
collapsed_height = self.sizeHint().height() - self.content.maximumHeight()
content_height = layout.sizeHint().height()
for i in range(self.animation.animationCount()-1):
animation = self.animation.animationAt(i)
animation.setDuration(300)
animation.setStartValue(collapsed_height)
animation.setEndValue(collapsed_height + content_height)
last = self.animation.animationAt(self.animation.animationCount() - 1)
last.setDuration(30)
last.setStartValue(0)
last.setEndValue(content_height)
class ConfigWidget(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.widgets = {}
def prettify(self, s):
return ' '.join(x.capitalize() for x in s.split('_'))
def render_item(self, layout, name, widget):
sub_layout = QtWidgets.QHBoxLayout()
sub_layout.addWidget(QtWidgets.QLabel(self.prettify(name)))
sub_layout.addWidget(widget)
layout.addLayout(sub_layout)
def render_section(self, widgets, sup=None, name=None):
layout = QtWidgets.QVBoxLayout()
for wname, widget in widgets.items():
if type(widget) is dict:
self.render_section(widget, layout, wname)
else:
self.render_item(layout, wname, widget)
if name and sup is not None:
layout.setContentsMargins(10, 0, 0, 0)
spoiler = Spoiler(self.prettify(name))
spoiler.set_layout(layout)
sup.addWidget(spoiler)
layout.setAlignment(QtCore.Qt.AlignTop)
return layout
def render_state(self):
self.layout = self.render_section(self.widgets)
self.setLayout(self.layout)
def widget_value(self, widget):
typ = type(widget)
if typ is QtWidgets.QComboBox:
return widget.currentText()
if typ is QtWidgets.QCheckBox:
return widget.isChecked()
if typ is QtWidgets.QLineEdit:
return widget.text()
return widget.value()
def widgets_to_state(self, widgets):
state = {}
for name, widget in widgets.items():
if type(widget) is dict:
state[name] = self.widgets_to_state(widget)
else:
state[name] = self.widget_value(widget)
return state
def gather_state(self):
return self.widgets_to_state(self.widgets)
class Debug(ConfigWidget):
def __init__(self, config):
super().__init__()
self.widgets['render_timer'] = QtWidgets.QCheckBox()
self.widgets['render_timer'].setChecked(config.get('render_timer'))
self.render_state()
class Env(ConfigWidget):
def __init__(self, config):
super().__init__()
self.widgets['TERM'] = QtWidgets.QLineEdit(config.get('TERM'))
self.render_state()
class Selection(ConfigWidget):
def __init__(self, config):
super().__init__()
self.widgets['semantic_escape_chars'] = QtWidgets.QLineEdit(
config.get('semantic_escape_chars')
)
self.render_state()
class Shell(ConfigWidget):
def __init__(self, config):
super().__init__()
self.widgets['program'] = QtWidgets.QLineEdit(config.get('program'))
self.render_state()
class Font(ConfigWidget):
def __init__(self, config):
super().__init__()
glyph_offset_x = QtWidgets.QSpinBox()
glyph_offset_x.setValue(config.get('glyph_offset', {}).get('x'))
glyph_offset_y = QtWidgets.QSpinBox()
glyph_offset_y.setValue(config.get('glyph_offset', {}).get('y'))
offset_x = QtWidgets.QSpinBox()
offset_x.setValue(config.get('offset', {}).get('x'))
offset_y = QtWidgets.QSpinBox()
offset_y.setValue(config.get('offset', {}).get('y'))
scale_with_dpi = QtWidgets.QCheckBox()
scale_with_dpi.setChecked(config.get('scale_with_dpi'))
size = QtWidgets.QDoubleSpinBox()
size.setValue(config.get('size'))
use_thin_strokes = QtWidgets.QCheckBox()
use_thin_strokes.setChecked(config.get('use_thin_strokes'))
normal_config = config.get('normal', {})
normal = FontSelect(normal_config['family'], normal_config['style'])
italic_config = config.get('italic', {})
italic = FontSelect(italic_config['family'], italic_config['style'])
bold_config = config.get('bold', {})
bold = FontSelect(bold_config['family'], bold_config['style'])
self.widgets = {
'scale_with_dpi': scale_with_dpi,
'size': size,
'use_thin_strokes': use_thin_strokes,
'glyph_offset': {
'x': glyph_offset_x,
'y': glyph_offset_y,
},
'offset': {
'x': offset_x,
'y': offset_y,
},
'normal': normal,
'italic': italic,
'bold': bold,
}
self.render_state()
class Colors(ConfigWidget):
def __init__(self, config):
super().__init__()
primary_background = ColorSelect(
config.get('primary', {}).get('background')
)
primary_foreground = ColorSelect(
config.get('primary', {}).get('foreground')
)
cursor_background = ColorSelect(
config.get('cursor', {}).get('cursor')
)
cursor_foreground = ColorSelect(
config.get('cursor', {}).get('text')
)
normal_black = ColorSelect(config.get('normal', {}).get('black'))
normal_blue = ColorSelect(config.get('normal', {}).get('blue'))
normal_cyan = ColorSelect(config.get('normal', {}).get('cyan'))
normal_green = ColorSelect(config.get('normal', {}).get('green'))
normal_magenta = ColorSelect(config.get('normal', {}).get('magenta'))
normal_red = ColorSelect(config.get('normal', {}).get('red'))
normal_white = ColorSelect(config.get('normal', {}).get('white'))
normal_yellow = ColorSelect(config.get('normal', {}).get('yellow'))
bright_black = ColorSelect(config.get('bright', {}).get('black'))
bright_blue = ColorSelect(config.get('bright', {}).get('blue'))
bright_cyan = ColorSelect(config.get('bright', {}).get('cyan'))
bright_green = ColorSelect(config.get('bright', {}).get('green'))
bright_magenta = ColorSelect(config.get('bright', {}).get('magenta'))
bright_red = ColorSelect(config.get('bright', {}).get('red'))
bright_white = ColorSelect(config.get('bright', {}).get('white'))
bright_yellow = ColorSelect(config.get('bright', {}).get('yellow'))
dim_black = ColorSelect(config.get('dim', {}).get('black'))
dim_blue = ColorSelect(config.get('dim', {}).get('blue'))
dim_cyan = ColorSelect(config.get('dim', {}).get('cyan'))
dim_green = ColorSelect(config.get('dim', {}).get('green'))
dim_magenta = ColorSelect(config.get('dim', {}).get('magenta'))
dim_red = ColorSelect(config.get('dim', {}).get('red'))
dim_white = ColorSelect(config.get('dim', {}).get('white'))
dim_yellow = ColorSelect(config.get('dim', {}).get('yellow'))
self.widgets = {
'primary': {
'background': primary_background,
'foreground': primary_foreground,
},
'cursor': {
'cursor': cursor_background,
'text': cursor_foreground,
},
'normal': {
'black': normal_black,
'blue': normal_blue,
'cyan': normal_cyan,
'green': normal_green,
'magenta': normal_magenta,
'red': normal_red,
'white': normal_white,
'yellow': normal_yellow,
},
'bright': {
'black': bright_black,
'blue': bright_blue,
'cyan': bright_cyan,
'green': bright_green,
'magenta': bright_magenta,
'red': bright_red,
'white': bright_white,
'yellow': bright_yellow,
},
'dim': {
'black': dim_black,
'blue': dim_blue,
'cyan': dim_cyan,
'green': dim_green,
'magenta': dim_magenta,
'red': dim_red,
'white': dim_white,
'yellow': dim_yellow,
},
}
self.render_state()
class Window(ConfigWidget):
decoration_options = (
['full', 'none'] +
(['transparent', 'buttonless'] if platform.system() == 'Darwin' else [])
)
startup_options = (
['Windowed', 'FullScreen', 'Maximized'] +
(['SimpleFullScreen'] if platform.system() == 'Darwin' else [])
)
def __init__(self, config):
super().__init__()
decorations = QtWidgets.QComboBox()
for option in self.decoration_options:
decorations.addItem(option)
dec = config.get('decorations')
if dec in self.decoration_options:
decorations.setCurrentIndex(self.decoration_options.index(dec))
startup_mode = QtWidgets.QComboBox()
for mode in self.startup_options:
startup_mode.addItem(mode)
dec = config.get('startup_mode')
if dec in self.startup_options:
startup_mode.setCurrentIndex(self.startup_options.index(dec))
columns = QtWidgets.QSpinBox()
columns.setValue(config.get('dimensions', {}).get('columns'))
lines = QtWidgets.QSpinBox()
lines.setValue(config.get('dimensions', {}).get('lines'))
padding_x = QtWidgets.QSpinBox()
padding_x.setValue(config.get('padding', {}).get('x'))
padding_y = QtWidgets.QSpinBox()
padding_y.setValue(config.get('padding', {}).get('y'))
dynamic_padding = QtWidgets.QCheckBox()
dynamic_padding.setChecked(config.get('dynamic_padding', False))
title = QtWidgets.QLineEdit(config.get('title', 'Alacritty'))
self.widgets = {
'title': title,
'decorations': decorations,
'startup_mode': startup_mode,
'dynamic_padding': dynamic_padding,
'padding': {
'x': padding_x,
'y': padding_y,
},
'dimensions': {
'columns': columns,
'lines': lines,
}
}
if platform.system() == 'Linux':
self.widgets['class'] = QtWidgets.QLineEdit(
config.get('class', 'Alacritty')
)
self.render_state()
class Scrolling(ConfigWidget):
def __init__(self, config):
super().__init__()
history = QtWidgets.QSpinBox(config.get('history'))
multiplier = QtWidgets.QSpinBox(config.get('multiplier'))
faux_multiplier = QtWidgets.QSpinBox(config.get('faux_multiplier'))
autoscroll = QtWidgets.QCheckBox()
autoscroll.setChecked(config.get('autoscroll', False))
self.widgets = {
'history': history,
'multiplier': multiplier,
'faux_multiplier': faux_multiplier,
'faux_multiplier': faux_multiplier,
'autoscroll': autoscroll,
}
self.render_state()
class Config(QtWidgets.QWidget):
def __init__(self, config):
super().__init__()
self.layout = QtWidgets.QVBoxLayout()
self.config = config
self.add_tabs(config)
self.add_buttons()
self.setWindowTitle(NAME)
self.setLayout(self.layout)
def add_tabs(self, config):
self.tabs = QtWidgets.QTabWidget()
self.tabs.addTab(Window(config.get('window', {})), "Window")
self.tabs.addTab(Font(config.get('font', {})), "Font")
self.tabs.addTab(Debug(config.get('debug', {})), "Debug")
self.tabs.addTab(Env(config.get('env', {})), "Env")
self.tabs.addTab(Selection(config.get('selection', {})), "Selection")
self.tabs.addTab(Shell(config.get('shell', {})), "Shell")
self.tabs.addTab(Colors(config.get('colors', {})), "Colors")
self.tabs.addTab(Scrolling(config.get('scrolling', {})), "Scrolling")
self.layout.addWidget(self.tabs)
def add_buttons(self):
self.buttons = QtWidgets.QDialogButtonBox()
ok_button = self.buttons.addButton(self.buttons.Ok)
self.buttons.addButton(self.buttons.Cancel)
self.buttons.accepted.connect(self.save)
self.buttons.rejected.connect(sys.exit)
self.layout.addWidget(self.buttons)
def gather_state(self):
state = self.config
for idx in range(self.tabs.count()):
tab = self.tabs.widget(idx)
title = self.tabs.tabText(idx).lower()
state[title] = {**state.get(title, {}), **tab.gather_state()}
return state
def save(self):
state = self.gather_state()
with open(ALACRITTY_CONFIG, 'w+') as f:
f.write(yaml.dump(state))
sys.exit()
def addDialog(self):
dialog = QtWidgets.QColorDialog()
dialog.show()
if __name__ == '__main__':
app = QtWidgets.QApplication([NAME])
app.setApplicationName(NAME)
with open(ALACRITTY_CONFIG) as f:
config = yaml.safe_load(f.read())
conf = Config(config)
conf.show()
app.exec_()