Skip to content

Instantly share code, notes, and snippets.

@yspkm
Last active August 15, 2026 15:30
Show Gist options
  • Select an option

  • Save yspkm/f1185dc4c7b5b94fab386dd68ad3884d to your computer and use it in GitHub Desktop.

Select an option

Save yspkm/f1185dc4c7b5b94fab386dd68ad3884d to your computer and use it in GitHub Desktop.
KLayout Dark mode
module MyMacro
include RBA
# Use the platform-independent Qt Fusion style
RBA::QApplication::setStyle("Fusion")
palette = RBA::QPalette::new
# Common colors for active, inactive and disabled states.
# State-specific exceptions are defined below.
role_colors = [
# Main surfaces
[RBA::QPalette::Window, "#24282F"],
[RBA::QPalette::Base, "#1B1F24"],
[RBA::QPalette::AlternateBase, "#282D35"],
[RBA::QPalette::Button, "#303640"],
# Main text
[RBA::QPalette::WindowText, "#D4DAE3"],
[RBA::QPalette::Text, "#D4DAE3"],
[RBA::QPalette::ButtonText, "#E1E6ED"],
[RBA::QPalette::BrightText, "#FFFFFF"],
# Borders and depth
[RBA::QPalette::Light, "#4A525E"],
[RBA::QPalette::Midlight, "#3B424C"],
[RBA::QPalette::Mid, "#30363F"],
[RBA::QPalette::Dark, "#171A1F"],
[RBA::QPalette::Shadow, "#0C0E11"],
# Selection
[RBA::QPalette::Highlight, "#316DCA"],
[RBA::QPalette::HighlightedText, "#FFFFFF"],
# Tooltips
[RBA::QPalette::ToolTipBase, "#343A43"],
[RBA::QPalette::ToolTipText, "#F3F6F9"],
# Links and placeholders
[RBA::QPalette::Link, "#6CB6FF"],
[RBA::QPalette::LinkVisited, "#C49AF6"],
[RBA::QPalette::PlaceholderText, "#8B95A3"]
]
role_colors.each do |role, hex|
palette.setColor(
role,
RBA::QColor::new(hex)
)
end
# Inactive windows:
# keep selections visible, but less prominent
palette.setColor(
RBA::QPalette::Inactive,
RBA::QPalette::Highlight,
RBA::QColor::new("#3B4655")
)
palette.setColor(
RBA::QPalette::Inactive,
RBA::QPalette::HighlightedText,
RBA::QColor::new("#D4DAE3")
)
# Disabled controls:
# visibly disabled without becoming unreadable
[
RBA::QPalette::WindowText,
RBA::QPalette::Text,
RBA::QPalette::ButtonText
].each do |role|
palette.setColor(
RBA::QPalette::Disabled,
role,
RBA::QColor::new("#6F7885")
)
end
palette.setColor(
RBA::QPalette::Disabled,
RBA::QPalette::Button,
RBA::QColor::new("#282D34")
)
palette.setColor(
RBA::QPalette::Disabled,
RBA::QPalette::Base,
RBA::QColor::new("#20242A")
)
palette.setColor(
RBA::QPalette::Disabled,
RBA::QPalette::Highlight,
RBA::QColor::new("#343B45")
)
palette.setColor(
RBA::QPalette::Disabled,
RBA::QPalette::HighlightedText,
RBA::QColor::new("#7F8996")
)
RBA::QApplication::setPalette(palette)
# ------------------------------------------------------------
# Macro Editor syntax theme
# Low-chroma, high-readability dark theme
# ------------------------------------------------------------
app = RBA::Application::instance
editor_styles = <<~STYLES
basic:
'Alert'=(bold:true,color:'#E78284',background:'#3A2024'),
'Attribute'=(bold:false,color:'#B8C4D4'),
'Base-N Integer'=(bold:false,color:'#D9A66C'),
'Built-in'=(bold:false,color:'#88C0D0'),
'Character'=(bold:false,color:'#A7C080'),
'Comment'=(italic:false,bold:false,color:'#8B95A5'),
'Control Flow'=(bold:false,color:'#C9A0DC'),
'Data Type'=(bold:false,color:'#B8C4D4'),
'Decimal/Value'=(bold:false,color:'#D9A66C'),
'Error'=(bold:true,color:'#E78284',background:'#3A2024'),
'Extension'=(bold:false,color:'#B8C4D4'),
'Floating Point'=(bold:false,color:'#D9A66C'),
'Function'=(bold:false,color:'#88C0D0'),
'Import'=(bold:false,color:'#C9A0DC'),
'Keyword'=(bold:false,color:'#C9A0DC'),
'Normal'=(bold:false,color:'#D8DEE9'),
'Operator'=(bold:false,color:'#C9A0DC'),
'Others'=(bold:false,color:'#D8DEE9'),
'Preprocessor'=(bold:false,color:'#A7C080'),
'Region Marker'=(bold:false,color:'#88C0D0',background:'#28313B'),
'Special Character'=(bold:false,color:'#88C0D0'),
'Special String'=(bold:false,color:'#A7C080'),
'String'=(bold:false,color:'#A7C080'),
'Variable'=(bold:false,color:'#D8DEE9'),
'Verbatim String'=(bold:false,color:'#A7C080');
ruby:
'Access Control'=(bold:false,color:'#C9A0DC'),
'Attribute Definition'=(bold:false,color:'#88C0D0'),
'Bin'=(bold:false,color:'#D9A66C'),
'Blockcomment'=(italic:false,bold:false,color:'#8B95A5'),
'Char'=(bold:false,color:'#A7C080'),
'Class Variable'=(bold:false,color:'#D8DEE9'),
'Command'=(bold:false,color:'#88C0D0'),
'Comment'=(italic:false,bold:false,color:'#8B95A5'),
'Constant'=(bold:false,color:'#B8C4D4'),
'Constant Value'=(bold:false,color:'#D9A66C'),
'Data'=(bold:false,color:'#A7C080'),
'Dec'=(bold:false,color:'#D9A66C'),
'Default globals'=(bold:false,color:'#B8C4D4'),
'Definition'=(bold:false,color:'#88C0D0'),
'Delimiter'=(bold:false,color:'#C9A0DC'),
'Error'=(bold:true,color:'#E78284',background:'#3A2024'),
'Expression'=(bold:false,color:'#D8DEE9'),
'Float'=(bold:false,color:'#D9A66C'),
'GDL input'=(bold:false,color:'#A7C080'),
'Global Constant'=(bold:false,color:'#B8C4D4'),
'Global Variable'=(bold:false,color:'#D8DEE9'),
'Here Document'=(bold:false,color:'#A7C080'),
'Hex'=(bold:false,color:'#D9A66C'),
'Instance Variable'=(bold:false,color:'#D8DEE9'),
'Kernel methods'=(bold:false,color:'#88C0D0'),
'Keyword'=(bold:false,color:'#C9A0DC'),
'Member'=(bold:false,color:'#88C0D0'),
'Message'=(bold:false,color:'#88C0D0'),
'Module mixin methods'=(bold:false,color:'#88C0D0'),
'Normal Text'=(bold:false,color:'#D8DEE9'),
'Octal'=(bold:false,color:'#D9A66C'),
'Operator'=(bold:false,color:'#C9A0DC'),
'Pseudo variable'=(bold:false,color:'#B8C4D4'),
'RDoc Value'=(italic:false,bold:false,color:'#8B95A5'),
'Raw String'=(bold:false,color:'#A7C080'),
'Region Marker'=(bold:false,color:'#88C0D0',background:'#28313B'),
'Regular Expression'=(bold:false,color:'#A7C080'),
'String'=(bold:false,color:'#A7C080'),
'Substitution'=(bold:false,color:'#88C0D0'),
'Symbol'=(bold:false,color:'#A7C080');
STYLES
app.set_config("macro-editor-styles", editor_styles)
# Windows에서 가장 안정적으로 읽히는 기본 monospace font
app.set_config("macro-editor-font-family", "Consolas")
app.set_config("macro-editor-font-size", "14")
app.commit_config
# ------------------------------------------------------------
# Macro Development console theme
# ------------------------------------------------------------
# Re-running this macro should not leave multiple timers active.
if defined?($dark_console_timer) && $dark_console_timer
begin
$dark_console_timer.stop
rescue
end
end
$dark_console_last_text = nil
# The Macro Development window may open after this startup macro,
# so periodically look for its console.
$dark_console_timer = RBA::QTimer::new
$dark_console_timer.interval = 500
$dark_console_timer.timeout do
begin
macro_dialog = RBA::QApplication::topLevelWidgets.find do |widget|
widget.objectName == "MacroEditorDialog"
end
next unless macro_dialog
console_frame = macro_dialog.findChild("console_text_frame")
next unless console_frame
console = console_frame.findChildren.find do |object|
object.inherits("QPlainTextEdit") ||
object.inherits("QTextEdit")
end
next unless console
# Apply the widget appearance once per console instance.
if console.objectName != "DarkModeMacroConsole"
console.objectName = "DarkModeMacroConsole"
console.setStyleSheet(<<~QSS)
QPlainTextEdit, QTextEdit {
background-color: #0D1117;
color: #C9D1D9;
border: 1px solid #30363D;
border-radius: 2px;
padding: 5px;
font-family: Consolas;
font-size: 11pt;
selection-background-color: #264F78;
selection-color: #FFFFFF;
}
QSS
$dark_console_last_text = nil
end
# KLayout inserts pure-blue and pure-red character formatting.
# Recolor the document whenever new console text appears.
console_text = console.toPlainText
next if console_text == $dark_console_last_text
cursor = RBA::QTextCursor::new(console.document)
cursor.select(RBA::QTextCursor::Document)
text_format = RBA::QTextCharFormat::new
text_format.foreground = RBA::QBrush::new(
RBA::QColor::new("#C9D1D9")
)
cursor.mergeCharFormat(text_format)
$dark_console_last_text = console_text
rescue
# The dialog may be in the middle of opening or closing.
# The timer will safely retry on the next interval.
end
end
$dark_console_timer.start
end
import builtins
import pya
# ============================================================
# User-adjustable settings
# ============================================================
FONT_FAMILY = "Consolas"
EDITOR_FONT_SIZE = 14
CONSOLE_FONT_SIZE = 11
def color(value):
return pya.QColor(value)
# ============================================================
# 1. Application-wide dark palette
# ============================================================
def apply_dark_palette():
pya.QApplication.setStyle("Fusion")
palette = pya.QPalette()
roles = [
# Main surfaces
(pya.QPalette.Window, "#24282F"),
(pya.QPalette.Base, "#1B1F24"),
(pya.QPalette.AlternateBase, "#282D35"),
(pya.QPalette.Button, "#303640"),
# Main text
(pya.QPalette.WindowText, "#D4DAE3"),
(pya.QPalette.Text, "#D4DAE3"),
(pya.QPalette.ButtonText, "#E1E6ED"),
(pya.QPalette.BrightText, "#FFFFFF"),
# Borders and depth
(pya.QPalette.Light, "#4A525E"),
(pya.QPalette.Midlight, "#3B424C"),
(pya.QPalette.Mid, "#30363F"),
(pya.QPalette.Dark, "#171A1F"),
(pya.QPalette.Shadow, "#0C0E11"),
# Selection
(pya.QPalette.Highlight, "#316DCA"),
(pya.QPalette.HighlightedText, "#FFFFFF"),
# Tooltips and links
(pya.QPalette.ToolTipBase, "#343A43"),
(pya.QPalette.ToolTipText, "#F3F6F9"),
(pya.QPalette.Link, "#78B8FF"),
]
# These roles depend slightly on the bundled Qt version.
for optional_role, value in (
("LinkVisited", "#C7A0F4"),
("PlaceholderText", "#8B95A3"),
):
if hasattr(pya.QPalette, optional_role):
roles.append(
(
getattr(pya.QPalette, optional_role),
value,
)
)
for role, value in roles:
palette.setColor(
role,
color(value),
)
# Inactive windows
palette.setColor(
pya.QPalette.Inactive,
pya.QPalette.Highlight,
color("#3B4655"),
)
palette.setColor(
pya.QPalette.Inactive,
pya.QPalette.HighlightedText,
color("#D4DAE3"),
)
# Disabled text
for role in (
pya.QPalette.WindowText,
pya.QPalette.Text,
pya.QPalette.ButtonText,
):
palette.setColor(
pya.QPalette.Disabled,
role,
color("#6F7885"),
)
# Disabled surfaces
for role, value in (
(pya.QPalette.Button, "#282D34"),
(pya.QPalette.Base, "#20242A"),
(pya.QPalette.Highlight, "#343B45"),
(pya.QPalette.HighlightedText, "#7F8996"),
):
palette.setColor(
pya.QPalette.Disabled,
role,
color(value),
)
pya.QApplication.setPalette(palette)
# ============================================================
# 2. Macro Development editor theme
# ============================================================
def apply_macro_editor_theme():
app = pya.Application.instance()
editor_styles = r"""
basic:
'Alert'=(bold:true,color:'#F7768E',background:'#3A2028'),
'Attribute'=(bold:false,color:'#B8C4D4'),
'Base-N Integer'=(bold:false,color:'#F2B880'),
'Built-in'=(bold:false,color:'#7DCFFF'),
'Character'=(bold:false,color:'#9ECE6A'),
'Comment'=(italic:false,bold:false,color:'#8B95A5'),
'Control Flow'=(bold:false,color:'#C099FF'),
'Data Type'=(bold:false,color:'#7AA2F7'),
'Decimal/Value'=(bold:false,color:'#F2B880'),
'Error'=(bold:true,color:'#F7768E',background:'#3A2028'),
'Extension'=(bold:false,color:'#7DCFFF'),
'Floating Point'=(bold:false,color:'#F2B880'),
'Function'=(bold:false,color:'#7DCFFF'),
'Import'=(bold:false,color:'#C099FF'),
'Keyword'=(bold:false,color:'#C099FF'),
'Normal'=(bold:false,color:'#D8DEE9'),
'Operator'=(bold:false,color:'#89DDFF'),
'Others'=(bold:false,color:'#D8DEE9'),
'Preprocessor'=(bold:false,color:'#E0AF68'),
'Region Marker'=(bold:false,color:'#7DCFFF',background:'#28313B'),
'Special Character'=(bold:false,color:'#7DCFFF'),
'Special String'=(bold:false,color:'#9ECE6A'),
'String'=(bold:false,color:'#9ECE6A'),
'Variable'=(bold:false,color:'#D8DEE9'),
'Verbatim String'=(bold:false,color:'#9ECE6A');
ruby:
'Access Control'=(bold:false,color:'#C099FF'),
'Attribute Definition'=(bold:false,color:'#7DCFFF'),
'Bin'=(bold:false,color:'#F2B880'),
'Blockcomment'=(italic:false,bold:false,color:'#8B95A5'),
'Char'=(bold:false,color:'#9ECE6A'),
'Class Variable'=(bold:false,color:'#D8DEE9'),
'Command'=(bold:false,color:'#7DCFFF'),
'Comment'=(italic:false,bold:false,color:'#8B95A5'),
'Constant'=(bold:false,color:'#7AA2F7'),
'Constant Value'=(bold:false,color:'#F2B880'),
'Data'=(bold:false,color:'#9ECE6A'),
'Dec'=(bold:false,color:'#F2B880'),
'Default globals'=(bold:false,color:'#E0AF68'),
'Definition'=(bold:false,color:'#7DCFFF'),
'Delimiter'=(bold:false,color:'#89DDFF'),
'Error'=(bold:true,color:'#F7768E',background:'#3A2028'),
'Expression'=(bold:false,color:'#D8DEE9'),
'Float'=(bold:false,color:'#F2B880'),
'GDL input'=(bold:false,color:'#9ECE6A'),
'Global Constant'=(bold:false,color:'#7AA2F7'),
'Global Variable'=(bold:false,color:'#D8DEE9'),
'Here Document'=(bold:false,color:'#9ECE6A'),
'Hex'=(bold:false,color:'#F2B880'),
'Instance Variable'=(bold:false,color:'#D8DEE9'),
'Kernel methods'=(bold:false,color:'#7DCFFF'),
'Keyword'=(bold:false,color:'#C099FF'),
'Member'=(bold:false,color:'#7DCFFF'),
'Message'=(bold:false,color:'#7DCFFF'),
'Module mixin methods'=(bold:false,color:'#7DCFFF'),
'Normal Text'=(bold:false,color:'#D8DEE9'),
'Octal'=(bold:false,color:'#F2B880'),
'Operator'=(bold:false,color:'#89DDFF'),
'Pseudo variable'=(bold:false,color:'#E0AF68'),
'RDoc Value'=(italic:false,bold:false,color:'#8B95A5'),
'Raw String'=(bold:false,color:'#9ECE6A'),
'Region Marker'=(bold:false,color:'#7DCFFF',background:'#28313B'),
'Regular Expression'=(bold:false,color:'#9ECE6A'),
'String'=(bold:false,color:'#9ECE6A'),
'Substitution'=(bold:false,color:'#7DCFFF'),
'Symbol'=(bold:false,color:'#E0AF68');
python:
'Builtin Function'=(bold:false,color:'#7DCFFF'),
'ClassNames'=(italic:false,bold:false,color:'#7AA2F7'),
'Comment'=(italic:false,bold:false,color:'#8B95A5'),
'Complex'=(bold:false,color:'#F2B880'),
'Decorator'=(bold:false,color:'#E0AF68'),
'Definition Keyword'=(bold:false,color:'#C099FF'),
'Error'=(bold:true,color:'#F7768E',background:'#3A2028'),
'Exceptions'=(bold:false,color:'#F7768E'),
'Extensions'=(bold:false,color:'#7DCFFF'),
'F-String'=(bold:false,color:'#9ECE6A'),
'Float'=(bold:false,color:'#F2B880'),
'Flow Control Keyword'=(bold:false,color:'#C099FF'),
'Hex'=(bold:false,color:'#F2B880'),
'Import'=(bold:false,color:'#C099FF'),
'Int'=(bold:false,color:'#F2B880'),
'Normal Text'=(bold:false,color:'#D8DEE9'),
'Octal'=(bold:false,color:'#F2B880'),
'Operator'=(bold:false,color:'#89DDFF'),
'Operator Keyword'=(bold:false,color:'#C099FF'),
'Overloaders'=(bold:false,color:'#7DCFFF'),
'Raw F-String'=(bold:false,color:'#9ECE6A'),
'Raw String'=(bold:false,color:'#9ECE6A'),
'Special Variable'=(bold:false,color:'#E0AF68'),
'String'=(bold:false,color:'#9ECE6A'),
'String Char'=(bold:false,color:'#7DCFFF'),
'String Substitution'=(bold:false,color:'#7DCFFF');
"""
app.set_config(
"macro-editor-styles",
editor_styles,
)
app.set_config(
"macro-editor-font-family",
FONT_FAMILY,
)
app.set_config(
"macro-editor-font-size",
str(EDITOR_FONT_SIZE),
)
try:
app.commit_config()
except Exception:
# Some KLayout builds apply set_config immediately.
pass
# ============================================================
# 3. Macro Development console theme
# ============================================================
CONSOLE_STYLE = f"""
QPlainTextEdit, QTextEdit {{
background-color: #0D1117;
color: #C9D1D9;
border: 1px solid #30363D;
border-radius: 2px;
padding: 5px;
font-family: {FONT_FAMILY};
font-size: {CONSOLE_FONT_SIZE}pt;
selection-background-color: #264F78;
selection-color: #FFFFFF;
}}
"""
def install_console_theme():
# Stop the old timer when this macro is run repeatedly with F5.
old_timer = getattr(
builtins,
"_klayout_dark_console_timer",
None,
)
if old_timer is not None:
try:
old_timer.stop()
except Exception:
pass
state = {
"last_text": None,
}
def refresh_console():
try:
# Find the Macro Development window.
macro_dialog = next(
(
widget
for widget in pya.QApplication.topLevelWidgets()
if widget.objectName == "MacroEditorDialog"
),
None,
)
if macro_dialog is None:
state["last_text"] = None
return
# Find the console container.
frame = macro_dialog.findChild(
"console_text_frame"
)
if frame is None:
return
# Find the actual QPlainTextEdit/QTextEdit console.
console = next(
(
child
for child in frame.findChildren()
if child.inherits("QPlainTextEdit")
or child.inherits("QTextEdit")
),
None,
)
if console is None:
return
# Apply the console widget style once.
if console.objectName != "DarkModeMacroConsole":
console.objectName = "DarkModeMacroConsole"
console.setStyleSheet(CONSOLE_STYLE)
state["last_text"] = None
current_text = console.toPlainText()
if current_text == state["last_text"]:
return
# KLayout inserts some console output using direct
# per-character colors. Replace foreground only.
# Bold formatting is retained.
cursor = pya.QTextCursor(
console.document
)
cursor.select(
pya.QTextCursor.Document
)
text_format = pya.QTextCharFormat()
text_format.foreground = pya.QBrush(
color("#C9D1D9")
)
cursor.mergeCharFormat(
text_format
)
state["last_text"] = current_text
except Exception:
# The Macro Development window may currently be
# opening or closing. Retry on the next timer tick.
pass
timer = pya.QTimer()
timer.interval = 400
timer.timeout = refresh_console
refresh_console()
timer.start()
# Keep the timer and callback alive after macro execution.
builtins._klayout_dark_console_timer = timer
builtins._klayout_dark_console_refresh = refresh_console
builtins._klayout_dark_console_state = state
# ============================================================
# Apply everything
# ============================================================
apply_dark_palette()
apply_macro_editor_theme()
install_console_theme()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment