pax_global_header00006660000000000000000000000064147435613450014526gustar00rootroot0000000000000052 comment=986439c5367eea41d24952e55684efc8899d8809 nwg-look-1.0.2/000077500000000000000000000000001474356134500132635ustar00rootroot00000000000000nwg-look-1.0.2/.github/000077500000000000000000000000001474356134500146235ustar00rootroot00000000000000nwg-look-1.0.2/.github/FUNDING.yml000066400000000000000000000000411474356134500164330ustar00rootroot00000000000000github: nwg-piotr liberapay: nwg nwg-look-1.0.2/.gitignore000066400000000000000000000004771474356134500152630ustar00rootroot00000000000000# Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, built with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Dependency directories (remove the comment below to include it) # vendor/ .idea/ bin/ nwg-look *.glade~ stuff/#main.glade# nwg-look-1.0.2/LICENSE000066400000000000000000000021011474356134500142620ustar00rootroot00000000000000MIT License Copyright (c) 2022-2025 Piotr Miller & Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. nwg-look-1.0.2/Makefile000066400000000000000000000023631474356134500147270ustar00rootroot00000000000000# For alternate install dir (e.g. "/usr/local") # specify PREFIX in make command: # sudo make PREFIX=/usr/local install # Defaults to "/usr" if not specified. PREFIX ?= /usr get: go get github.com/gotk3/gotk3 go get github.com/gotk3/gotk3/gdk go get "github.com/sirupsen/logrus" build: go build -v -o bin/nwg-look . install: mkdir -p $(DESTDIR)$(PREFIX)/share/nwg-look mkdir -p $(DESTDIR)$(PREFIX)/share/nwg-look/langs mkdir -p $(DESTDIR)$(PREFIX)/bin mkdir -p $(DESTDIR)$(PREFIX)/share/applications mkdir -p $(DESTDIR)$(PREFIX)/share/pixmaps mkdir -p $(DESTDIR)$(PREFIX)/share/doc/nwg-look mkdir -p $(DESTDIR)$(PREFIX)/share/licenses/nwg-look cp stuff/main.glade $(DESTDIR)$(PREFIX)/share/nwg-look/ cp langs/* $(DESTDIR)$(PREFIX)/share/nwg-look/langs/ cp stuff/nwg-look.desktop $(DESTDIR)$(PREFIX)/share/applications/ cp stuff/nwg-look.svg $(DESTDIR)$(PREFIX)/share/pixmaps/ cp bin/nwg-look $(DESTDIR)$(PREFIX)/bin cp README.md $(DESTDIR)$(PREFIX)/share/doc/nwg-look cp LICENSE $(DESTDIR)$(PREFIX)/share/licenses/nwg-look uninstall: rm -r $(DESTDIR)$(PREFIX)/share/nwg-look rm $(DESTDIR)$(PREFIX)/share/applications/nwg-look.desktop rm $(DESTDIR)$(PREFIX)/share/pixmaps/nwg-look.svg rm $(DESTDIR)$(PREFIX)/bin/nwg-look run: go run . nwg-look-1.0.2/README.md000066400000000000000000000074061474356134500145510ustar00rootroot00000000000000logo

nwg-look


This application is a part of the [nwg-shell](https://nwg-piotr.github.io/nwg-shell) project. Nwg-look is a GTK settings editor, designed to work properly in wlroots-based Wayland environment. The look and feel is strongly influenced by [LXAppearance](https://wiki.lxde.org/en/LXAppearance), but nwg-look is intended to free the user from a few inconveniences: - It works natively on Wayland. You no longer need Xwayland, nor strange env variables for it to run. - It applies gsettings directly, with no need to use [workarounds](https://github.com/swaywm/sway/wiki/GTK-3-settings-on-Wayland). You don't need to set gsettings in the sway config file. You don't need the `import-gsettings` script. ![nwg-look](https://raw.githubusercontent.com/nwg-piotr/nwg-shell-resources/master/images/nwg-look/nwg-look-0.1.3.png) ## Dependencies - go (build dependency) - gtk3 - [xcur2png](https://github.com/eworm-de/xcur2png) - gsettings Depending on your distro, you may also need to install [gotk3 dependencies](https://github.com/gotk3/gotk3#installation). ## Installation [![Packaging status](https://repology.org/badge/vertical-allrepos/nwg-look.svg)](https://repology.org/project/nwg-look/versions) If nwg-look has not yet been packaged for your Linux distribution: 1. Clone the repository, cd into it. 2. `make build` 3. `sudo make install` ## Usage ```text $ nwg-look -h Usage of nwg-look: -a Apply stored gsetting and quit -d turn on Debug messages -r Restore default values and quit -v display Version information -x eXport config files and quit ``` The `-a` flag has been added just in case. When you press the "Apply" button, in addition to applying the changes, a backup file is also created. You may apply gsetting again w/o running the GUI, by just `nwg-look -a`. No idea if it's going to be useful in real life. ;) ### Usage in sway The default way to apply GTK setting on [sway](https://github.com/swaywm/sway) Wayland compositor has been described in the [GTK 3 settings on Wayland](https://github.com/swaywm/sway/wiki/GTK-3-settings-on-Wayland) Wiki section. **You no longer need it**. Nwg-look loads and saves gsettings values directly, and does not care about the `~/.config/gtk-3.0/settings.ini` file. It only exports your settings to it, unless you use the `-n` flag. Therefore, if your sway config file contains either ```text set $gnome-schema org.gnome.desktop.interface exec_always { gsettings set $gnome-schema gtk-theme 'Your theme' gsettings set $gnome-schema icon-theme 'Your icon theme' gsettings set $gnome-schema cursor-theme 'Your cursor Theme' gsettings set $gnome-schema font-name 'Your font name' } ``` or if you use the `import-gsettings` script: ```text exec_always import-gsettings ``` to parse and apply the settings.ini file, **remove these lines**. ## Backward compatibility Some gsetting keys have no direct counterparts in the Gtk.Settings type. While exporting the settings.ini file, nwg-look uses the most similar values: | gsettings | Gtk.Settings | | --------- | ------------ | | `font-hinting` | `gtk-xft-hintstyle` | | `font-antialiasing` | `gtk-xft-antialias` | | `font-rgba-order` | `gtk-xft-rgba` | Some **Other** settings have been left just for LXAppearance compatibility, and possible use of your settings.ini file elsewhere: - Toolbar style - Toolbar icon size have been deprecated since GTK 3.10, and the values are ignored. - Show button images - Show menu images have been deprecated since GTK 3.10, and have no corresponding gsettings values. - Enable event sounds - Enable input feedback sounds don't seem to change anything in non-GNOME environment. nwg-look-1.0.2/go.mod000066400000000000000000000003111474356134500143640ustar00rootroot00000000000000module github.com/nwg-piotr/nwg-look go 1.23 require ( github.com/gotk3/gotk3 v0.6.5-0.20240618185848-ff349ae13f56 github.com/sirupsen/logrus v1.9.3 ) require golang.org/x/sys v0.29.0 // indirect nwg-look-1.0.2/go.sum000066400000000000000000000032021474356134500144130ustar00rootroot00000000000000github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gotk3/gotk3 v0.6.5-0.20240618185848-ff349ae13f56 h1:eR+xxC8qqKuPMTucZqaklBxLIT7/4L7dzhlwKMrDbj8= github.com/gotk3/gotk3 v0.6.5-0.20240618185848-ff349ae13f56/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= nwg-look-1.0.2/langs/000077500000000000000000000000001474356134500143675ustar00rootroot00000000000000nwg-look-1.0.2/langs/en_US.json000066400000000000000000000032761474356134500163030ustar00rootroot00000000000000{ "apply": "Apply", "button": "Button", "check-button": "Check button", "clear": "Clear", "close": "Close", "color-scheme": "Color scheme", "cursor-size": "Cursor size", "cursor-theme-preview": "Cursor theme preview", "default": "Default", "default-font": "Default font", "deprecated": "deprecated", "deprecated-since-gtk-310": "Deprecated since GTK 3.10", "enable-event-sounds": "Enable event sounds", "enable-input-feedback-sounds": "Enable input feedback sounds", "entry": "Entry", "files-to-export": "Files to export", "font": "Font", "font-antialiasing": "Font antialiasing", "font-hinting": "Font hinting", "font-rgba-order": "Font RGBA order", "font-settings": "Font settings", "full": "Full", "grayscale": "Greyscale", "icon-theme": "Icon theme", "icon-theme-preview": "Icon theme preview", "icons": "Icons", "ignored": "ignored", "large": "Large", "medium": "Medium", "mouse-cursor": "Mouse cursor", "none": "None", "other": "Other", "other-settings": "Other settings", "prefer-dark": "Prefer dark", "prefer-light": "Prefer light", "preferences": "Preferences", "program-settings": "Program settings", "radio-button": "Radio button", "show-button-images": "Show button images", "show-menu-images": "Show menu images", "slight": "Slight", "small": "Small", "sound-effects": "Sound effects", "text": "Text", "text-below-icons": "Text below icons", "text-next-to-icons": "Text next to icons", "text-scaling-factor": "Text scaling factor", "toolbar-icon-size": "Toolbar icon size", "toolbar-style": "Toolbar style", "ui-settings": "UI settings", "widgets": "Widgets", "widget-style-preview": "Widget style preview" } nwg-look-1.0.2/langs/ja_JP.json000066400000000000000000000042071474356134500162500ustar00rootroot00000000000000{ "apply": "適用", "button": "ボタン", "check-button": "チェックボタン", "clear": "設定をクリア", "close": "閉じる", "color-scheme": "カラースキーマ", "cursor-size": "カーソルサイズ", "cursor-theme-preview": "カーソルテーマ プレビュー", "default": "既定", "default-font": "既定のフォント", "deprecated": "非推奨", "deprecated-since-gtk-310": "GTK 3.10 以降で非推奨", "enable-event-sounds": "イベントサウンドを有効", "enable-input-feedback-sounds": "入力フィードバック音を有効", "entry": "エントリ", "files-to-export": "エクスポートするファイル", "font": "フォント", "font-antialiasing": "フォントアンチエイリアス", "font-hinting": "フォントヒンティング", "font-rgba-order": "フォント RGBA の順序", "font-settings": "フォント設定", "full": "全体", "grayscale": "グレースケール", "icon-theme": "アイコンテーマ", "icon-theme-preview": "アイコンテーマ プレビュー", "icons": "アイコン", "ignored": "無視される", "large": "大", "medium": "中", "mouse-cursor": "マウスカーソル", "none": "なし", "other": "その他", "other-settings": "その他の設定", "prefer-dark": "ダークモード優先", "prefer-light": "ライトモード優先", "preferences": "設定", "program-settings": "プログラム設定", "radio-button": "ラジオボタン", "show-button-images": "ボタン画像を表示", "show-menu-images": "メニュー画像を表示", "slight": "わずか", "small": "小", "sound-effects": "サウンドエフェクト", "text": "テキスト", "text-below-icons": "アイコンの下にテキスト", "text-next-to-icons": "アイコンの隣にテキスト", "text-scaling-factor": "テキストスケーリング係数", "toolbar-icon-size": "ツールバーのアイコンサイズ", "toolbar-style": "ツールバーのスタイル", "ui-settings": "UI 設定", "widgets": "ウィジェット", "widget-style-preview": "ウィジェットスタイル プレビュー" } nwg-look-1.0.2/langs/pl_PL.json000066400000000000000000000035671474356134500163030ustar00rootroot00000000000000{ "apply": "Zastosuj", "button": "Przycisk", "check-button": "Przycisk wyboru", "clear": "Wyczyść", "close": "Zamknij", "color-scheme": "Schemat kolorów", "cursor-size": "Rozmiar kursora", "cursor-theme-preview": "Podgląd motywu kursora", "default": "Domyślny", "default-font": "Domyślna czcionka", "deprecated": "przestarzałe", "deprecated-since-gtk-310": "Przestarzałe od GTK 3.10", "enable-event-sounds": "Włącz dźwięki zdarzeń", "enable-input-feedback-sounds": "Włącz dźwięki zwrotne wejścia", "entry": "Pozycja", "files-to-export": "Pliki do wyeksportowania", "font": "Czcionka", "font-antialiasing": "Wygładzanie czcionki", "font-hinting": "Hinting czcionki", "font-rgba-order": "Kolejność RGBA czcionki", "font-settings": "Ustawienia czcionki", "full": "Pełny", "grayscale": "Skala szarości", "icon-theme": "Motyw ikon", "icon-theme-preview": "Podgląd motywu ikon", "icons": "Ikony", "ignored": "ignorowane", "large": "Duży", "medium": "Średni", "mouse-cursor": "Kursor myszy", "none": "Brak", "other": "Inne", "other-settings": "Inne ustawienia", "prefer-dark": "Preferuj ciemny", "prefer-light": "Preferuj jasny", "preferences": "Ustawienia", "program-settings": "Ustawienia programu", "radio-button": "Przycisk opcji", "show-button-images": "Pokazuj ikony przycisku", "show-menu-images": "Pokazuj ikony menu", "slight": "Lekki", "small": "Mały", "sound-effects": "Efekty dźwiękowe", "text": "Tekst", "text-below-icons": "Tekst poniżej ikon", "text-next-to-icons": "Tekst obok ikon", "text-scaling-factor": "Współczynnik skalowania tekstu", "toolbar-icon-size": "Rozmiar ikony paska narzędzi", "toolbar-style": "Styl paska narzędzi", "ui-settings": "Ustawienia interfejsu użytkownika", "widgets": "Widżety", "widget-style-preview": "Podgląd stylu widżetów" } nwg-look-1.0.2/langs/ru_RU.json000066400000000000000000000051641474356134500163240ustar00rootroot00000000000000{ "apply": "Применить", "button": "Кнопка", "check-button": "Чекбокс", "clear": "Очистить", "close": "Закрыть", "color-scheme": "Цветовая схема", "cursor-size": "Размер курсора", "cursor-theme-preview": "Предпросмотр темы курсора", "default": "По умолчанию", "default-font": "Шрифт по умолчанию", "deprecated": "не поддерживается", "deprecated-since-gtk-310": "Не поддерживается с GTK 3.10", "enable-event-sounds": "Включить звуки событий", "enable-input-feedback-sounds": "Включить звуки обратной связи ввода", "entry": "Позиция", "files-to-export": "Файлы для экспорта", "font": "Шрифт", "font-antialiasing": "Сглаживание шрифта", "font-hinting": "Хинтинг шрифта", "font-rgba-order": "Порядок шрифта RGBA", "font-settings": "Настройки шрифта", "full": "Полный", "grayscale": "Градации серого", "icon-theme": "Тема значков", "icon-theme-preview": "Предпросмотр темы значков", "icons": "Значки", "ignored": "игнорируется", "large": "Большой", "medium": "Средний", "mouse-cursor": "Курсор мыши", "none": "Нет", "other": "Прочее", "other-settings": "Прочие настройки", "prefer-dark": "Предпочитать темную", "prefer-light": "Предпочитать светлую", "preferences": "Настройки", "program-settings": "Настройки программы", "radio-button": "Переключатель", "show-button-images": "Показывать изображения на кнопках", "show-menu-images": "Показывать изображения меню", "slight": "Легкий", "small": "Маленький", "sound-effects": "Звуковые эффекты", "text": "Текст", "text-below-icons": "Текст под значками", "text-next-to-icons": "Текст рядом со значками", "text-scaling-factor": "Коэффициент масштабирования текста", "toolbar-icon-size": "Размер значков панели инструментов", "toolbar-style": "Стиль панели инструментов", "ui-settings": "Настройки интерфейса", "widgets": "Виджеты", "widget-style-preview": "Предпросмотр стиля виджетов" } nwg-look-1.0.2/langs/zh_CN.json000066400000000000000000000034741474356134500162730ustar00rootroot00000000000000{ "apply": "应用", "button": "按钮", "check-button": "复选按钮", "clear": "清除设置", "close": "关闭", "color-scheme": "颜色方案", "cursor-size": "光标大小", "cursor-theme-preview": "光标样式预览", "default": "默认", "default-font": "默认字体", "deprecated": "已弃用", "deprecated-since-gtk-310": "自 GTK 3.10 起已弃用", "enable-event-sounds": "启用事件声音", "enable-input-feedback-sounds": "启用输入反馈声音", "entry": "输入框", "files-to-export": "要导出的文件", "font": "字体", "font-antialiasing": "字体抗锯齿", "font-hinting": "字体微调", "font-rgba-order": "字体 RGBA 顺序", "font-settings": "字体设置", "full": "完全", "grayscale": "灰度", "icon-theme": "图标主题", "icon-theme-preview": "图标主题预览", "icons": "图标", "ignored": "已忽略", "large": "大", "medium": "中等", "mouse-cursor": "鼠标光标", "none": "无", "other": "其他", "other-settings": "其他设置", "prefer-dark": "倾向暗色", "prefer-light": "倾向亮色", "preferences": "偏好设置", "program-settings": "程序设置", "radio-button": "单选按钮", "show-button-images": "显示按钮图片", "show-menu-images": "显示菜单图片", "slight": "轻微", "small": "小", "sound-effects": "声音效果", "text": "文本", "text-below-icons": "文本在图标下方", "text-next-to-icons": "文本在图标侧面", "text-scaling-factor": "文本缩放因子", "toolbar-icon-size": "工具栏图标大小", "toolbar-style": "工具栏样式", "ui-settings": "界面设置", "widgets": "组件", "widget-style-preview": "组件风格预览" } nwg-look-1.0.2/main.go000066400000000000000000000253651474356134500145510ustar00rootroot00000000000000/* GTK settings editor adapted to work in the sway / wlroots environment Project: https://github.com/nwg-piotr/nwg-look Author's email: nwg.piotr@gmail.com Copyright (c) 2022-2025 Piotr Miller & Contributors License: MIT */ package main import ( "flag" "fmt" "os" "path/filepath" "strings" log "github.com/sirupsen/logrus" "github.com/gotk3/gotk3/gdk" "github.com/gotk3/gotk3/gtk" ) const version = "1.0.2" var ( preferences programSettings originalGtkConfig []string // we will append not parsed settings.ini lines from here gtkConfig gtkConfigProperties gtkSettings *gtk.Settings gsettings gsettingsValues dataDirs []string cursorThemes map[string]string // theme name to path cursorThemeNames map[string]string // theme name to theme folder name viewport *gtk.Viewport scrolledWindow *gtk.ScrolledWindow listBox *gtk.ListBox menuBar *gtk.MenuBar themeSettingsSelector *gtk.Grid grid *gtk.Grid preview *gtk.Frame cursorSizeSelector *gtk.Box rowToFocus *gtk.ListBoxRow voc map[string]string gtkThemePaths map[string]string // theme name to path ) type programSettings struct { ExportSettingsIni bool `json:"export-settings-ini"` ExportGtkRc20 bool `json:"export-gtkrc-20"` ExportIndexTheme bool `json:"export-index-theme"` ExportXsettingsd bool `json:"export-xsettingsd"` ExportGtk4Symlinks bool `json:"export-gtk4-symlinks"` } func programSettingsNewWithDefaults() programSettings { p := programSettings{} p.ExportSettingsIni = true p.ExportGtkRc20 = true p.ExportIndexTheme = true p.ExportXsettingsd = true p.ExportGtk4Symlinks = true return p } type gtkConfigProperties struct { themeName string iconThemeName string fontName string cursorThemeName string cursorThemeSize int toolbarStyle string toolbarIconSize string buttonImages bool menuImages bool enableEventSounds bool enableInputFeedbackSounds bool xftAntialias int fontAntialiasing string xftDpi int xftHinting int xftHintstyle string xftRgba string applicationPreferDarkTheme bool } func gtkConfigPropertiesNewWithDefaults() gtkConfigProperties { s := gtkConfigProperties{} // 'ignored' and 'deprecated' values left for lxappearance compatibility s.themeName = "Adwaita" s.iconThemeName = "Adwaita" s.fontName = "Sans 10" s.cursorThemeName = "" s.cursorThemeSize = 0 s.toolbarStyle = "GTK_TOOLBAR_ICONS" // ignored s.toolbarIconSize = "GTK_ICON_SIZE_LARGE_TOOLBAR" // ignored s.buttonImages = false // deprecated s.menuImages = false // deprecated s.enableEventSounds = true s.enableInputFeedbackSounds = true s.xftAntialias = -1 s.applicationPreferDarkTheme = false val, err := getGsettingsValue("org.gnome.desktop.interface", "font-antialiasing") if err == nil { s.fontAntialiasing = val } else { log.Warn(err) } s.xftHinting = -1 s.xftHintstyle = "hintmedium" s.xftRgba = "none" return s } type gsettingsValues struct { // org.gnome.desktop.interface gtkTheme string iconTheme string fontName string cursorTheme string cursorSize int toolbarStyle string toolbarIconsSize string fontHinting string fontAntialiasing string fontRgbaOrder string textScalingFactor float64 colorScheme string // org.gnome.desktop.sound eventSounds bool inputFeedbackSounds bool } func gsettingsNewWithDefaults() gsettingsValues { g := gsettingsValues{} g.gtkTheme = "Adwaita" g.iconTheme = "Adwaita" g.fontName = "Sans 10" g.cursorTheme = "Adwaita" g.cursorSize = 24 g.toolbarStyle = "both-horiz" g.toolbarIconsSize = "large" g.fontHinting = "medium" g.fontAntialiasing = "grayscale" g.fontRgbaOrder = "rgb" g.textScalingFactor = 1.0 g.eventSounds = true g.inputFeedbackSounds = false g.colorScheme = "default" return g } func displayThemes() { destroyContent() rowToFocus = nil listBox = setUpThemeListBox(gsettings.gtkTheme) viewport.Add(listBox) menuBar.Deactivate() if rowToFocus != nil { rowToFocus.GrabFocus() } preview = setUpWidgetsPreview() grid.Attach(preview, 1, 1, 1, 1) themeSettingsSelector = setUpThemeSettingsForm(gsettings.fontName) themeSettingsSelector.SetProperty("vexpand", true) themeSettingsSelector.SetProperty("valign", gtk.ALIGN_START) grid.Attach(themeSettingsSelector, 1, 2, 1, 1) viewport.ShowAll() grid.ShowAll() } func displayIconThemes() { destroyContent() rowToFocus = nil listBox = setUpIconThemeListBox(gsettings.iconTheme) viewport.Add(listBox) menuBar.Deactivate() if rowToFocus != nil { rowToFocus.GrabFocus() } preview = setUpIconsPreview() grid.Attach(preview, 1, 1, 1, 1) viewport.ShowAll() grid.ShowAll() } func displayCursorThemes() { destroyContent() rowToFocus = nil listBox = setUpCursorThemeListBox(gsettings.cursorTheme) viewport.Add(listBox) menuBar.Deactivate() if rowToFocus != nil { rowToFocus.GrabFocus() } preview = setUpCursorsPreview(cursorThemes[gsettings.cursorTheme]) grid.Attach(preview, 1, 1, 1, 1) cursorSizeSelector = setUpCursorSizeSelector() grid.Attach(cursorSizeSelector, 1, 2, 1, 1) viewport.ShowAll() grid.ShowAll() } func displayFontSettingsForm() { destroyContent() preview = setUpFontSettingsForm() grid.Attach(preview, 0, 1, 1, 1) menuBar.Deactivate() grid.ShowAll() scrolledWindow.Hide() } func displayOtherSettingsForm() { destroyContent() preview = setUpOtherSettingsForm() grid.Attach(preview, 0, 1, 1, 1) menuBar.Deactivate() grid.ShowAll() scrolledWindow.Hide() } func displayProgramSettingsForm() { destroyContent() preview = setUpProgramSettingsForm() grid.Attach(preview, 0, 1, 1, 1) menuBar.Deactivate() grid.ShowAll() scrolledWindow.Hide() } func destroyContent() { if listBox != nil { listBox.Destroy() } if preview != nil { preview.Destroy() } if themeSettingsSelector != nil { themeSettingsSelector.Destroy() } if cursorSizeSelector != nil { cursorSizeSelector.Destroy() } } func main() { var debug = flag.Bool("d", false, "turn on Debug messages") var displayVersion = flag.Bool("v", false, "display Version information") var applyGs = flag.Bool("a", false, "Apply stored gsetting and quit") var restoreDefaults = flag.Bool("r", false, "Restore default values and quit") var exportConfigs = flag.Bool("x", false, "eXport config files and quit") flag.Parse() if *displayVersion { fmt.Printf("nwg-look version %s\n", version) os.Exit(0) } if *debug { log.SetLevel(log.DebugLevel) } loadPreferences() lang := detectLang() log.Infof("lang: %s", lang) voc = loadVocabulary(lang) // initialize gsettings type with default gtk values gsettings = gsettingsNewWithDefaults() // initialize gtkConfigProperties type with default gtk.Settings values gtkConfig = gtkConfigPropertiesNewWithDefaults() if *restoreDefaults { fmt.Print("Restore default gtk settings? y/N ") var input string fmt.Scanln(&input) fmt.Println(input) if strings.ToUpper(input) == "Y" { applyGsettings() saveGsettingsBackup() if preferences.ExportSettingsIni { saveGtkIni() } if preferences.ExportGtkRc20 { saveGtkRc20() } if preferences.ExportIndexTheme { saveIndexTheme() } if preferences.ExportXsettingsd { saveXsettingsd() } if preferences.ExportGtk4Symlinks { linkGtk4Stuff() } else { clearGtk4Symlinks() } } os.Exit(0) } readGsettings() if *applyGs || *exportConfigs { if *applyGs { applyGsettingsFromFile() } if *exportConfigs { if preferences.ExportSettingsIni { saveGtkIni() } if preferences.ExportGtkRc20 { saveGtkRc20() } if preferences.ExportIndexTheme { saveIndexTheme() } if preferences.ExportXsettingsd { saveXsettingsd() } if preferences.ExportGtk4Symlinks { _, gtkThemePaths = getThemeNames() linkGtk4Stuff() } } os.Exit(0) } dataDirs = getDataDirs() cursorThemes, cursorThemeNames = getCursorThemes() gtk.Init(nil) // update gtkConfig from gtk-3.0/settings.ini if preferences.ExportSettingsIni { loadGtkConfig() } gtkSettings, _ = gtk.SettingsGetDefault() gladeFile := "" for _, d := range dataDirs { gladeFile = filepath.Join(d, "/nwg-look/main.glade") if pathExists(gladeFile) { break } } builder, _ := gtk.BuilderNewFromFile(gladeFile) win, _ := getWindow(builder, "window") win.Connect("destroy", func() { gtk.MainQuit() }) win.Connect("key-release-event", func(window *gtk.Window, event *gdk.Event) bool { key := &gdk.EventKey{Event: event} if key.KeyVal() == gdk.KEY_Escape { gtk.MainQuit() return true } return false }) viewport, _ = getViewPort(builder, "viewport-list") scrolledWindow, _ = getScrolledWindow(builder, "scrolled-window") grid, _ = getGrid(builder, "grid") menuBar, _ = getMenuBar(builder, "menubar") item1, _ := getMenuItem(builder, "item-widgets") item1.SetLabel(voc["widgets"]) item1.Connect("button-release-event", displayThemes) item2, _ := getMenuItem(builder, "item-icons") item2.SetLabel(voc["icon-theme"]) item2.Connect("button-release-event", displayIconThemes) item3, _ := getMenuItem(builder, "item-cursors") item3.SetLabel(voc["mouse-cursor"]) item3.Connect("button-release-event", displayCursorThemes) item4, _ := getMenuItem(builder, "item-font") item4.SetLabel(voc["font"]) item4.Connect("button-release-event", displayFontSettingsForm) item5, _ := getMenuItem(builder, "item-other") item5.SetLabel(voc["other"]) item5.Connect("button-release-event", displayOtherSettingsForm) item6, _ := getMenuItem(builder, "item-preferences") item6.SetLabel(voc["preferences"]) item6.Connect("button-release-event", displayProgramSettingsForm) btnClose, _ := getButton(builder, "btn-close") btnClose.SetLabel(voc["close"]) btnClose.Connect("clicked", func() { gtk.MainQuit() }) btnApply, _ := getButton(builder, "btn-apply") btnApply.SetLabel(voc["apply"]) btnApply.Connect("clicked", func() { applyGsettings() saveGsettingsBackup() if preferences.ExportSettingsIni { saveGtkIni() } if preferences.ExportGtkRc20 { saveGtkRc20() } if preferences.ExportIndexTheme { saveIndexTheme() } if preferences.ExportXsettingsd { saveXsettingsd() } if preferences.ExportGtk4Symlinks { linkGtk4Stuff() } savePreferences() }) verLabel, _ := getLabel(builder, "version-label") verLabel.SetMarkup(fmt.Sprintf("nwg-look v%s GitHub", version)) displayThemes() win.ShowAll() gtk.Main() } nwg-look-1.0.2/stuff/000077500000000000000000000000001474356134500144125ustar00rootroot00000000000000nwg-look-1.0.2/stuff/main.glade000066400000000000000000000221521474356134500163360ustar00rootroot00000000000000 False True True True False 6 6 6 6 True False 6 6 False True False Widgets True False Icon Theme True False Mouse Cursor True False Font True False Other True False Preferences 0 0 2 300 True True 6 6 6 True True never in True True False 0 1 2 True False 6 6 12 True 6 True False True False nwg-look 3 False True 0 True False start 6 nwg-look v False True 1 False True 0 True False 12 True Apply True True True 6 6 6 False True end 0 Close True True True 6 False True end 2 False True end 1 0 3 2 nwg-look-1.0.2/stuff/nwg-look.desktop000066400000000000000000000007761474356134500175540ustar00rootroot00000000000000[Desktop Entry] Type=Application Name=GTK Settings Name[pl]=Ustawienia GTK GenericName=Adjust Look and Feel GenericName[pl]=Dopasuj ustawienia wyglądu Comment=Customizes GTK3 look and feel settings Comment[pl]=Dostosowuje ustawienia środowiska graficznego GTK3 Keywords=windows;preferences;settings;theme;style;appearance;look; Keywords[pl]=okna;preferencje;ustawienia;motyw;styl;wygląd; Icon=nwg-look Exec=nwg-look NotShowIn=GNOME;KDE;XFCE;MATE; StartupNotify=true Categories=GTK;Settings;DesktopSettings; nwg-look-1.0.2/stuff/nwg-look.svg000066400000000000000000000247461474356134500167050ustar00rootroot00000000000000 image/svg+xml nwg-look-1.0.2/tools.go000066400000000000000000001123041474356134500147530ustar00rootroot00000000000000// tools package main import ( "bufio" "encoding/json" "fmt" "io" "os" "os/exec" "path/filepath" "sort" "strconv" "strings" "github.com/gotk3/gotk3/gtk" log "github.com/sirupsen/logrus" ) func configHome() string { cHome := os.Getenv("XDG_CONFIG_HOME") if cHome != "" { return cHome } return filepath.Join(os.Getenv("HOME"), ".config/") } func loadPreferences() { cH := configHome() preferencesFile := filepath.Join(cH, "/nwg-look/config") if !pathExists(preferencesFile) { log.Infof("%s file not found, creating", preferencesFile) makeDir(filepath.Join(cH, "/nwg-look/")) preferences = programSettingsNewWithDefaults() savePreferences() } else { file, err := os.Open(preferencesFile) defer file.Close() if err != nil { fmt.Println(err.Error()) } log.Info(">>> Loading preferences") jsonParser := json.NewDecoder(file) jsonParser.Decode(&preferences) jsonData, err := json.Marshal(preferences) if err == nil { log.Debugf("Loaded preferences: %s", string(jsonData)) } } } func savePreferences() { preferencesFile := filepath.Join(configHome(), "/nwg-look/config") jsonData, err := json.MarshalIndent(preferences, "", " ") if err != nil { log.Warn(err) return } err = os.WriteFile(preferencesFile, jsonData, 0644) if err == nil { log.Debugf("Saved config: %s", string(jsonData)) } } func loadGtkConfig() { // parse gtk settings file originalGtkConfig = []string{} configFile := filepath.Join(configHome(), "gtk-3.0/settings.ini") if pathExists(configFile) { lines, err := loadTextFile(configFile) if err == nil { log.Infof(">>> Parsing original %s", configFile) } else { log.Warnf("Couldn't load %s", configFile) } for _, line := range lines { // In case users settings.ini had some lines we didn't expect, // we'll append them back from here. if !strings.HasPrefix(line, "[") { originalGtkConfig = append(originalGtkConfig, line) } if !strings.HasPrefix(line, "[") && !strings.HasPrefix(line, "#") && strings.Contains(line, "=") { parts := strings.Split(line, "=") key := strings.TrimSpace(parts[0]) value := strings.TrimSpace(parts[1]) switch key { case "gtk-theme-name": gtkConfig.themeName = value case "gtk-icon-theme-name": gtkConfig.iconThemeName = value case "gtk-font-name": gtkConfig.fontName = value case "gtk-cursor-theme-name": gtkConfig.cursorThemeName = value case "gtk-cursor-theme-size": i := intValue(value) if i != -1 { gtkConfig.cursorThemeSize = i } else { gtkConfig.cursorThemeSize = 0 } case "gtk-toolbar-style": gtkConfig.toolbarStyle = value case "gtk-toolbar-icon-size": gtkConfig.toolbarIconSize = value case "gtk-button-images": gtkConfig.buttonImages = value == "1" case "gtk-menu-images": gtkConfig.menuImages = value == "1" case "gtk-enable-event-sounds": gtkConfig.enableEventSounds = value == "1" case "gtk-enable-input-feedback-sounds": gtkConfig.enableInputFeedbackSounds = value == "1" case "gtk-xft-antialias": gtkConfig.xftAntialias = intValue(value) case "gtk-xft-hinting": gtkConfig.xftHinting = intValue(value) case "gtk-xft-hintstyle": gtkConfig.xftHintstyle = value case "gtk-xft-rgba": gtkConfig.xftRgba = value case "gtk-application-prefer-dark-theme": gtkConfig.applicationPreferDarkTheme = value == "1" default: log.Warnf("Unsupported config key: %s", key) } } } } else { log.Warnf("Could'n find %s", configFile) } log.Debugf("gtk-theme-name: %s", gtkConfig.themeName) log.Debugf("gtk-icon-theme-name: %s", gtkConfig.iconThemeName) log.Debugf("gtk-font-name: %s", gtkConfig.fontName) log.Debugf("gtk-cursor-theme-name: %s", gtkConfig.cursorThemeName) log.Debugf("gtk-cursor-theme-size: %v", gtkConfig.cursorThemeSize) log.Debugf("gtk-toolbar-style: %s", gtkConfig.toolbarStyle) log.Debugf("gtk-toolbar-icon-size: %s", gtkConfig.toolbarIconSize) log.Debugf("gtk-button-images: %v", gtkConfig.buttonImages) log.Debugf("gtk-menu-images: %v", gtkConfig.menuImages) log.Debugf("gtk-enable-event-sounds: %v", gtkConfig.enableEventSounds) log.Debugf("gtk-enable-input-feedback-sounds: %v", gtkConfig.enableInputFeedbackSounds) log.Debugf("gtk-xft-antialias: %v", gtkConfig.xftAntialias) log.Debugf("gtk-xft-hinting: %v", gtkConfig.xftHinting) log.Debugf("gtk-xft-hintstyle: %v", gtkConfig.xftHintstyle) log.Debugf("gtk-xft-rgba: %v", gtkConfig.xftRgba) log.Debugf("gtk-application-prefer-dark-theme: %v", gtkConfig.applicationPreferDarkTheme) } func intValue(s string) int { i, err := strconv.Atoi(s) if err == nil { return i } // -1 is default return -1 } func readGsettings() { log.Info(">>> Reading gsettings") val, err := getGsettingsValue("org.gnome.desktop.interface", "gtk-theme") if err == nil { gsettings.gtkTheme = val log.Infof("gtk-theme: %s", gsettings.gtkTheme) } else { log.Warnf("Couldn't read gtk-theme, leaving default %s", gsettings.gtkTheme) } val, err = getGsettingsValue("org.gnome.desktop.interface", "icon-theme") if err == nil { gsettings.iconTheme = val log.Infof("icon-theme: %s", gsettings.iconTheme) } else { log.Warnf("Couldn't read icon-theme, leaving default %s", gsettings.iconTheme) } val, err = getGsettingsValue("org.gnome.desktop.interface", "font-name") if err == nil { gsettings.fontName = val log.Infof("font-name: %s", gsettings.fontName) } else { log.Warnf("Couldn't read font-name, leaving default %s", gsettings.fontName) } val, err = getGsettingsValue("org.gnome.desktop.interface", "cursor-theme") if err == nil { gsettings.cursorTheme = val log.Infof("cursor-theme: %s", gsettings.cursorTheme) } else { gsettings.cursorTheme = "" log.Warnf("Couldn't read cursor-theme, leaving default %s", gsettings.cursorTheme) } val, err = getGsettingsValue("org.gnome.desktop.interface", "cursor-size") if err == nil { v, e := strconv.Atoi(val) if e == nil { gsettings.cursorSize = v log.Infof("cursor-size: %v", gsettings.cursorSize) } } else { log.Warnf("Couldn't read cursorSize, leaving default %s", gsettings.cursorSize) } val, err = getGsettingsValue("org.gnome.desktop.interface", "toolbar-style") if err == nil { gsettings.toolbarStyle = val log.Infof("toolbar-style: %s", gsettings.toolbarStyle) } else { log.Warnf("Couldn't read toolbar-style, leaving default %s", gsettings.toolbarStyle) } val, err = getGsettingsValue("org.gnome.desktop.interface", "toolbar-icons-size") if err == nil { gsettings.toolbarIconsSize = val log.Infof("toolbar-icons-size: %s", gsettings.toolbarIconsSize) } else { log.Warnf("Couldn't read toolbar-icons-size, leaving default %s", gsettings.toolbarIconsSize) } val, err = getGsettingsValue("org.gnome.desktop.interface", "font-hinting") if err == nil { gsettings.fontHinting = val log.Infof("font-hinting: %s", gsettings.fontHinting) } else { log.Warnf("Couldn't read font-hinting, leaving default %s", gsettings.fontHinting) } val, err = getGsettingsValue("org.gnome.desktop.interface", "font-antialiasing") if err == nil { gsettings.fontAntialiasing = val log.Infof("font-antialiasing: %s", gsettings.fontAntialiasing) } else { log.Warnf("Couldn't read font-antialiasing, leaving default %s", gsettings.fontAntialiasing) } val, err = getGsettingsValue("org.gnome.desktop.interface", "font-rgba-order") if err == nil { gsettings.fontRgbaOrder = val log.Infof("font-rgba-order: %s", gsettings.fontRgbaOrder) } else { log.Warnf("Couldn't read font-rgba-order, leaving default %s", gsettings.fontRgbaOrder) } val, err = getGsettingsValue("org.gnome.desktop.interface", "text-scaling-factor") if err == nil { v, e := strconv.ParseFloat(val, 32) if e == nil { gsettings.textScalingFactor = v log.Infof("text-scaling-factor: %v", gsettings.textScalingFactor) } } else { log.Warnf("Couldn't read textScalingFactor, leaving default %s", gsettings.textScalingFactor) } val, err = getGsettingsValue("org.gnome.desktop.interface", "color-scheme") if err == nil { gsettings.colorScheme = val log.Infof("color-scheme: %s", gsettings.colorScheme) } else { log.Warnf("Couldn't read color-scheme, leaving default %s", gsettings.colorScheme) } val, err = getGsettingsValue("org.gnome.desktop.sound", "event-sounds") if err == nil { if val == "true" { gsettings.eventSounds = true } else { gsettings.eventSounds = false } log.Infof("event-sounds: %v", gsettings.eventSounds) } else { log.Warnf("Couldn't read event-sounds, leaving default %v", gsettings.eventSounds) } val, err = getGsettingsValue("org.gnome.desktop.sound", "input-feedback-sounds") if err == nil { if val == "true" { gsettings.inputFeedbackSounds = true } else { gsettings.inputFeedbackSounds = false } log.Infof("input-feedback-sounds: %v", gsettings.inputFeedbackSounds) } else { log.Warnf("Couldn't read input-feedback-sounds, leaving default %v", gsettings.inputFeedbackSounds) } } func saveGsettingsBackup() { gsettingsFile := filepath.Join(dataHome(), "nwg-look/") makeDir(gsettingsFile) log.Infof(">>> Backing up gsettings to %s", gsettingsFile) lines := []string{"# Generated by nwg-look, do not edit this file."} for _, key := range []string{ "gtk-theme", "icon-theme", "font-name", "cursor-theme", "cursor-size", "toolbar-style", "toolbar-icons-size", "font-hinting", "font-antialiasing", "font-rgba-order", "text-scaling-factor", "color-scheme"} { val, err := getGsettingsValue("org.gnome.desktop.interface", key) if err == nil { line := fmt.Sprintf("%s=%s", key, val) lines = append(lines, line) } else { log.Warnf("Couldn't get gsettings key: $s", key) } } for _, key := range []string{"event-sounds", "input-feedback-sounds"} { val, err := getGsettingsValue("org.gnome.desktop.sound", key) if err == nil { line := fmt.Sprintf("%s=%s", key, val) lines = append(lines, line) } else { log.Warnf("Couldn't get gsettings key: $s", key) } } saveTextFile(lines, filepath.Join(dataHome(), "nwg-look/gsettings")) } func getGsettingsValue(schema, key string) (string, error) { cmd := exec.Command("gsettings", "get", schema, key) out, err := cmd.CombinedOutput() if err == nil { s := fmt.Sprintf("%s", strings.TrimSpace(string(out))) if strings.HasPrefix(s, "'") { return s[1 : len(s)-1], nil } return s, nil } return "", err } func applyGsettings() { gnomeSchema := "org.gnome.desktop.interface" log.Info(">>> Applying gsettings") log.Infof(">> %s", gnomeSchema) cmd := exec.Command("gsettings", "set", gnomeSchema, "gtk-theme", gsettings.gtkTheme) err := cmd.Run() if err != nil { log.Warnf("gtk-theme: %s", err) } else { log.Infof("gtk-theme: %s OK", gsettings.gtkTheme) } cmd = exec.Command("gsettings", "set", gnomeSchema, "icon-theme", gsettings.iconTheme) err = cmd.Run() if err != nil { log.Warnf("icon-theme: %s", err) } else { log.Infof("icon-theme: %s OK", gsettings.iconTheme) } cmd = exec.Command("gsettings", "set", gnomeSchema, "cursor-theme", gsettings.cursorTheme) err = cmd.Run() if err != nil { log.Warnf("cursor-theme: %s", err) } else { log.Infof("cursor-theme: %s OK", gsettings.cursorTheme) } var val string val = strconv.Itoa(gsettings.cursorSize) cmd = exec.Command("gsettings", "set", gnomeSchema, "cursor-size", val) err = cmd.Run() if err != nil { log.Warnf("cursor-size: %s", err) } else { log.Infof("cursor-size: %s OK", val) } cmd = exec.Command("gsettings", "set", gnomeSchema, "font-name", gsettings.fontName) err = cmd.Run() if err != nil { log.Warnf("font-name: %s %s", gsettings.fontName, err) } else { log.Infof("font-name: %s OK", gsettings.fontName) } cmd = exec.Command("gsettings", "set", gnomeSchema, "font-hinting", gsettings.fontHinting) err = cmd.Run() if err != nil { log.Warnf("font-hinting: %s %s", gsettings.fontHinting, err) } else { log.Infof("font-hinting: %s OK", gsettings.fontHinting) } cmd = exec.Command("gsettings", "set", gnomeSchema, "font-antialiasing", gsettings.fontAntialiasing) err = cmd.Run() if err != nil { log.Warnf("font-antialiasing: %s %s", gsettings.fontAntialiasing, err) } else { log.Infof("font-antialiasing: %s OK", gsettings.fontAntialiasing) } cmd = exec.Command("gsettings", "set", gnomeSchema, "font-rgba-order", gsettings.fontRgbaOrder) err = cmd.Run() if err != nil { log.Warnf("font-rgba-order: %s %s", gsettings.fontRgbaOrder, err) } else { log.Infof("font-rgba-order: %s OK", gsettings.fontRgbaOrder) } cmd = exec.Command("gsettings", "set", gnomeSchema, "text-scaling-factor", fmt.Sprintf("%f", gsettings.textScalingFactor)) err = cmd.Run() if err != nil { log.Warnf("text-scaling-factor: %s %s", gsettings.textScalingFactor, err) } else { log.Infof("text-scaling-factor: %v OK", gsettings.textScalingFactor) } cmd = exec.Command("gsettings", "set", gnomeSchema, "toolbar-style", gsettings.toolbarStyle) err = cmd.Run() if err != nil { log.Warnf("toolbar-style: %s %s", gsettings.toolbarStyle, err) } else { log.Infof("toolbar-style: %s OK", gsettings.toolbarStyle) } cmd = exec.Command("gsettings", "set", gnomeSchema, "toolbar-icons-size", gsettings.toolbarIconsSize) err = cmd.Run() if err != nil { log.Warnf("toolbar-icons-size: %s %s", gsettings.toolbarIconsSize, err) } else { log.Infof("toolbar-icons-size: %s OK", gsettings.toolbarIconsSize) } cmd = exec.Command("gsettings", "set", gnomeSchema, "color-scheme", gsettings.colorScheme) err = cmd.Run() if err != nil { log.Warnf("color-scheme: %s %s", gsettings.colorScheme, err) } else { log.Infof("color-scheme: %s OK", gsettings.colorScheme) } gnomeSchema = "org.gnome.desktop.sound" log.Infof(">> %s", gnomeSchema) if gsettings.eventSounds { val = "true" } else { val = "false" } cmd = exec.Command("gsettings", "set", gnomeSchema, "event-sounds", val) err = cmd.Run() if err != nil { log.Warnf("event-sounds: %s %s", val, err) } else { log.Infof("event-sounds: %s OK", val) } if gsettings.inputFeedbackSounds { val = "true" } else { val = "false" } cmd = exec.Command("gsettings", "set", gnomeSchema, "input-feedback-sounds", val) err = cmd.Run() if err != nil { log.Warnf("input-feedback-sounds: %s %s", val, err) } else { log.Infof("input-feedback-sounds: %s OK", val) } } func applyGsettingsFromFile() { gsettingsFile := filepath.Join(dataHome(), "nwg-look/gsettings") if pathExists(gsettingsFile) { log.Infof("Loading gsettings from %s", gsettingsFile) lines, err := loadTextFile(gsettingsFile) if err != nil { log.Fatalf("Failed loading file: %s", err) } var key, value string for _, line := range lines { if !strings.HasPrefix(line, "#") { parts := strings.Split(line, "=") if len(parts) == 2 { key = parts[0] value = parts[1] switch key { case "gtk-theme": gsettings.gtkTheme = value case "icon-theme": gsettings.iconTheme = value case "font-name": gsettings.fontName = value case "cursor-theme": gsettings.cursorTheme = value case "cursor-size": v, err := strconv.Atoi(value) if err == nil { gsettings.cursorSize = v } case "toolbar-style": gsettings.toolbarStyle = value case "toolbar-icons-size": gsettings.toolbarIconsSize = value case "font-hinting": gsettings.fontHinting = value case "font-antialiasing": gsettings.fontAntialiasing = value case "font-rgba-order": gsettings.fontRgbaOrder = value case "text-scaling-factor": v, err := strconv.ParseFloat(value, 64) if err == nil { gsettings.textScalingFactor = v } case "event-sounds": gsettings.eventSounds = value == "true" case "input-feedback-sounds": gsettings.inputFeedbackSounds = value == "true" case "color-scheme": gsettings.colorScheme = value } } } } applyGsettings() } else { log.Warnf("Couldn't find file: %s", gsettingsFile) os.Exit(1) } } func saveGtkIni() { configFile := filepath.Join(configHome(), "gtk-3.0/settings.ini") if !pathExists(configFile) { makeDir(filepath.Join(configHome(), "gtk-3.0/")) } log.Infof(">>> Exporting %s", configFile) lines := []string{"[Settings]"} lines = append(lines, fmt.Sprintf("gtk-theme-name=%s", gsettings.gtkTheme)) lines = append(lines, fmt.Sprintf("gtk-icon-theme-name=%s", gsettings.iconTheme)) lines = append(lines, fmt.Sprintf("gtk-font-name=%s", gsettings.fontName)) lines = append(lines, fmt.Sprintf("gtk-cursor-theme-name=%s", gsettings.cursorTheme)) lines = append(lines, fmt.Sprintf("gtk-cursor-theme-size=%v", gsettings.cursorSize)) // Ignored lines = append(lines, fmt.Sprintf("gtk-toolbar-style=%s", gtkConfig.toolbarStyle)) lines = append(lines, fmt.Sprintf("gtk-toolbar-icon-size=%s", gtkConfig.toolbarIconSize)) // Deprecated v := 0 if gtkConfig.buttonImages { v = 1 } lines = append(lines, fmt.Sprintf("gtk-button-images=%v", v)) if gtkConfig.menuImages { v = 1 } else { v = 0 } lines = append(lines, fmt.Sprintf("gtk-menu-images=%v", v)) if gsettings.eventSounds { v = 1 } else { v = 0 } lines = append(lines, fmt.Sprintf("gtk-enable-event-sounds=%v", v)) if gsettings.inputFeedbackSounds { v = 1 } else { v = 0 } lines = append(lines, fmt.Sprintf("gtk-enable-input-feedback-sounds=%v", v)) if gsettings.fontAntialiasing != "none" { v = 1 } else { v = 0 } lines = append(lines, fmt.Sprintf("gtk-xft-antialias=%v", v)) if gsettings.fontHinting != "none" { v = 1 } else { v = 0 } lines = append(lines, fmt.Sprintf("gtk-xft-hinting=%v", v)) var fh string switch gsettings.fontHinting { case "slight": fh = "hintslight" case "medium": fh = "hintmedium" case "full": fh = "hintfull" default: fh = "hintnone" } lines = append(lines, fmt.Sprintf("gtk-xft-hintstyle=%s", fh)) lines = append(lines, fmt.Sprintf("gtk-xft-rgba=%s", gsettings.fontRgbaOrder)) if gsettings.colorScheme == "prefer-dark" { v = 1 } else { v = 0 } lines = append(lines, fmt.Sprintf("gtk-application-prefer-dark-theme=%v", v)) // append unsupported lines / comments from the original settings.ini file for _, l := range originalGtkConfig { if l != "" && !isSupported(l) { lines = append(lines, l) } } for _, l := range lines { log.Debug(l) } saveTextFile(lines, configFile) } func isSupported(line string) bool { supported := []string{ "gtk-theme-name", "gtk-icon-theme-name", "gtk-font-name", "gtk-cursor-theme-name", "gtk-cursor-theme-size", "gtk-toolbar-style", "gtk-toolbar-icon-size", "gtk-button-images", "gtk-menu-images", "gtk-enable-event-sounds", "gtk-enable-input-feedback-sounds", "gtk-xft-antialias", "gtk-xft-hinting", "gtk-xft-hintstyle", "gtk-xft-rgba", "gtk-application-prefer-dark-theme", } for _, d := range supported { if strings.HasPrefix(line, d) { return true } } return false } func saveGtkRc20() { home := os.Getenv("HOME") var configFile string if os.Getenv("GTK2_RC_FILES") != "" { configFile = os.Getenv("GTK2_RC_FILES") } else { configFile = filepath.Join(home, ".gtkrc-2.0") } log.Infof(">>> Exporting %s", configFile) lines := []string{ "# DO NOT EDIT! This file will be overwritten by nwg-look.", "# Any customization should be done in ~/.gtkrc-2.0.mine instead.", "", } lines = append(lines, fmt.Sprintf("include \"%s/.gtkrc-2.0.mine\"", home)) lines = append(lines, fmt.Sprintf("gtk-theme-name=\"%s\"", gsettings.gtkTheme)) lines = append(lines, fmt.Sprintf("gtk-icon-theme-name=\"%s\"", gsettings.iconTheme)) lines = append(lines, fmt.Sprintf("gtk-font-name=\"%s\"", gsettings.fontName)) lines = append(lines, fmt.Sprintf("gtk-cursor-theme-name=\"%s\"", gsettings.cursorTheme)) lines = append(lines, fmt.Sprintf("gtk-cursor-theme-size=%v", gsettings.cursorSize)) lines = append(lines, fmt.Sprintf("gtk-toolbar-style=%s", gtkConfig.toolbarStyle)) lines = append(lines, fmt.Sprintf("gtk-toolbar-icon-size=%s", gtkConfig.toolbarIconSize)) v := 0 if gtkConfig.buttonImages { v = 1 } lines = append(lines, fmt.Sprintf("gtk-button-images=%v", v)) if gtkConfig.menuImages { v = 1 } else { v = 0 } lines = append(lines, fmt.Sprintf("gtk-menu-images=%v", v)) if gsettings.eventSounds { v = 1 } else { v = 0 } lines = append(lines, fmt.Sprintf("gtk-enable-event-sounds=%v", v)) if gsettings.inputFeedbackSounds { v = 1 } else { v = 0 } lines = append(lines, fmt.Sprintf("gtk-enable-input-feedback-sounds=%v", v)) if gsettings.fontAntialiasing != "none" { v = 1 } else { v = 0 } lines = append(lines, fmt.Sprintf("gtk-xft-antialias=%v", v)) if gsettings.fontHinting != "none" { v = 1 } else { v = 0 } lines = append(lines, fmt.Sprintf("gtk-xft-hinting=%v", v)) var fh string switch gsettings.fontHinting { case "slight": fh = "hintslight" case "medium": fh = "hintmedium" case "full": fh = "hintfull" default: fh = "hintnone" } lines = append(lines, fmt.Sprintf("gtk-xft-hintstyle=\"%s\"", fh)) lines = append(lines, fmt.Sprintf("gtk-xft-rgba=\"%s\"", gsettings.fontRgbaOrder)) if gtkConfig.applicationPreferDarkTheme { v = 1 } else { v = 0 } for _, l := range lines { log.Debug(l) } saveTextFile(lines, configFile) } func saveXsettingsd() { configFile := filepath.Join(configHome(), "xsettingsd/xsettingsd.conf") if !pathExists(configFile) { makeDir(filepath.Join(configHome(), "xsettingsd/")) } log.Infof(">>> Exporting %s", configFile) lines := []string{} lines = append(lines, fmt.Sprintf("Net/ThemeName \"%s\"", gsettings.gtkTheme)) lines = append(lines, fmt.Sprintf("Net/IconThemeName \"%s\"", gsettings.iconTheme)) lines = append(lines, fmt.Sprintf("Gtk/CursorThemeName \"%s\"", gsettings.cursorTheme)) var v int if gsettings.eventSounds { v = 1 } else { v = 0 } lines = append(lines, fmt.Sprintf("Net/EnableEventSounds %v", v)) if gsettings.inputFeedbackSounds { v = 1 } else { v = 0 } lines = append(lines, fmt.Sprintf("EnableInputFeedbackSounds %v", v)) if gsettings.fontAntialiasing != "none" { v = 1 } else { v = 0 } lines = append(lines, fmt.Sprintf("Xft/Antialias %v", v)) if gsettings.fontHinting != "none" { v = 1 } else { v = 0 } lines = append(lines, fmt.Sprintf("Xft/Hinting %v", v)) var fh string switch gsettings.fontHinting { case "slight": fh = "hintslight" case "medium": fh = "hintmedium" case "full": fh = "hintfull" default: fh = "hintnone" } lines = append(lines, fmt.Sprintf("Xft/HintStyle \"%s\"", fh)) lines = append(lines, fmt.Sprintf("Xft/RGBA \"%s\"", gsettings.fontRgbaOrder)) for _, l := range lines { log.Debug(l) } saveTextFile(lines, configFile) } func linkGtk4Stuff() { home := os.Getenv("HOME") configPath := filepath.Join(home, ".config") themeName := gsettings.gtkTheme if gsettings.gtkTheme != "" { log.Infof(">>> Symlinking files in %s", filepath.Join(configPath, "/gtk-4.0")) log.Debugf("GTK Theme: '%s' at '%s'", themeName, gtkThemePaths[themeName]) log.Debugf("Config path: '%s'", configPath) themePath := gtkThemePaths[themeName] log.Debugf("Theme path: '%s'", gtkThemePaths[gsettings.gtkTheme]) if themePath == "" { log.Warnf("Unknown theme path: '%s'", themePath) return } if !pathExists(filepath.Join(themePath, "gtk-4.0")) { log.Warnf("%s theme has no gtk-4.0 directory", themePath) return } clearGtk4Symlinks() // Create symlinks if pathExists(filepath.Join(themePath, "gtk-4.0/gtk.css")) { cmd := exec.Command("ln", "-s", filepath.Join(themePath, "gtk-4.0/gtk.css"), filepath.Join(configPath, "gtk-4.0/gtk.css")) err := cmd.Run() if err != nil { log.Warnf("Couldn't symlink '%s': %s", filepath.Join(themePath, "gtk-4.0/gtk.css"), err) } else { log.Debugf("Created symlink to '%s'", filepath.Join(themePath, "gtk-4.0/gtk.css")) } } if pathExists(filepath.Join(themePath, "gtk-4.0/gtk-dark.css")) { cmd := exec.Command("ln", "-s", filepath.Join(themePath, "gtk-4.0/gtk-dark.css"), filepath.Join(configPath, "gtk-4.0/gtk-dark.css")) err := cmd.Run() if err != nil { log.Warnf("Couldn't symlink '%s': %s", filepath.Join(themePath, "gtk-4.0/gtk-dark.css"), err) } else { log.Debugf("Created symlink to '%s'", filepath.Join(themePath, "gtk-4.0/gtk-dark.css")) } } if pathExists(filepath.Join(themePath, "gtk-4.0/assets")) { cmd := exec.Command("ln", "-s", filepath.Join(themePath, "gtk-4.0/assets"), filepath.Join(configPath, "gtk-4.0/assets")) err := cmd.Run() if err != nil { log.Warnf("Couldn't symlink '%s': %s", filepath.Join(themePath, "gtk-4.0/assets"), err) } else { log.Debugf("Created symlink to '%s'", filepath.Join(themePath, "gtk-4.0/assets")) } } if pathExists(filepath.Join(themePath, "assets")) { cmd := exec.Command("ln", "-s", filepath.Join(themePath, "assets"), filepath.Join(configPath, "assets")) err := cmd.Run() if err != nil { log.Warnf("Couldn't symlink '%s': %s", filepath.Join(themePath, "assets"), err) } else { log.Debugf("Created symlink to '%s'", filepath.Join(themePath, "assets")) } } } else { log.Warnf("GTK theme name unknown") } } func clearGtk4Symlinks() { home := os.Getenv("HOME") configPath := filepath.Join(home, ".config") items := []string{"gtk-4.0/gtk.css", "gtk-4.0/gtk-dark.css", "gtk-4.0/assets", "assets"} for _, item := range items { p := filepath.Join(configPath, item) if pathExists(p) { log.Debugf("Removing '%s'", p) info, _ := os.Stat(p) if info.IsDir() { cmd := exec.Command("rm", "-r", p) err := cmd.Run() if err != nil { log.Warnf("Couldn't remove '%s': %s", p, err) } } else { cmd := exec.Command("rm", p) err := cmd.Run() if err != nil { log.Warnf("Couldn't remove '%s': %s", p, err) } } } } } func saveIndexTheme() { home := os.Getenv("HOME") iconsFolder := "" if pathExists(filepath.Join(home, ".icons")) { iconsFolder = filepath.Join(home, ".icons") } else { if os.Getenv("XDG_DATA_HOME") != "" { if pathExists(filepath.Join(os.Getenv("XDG_DATA_HOME"), "icons")) { iconsFolder = filepath.Join(os.Getenv("XDG_DATA_HOME"), "icons") } } else { if pathExists(filepath.Join(home, ".local/share/icons")) { iconsFolder = filepath.Join(home, ".local/share/icons") } } } if iconsFolder != "" { indexThemeFile := filepath.Join(iconsFolder, "/default/index.theme") if !pathExists(filepath.Join(iconsFolder, "default")) { makeDir(filepath.Join(iconsFolder, "default")) } log.Infof(">>> Exporting %s", indexThemeFile) lines := []string{ "# This file is written by nwg-look. Do not edit.", "[Icon Theme]", "Name=Default", "Comment=Default Cursor Theme", } lines = append(lines, fmt.Sprintf("Inherits=%s", gsettings.cursorTheme)) saveTextFile(lines, indexThemeFile) } else { log.Warn("Couldn't find icons folder") } } func getThemeNames() ([]string, map[string]string) { var dirs []string themePaths := make(map[string]string) // theme name 2 theme path // get theme dirs for _, dir := range dataDirs { if pathExists(filepath.Join(dir, "themes")) { dirs = append(dirs, filepath.Join(dir, "themes")) } } home := os.Getenv("HOME") if home != "" { if pathExists(filepath.Join(home, ".themes")) { dirs = append(dirs, filepath.Join(home, ".themes")) } } exclusions := []string{"Default", "Emacs"} var names []string for _, d := range dirs { files, err := listFiles(d) if err == nil { for _, f := range files { if f.IsDir() { subdirs, err := listFiles(filepath.Join(d, f.Name())) if err == nil { for _, sd := range subdirs { if sd.IsDir() && strings.HasPrefix(sd.Name(), "gtk-") { if !isIn(names, f.Name()) { if !isIn(exclusions, f.Name()) { names = append(names, f.Name()) themePaths[f.Name()] = filepath.Join(d, f.Name()) log.Debugf("Theme found: '%s' at '%s'", f.Name(), filepath.Join(d, f.Name())) } else { log.Debugf("Excluded theme: %s", f.Name()) } break } } } } } } } } sort.Slice(names, func(i, j int) bool { return names[i] < names[j] }) return names, themePaths } // returns map[displayName]folderName func getIconThemeNames() map[string]string { var dirs []string name2folderName := make(map[string]string) // get icon theme dirs for _, dir := range dataDirs { if pathExists(filepath.Join(dir, "icons")) { dirs = append(dirs, filepath.Join(dir, "icons")) } } home := os.Getenv("HOME") if home != "" { if pathExists(filepath.Join(home, ".icons")) { dirs = append(dirs, filepath.Join(home, ".icons")) } } exclusions := []string{"default", "hicolor", "locolor"} var names []string for _, d := range dirs { files, err := listFiles(d) if err == nil { for _, f := range files { if f.IsDir() { if !isIn(exclusions, f.Name()) { name, hasDirs, err := iconThemeName(filepath.Join(d, f.Name())) if err == nil && hasDirs { names = append(names, name) name2folderName[name] = f.Name() log.Debugf("Icon theme found: %s", name) } } else { log.Debugf("Excluded icon theme: %s", f.Name()) } } } } } sort.Slice(names, func(i, j int) bool { return strings.ToUpper(names[i]) < strings.ToUpper(names[j]) }) return name2folderName } func getCursorThemes() (map[string]string, map[string]string) { var dirs []string name2path := make(map[string]string) name2FolderName := make(map[string]string) // get icon theme dirs for _, dir := range dataDirs { if pathExists(filepath.Join(dir, "icons")) { dirs = append(dirs, filepath.Join(dir, "icons")) } } home := os.Getenv("HOME") if home != "" { if pathExists(filepath.Join(home, ".icons")) { dirs = append(dirs, filepath.Join(home, ".icons")) } } exclusions := []string{"default", "hicolor", "locolor"} for _, d := range dirs { files, err := listFiles(d) if err == nil { for _, f := range files { if f.IsDir() { if !isIn(exclusions, f.Name()) { content, _ := listFiles(filepath.Join(d, f.Name())) if err == nil { for _, item := range content { if item.Name() == "cursors" { name, _, err := iconThemeName(filepath.Join(d, f.Name())) if err == nil { name2FolderName[name] = f.Name() } log.Debugf("Cursor theme found: %s", f.Name()) name2path[f.Name()] = filepath.Join(d, f.Name(), "cursors") } } } } } } } } return name2path, name2FolderName } func dataHome() string { xdgDataHome := os.Getenv("XDG_DATA_HOME") if xdgDataHome != "" { return xdgDataHome } return filepath.Join(os.Getenv("HOME"), ".local/share") } func getDataDirs() []string { var dirs []string xdgDataDirs := "" dirs = append(dirs, dataHome()) if os.Getenv("XDG_DATA_DIRS") != "" { xdgDataDirs = os.Getenv("XDG_DATA_DIRS") } else { xdgDataDirs = "/usr/local/share/:/usr/share/" } for _, d := range strings.Split(xdgDataDirs, ":") { dirs = append(dirs, d) } var confirmedDirs []string for _, d := range dirs { if pathExists(d) { confirmedDirs = append(confirmedDirs, d) } } return confirmedDirs } func iconThemeName(path string) (string, bool, error) { name := "" hasDirs := false lines, err := loadTextFile(filepath.Join(path, "index.theme")) if err != nil { return name, hasDirs, err } for _, line := range lines { if strings.HasPrefix(line, "Name=") || strings.HasPrefix(line, "Name =") { name = strings.Split(line, "=")[1] name = strings.TrimSpace(name) break } } for _, line := range lines { if strings.HasPrefix(line, "Directories=") || strings.HasPrefix(line, "Directories =") { hasDirs = true break } } return name, hasDirs, err } func loadTextFile(path string) ([]string, error) { bytes, err := os.ReadFile(path) if err != nil { return nil, err } lines := strings.Split(string(bytes), "\n") var output []string for _, line := range lines { line = strings.TrimSpace(line) output = append(output, line) } return output, nil } func saveTextFile(text []string, path string) { file, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) if err != nil { log.Warnf("Failed creating file: %s", err) } datawriter := bufio.NewWriter(file) for _, data := range text { _, _ = datawriter.WriteString(data + "\n") } datawriter.Flush() file.Close() } func listFiles(dir string) ([]os.DirEntry, error) { files, err := os.ReadDir(dir) if err == nil { return files, nil } return nil, err } func isIn(slice []string, val string) bool { for _, item := range slice { if item == val { return true } } return false } func pathExists(name string) bool { if _, err := os.Stat(name); err != nil { if os.IsNotExist(err) { return false } } return true } func tempDir() string { if os.Getenv("TMPDIR") != "" { return os.Getenv("TMPDIR") } else if os.Getenv("TEMP") != "" { return os.Getenv("TEMP") } else if os.Getenv("TMP") != "" { return os.Getenv("TMP") } return "/tmp" } func makeDir(dir string) { if _, err := os.Stat(dir); os.IsNotExist(err) { err := os.MkdirAll(dir, os.ModePerm) if err == nil { log.Debugf("Creating dir: %s", dir) } } } // Assert types to gtk.Builder objects func getWindow(b *gtk.Builder, id string) (*gtk.Window, error) { obj, err := b.GetObject(id) if err != nil { return nil, err } window, ok := obj.(*gtk.Window) if !ok { return nil, err } return window, nil } func getScrolledWindow(b *gtk.Builder, id string) (*gtk.ScrolledWindow, error) { obj, err := b.GetObject(id) if err != nil { return nil, err } window, ok := obj.(*gtk.ScrolledWindow) if !ok { return nil, err } return window, nil } func getViewPort(b *gtk.Builder, id string) (*gtk.Viewport, error) { obj, err := b.GetObject(id) if err != nil { return nil, err } viewport, ok := obj.(*gtk.Viewport) if !ok { return nil, err } return viewport, nil } func getButton(b *gtk.Builder, id string) (*gtk.Button, error) { obj, err := b.GetObject(id) if err != nil { return nil, err } btn, ok := obj.(*gtk.Button) if !ok { return nil, err } return btn, nil } func getGrid(b *gtk.Builder, id string) (*gtk.Grid, error) { obj, err := b.GetObject(id) if err != nil { return nil, err } grid, ok := obj.(*gtk.Grid) if !ok { return nil, err } return grid, nil } func getLabel(b *gtk.Builder, id string) (*gtk.Label, error) { obj, err := b.GetObject(id) if err != nil { return nil, err } label, ok := obj.(*gtk.Label) if !ok { return nil, err } return label, nil } func getMenuBar(b *gtk.Builder, id string) (*gtk.MenuBar, error) { obj, err := b.GetObject(id) if err != nil { return nil, err } menuBar, ok := obj.(*gtk.MenuBar) if !ok { return nil, err } return menuBar, nil } func getMenuItem(b *gtk.Builder, id string) (*gtk.MenuItem, error) { obj, err := b.GetObject(id) if err != nil { return nil, err } item, ok := obj.(*gtk.MenuItem) if !ok { return nil, err } return item, nil } func detectLang() string { lang := "" shellDataFile := filepath.Join(dataHome(), "/nwg-shell/data") if pathExists(shellDataFile) { jsonFile, err := os.Open(shellDataFile) if err == nil { byteValue, _ := io.ReadAll(jsonFile) var result map[string]interface{} err = json.Unmarshal([]byte(byteValue), &result) if err == nil { if result["interface-locale"] != "" { lang = fmt.Sprintf("%s", result["interface-locale"]) log.Infof("lang '%s' set from nwg-shell settings", lang) } } } defer jsonFile.Close() } if lang == "" { if os.Getenv("LANG") != "" { lang = strings.Split(os.Getenv("LANG"), ".")[0] log.Debugf("lang '%s' set from the $LANG variable", lang) } else { lang = "en_US" log.Warn("Couldn't determine your lang") } } return lang } func loadVocabulary(lang string) map[string]string { var dataDirs []string dataDirs = getDataDirs() for _, d := range dataDirs { langsDir := filepath.Join(d, "/nwg-look/langs/") enUSFile := filepath.Join(langsDir, "en_US.json") if pathExists(enUSFile) { log.Infof(">>> Loading basic lang from '%s'", enUSFile) jsonFile, err := os.Open(enUSFile) if err != nil { log.Errorf("Error loading basic lang: %s", err) os.Exit(1) } else { byteValue, _ := io.ReadAll(jsonFile) var result map[string]string err = json.Unmarshal([]byte(byteValue), &result) if err != nil { log.Errorf("Error unmarshalling '%s': %s", enUSFile, err) // We can't continue w/o the basic dictionary! os.Exit(1) } else { translationFile := filepath.Join(langsDir, fmt.Sprintf("%s.json", lang)) if lang == "en_US" || !pathExists(translationFile) { // Users lang is en_US, or we have no translation into users lang return result } else { log.Infof(">>> Loading translation from '%s'", translationFile) jsonFile, err = os.Open(translationFile) if err != nil { log.Errorf("Error loading translation: %s", err) } else { byteValue, _ = io.ReadAll(jsonFile) var result1 map[string]string err = json.Unmarshal([]byte(byteValue), &result1) if err != nil { log.Errorf("Error unmarshalling '%s': %s", translationFile, err) // We can continue, we just have no translation return result } else { // Translate for key, _ := range result1 { if _, ok := result[key]; ok { result[key] = result1[key] } } return result } } } } } } } log.Errorf("Couldn't load the basic lang file") os.Exit(1) return nil } nwg-look-1.0.2/uicomponents.go000066400000000000000000000471721474356134500163500ustar00rootroot00000000000000package main import ( "fmt" "os" "os/exec" "path/filepath" "sort" "strings" "github.com/gotk3/gotk3/gdk" "github.com/gotk3/gotk3/gtk" log "github.com/sirupsen/logrus" ) func setUpThemeListBox(currentTheme string) *gtk.ListBox { listBox, _ := gtk.ListBoxNew() var rowToSelect *gtk.ListBoxRow themeNames, themePaths := getThemeNames() gtkThemePaths = themePaths for _, name := range themeNames { row, _ := gtk.ListBoxRowNew() eventBox, _ := gtk.EventBoxNew() box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 6) eventBox.Add(box) lbl, _ := gtk.LabelNew(name) lbl.SetProperty("margin-start", 6) lbl.SetProperty("margin-end", 6) n := name eventBox.Connect("button-press-event", func() { gtkSettings.SetProperty("gtk-theme-name", n) gsettings.gtkTheme = n }) row.Connect("focus-in-event", func() { gtkSettings.SetProperty("gtk-theme-name", n) gsettings.gtkTheme = n }) if n == currentTheme { rowToSelect = row } box.PackStart(lbl, false, false, 0) row.Add(eventBox) listBox.Add(row) } if rowToSelect != nil { listBox.SelectRow(rowToSelect) rowToFocus = rowToSelect } return listBox } func setUpIconThemeListBox(currentIconTheme string) *gtk.ListBox { listBox, _ := gtk.ListBoxNew() var rowToSelect *gtk.ListBoxRow // map[displayName]folderName namesMap := getIconThemeNames() var displayNames []string for name := range namesMap { displayNames = append(displayNames, name) } sort.Slice(displayNames, func(i, j int) bool { return strings.ToUpper(displayNames[i]) < strings.ToUpper(displayNames[j]) }) for _, name := range displayNames { row, _ := gtk.ListBoxRowNew() eventBox, _ := gtk.EventBoxNew() box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 6) eventBox.Add(box) lbl, _ := gtk.LabelNew(name) lbl.SetProperty("margin-start", 6) lbl.SetProperty("margin-end", 6) n := name eventBox.Connect("button-press-event", func() { gtkSettings.SetProperty("gtk-icon-theme-name", namesMap[n]) gsettings.iconTheme = namesMap[n] }) row.Connect("focus-in-event", func() { gtkSettings.SetProperty("gtk-icon-theme-name", namesMap[n]) gsettings.iconTheme = namesMap[n] }) if namesMap[n] == currentIconTheme || n == currentIconTheme { rowToSelect = row } box.PackStart(lbl, false, false, 0) row.Add(eventBox) listBox.Add(row) } if rowToSelect != nil { listBox.SelectRow(rowToSelect) rowToFocus = rowToSelect } return listBox } func setUpCursorThemeListBox(currentCursorTheme string) *gtk.ListBox { listBox, _ := gtk.ListBoxNew() var rowToSelect *gtk.ListBoxRow var names []string for name := range cursorThemeNames { names = append(names, name) } sort.Slice(names, func(i, j int) bool { return strings.ToUpper(names[i]) < strings.ToUpper(names[j]) }) for _, name := range names { row, _ := gtk.ListBoxRowNew() eventBox, _ := gtk.EventBoxNew() box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 6) eventBox.Add(box) lbl, _ := gtk.LabelNew(name) lbl.SetProperty("margin-start", 6) lbl.SetProperty("margin-end", 6) n := name eventBox.Connect("button-press-event", func() { gtkSettings.SetProperty("gtk-cursor-theme-name", cursorThemeNames[n]) gsettings.cursorTheme = cursorThemeNames[n] displayCursorThemes() }) row.Connect("focus-in-event", func() { gtkSettings.SetProperty("gtk-cursor-theme-name", cursorThemeNames[n]) gsettings.cursorTheme = cursorThemeNames[n] }) if cursorThemeNames[n] == currentCursorTheme { rowToSelect = row } box.PackStart(lbl, false, false, 0) row.Add(eventBox) listBox.Add(row) } if rowToSelect != nil { listBox.SelectRow(rowToSelect) rowToFocus = rowToSelect } return listBox } func setUpWidgetsPreview() *gtk.Frame { frame, _ := gtk.FrameNew(fmt.Sprintf(" %s ", voc["widget-style-preview"])) frame.SetLabelAlign(0.5, 0.5) frame.SetProperty("margin", 6) frame.SetProperty("valign", gtk.ALIGN_START) grid, _ := gtk.GridNew() grid.SetRowSpacing(6) grid.SetColumnSpacing(12) grid.SetProperty("margin", 6) frame.Add(grid) box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 6) box.SetProperty("hexpand", true) grid.Attach(box, 0, 0, 3, 1) btn, _ := gtk.ButtonNewFromIconName("go-previous", gtk.ICON_SIZE_BUTTON) btn.SetProperty("can-focus", false) box.PackStart(btn, false, false, 0) btn, _ = gtk.ButtonNewFromIconName("go-next", gtk.ICON_SIZE_BUTTON) btn.SetProperty("can-focus", false) box.PackStart(btn, false, false, 0) btn, _ = gtk.ButtonNewFromIconName("process-stop", gtk.ICON_SIZE_BUTTON) btn.SetProperty("can-focus", false) box.PackStart(btn, false, false, 0) entry, _ := gtk.EntryNew() entry.SetProperty("can-focus", false) box.PackStart(entry, true, true, 0) checkButton, _ := gtk.CheckButtonNew() checkButton.SetProperty("can-focus", false) checkButton.SetLabel(voc["check-button"]) grid.Attach(checkButton, 0, 1, 1, 1) radioButton, _ := gtk.RadioButtonNew(nil) radioButton.SetProperty("can-focus", false) radioButton.SetLabel(voc["radio-button"]) grid.Attach(radioButton, 0, 2, 1, 1) spinButton, _ := gtk.SpinButtonNewWithRange(0, 1000, 10) spinButton.SetProperty("can-focus", false) grid.Attach(spinButton, 0, 3, 1, 1) button, _ := gtk.ButtonNewFromIconName("search", gtk.ICON_SIZE_BUTTON) button.SetProperty("can-focus", false) button.SetLabel(voc["button"]) grid.Attach(button, 1, 3, 1, 1) scale, _ := gtk.ScaleNewWithRange(gtk.ORIENTATION_HORIZONTAL, 0, 100, 1) scale.SetProperty("can-focus", false) scale.SetDrawValue(true) scale.SetValue(50) grid.Attach(scale, 1, 1, 2, 1) separator, _ := gtk.SeparatorNew(gtk.ORIENTATION_HORIZONTAL) separator.SetProperty("can-focus", false) separator.SetProperty("valign", gtk.ALIGN_CENTER) grid.Attach(separator, 1, 2, 2, 1) combo, _ := gtk.ComboBoxTextNew() combo.Append("entry #1", fmt.Sprintf("%s 1", voc["entry"])) combo.Append("entry #2", fmt.Sprintf("%s 2", voc["entry"])) combo.SetProperty("can-focus", false) grid.Attach(combo, 2, 3, 1, 1) progressBar, _ := gtk.ProgressBarNew() progressBar.SetFraction(0.3) progressBar.SetText("30%") progressBar.SetShowText(true) progressBar.SetProperty("margin-bottom", 6) grid.Attach(progressBar, 0, 4, 3, 1) return frame } func setUpThemeSettingsForm(defaultFontName string) *gtk.Grid { grid, _ := gtk.GridNew() grid.SetColumnSpacing(12) grid.SetRowSpacing(6) grid.SetProperty("margin", 12) label, _ := gtk.LabelNew(fmt.Sprintf("%s:", voc["default-font"])) label.SetProperty("halign", gtk.ALIGN_END) grid.Attach(label, 0, 0, 1, 1) fontButton, _ := gtk.FontButtonNew() fontButton.SetProperty("valign", gtk.ALIGN_CENTER) fontButton.SetFont(defaultFontName) fontButton.Connect("font-set", func() { fontName := fontButton.GetFont() gtkSettings.SetProperty("gtk-font-name", fontName) gsettings.fontName = fontName }) grid.Attach(fontButton, 1, 0, 1, 1) label, _ = gtk.LabelNew(fmt.Sprintf("%s:", voc["color-scheme"])) label.SetProperty("halign", gtk.ALIGN_END) grid.Attach(label, 0, 1, 1, 1) combo, _ := gtk.ComboBoxTextNew() combo.Append("default", voc["default"]) combo.Append("prefer-dark", voc["prefer-dark"]) combo.Append("prefer-light", voc["prefer-light"]) combo.SetActiveID(gsettings.colorScheme) combo.SetProperty("can-focus", false) combo.Connect("changed", func() { id := combo.GetActiveID() gsettings.colorScheme = id if id == "prefer-dark" { gtkConfig.applicationPreferDarkTheme = true gtkSettings.SetProperty("gtk-application-prefer-dark-theme", true) } else { gtkConfig.applicationPreferDarkTheme = false gtkSettings.SetProperty("gtk-application-prefer-dark-theme", false) } }) grid.Attach(combo, 1, 1, 1, 1) return grid } func setUpIconsPreview() *gtk.Frame { frame, _ := gtk.FrameNew(fmt.Sprintf(" %s ", voc["icon-theme-preview"])) frame.SetLabelAlign(0.5, 0.5) frame.SetProperty("margin", 6) frame.SetProperty("valign", gtk.ALIGN_START) box, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 12) box.SetProperty("hexpand", true) frame.Add(box) flowBox, _ := gtk.FlowBoxNew() flowBox.SetMaxChildrenPerLine(7) flowBox.SetMinChildrenPerLine(7) box.PackStart(flowBox, false, false, 0) icons := []string{ "user-home", "user-desktop", "folder", "folder-remote", "user-trash", "x-office-document", "application-x-executable", "image-x-generic", "package-x-generic", "emblem-mail", "utilities-terminal", "chromium", "firefox", "gimp"} for _, name := range icons { img, err := gtk.ImageNewFromIconName(name, gtk.ICON_SIZE_DIALOG) if err == nil { flowBox.Add(img) log.Debugf("Added icon: '%s'", name) } else { log.Warnf("Couldn't create image: '%s'", name) } } flowBox, _ = gtk.FlowBoxNew() box.PackStart(flowBox, false, false, 12) icons = []string{ "network-wired-symbolic", "network-wireless-symbolic", "bluetooth-active-symbolic", "computer-symbolic", "audio-volume-high-symbolic", "battery-low-charging-symbolic", "display-brightness-medium-symbolic", } for _, name := range icons { img, err := gtk.ImageNewFromIconName(name, gtk.ICON_SIZE_MENU) if err == nil { flowBox.Add(img) log.Debugf("Added icon: '%s'", name) } else { log.Warnf("Couldn't create image: '%s'", name) } } return frame } func setUpCursorsPreview(path string) *gtk.Frame { frame, _ := gtk.FrameNew(fmt.Sprintf(" %s ", voc["cursor-theme-preview"])) frame.SetLabelAlign(0.5, 0.5) frame.SetProperty("margin", 6) box, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 12) box.SetProperty("margin", 12) box.SetProperty("hexpand", true) frame.Add(box) flowBox, _ := gtk.FlowBoxNew() flowBox.SetMaxChildrenPerLine(8) box.Add(flowBox) images := []string{ "left_ptr", "hand2", "sb_v_double_arrow", "fleur", "xterm", "left_side", "top_left_corner", "h_double_arrow", } if path != "" { // As I have no better idea, we'll use the external `xcur2png` tool // to extract images from xcursor files, and save them to tmp dir. cursorsDir := filepath.Join(tempDir(), "nwg-look-cursors") dir, err := os.ReadDir(cursorsDir) if err == nil { for _, d := range dir { os.RemoveAll(filepath.Join([]string{cursorsDir, d.Name()}...)) } } // just in case it didn't yet exist makeDir(cursorsDir) for _, name := range images { imgPath := filepath.Join(path, name) args := []string{imgPath, "-d", cursorsDir, "-c", cursorsDir, "-q"} cmd := exec.Command("xcur2png", args...) cmd.Run() fName := fmt.Sprintf("%s_000.png", name) pngPath := filepath.Join(cursorsDir, fName) pixbuf, err := gdk.PixbufNewFromFileAtSize(pngPath, 24, 24) if err == nil { img, err := gtk.ImageNewFromPixbuf(pixbuf) if err == nil { flowBox.Add(img) p, _ := img.GetParent() parent, _ := p.(*gtk.FlowBoxChild) parent.SetProperty("can-focus", false) log.Debugf("Added icon: '%s'", pngPath) } else { log.Warnf("Couldn't create pixbuf from '%s'", pngPath) } } else { log.Warnf("Couldn't create image from '%s'", pngPath) } } } return frame } func setUpCursorSizeSelector() *gtk.Box { box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 6) box.SetProperty("margin", 12) box.SetProperty("hexpand", true) box.SetProperty("vexpand", true) box.SetProperty("valign", gtk.ALIGN_START) lbl, _ := gtk.LabelNew(fmt.Sprintf("%s:", voc["cursor-size"])) box.PackStart(lbl, false, false, 0) sb, _ := gtk.SpinButtonNewWithRange(6, 1024, 1) sb.SetValue(float64(gsettings.cursorSize)) sb.Connect("value-changed", func() { v := int(sb.GetValue()) gtkSettings.SetProperty("gtk-cursor-theme-size", v) gsettings.cursorSize = v }) box.PackStart(sb, false, false, 6) lbl, _ = gtk.LabelNew(fmt.Sprintf("(%s: 24)", voc["default"])) box.PackStart(lbl, false, false, 0) return box } func setUpFontSettingsForm() *gtk.Frame { // We wont be applying these properties to gtk.Settings for preview, // as they remain unchanged in once open window. frame, _ := gtk.FrameNew(fmt.Sprintf(" %s ", voc["font-settings"])) frame.SetLabelAlign(0.5, 0.5) frame.SetProperty("margin", 6) g, _ := gtk.GridNew() g.SetRowSpacing(12) g.SetColumnSpacing(12) g.SetProperty("margin", 6) g.SetProperty("hexpand", true) g.SetProperty("vexpand", true) frame.Add(g) lbl, _ := gtk.LabelNew(fmt.Sprintf("%s:", voc["font-hinting"])) lbl.SetProperty("halign", gtk.ALIGN_END) g.Attach(lbl, 0, 0, 1, 1) comboHinting, _ := gtk.ComboBoxTextNew() comboHinting.Append("none", voc["none"]) comboHinting.Append("slight", voc["slight"]) comboHinting.Append("medium", voc["medium"]) comboHinting.Append("full", voc["full"]) comboHinting.SetActiveID(gsettings.fontHinting) g.Attach(comboHinting, 1, 0, 1, 1) comboHinting.Connect("changed", func() { id := comboHinting.GetActiveID() gsettings.fontHinting = id }) lbl, _ = gtk.LabelNew(fmt.Sprintf("%s:", voc["font-antialiasing"])) lbl.SetProperty("halign", gtk.ALIGN_END) g.Attach(lbl, 0, 1, 1, 1) comboRgba, _ := gtk.ComboBoxTextNew() comboAntialiasing, _ := gtk.ComboBoxTextNew() comboAntialiasing.Append("none", voc["none"]) comboAntialiasing.Append("grayscale", voc["grayscale"]) comboAntialiasing.Append("rgba", "rgba") comboAntialiasing.SetActiveID(gsettings.fontAntialiasing) g.Attach(comboAntialiasing, 1, 1, 1, 1) comboAntialiasing.Connect("changed", func() { id := comboAntialiasing.GetActiveID() gsettings.fontAntialiasing = id comboRgba.SetSensitive(id == "rgba") }) lbl, _ = gtk.LabelNew(fmt.Sprintf("%s", voc["font-rgba-order"])) lbl.SetProperty("halign", gtk.ALIGN_END) g.Attach(lbl, 0, 2, 1, 1) comboRgba.Append("rgb", "RGB") comboRgba.Append("bgr", "BGR") comboRgba.Append("vrgb", "VRGB") comboRgba.Append("vbgr", "VBGR") comboRgba.SetActiveID(gsettings.fontRgbaOrder) comboRgba.SetSensitive(comboAntialiasing.GetActiveID() == "rgba") g.Attach(comboRgba, 1, 2, 1, 1) comboRgba.Connect("changed", func() { gsettings.fontRgbaOrder = comboRgba.GetActiveID() }) lbl, _ = gtk.LabelNew(fmt.Sprintf("%s:", voc["text-scaling-factor"])) lbl.SetProperty("halign", gtk.ALIGN_END) g.Attach(lbl, 0, 3, 1, 1) sb, _ := gtk.SpinButtonNewWithRange(0.5, 3, 0.01) sb.SetValue(gsettings.textScalingFactor) sb.Connect("value-changed", func() { v := sb.GetValue() gsettings.textScalingFactor = v }) g.Attach(sb, 1, 3, 1, 1) return frame } func setUpOtherSettingsForm() *gtk.Frame { // We won't be applying these properties to gtk.Settings for preview, // as they remain unchanged in once open window. frame, _ := gtk.FrameNew(fmt.Sprintf(" %s ", voc["other-settings"])) frame.SetLabelAlign(0.5, 0.5) frame.SetProperty("margin", 6) g, _ := gtk.GridNew() g.SetRowSpacing(12) g.SetColumnSpacing(12) g.SetProperty("margin", 6) g.SetProperty("hexpand", true) g.SetProperty("vexpand", true) frame.Add(g) lbl, _ := gtk.LabelNew("") lbl.SetMarkup(fmt.Sprintf("%s (%s)", voc["ui-settings"], voc["deprecated"])) lbl.SetProperty("halign", gtk.ALIGN_START) g.Attach(lbl, 0, 0, 2, 1) lbl, _ = gtk.LabelNew(fmt.Sprintf("%s:", voc["toolbar-style"])) lbl.SetProperty("halign", gtk.ALIGN_END) g.Attach(lbl, 0, 1, 1, 1) comboToolbarStyle, _ := gtk.ComboBoxTextNew() comboToolbarStyle.SetTooltipText(fmt.Sprintf("%s, %s", voc["deprecated-since-gtk-310"], voc["ignored"])) comboToolbarStyle.Append("both", voc["text-below-icons"]) comboToolbarStyle.Append("both-horiz", voc["text-next-to-icons"]) comboToolbarStyle.Append("icons", voc["icons"]) comboToolbarStyle.Append("text", voc["text"]) comboToolbarStyle.SetActiveID(gsettings.toolbarStyle) g.Attach(comboToolbarStyle, 1, 1, 1, 1) comboToolbarStyle.Connect("changed", func() { gsettings.toolbarStyle = comboToolbarStyle.GetActiveID() switch gsettings.toolbarStyle { case "both": gtkConfig.toolbarStyle = "GTK_TOOLBAR_BOTH" case "icons": gtkConfig.toolbarStyle = "GTK_TOOLBAR_ICONS" case "text": gtkConfig.toolbarStyle = "GTK_TOOLBAR_TEXT" default: gtkConfig.toolbarStyle = "GTK_TOOLBAR_BOTH_HORIZ" } }) lbl, _ = gtk.LabelNew(fmt.Sprintf("%s:", voc["toolbar-icon-size"])) lbl.SetProperty("halign", gtk.ALIGN_END) g.Attach(lbl, 0, 2, 1, 1) comboToolbarIconSize, _ := gtk.ComboBoxTextNew() comboToolbarIconSize.SetTooltipText(fmt.Sprintf("%s, %s", voc["deprecated-since-gtk-310"], voc["ignored"])) comboToolbarIconSize.Append("small", voc["small"]) comboToolbarIconSize.Append("large", voc["large"]) comboToolbarIconSize.SetActiveID(gsettings.toolbarIconsSize) g.Attach(comboToolbarIconSize, 1, 2, 1, 1) comboToolbarIconSize.Connect("changed", func() { gsettings.toolbarIconsSize = comboToolbarIconSize.GetActiveID() if gsettings.toolbarIconsSize == "small" { gtkConfig.toolbarIconSize = "GTK_ICON_SIZE_SMALL_TOOLBAR" } else { gtkConfig.toolbarIconSize = "GTK_ICON_SIZE_LARGE_TOOLBAR" } }) cbBtn, _ := gtk.CheckButtonNewWithLabel(voc["show-button-images"]) cbBtn.SetTooltipText(fmt.Sprintf("%s", voc["deprecated-since-gtk-310"])) cbBtn.SetActive(gtkConfig.buttonImages) cbBtn.Connect("toggled", func() { gtkConfig.buttonImages = cbBtn.GetActive() }) g.Attach(cbBtn, 0, 3, 1, 1) cbMnu, _ := gtk.CheckButtonNewWithLabel(voc["show-menu-images"]) cbMnu.SetTooltipText(fmt.Sprintf("%s, %s", voc["deprecated-since-gtk-310"], voc["ignored"])) cbMnu.SetActive(gtkConfig.menuImages) cbMnu.Connect("toggled", func() { gtkConfig.menuImages = cbMnu.GetActive() }) g.Attach(cbMnu, 0, 4, 1, 1) lbl, _ = gtk.LabelNew("") lbl.SetMarkup(fmt.Sprintf("%s", voc["sound-effects"])) lbl.SetProperty("halign", gtk.ALIGN_START) g.Attach(lbl, 0, 5, 1, 1) cbEventSounds, _ := gtk.CheckButtonNewWithLabel(voc["enable-event-sounds"]) cbEventSounds.SetActive(gsettings.eventSounds) cbEventSounds.Connect("toggled", func() { gsettings.eventSounds = cbEventSounds.GetActive() gtkConfig.enableEventSounds = cbEventSounds.GetActive() }) g.Attach(cbEventSounds, 0, 6, 1, 1) cbInputSounds, _ := gtk.CheckButtonNewWithLabel(voc["enable-input-feedback-sounds"]) cbInputSounds.SetActive(gsettings.inputFeedbackSounds) cbInputSounds.Connect("toggled", func() { gsettings.inputFeedbackSounds = cbInputSounds.GetActive() gtkConfig.enableInputFeedbackSounds = cbInputSounds.GetActive() }) g.Attach(cbInputSounds, 0, 7, 2, 1) return frame } func setUpProgramSettingsForm() *gtk.Frame { frame, _ := gtk.FrameNew(fmt.Sprintf(" %s ", voc["program-settings"])) frame.SetLabelAlign(0.5, 0.5) frame.SetProperty("margin", 6) g, _ := gtk.GridNew() g.SetRowSpacing(12) g.SetColumnSpacing(12) g.SetProperty("margin", 6) g.SetProperty("hexpand", true) g.SetProperty("vexpand", true) frame.Add(g) lbl, _ := gtk.LabelNew("") lbl.SetMarkup(fmt.Sprintf("%s", voc["files-to-export"])) lbl.SetProperty("halign", gtk.ALIGN_START) g.Attach(lbl, 0, 0, 1, 1) cb1, _ := gtk.CheckButtonNewWithLabel("~/.config/gtk-3.0/settings.ini") cb1.SetActive(preferences.ExportSettingsIni) cb1.Connect("toggled", func() { preferences.ExportSettingsIni = cb1.GetActive() }) g.Attach(cb1, 0, 1, 1, 1) cb2, _ := gtk.CheckButtonNewWithLabel("~/.gtkrc-2.0") cb2.SetActive(preferences.ExportGtkRc20) cb2.Connect("toggled", func() { preferences.ExportGtkRc20 = cb2.GetActive() }) g.Attach(cb2, 0, 2, 1, 1) cb3, _ := gtk.CheckButtonNewWithLabel("~/.icons/default/index.theme") cb3.SetActive(preferences.ExportIndexTheme) cb3.Connect("toggled", func() { preferences.ExportIndexTheme = cb3.GetActive() }) g.Attach(cb3, 0, 3, 1, 1) cb4, _ := gtk.CheckButtonNewWithLabel("~/.config/xsettingsd/xsettingsd.conf") cb4.SetActive(preferences.ExportXsettingsd) cb4.Connect("toggled", func() { preferences.ExportXsettingsd = cb4.GetActive() }) g.Attach(cb4, 0, 4, 1, 1) cb5, _ := gtk.CheckButtonNewWithLabel("~/.config/gtk-4.0/*") cb5.SetActive(preferences.ExportGtk4Symlinks) cb5.Connect("toggled", func() { preferences.ExportGtk4Symlinks = cb5.GetActive() }) g.Attach(cb5, 0, 5, 1, 1) btn, _ := gtk.ButtonNewWithLabel(voc["clear"]) btn.Connect("clicked", clearGtk4Symlinks) g.Attach(btn, 1, 5, 1, 1) return frame }