Files
alacritty-config/alacritty_config_gui.py
2019-06-26 13:25:48 +02:00

765 lines
26 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 os
import platform
import sys
import yaml
try:
from PyQt5 import QtWidgets, QtGui, QtCore
except:
print("alacritty-config-gui depends on PyQt5. I couldnt locate PyQt5.")
print("Sorry about that.")
print("\nRunning `pip3 install PyQt5` could help.")
sys.exit(1)
ALACRITTY_CONFIG = os.path.expanduser("~/.config/alacritty/alacritty.yml")
NAME = 'Alacritty Config'
def delete_layout_items(layout):
while layout.count():
item = layout.takeAt(0)
widget = item.widget()
if widget is not None:
widget.setParent(None)
else:
delete_layout_items(item.layout())
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(QtGui.QColor(self._value))
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 KeyBindingDialog(QtWidgets.QDialog):
action_options = ([
"", "Copy", "Paste", "PasteSelection", "IncreaseFontSize", "DecreaseFontSize",
"ResetFontSize", "ScrollPageUp", "ScrollPageDown", "ScrollLineUp",
"ScrollLineDown", "ScrollToTop", "ScrollToBottom", "ClearHistory", "Hide",
"Quit", "ToggleFullscreen", "SpawnNewInstance", "ClearLogNotice", "None",
] + (['ToggleSimpleFullscreen'] if platform.system() == 'Darwin' else []))
modifier_options = ["Command", "Control", "Option", "Super", "Shift", "Alt"]
def __init__(self):
super().__init__()
self.result = {}
self.layout = QtWidgets.QFormLayout()
self.layout.setFieldGrowthPolicy(
QtWidgets.QFormLayout.AllNonFixedFieldsGrow
)
self.key = QtWidgets.QLineEdit()
self.layout.addRow(QtWidgets.QLabel('Key'), self.key)
self.mods = QtWidgets.QListWidget()
for option in self.modifier_options:
self.mods.addItem(option)
self.mods.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
self.layout.addRow(QtWidgets.QLabel('Mods'), self.mods)
self.action = QtWidgets.QComboBox()
for option in self.action_options:
self.action.addItem(option)
self.layout.addRow(QtWidgets.QLabel('Action'), self.action)
self.chars = QtWidgets.QLineEdit()
self.layout.addRow(QtWidgets.QLabel('Chars'), self.chars)
buttons = QtWidgets.QDialogButtonBox()
buttons.addButton(buttons.Ok)
buttons.addButton(buttons.Cancel)
buttons.accepted.connect(self.do_accept)
buttons.rejected.connect(self.reject)
self.layout.addWidget(buttons)
self.setLayout(self.layout)
def do_accept(self):
key = self.key.text()
# TODO: this is an ugly heuristic. should we parse yaml here?
self.result['key'] = int(key) if key.isdigit() else key
action = self.action.currentText()
if action:
self.result['action'] = action
chars = self.chars.text()
if chars:
self.result['chars'] = chars
mods = self.mods.selectedItems()
if len(mods):
self.result['mods'] = '|'.join(mod.text() for mod in mods)
self.accept()
class KeyBindingsWidget(QtWidgets.QWidget):
def __init__(self, key_bindings):
super().__init__()
self.key_bindings = key_bindings
self.lst = None
self.init()
def init(self):
self.layout = QtWidgets.QVBoxLayout()
self.layout.setContentsMargins(0, 0, 0, 0)
self.update()
btn_group = QtWidgets.QHBoxLayout()
btn_group.setAlignment(QtCore.Qt.AlignLeft)
self.layout.addLayout(btn_group)
btn = QtWidgets.QPushButton('+')
btn.setMaximumSize(40, 40)
btn.clicked.connect(self.open_dialog)
btn_group.addWidget(btn)
btn = QtWidgets.QPushButton('-')
btn.setMaximumSize(40, 40)
btn.clicked.connect(self.delete_selected)
btn_group.addWidget(btn)
self.setLayout(self.layout)
def update(self):
if self.lst:
self.lst.setParent(None)
self.lst = QtWidgets.QTableWidget(len(self.key_bindings), 2)
self.lst.setShowGrid(False)
horizontal_header = self.lst.horizontalHeader()
horizontal_header.setVisible(False)
# TODO: why do i need this?
horizontal_header.resizeSection(0, 200)
horizontal_header.resizeSection(1, 200)
self.lst.verticalHeader().setVisible(False)
self.lst.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
self.lst.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
for i, binding in enumerate(self.key_bindings):
key, action = self.itemize(binding)
self.lst.setItem(i, 0, QtWidgets.QTableWidgetItem(key))
self.lst.setItem(i, 1, QtWidgets.QTableWidgetItem(action))
self.layout.insertWidget(0, self.lst)
def open_dialog(self):
dialog = KeyBindingDialog()
if dialog.exec() == QtWidgets.QDialog.Accepted:
key_binding = dialog.result
self.key_bindings.append(key_binding)
self.update()
def delete_selected(self):
to_delete = set()
for i in self.lst.selectedIndexes():
to_delete.add(i.row())
for i in sorted(to_delete, reverse=True):
del self.key_bindings[i]
self.update()
def itemize(self, binding):
key = binding['key']
mods = binding.get('mods')
if mods:
key = '{} {}'.format(' '.join(mods.split('|')), key)
action = binding.get('action')
command = binding.get('command')
if command:
action = 'cmd({} {})'.format(command['program'], ' '.join(command['args']))
chars = binding.get('chars')
if chars:
action = 'chars({})'.format(chars)
return key, action
def value(self):
return self.key_bindings
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_section(self, widgets, sup=None, name=None):
layout = QtWidgets.QFormLayout()
layout.setFieldGrowthPolicy(QtWidgets.QFormLayout.AllNonFixedFieldsGrow)
for wname, widget in widgets.items():
if type(widget) is dict:
self.render_section(widget, layout, wname)
else:
layout.addRow(QtWidgets.QLabel(self.prettify(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()
if typ is QtWidgets.QListWidget:
res = []
for i in range(widget.count()):
item = widget.item(i).text()
if item:
res.append(item)
return res
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):
log_levels = ['None', 'Error', 'Warn', 'Info', 'Debug', 'Trace']
def __init__(self, config):
super().__init__()
self.widgets['render_timer'] = QtWidgets.QCheckBox()
self.widgets['render_timer'].setChecked(
config.get('render_timer', False)
)
self.widgets['persistent_logging'] = QtWidgets.QCheckBox()
self.widgets['persistent_logging'].setChecked(
config.get('persistent_logging', False)
)
self.widgets['log_level'] = QtWidgets.QComboBox()
for option in self.log_levels:
self.widgets['log_level'].addItem(option)
dec = config.get('log_level')
if dec in self.log_levels:
self.widgets['log_level'].setCurrentIndex(
self.log_levels.index(dec)
)
self.widgets['print_events'] = QtWidgets.QCheckBox()
self.widgets['print_events'].setChecked(
config.get('print_events', False)
)
self.widgets['ref_test'] = QtWidgets.QCheckBox()
self.widgets['ref_test'].setChecked(config.get('ref_test', False))
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.widgets['save_to_clipboard'] = QtWidgets.QCheckBox()
self.widgets['save_to_clipboard'].setChecked(
config.get('save_to_clipboard', False)
)
self.render_state()
class Shell(ConfigWidget):
def __init__(self, config):
super().__init__()
self.widgets['program'] = QtWidgets.QLineEdit(config.get('program'))
self.widgets['args'] = QtWidgets.QListWidget()
for elem in config.get('args', []):
self.widgets['args'].addItem(elem)
for _ in range(3):
widget = QtWidgets.QListWidgetItem("")
widget.setFlags(widget.flags() | QtCore.Qt.ItemIsEditable)
self.widgets['args'].addItem(widget)
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.setMaximum(300)
columns.setValue(config.get('dimensions', {}).get('columns', 80))
lines = QtWidgets.QSpinBox()
columns.setMaximum(200)
lines.setValue(config.get('dimensions', {}).get('lines', 24))
padding_x = QtWidgets.QSpinBox()
padding_x.setValue(config.get('padding', {}).get('x', 0))
padding_y = QtWidgets.QSpinBox()
padding_y.setValue(config.get('padding', {}).get('y', 0))
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()
history.setMaximum(2**30)
history.setValue(config.get('history', 10000))
multiplier = QtWidgets.QSpinBox()
multiplier.setValue(config.get('multiplier', 3))
faux_multiplier = QtWidgets.QSpinBox()
faux_multiplier.setValue(config.get('faux_multiplier', 3))
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 KeyBindings(ConfigWidget):
def __init__(self, config):
super().__init__()
keybindings = KeyBindingsWidget(config)
self.widgets = {
'key_bindings': keybindings,
}
self.render_state()
class Config(QtWidgets.QWidget):
def __init__(self, config):
super().__init__()
self.layout = QtWidgets.QVBoxLayout()
self.config = config
self.add_tabs()
self.add_buttons()
self.setLayout(self.layout)
def add_tabs(self):
self.tabs = QtWidgets.QTabWidget()
self.tabs.addTab(Window(self.config.get('window', {})), "Window")
self.tabs.addTab(Font(self.config.get('font', {})), "Font")
self.tabs.addTab(Debug(self.config.get('debug', {})), "Debug")
self.tabs.addTab(Env(self.config.get('env', {})), "Env")
self.tabs.addTab(Selection(self.config.get('selection', {})), "Selection")
self.tabs.addTab(Shell(self.config.get('shell', {})), "Shell")
self.tabs.addTab(Colors(self.config.get('colors', {})), "Colors")
self.tabs.addTab(Scrolling(self.config.get('scrolling', {})), "Scrolling")
self.tabs.addTab(
KeyBindings(self.config.get('key_bindings', [])), "Key Bindings"
)
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 main():
app = QtWidgets.QApplication([NAME])
app.setApplicationName(NAME)
with open(ALACRITTY_CONFIG) as f:
config = yaml.safe_load(f.read())
window = QtWidgets.QMainWindow()
window.setCentralWidget(Config(config))
window.setGeometry(
QtWidgets.QStyle.alignedRect(
QtCore.Qt.LeftToRight,
QtCore.Qt.AlignCenter,
QtCore.QSize(300, 400),
app.desktop().availableGeometry()
)
)
window.setWindowFlags(QtCore.Qt.FramelessWindowHint)
window.setWindowTitle(NAME)
window.setUnifiedTitleAndToolBarOnMac(True)
window.show()
app.exec_()
if __name__ == '__main__':
main()