mirror of
https://github.com/vinceliuice/Colloid-gtk-theme.git
synced 2026-09-08 17:23:05 -07:00
Create gnome-theme-switcher
This commit is contained in:
Executable
+382
@@ -0,0 +1,382 @@
|
||||
#!/usr/bin/python3
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
import pathlib
|
||||
import logging
|
||||
|
||||
# 定义统一的应用 ID (需与 .desktop 文件名对应,如 org.gnome.GTK4ThemeSwitcher.desktop)
|
||||
APP_ID = "org.gnome.GTK4ThemeSwitcher"
|
||||
|
||||
# Ensure PyGObject and GTK4 are available
|
||||
try:
|
||||
import gi
|
||||
gi.require_version('Gtk', '4.0')
|
||||
from gi.repository import Gtk, Gio, Pango, GLib
|
||||
except ImportError as e:
|
||||
print("错误: 缺少必要的依赖。请确保已安装 PyGObject 和 GTK4。")
|
||||
print("在 Ubuntu/Debian 上运行: sudo apt install python3-gi libgtk-4-dev")
|
||||
print("在 Fedora 上运行: sudo dnf install python3-gobject gtk4")
|
||||
print("在 Arch Linux 上运行: sudo pacman -S python-gobject gtk4")
|
||||
sys.exit(1)
|
||||
|
||||
# Set up logging
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
||||
|
||||
class ThemeManager:
|
||||
"""负责搜索、解析以及处理 GTK4 主题文件软链接的核心类"""
|
||||
|
||||
SEARCH_PATHS = [
|
||||
os.path.expanduser("~/.themes"),
|
||||
os.path.expanduser("~/.local/share/themes"),
|
||||
"/usr/share/themes" # 包含系统全局主题供参考选择
|
||||
]
|
||||
TARGET_CONFIG_DIR = os.path.expanduser("~/.config/gtk-4.0")
|
||||
|
||||
@classmethod
|
||||
def get_available_themes(cls):
|
||||
"""扫描所有指定目录,搜寻存在 gtk-4.0 子目录的主题文件夹"""
|
||||
themes = []
|
||||
|
||||
for base_path in cls.SEARCH_PATHS:
|
||||
if not os.path.exists(base_path) or not os.path.isdir(base_path):
|
||||
continue
|
||||
|
||||
try:
|
||||
for entry in os.listdir(base_path):
|
||||
full_path = os.path.join(base_path, entry)
|
||||
if os.path.isdir(full_path):
|
||||
gtk4_path = os.path.join(full_path, "gtk-4.0")
|
||||
has_gtk4 = os.path.exists(gtk4_path) and os.path.isdir(gtk4_path)
|
||||
|
||||
themes.append({
|
||||
"name": entry,
|
||||
"path": full_path,
|
||||
"gtk4_path": gtk4_path if has_gtk4 else None,
|
||||
"is_local": base_path.startswith(os.path.expanduser("~")),
|
||||
"has_gtk4": has_gtk4
|
||||
})
|
||||
except Exception as err:
|
||||
logging.error(f"读取目录 {base_path} 失败: {err}")
|
||||
|
||||
# 去重并排序 (优先保留用户目录下的同名主题)
|
||||
unique_themes = {}
|
||||
for theme in themes:
|
||||
name = theme["name"]
|
||||
if name not in unique_themes or theme["is_local"]:
|
||||
unique_themes[name] = theme
|
||||
|
||||
sorted_themes = sorted(list(unique_themes.values()), key=lambda x: (not x["has_gtk4"], x["name"].lower()))
|
||||
return sorted_themes
|
||||
|
||||
@classmethod
|
||||
def apply_gtk4_theme(cls, theme_info):
|
||||
"""将选定主题的 gtk-4.0 内容软链接到 ~/.config/gtk-4.0/"""
|
||||
src_gtk4_dir = theme_info.get("gtk4_path")
|
||||
if not src_gtk4_dir or not os.path.exists(src_gtk4_dir):
|
||||
return False, f"主题 '{theme_info['name']}' 不包含 gtk-4.0 文件夹!"
|
||||
|
||||
target_dir = cls.TARGET_CONFIG_DIR
|
||||
|
||||
try:
|
||||
# 确保目标配置目录存在
|
||||
os.makedirs(target_dir, exist_ok=True)
|
||||
|
||||
# 清理 ~/.config/gtk-4.0 目录下的旧链接和文件
|
||||
for item in os.listdir(target_dir):
|
||||
item_path = os.path.join(target_dir, item)
|
||||
if os.path.islink(item_path) or os.path.isfile(item_path):
|
||||
os.unlink(item_path)
|
||||
elif os.path.isdir(item_path):
|
||||
shutil.rmtree(item_path)
|
||||
|
||||
# 遍历源主题 gtk-4.0 中的所有内容并建立软链接
|
||||
linked_files = []
|
||||
for item in os.listdir(src_gtk4_dir):
|
||||
src_item = os.path.join(src_gtk4_dir, item)
|
||||
dst_item = os.path.join(target_dir, item)
|
||||
|
||||
os.symlink(src_item, dst_item)
|
||||
linked_files.append(item)
|
||||
|
||||
# 设置 GTK 3.0 主题 (通过 GSettings: org.gnome.desktop.interface gtk-theme)
|
||||
gtk3_msg = ""
|
||||
try:
|
||||
settings = Gio.Settings.new("org.gnome.desktop.interface")
|
||||
settings.set_string("gtk-theme", theme_info["name"])
|
||||
gtk3_msg = f"\n同时已将 GTK 3 主题设置为 '{theme_info['name']}'。"
|
||||
except Exception as gset_err:
|
||||
logging.error(f"设置 GTK 3 主题失败: {gset_err}")
|
||||
gtk3_msg = f"\n(注意: 设置 GTK 3 主题失败: {gset_err})"
|
||||
|
||||
logging.info(f"成功为主题 {theme_info['name']} 创建软链接: {linked_files}")
|
||||
return True, f"已成功将主题 '{theme_info['name']}' 应用到 GTK 4!\n共链接了 {len(linked_files)} 个项。{gtk3_msg}"
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"应用主题失败: {e}")
|
||||
return False, f"发生错误: {str(e)}"
|
||||
|
||||
@classmethod
|
||||
def get_active_theme_name(cls):
|
||||
"""通过分析 ~/.config/gtk-4.0 中的软链接目标,判断当前生效的主题"""
|
||||
target_dir = cls.TARGET_CONFIG_DIR
|
||||
if not os.path.exists(target_dir):
|
||||
return None
|
||||
|
||||
try:
|
||||
for item in os.listdir(target_dir):
|
||||
item_path = os.path.join(target_dir, item)
|
||||
if os.path.islink(item_path):
|
||||
real_path = os.path.realpath(item_path)
|
||||
for base_path in cls.SEARCH_PATHS:
|
||||
if real_path.startswith(base_path):
|
||||
rel_path = os.path.relpath(real_path, base_path)
|
||||
parts = rel_path.split(os.sep)
|
||||
if len(parts) >= 1:
|
||||
return parts[0]
|
||||
except Exception as e:
|
||||
logging.error(f"检测当前生效主题失败: {e}")
|
||||
return None
|
||||
|
||||
|
||||
class ThemeRow(Gtk.ListBoxRow):
|
||||
"""用于在列表中展示主题条目的自定义 Widget"""
|
||||
|
||||
def __init__(self, theme_data, is_active=False):
|
||||
super().__init__()
|
||||
self.theme_data = theme_data
|
||||
self.is_active = is_active
|
||||
|
||||
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
|
||||
box.set_margin_top(10)
|
||||
box.set_margin_bottom(10)
|
||||
box.set_margin_start(16)
|
||||
box.set_margin_end(16)
|
||||
|
||||
# 图标展示
|
||||
icon_name = "preferences-desktop-theme-symbolic" if theme_data["has_gtk4"] else "dialog-warning-symbolic"
|
||||
icon = Gtk.Image.new_from_icon_name(icon_name)
|
||||
icon.set_pixel_size(24)
|
||||
box.append(icon)
|
||||
|
||||
# 文字信息
|
||||
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
|
||||
vbox.set_hexpand(True)
|
||||
|
||||
title_label = Gtk.Label(label=theme_data["name"])
|
||||
title_label.set_xalign(0)
|
||||
title_label.add_css_class("title-4")
|
||||
vbox.append(title_label)
|
||||
|
||||
sub_text = f"路径: {theme_data['path']}"
|
||||
if not theme_data["has_gtk4"]:
|
||||
sub_text += " (无 gtk-4.0 配置)"
|
||||
subtitle_label = Gtk.Label(label=sub_text)
|
||||
subtitle_label.set_xalign(0)
|
||||
subtitle_label.add_css_class("dim-label")
|
||||
subtitle_label.add_css_class("caption")
|
||||
vbox.append(subtitle_label)
|
||||
|
||||
box.append(vbox)
|
||||
|
||||
# 状态或标签
|
||||
if theme_data["has_gtk4"]:
|
||||
badge = Gtk.Label(label="GTK 4 支持")
|
||||
badge.add_css_class("accent")
|
||||
badge.add_css_class("caption")
|
||||
box.append(badge)
|
||||
else:
|
||||
badge = Gtk.Label(label="缺少 GTK4")
|
||||
badge.add_css_class("dim-label")
|
||||
badge.add_css_class("caption")
|
||||
box.append(badge)
|
||||
|
||||
# 当前生效勾选标志 (图标 + 文字)
|
||||
if is_active:
|
||||
active_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=4)
|
||||
check_icon = Gtk.Image.new_from_icon_name("emblem-ok-symbolic")
|
||||
check_icon.add_css_class("accent")
|
||||
active_label = Gtk.Label(label=" 已应用")
|
||||
active_label.add_css_class("accent")
|
||||
active_label.add_css_class("caption")
|
||||
|
||||
active_box.append(check_icon)
|
||||
active_box.append(active_label)
|
||||
box.append(active_box)
|
||||
|
||||
self.set_child(box)
|
||||
|
||||
|
||||
class MainWindow(Gtk.ApplicationWindow):
|
||||
"""主程序窗口组件"""
|
||||
|
||||
def __init__(self, app):
|
||||
super().__init__(application=app)
|
||||
self.set_title("GNOME GTK4 主题管理器")
|
||||
self.set_default_size(580, 620)
|
||||
|
||||
self.all_themes = []
|
||||
self.selected_theme = None
|
||||
self.active_theme_name = None
|
||||
|
||||
# 主垂直布局容器
|
||||
main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
|
||||
self.set_child(main_box)
|
||||
|
||||
# --- 顶栏 HeaderBar ---
|
||||
header = Gtk.HeaderBar()
|
||||
self.set_titlebar(header)
|
||||
|
||||
# 刷新按钮
|
||||
refresh_btn = Gtk.Button.new_from_icon_name("view-refresh-symbolic")
|
||||
refresh_btn.set_tooltip_text("重新检索主题目录")
|
||||
refresh_btn.connect("clicked", lambda x: self.load_themes())
|
||||
header.pack_start(refresh_btn)
|
||||
|
||||
# --- 搜索与筛选工具栏 ---
|
||||
toolbar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
|
||||
toolbar.set_margin_top(12)
|
||||
toolbar.set_margin_bottom(12)
|
||||
toolbar.set_margin_start(16)
|
||||
toolbar.set_margin_end(16)
|
||||
|
||||
self.search_entry = Gtk.SearchEntry()
|
||||
self.search_entry.set_placeholder_text("搜索本地主题...")
|
||||
self.search_entry.set_hexpand(True)
|
||||
self.search_entry.connect("search-changed", self.on_search_changed)
|
||||
toolbar.append(self.search_entry)
|
||||
|
||||
main_box.append(toolbar)
|
||||
|
||||
# --- 主题列表区域 ---
|
||||
scrolled = Gtk.ScrolledWindow()
|
||||
scrolled.set_vexpand(True)
|
||||
|
||||
self.list_box = Gtk.ListBox()
|
||||
self.list_box.set_selection_mode(Gtk.SelectionMode.SINGLE)
|
||||
self.list_box.add_css_class("rich-list")
|
||||
self.list_box.connect("row-selected", self.on_row_selected)
|
||||
scrolled.set_child(self.list_box)
|
||||
|
||||
main_box.append(scrolled)
|
||||
|
||||
# --- 底部动作栏 ---
|
||||
bottom_bar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
|
||||
bottom_bar.set_margin_top(12)
|
||||
bottom_bar.set_margin_bottom(12)
|
||||
bottom_bar.set_margin_start(16)
|
||||
bottom_bar.set_margin_end(16)
|
||||
|
||||
# 状态文字
|
||||
self.status_label = Gtk.Label(label="正在加载主题...")
|
||||
self.status_label.set_xalign(0)
|
||||
self.status_label.set_hexpand(True)
|
||||
self.status_label.set_ellipsize(3) # Pango.EllipsizeMode.END
|
||||
bottom_bar.append(self.status_label)
|
||||
|
||||
# 应用按钮
|
||||
self.apply_button = Gtk.Button(label="应用所选主题到 GTK 4")
|
||||
self.apply_button.add_css_class("suggested-action")
|
||||
self.apply_button.set_sensitive(False)
|
||||
self.apply_button.connect("clicked", self.on_apply_clicked)
|
||||
self.apply_button.connect("clicked", lambda x: self.load_themes())
|
||||
bottom_bar.append(self.apply_button)
|
||||
|
||||
main_box.append(bottom_bar)
|
||||
|
||||
# 初始加载主题数据
|
||||
self.load_themes()
|
||||
|
||||
def load_themes(self):
|
||||
"""从磁盘重新加载和渲染主题列表"""
|
||||
self.all_themes = ThemeManager.get_available_themes()
|
||||
self.active_theme_name = ThemeManager.get_active_theme_name()
|
||||
self.filter_and_render_themes()
|
||||
|
||||
count_gtk4 = sum(1 for t in self.all_themes if t["has_gtk4"])
|
||||
status_msg = f"已找到 {len(self.all_themes)} 个主题 (其中 {count_gtk4} 个包含 GTK 4 配置)"
|
||||
if self.active_theme_name:
|
||||
status_msg += f" | 当前生效: {self.active_theme_name}"
|
||||
self.status_label.set_text(status_msg)
|
||||
|
||||
def filter_and_render_themes(self):
|
||||
"""根据搜索关键词过滤并填充 ListBox"""
|
||||
# 清除原有 ListBox 节点
|
||||
while True:
|
||||
row = self.list_box.get_row_at_index(0)
|
||||
if row is None:
|
||||
break
|
||||
self.list_box.remove(row)
|
||||
|
||||
query = self.search_entry.get_text().strip().lower()
|
||||
|
||||
for theme in self.all_themes:
|
||||
if query and query not in theme["name"].lower():
|
||||
continue
|
||||
is_active = (theme["name"] == self.active_theme_name)
|
||||
row = ThemeRow(theme, is_active=is_active)
|
||||
self.list_box.append(row)
|
||||
|
||||
self.apply_button.set_sensitive(False)
|
||||
self.selected_theme = None
|
||||
|
||||
def on_search_changed(self, entry):
|
||||
"""搜索框输入变更事件处理"""
|
||||
self.filter_and_render_themes()
|
||||
|
||||
def on_row_selected(self, listbox, row):
|
||||
"""用户选中列表中某行主题的处理"""
|
||||
if row is None:
|
||||
self.selected_theme = None
|
||||
self.apply_button.set_sensitive(False)
|
||||
return
|
||||
|
||||
self.selected_theme = row.theme_data
|
||||
|
||||
# 仅当主题包含 gtk-4.0 文件夹时才启用应用按钮
|
||||
if self.selected_theme["has_gtk4"]:
|
||||
self.apply_button.set_sensitive(True)
|
||||
self.status_label.set_text(f"已选中: {self.selected_theme['name']}")
|
||||
else:
|
||||
self.apply_button.set_sensitive(False)
|
||||
self.status_label.set_text(f"警告: {self.selected_theme['name']} 缺少 gtk-4.0 目录")
|
||||
|
||||
def on_apply_clicked(self, button):
|
||||
"""点击应用按钮的响应逻辑"""
|
||||
if not self.selected_theme:
|
||||
return
|
||||
|
||||
success, message = ThemeManager.apply_gtk4_theme(self.selected_theme)
|
||||
|
||||
if success:
|
||||
self.active_theme_name = self.selected_theme["name"]
|
||||
self.status_label.set_text(f"当前生效主题: {self.active_theme_name}")
|
||||
self.filter_and_render_themes()
|
||||
|
||||
|
||||
class Application(Gtk.Application):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
application_id=APP_ID,
|
||||
flags=Gio.ApplicationFlags.FLAGS_NONE
|
||||
)
|
||||
|
||||
def do_activate(self):
|
||||
win = self.props.active_window
|
||||
if not win:
|
||||
win = MainWindow(self)
|
||||
win.present()
|
||||
|
||||
|
||||
def main():
|
||||
# 显式设置进程名 (prgname) 和应用名称
|
||||
# 解决 Python 脚本运行时默认进程名被识别为 python3 导致 Wayland / X11 WM_CLASS 不匹配图标的问题
|
||||
GLib.set_prgname(APP_ID)
|
||||
GLib.set_application_name("GNOME GTK4 主题管理器")
|
||||
|
||||
app = Application()
|
||||
return app.run(sys.argv)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user