#!/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 couldn’t 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' MODIFIERS = ['None', 'Command', 'Control', 'Option', 'Super', 'Shift', 'Alt'] 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 CommandWidget(QtWidgets.QLineEdit): def __init__(self, value): super().__init__() if value: self.setText('{}{}'.format(value['program'], ''.join(value['args']))) def value(self): res = self.text().split(' ') return {'program': res[0], 'args': res[1:]} 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 [])) 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.MODIFIERS: self.mods.addItem(option) self.mods.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) self.layout.addRow(QtWidgets.QLabel('Mods'), self.mods) self.tabs = QtWidgets.QTabWidget() self.action = QtWidgets.QComboBox() for option in self.action_options: self.action.addItem(option) self.tabs.addTab(self.action, 'Action') self.chars = QtWidgets.QLineEdit() self.tabs.addTab(self.chars, 'Chars') self.command = CommandWidget(None) self.tabs.addTab(self.command, 'Command') self.layout.addWidget(self.tabs) 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 mods = self.mods.selectedItems() if len(mods): self.result['mods'] = '|'.join(mod.text() for mod in mods) idx = self.tabs.currentIndex() title = self.tabs.tabText(idx).lower() if title == 'action': self.result['action'] = self.action.text() elif title == 'chars': self.result['chars'] = self.chars.text() elif title == 'command': self.result['command'] = self.command.value() self.accept() class MouseBindingDialog(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 [])) mouse_options = [ 'Middle', 'Left', 'Right', '1', '2', '3', '4', '5', '6', '7', '8', '9' ] def __init__(self): super().__init__() self.result = {} self.layout = QtWidgets.QFormLayout() self.layout.setFieldGrowthPolicy( QtWidgets.QFormLayout.AllNonFixedFieldsGrow ) self.mouse = QtWidgets.QComboBox() for option in self.mouse_options: self.mouse.addItem(option) self.layout.addRow(QtWidgets.QLabel('Mouse'), self.mouse) self.mods = QtWidgets.QListWidget() for option in MODIFIERS: 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) 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): mouse = self.mouse.currentText() # TODO: this is an ugly heuristic. should we parse yaml here? self.result['mouse'] = int(mouse) if mouse.isdigit() else mouse mods = self.mods.selectedItems() if len(mods): self.result['mods'] = '|'.join(mod.text() for mod in mods) self.result['action'] = self.action.currentText() self.accept() class MultiListWidget(QtWidgets.QWidget): def __init__(self, elems): super().__init__() self.lst = None self.elems = elems 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.elems), 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.elems): 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 = self.dialog() if dialog.exec() == QtWidgets.QDialog.Accepted: key_binding = dialog.result self.elems.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.elems[i] self.update() def value(self): return self.elems class KeyBindingsWidget(MultiListWidget): def __init__(self, *args): super().__init__(*args) self.dialog = KeyBindingDialog 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 class MouseBindingsWidget(MultiListWidget): def __init__(self, *args): super().__init__(*args) self.dialog = MouseBindingDialog def itemize(self, binding): key = binding['mouse'] mods = binding.get('mods') if mods: key = '{} {}'.format(' '.join(mods.split('|')), key) action = binding.get('action') return key, action 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.layout.setContentsMargins(100, 10, 100, 10) 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 not name: return self.widget_value(widget) elif 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 General(ConfigWidget): cursor_style_options = ['Block', 'Underline', 'Beam'] def __init__(self, config): super().__init__() self.widgets['background_opacity'] = QtWidgets.QDoubleSpinBox() self.widgets['background_opacity'].setValue(config.get('background_opacity')) self.widgets['cursor_style'] = QtWidgets.QComboBox() for option in self.cursor_style_options: self.widgets['cursor_style'].addItem(option) cur = config.get('cursor_style') if cur in self.cursor_style_options: self.widgets['cursor_style'].setCurrentIndex( self.cursor_style_options.index(cur) ) self.widgets['custom_cursor_colors'] = QtWidgets.QCheckBox() self.widgets['custom_cursor_colors'].setChecked( config.get('custom_cursor_colors', False) ) self.widgets['draw_bold_text_with_bright_colors'] = QtWidgets.QCheckBox() self.widgets['draw_bold_text_with_bright_colors'].setChecked( config.get('draw_bold_text_with_bright_colors', False) ) self.widgets['dynamic_title'] = QtWidgets.QCheckBox() self.widgets['dynamic_title'].setChecked( config.get('dynamic_title', False) ) self.widgets['hide_cursor_when_typing'] = QtWidgets.QCheckBox() self.widgets['hide_cursor_when_typing'].setChecked( config.get('hide_cursor_when_typing', False) ) self.widgets['live_config_reload'] = QtWidgets.QCheckBox() self.widgets['live_config_reload'].setChecked( config.get('live_config_reload', False) ) self.widgets['tabspaces'] = QtWidgets.QSpinBox() self.widgets['tabspaces'].setValue(config.get('tabspaces')) self.widgets['unfocused_hollow_cursor'] = QtWidgets.QCheckBox() self.widgets['unfocused_hollow_cursor'].setChecked( config.get('unfocused_hollow_cursor', False) ) self.render_state() 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__() key_bindings = KeyBindingsWidget(config) self.widgets = { '': key_bindings, } self.render_state() class MouseBindings(ConfigWidget): def __init__(self, config): super().__init__() mouse_bindings = MouseBindingsWidget(config) self.widgets = { '': mouse_bindings, } self.render_state() class Mouse(ConfigWidget): def __init__(self, config): super().__init__() double_threshold = QtWidgets.QSpinBox() double_threshold.setMaximum(10000) double_threshold.setValue(config.get('double_click', {}).get('threshold')) triple_threshold = QtWidgets.QSpinBox() triple_threshold.setMaximum(10000) triple_threshold.setValue(config.get('triple_click', {}).get('threshold')) hide_when_typing = QtWidgets.QCheckBox() hide_when_typing.setChecked(config.get('hide_when_typing', False)) launcher = CommandWidget(config.get('url', {}).get('launcher')) modifiers = QtWidgets.QComboBox() for modifier in MODIFIERS: modifiers.addItem(modifier) dec = config.get('modifiers') if dec in MODIFIERS: modifiers.setCurrentIndex(MODIFIERS.index(dec)) self.widgets = { 'hide_when_typing': hide_when_typing, 'url': { 'launcher': launcher, 'modifiers': modifiers, }, 'double_click': { 'threshold': double_threshold, }, 'triple_click': { 'threshold': triple_threshold, }, } self.render_state() class VisualBell(ConfigWidget): animation_options = [ 'Ease', 'EaseOut', 'EaseOutSine', 'EaseOutQuad', 'EaseOutCubic', 'EaseOutQuart', 'EaseOutQuint', 'EaseOutExpo', 'EaseOutCirc', 'Linear', ] def __init__(self, config): super().__init__() duration = QtWidgets.QSpinBox() duration.setMaximum(10000) duration.setValue(config.get('duration')) animation = QtWidgets.QComboBox() for a in self.animation_options: animation.addItem(a) dec = config.get('animation') if dec in self.animation_options: animation.setCurrentIndex(self.animation_options.index(dec)) self.widgets = { 'duration': duration, 'animation': animation, } self.render_state() class Config(QtWidgets.QWidget): def __init__(self, config, dry=False): super().__init__() self.config = config self.dry = dry self.layout = QtWidgets.QVBoxLayout() self.setLayout(self.layout) self.add_tabs() self.add_buttons() def add_tabs(self): self.tabs = QtWidgets.QTabWidget() self.tabs.addTab(General(self.config), 'General') self.tabs.setTabToolTip(0, 'General') self.tabs.addTab(Window(self.config.get('window', {})), 'Window') self.tabs.setTabToolTip(1, 'Window') self.tabs.addTab(Font(self.config.get('font', {})), 'Font') self.tabs.setTabToolTip(2, 'Font') self.tabs.addTab(Debug(self.config.get('debug', {})), 'Debug') self.tabs.setTabToolTip(3, 'Debug') self.tabs.addTab(Env(self.config.get('env', {})), 'Env') self.tabs.setTabToolTip(4, 'Env') self.tabs.addTab(Selection(self.config.get('selection', {})), 'Selection') self.tabs.setTabToolTip(5, 'Selection') self.tabs.addTab(Shell(self.config.get('shell', {})), 'Shell') self.tabs.setTabToolTip(6, 'Shell') self.tabs.addTab(Colors(self.config.get('colors', {})), 'Colors') self.tabs.setTabToolTip(7, 'Colors') self.tabs.addTab(Scrolling(self.config.get('scrolling', {})), 'Scrolling') self.tabs.setTabToolTip(8, 'Scrolling') self.tabs.addTab( KeyBindings(self.config.get('key_bindings', [])), 'Key Bindings' ) self.tabs.setTabToolTip(9, 'Key Bindings') self.tabs.addTab( MouseBindings(self.config.get('mouse_bindings', [])), 'Mouse Bindings' ) self.tabs.setTabToolTip(10, 'Mouse Bindings') self.tabs.addTab(Mouse(self.config.get('mouse', {})), 'Mouse') self.tabs.setTabToolTip(11, 'Mouse') self.tabs.addTab(VisualBell(self.config.get('visual_bell', {})), 'Visual Bell') self.tabs.setTabToolTip(12, 'Visual Bell') 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 = '_'.join(self.tabs.tabText(idx).lower().split(' ')) old = state.get(title, {}) if type(old) == list: state[title].extend(e for e in tab.gather_state() if e not in old) else: if title == 'general': state = {**state, **tab.gather_state()} else: state[title] = {**state.get(title, {}), **tab.gather_state()} return state def save(self): state = yaml.dump(self.gather_state()) if self.dry: print(state) else: with open(ALACRITTY_CONFIG, 'w+') as f: f.write(state) sys.exit() def main(): dry = False if len(sys.argv) == 2 and sys.argv[1] in ['--dry', '-d']: dry = True app = QtWidgets.QApplication([NAME]) app.setApplicationName(NAME) window = QtWidgets.QMainWindow() with open(ALACRITTY_CONFIG) as f: config = yaml.safe_load(f.read()) window.setCentralWidget(Config(config, dry=dry)) window.setWindowFlags(QtCore.Qt.FramelessWindowHint) window.setWindowTitle(NAME) window.setUnifiedTitleAndToolBarOnMac(True) window.setGeometry( QtWidgets.QStyle.alignedRect( QtCore.Qt.LeftToRight, QtCore.Qt.AlignCenter, QtCore.QSize(500, 300), app.desktop().availableGeometry() ) ) window.show() app.exec_() if __name__ == '__main__': main()