From 9000327973121d4c6b33b5a6e5f900c31654e55a Mon Sep 17 00:00:00 2001 From: Vishweshwar Saran Singh Deo Date: Tue, 17 Oct 2023 19:49:57 +0530 Subject: [PATCH 1/9] [bug 843] 843-Plugin-SaveLastSessionLayout-not-saving-layout-when-user-logs-out-or-shutdown-or-restart #843 - fixed Plugin SaveLastSessionLayout not saving layout when user logs out or shutdown or restart - signals added --- terminatorlib/plugins/save_last_session_layout.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/terminatorlib/plugins/save_last_session_layout.py b/terminatorlib/plugins/save_last_session_layout.py index 12a360ed..1d4e37c2 100644 --- a/terminatorlib/plugins/save_last_session_layout.py +++ b/terminatorlib/plugins/save_last_session_layout.py @@ -1,4 +1,5 @@ import os +import signal import sys # Fix imports when testing this file directly @@ -44,9 +45,21 @@ def save_session_layout(self, debugtab=False, widget=None, cwd=None, metadata=No r = config.add_layout("SaveLastSessionLayout", current_layout) config.save() return True + + def signal_handler(self,signum, frame): + + signame = signal.Signals(signum).name + dbg('signal handler called:signal %s (%s)' % + (signame, signum)) + self.save_session_layout() def connect_signals(self): dbg("SaveLastSessionLayout connect_signals") + + signal.signal(signal.SIGTERM, self.signal_handler) + signal.signal(signal.SIGCHLD, self.signal_handler) + signal.signal(signal.SIGHUP, self.signal_handler) + n = 0 for term in Terminator().terminals: dbg("SaveLastSessionLayout connect_signals to term num:(%d)" % n) From 03e5769bd175447aa586735bc5d7ad5094216eae Mon Sep 17 00:00:00 2001 From: Vishweshwar Saran Singh Deo Date: Thu, 26 Oct 2023 23:38:59 +0530 Subject: [PATCH 2/9] [bug 852] - 852-terminator_py_get_focussed_terminal_always_returns_none - added get_vte().has_focus() for a valid focussed terminal to return --- terminatorlib/terminator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terminatorlib/terminator.py b/terminatorlib/terminator.py index aede3336..1889cab6 100644 --- a/terminatorlib/terminator.py +++ b/terminatorlib/terminator.py @@ -614,7 +614,7 @@ def get_target_terms(self, widget): def get_focussed_terminal(self): """iterate over all the terminals to find which, if any, has focus""" for terminal in self.terminals: - if terminal.has_focus(): + if terminal.get_vte().has_focus(): return(terminal) return(None) From 3e7145034a7f1c5535b820e90bfc54f97e9ceaef Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Wed, 22 Nov 2023 11:24:25 +0000 Subject: [PATCH 3/9] Translate po/terminator.pot in ru 100% translated source file: 'po/terminator.pot' on 'ru'. --- po/ru.po | 341 +++++++++++++++++++------------------------------------ 1 file changed, 114 insertions(+), 227 deletions(-) diff --git a/po/ru.po b/po/ru.po index 6ccdae4e..4473f3d0 100644 --- a/po/ru.po +++ b/po/ru.po @@ -2,27 +2,25 @@ # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. -# +# # Translators: # Gnome Terminator , 2020 -# +# Mariya Shikunova , 2023 +# #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-01-22 00:51+0100\n" +"POT-Creation-Date: 2022-10-19 09:29-0400\n" "PO-Revision-Date: 2020-04-22 08:11+0000\n" -"Last-Translator: Gnome Terminator , 2020\n" -"Language-Team: Russian (https://www.transifex.com/terminator/teams/109338/" -"ru/)\n" -"Language: ru\n" +"Last-Translator: Mariya Shikunova , 2023\n" +"Language-Team: Russian (https://app.transifex.com/terminator/teams/109338/ru/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" -"%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n" -"%100>=11 && n%100<=14)? 2 : 3);\n" +"Language: ru\n" +"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);\n" #. Command uuid req. Description #: ../remotinator.py:39 @@ -47,7 +45,7 @@ msgstr "Вывести список всех терминалов" #: ../remotinator.py:44 msgid "Get the uuid of the current focused terminal" -msgstr "" +msgstr "Вывести UUID текущего терминала" #: ../remotinator.py:45 msgid "Get the UUID of a parent window" @@ -67,23 +65,23 @@ msgstr "Вывести заголовок родительской вкладк #: ../remotinator.py:49 msgid "Set the title of a parent tab" -msgstr "" +msgstr "Указать заголовок родительской вкладки" #: ../remotinator.py:50 msgid "Set the background image" -msgstr "" +msgstr "Указать фоновое изображение" #: ../remotinator.py:51 msgid "Set the background image for all terminals" -msgstr "" +msgstr "Указать фоновое изображение для всех терминалов" #: ../remotinator.py:52 msgid "Switch current terminal profile" -msgstr "" +msgstr "Переключиться на профиль текущего терминала" #: ../remotinator.py:53 msgid "Switch profile of all currently running terminals" -msgstr "" +msgstr "Переключиться на профиль всех текущих запущенных терминалов" #: ../remotinator.py:70 #, python-format @@ -112,23 +110,24 @@ msgstr "" #: ../remotinator.py:80 msgid "Profile name to switch to" -msgstr "" +msgstr "Имя профиля для переключения" #: ../remotinator.py:83 msgid "File to pass to command" -msgstr "" +msgstr "Файл для передачи команде" #: ../remotinator.py:86 msgid "Command to run in new terminal" -msgstr "" +msgstr "Команда для выполнения в новом терминале" #: ../remotinator.py:89 msgid "Tab name to set. Only used with \"set_tab_title\" command." msgstr "" +"Имя вкладки для установки. Используется только с командой «set_tab_title»." #: ../remotinator.py:92 msgid "Tab name to set." -msgstr "" +msgstr "Имя вкладки для установки." #: ../data/terminator.desktop.in.h:1 ../data/terminator.appdata.xml.in.h:1 #: ../terminatorlib/plugins/activitywatch.py:83 @@ -150,9 +149,14 @@ msgstr "Технологии будущего для терминалов" msgid "" "A power-user tool for arranging terminals. It is inspired by programs such " "as gnome-multi-term, quadkonsole, etc. in that the main focus is arranging " -"terminals in grids (tabs is the most common default method, which Terminator " -"also supports)." +"terminals in grids (tabs is the most common default method, which Terminator" +" also supports)." msgstr "" +"Удобный инструмент для настройки терминалов. На его создание вдохновили " +"такие программы, как gnome-multi-term и quadkonsole, в которых основное " +"внимание уделяется размещению терминалов в виде сетки (наиболее " +"распространённый метод по умолчанию — вкладки, который также поддерживается " +"в Terminator)" #: ../data/terminator.appdata.xml.in.h:5 msgid "" @@ -161,6 +165,10 @@ msgid "" "out in different directions with useful features for sysadmins and other " "users." msgstr "" +"Большая часть поведения Terminator основана на GNOME Terminal, и со временем" +" в приложение добавляется всё больше функций из него, однако также есть " +"планы по расширению Terminator в разных направлениях с помощью полезных " +"функций для системных администраторов и других пользователей." #: ../data/terminator.appdata.xml.in.h:6 msgid "Some highlights:" @@ -180,7 +188,7 @@ msgstr "Перетаскивание и изменение порядка тер #: ../data/terminator.appdata.xml.in.h:10 msgid "Lots of keyboard shortcuts" -msgstr "Множество сочетаний клавиш быстрого доступа" +msgstr "Множество комбинаций клавиш быстрого доступа" #: ../data/terminator.appdata.xml.in.h:11 msgid "Save multiple layouts and profiles via GUI preferences editor" @@ -225,7 +233,7 @@ msgid "" "This window has several terminals open. Closing the window will also close " "all terminals within it." msgstr "" -"Это окно имеет несколько запущенных терминалов. Закрытие окна повлечет за " +"Это окно имеет несколько запущенных терминалов. Закрытие окна повлечёт за " "собой так же и их закрытие." #: ../terminatorlib/container.py:178 @@ -233,8 +241,8 @@ msgid "" "This tab has several terminals open. Closing the tab will also close all " "terminals within it." msgstr "" -"Эта вкладка имеет несколько запущенных терминалов. Ее закрытие повлечет за " -"собой так же и их закрытие." +"Эта вкладка имеет несколько запущенных терминалов. Её закрытие повлечёт за " +"собой также и их закрытие." #: ../terminatorlib/container.py:198 msgid "Do not show this message next time" @@ -299,8 +307,8 @@ msgid "" "Use the rest of the command line as a command to execute inside the " "terminal, and its arguments" msgstr "" -"Использовать для выполнения в терминале остаток командной строки как команду " -"и её аргументы" +"Использовать для выполнения в терминале остаток командной строки как команду" +" и её аргументы" #: ../terminatorlib/optionparse.py:69 msgid "Specify a config file" @@ -308,7 +316,7 @@ msgstr "Укажите файл конфигурации" #: ../terminatorlib/optionparse.py:71 msgid "Specify a partial config json file" -msgstr "" +msgstr "Укажите частичный json-файл конфигурации" #: ../terminatorlib/optionparse.py:76 msgid "Set the working directory" @@ -316,7 +324,8 @@ msgstr "Установить рабочий каталог" #: ../terminatorlib/optionparse.py:77 msgid "Set a custom icon for the window (by file or name)" -msgstr "Установить пользовательский значок для этого окна (по файлу или имени)" +msgstr "" +"Установить пользовательский значок для этого окна (по файлу или имени)" #: ../terminatorlib/optionparse.py:80 msgid "Set a custom WM_WINDOW_ROLE property on the window" @@ -352,19 +361,19 @@ msgstr "Разделенный запятыми список методов дл #: ../terminatorlib/optionparse.py:96 msgid "If Terminator is already running, just open a new tab" -msgstr "Если Терминатор уже запущен, просто откройте новую вкладку" +msgstr "Если Terminator уже запущен, просто откройте новую вкладку" #: ../terminatorlib/optionparse.py:98 msgid "If Terminator is already running, just unhide all hidden windows" -msgstr "" +msgstr "Если Terminator уже запущен, раскройте все скрытые окна" #: ../terminatorlib/optionparse.py:100 msgid "List all profiles" -msgstr "" +msgstr "Вывести список всех профилей" #: ../terminatorlib/optionparse.py:102 msgid "List all layouts" -msgstr "" +msgstr "Вывести список всех компоновок" #: ../terminatorlib/plugins/activitywatch.py:54 msgid "Watch for _activity" @@ -482,7 +491,7 @@ msgstr "Название *%s* уже существует" #: ../terminatorlib/plugins/dir_open.py:26 msgid "Open current directory" -msgstr "" +msgstr "Открыть текущий каталог" #: ../terminatorlib/plugins/logger.py:21 #: ../terminatorlib/plugins/terminalshot.py:21 @@ -552,15 +561,15 @@ msgstr "Оставить терминал открытым" #: ../terminatorlib/preferences.glade.h:11 msgid "Black on light yellow" -msgstr "Черный на светло-жёлтом" +msgstr "Чёрный на светло-жёлтом" #: ../terminatorlib/preferences.glade.h:12 msgid "Black on white" -msgstr "Черный на белом" +msgstr "Чёрный на белом" #: ../terminatorlib/preferences.glade.h:13 msgid "Gray on black" -msgstr "серый на чёрном" +msgstr "Серый на чёрном" #: ../terminatorlib/preferences.glade.h:14 msgid "Green on black" @@ -572,7 +581,7 @@ msgstr "Белый на чёрном" #: ../terminatorlib/preferences.glade.h:16 msgid "Orange on black" -msgstr "Оранжевый на черном" +msgstr "Оранжевый на чёрном" #: ../terminatorlib/preferences.glade.h:17 msgid "Ambience" @@ -592,7 +601,7 @@ msgstr "Gruvbox светлая" #: ../terminatorlib/preferences.glade.h:21 msgid "Gruvbox dark" -msgstr "Gruvbox темная" +msgstr "Gruvbox тёмная" #: ../terminatorlib/preferences.glade.h:22 msgid "Custom" @@ -616,7 +625,7 @@ msgstr "Используемый в GNOME по умолчанию" #: ../terminatorlib/preferences.glade.h:27 msgid "Click to focus" -msgstr "Активизация при щелчке мышью" +msgstr "Фокус по щелчку" #: ../terminatorlib/preferences.glade.h:28 msgid "Follow mouse pointer" @@ -684,7 +693,7 @@ msgstr "Полноэкранный режим" #: ../terminatorlib/preferences.glade.h:45 msgid "Terminator Preferences" -msgstr "Терминатор Параметры" +msgstr "Параметры Terminator" #: ../terminatorlib/preferences.glade.h:46 msgid "Behavior" @@ -716,7 +725,7 @@ msgstr "Подсказка геометрии окна" #: ../terminatorlib/preferences.glade.h:53 msgid "DBus server" -msgstr "DBus сервер" +msgstr "Сервер DBus" #: ../terminatorlib/preferences.glade.h:54 msgid "Mouse focus:" @@ -728,11 +737,11 @@ msgstr "Режим трансляции нажатий клавиш" #: ../terminatorlib/preferences.glade.h:56 msgid "PuTTY style paste:" -msgstr "" +msgstr "Стиль вставки PuTTY:" #: ../terminatorlib/preferences.glade.h:57 msgid "Smart copy" -msgstr "\"Умное\" копирование" +msgstr "«Умное» копирование" #: ../terminatorlib/preferences.glade.h:58 msgid "Re-use profiles for new terminals" @@ -744,23 +753,23 @@ msgstr "Использовать пользовательский обработ #: ../terminatorlib/preferences.glade.h:60 msgid "PRIMARY" -msgstr "" +msgstr "PRIMARY" #: ../terminatorlib/preferences.glade.h:61 msgid "Clipboard" -msgstr "" +msgstr "Буфер обмена" #: ../terminatorlib/preferences.glade.h:62 msgid "Clear selection on copy" -msgstr "" +msgstr "Отменить выделение при копировании" #: ../terminatorlib/preferences.glade.h:63 msgid "Open links with a single click (instead of Ctrl-left click)" -msgstr "" +msgstr "Открывать ссылки щелчком мыши (вместо Ctrl+ЛКМ)" #: ../terminatorlib/preferences.glade.h:64 msgid "Disable mouse paste" -msgstr "" +msgstr "Отключить вставку мышью" #: ../terminatorlib/preferences.glade.h:65 msgid "Custom URL handler:" @@ -788,11 +797,11 @@ msgstr "Дополнительные стили (зависит от темы)" #: ../terminatorlib/preferences.glade.h:71 msgid "Cell Height:" -msgstr "" +msgstr "Высота ячейки:" #: ../terminatorlib/preferences.glade.h:72 msgid "Cell Width:" -msgstr "" +msgstr "Ширина ячейки:" #: ../terminatorlib/preferences.glade.h:73 msgid "Tab position:" @@ -808,7 +817,7 @@ msgstr "Кнопки переключения вкладок" #: ../terminatorlib/preferences.glade.h:76 msgid "Title bar at bottom (Require restart)" -msgstr "" +msgstr "Панель заголовка внизу (требуется перезапуск)" #: ../terminatorlib/preferences.glade.h:77 msgid "Global" @@ -844,11 +853,11 @@ msgstr "Копирование на выбор" #: ../terminatorlib/preferences.glade.h:85 msgid "Disable Ctrl+mousewheel zoom" -msgstr "" +msgstr "Отключить масштабирование через Ctrl+колёсико мыши" #: ../terminatorlib/preferences.glade.h:86 msgid "Select-by-_word characters:" -msgstr "Выбор _слов по символам:" +msgstr "Символы, выделяемые по _слову:" #: ../terminatorlib/preferences.glade.h:87 msgid "Cursor" @@ -864,11 +873,11 @@ msgstr "Мерцание" #: ../terminatorlib/preferences.glade.h:90 msgid "Use default colors" -msgstr "" +msgstr "Цвет по умолчанию" #: ../terminatorlib/preferences.glade.h:91 msgid "Foreground:" -msgstr "" +msgstr "Передний план:" #: ../terminatorlib/preferences.glade.h:92 msgid "Background:" @@ -928,11 +937,11 @@ msgstr "Встроенные с_хемы:" #: ../terminatorlib/preferences.glade.h:107 msgid "_Foreground:" -msgstr "" +msgstr "_Передний план" #: ../terminatorlib/preferences.glade.h:108 msgid "_Background:" -msgstr "" +msgstr "_Задний план:" #: ../terminatorlib/preferences.glade.h:109 msgid "Palette" @@ -948,7 +957,7 @@ msgstr "Цветовая _палитра:" #: ../terminatorlib/preferences.glade.h:112 msgid "Show b_old text in bright colors" -msgstr "" +msgstr "Показывать п_олужирный текст в светлых цветах" #: ../terminatorlib/preferences.glade.h:113 msgid "Colors" @@ -964,19 +973,19 @@ msgstr "_Прозрачный фон" #: ../terminatorlib/preferences.glade.h:116 msgid "Background Image" -msgstr "" +msgstr "Фоновое изображение" #: ../terminatorlib/preferences.glade.h:117 msgid "Background Image File:" -msgstr "" +msgstr "Файл фонового изображения" #: ../terminatorlib/preferences.glade.h:118 msgid "Choose file" -msgstr "" +msgstr "Выберите файл" #: ../terminatorlib/preferences.glade.h:119 msgid "S_hade background:" -msgstr "" +msgstr "З_атенение фона:" #: ../terminatorlib/preferences.glade.h:120 msgid "None" @@ -1022,12 +1031,12 @@ msgstr "Прокрутка" msgid "" "Note: These options may cause some applications to behave " "incorrectly. They are only here to allow you to work around certain " -"applications and operating systems that expect different terminal behavior." +"applications and operating systems that expect different terminal " +"behavior." msgstr "" "Замечание: Эти параметры могут вызвать некорректную работу " "некоторых приложений. Они представлены только для того, чтобы позволить " -"работать с некоторыми приложениями и операционными ситемами, ожидающими " +"работать с некоторыми приложениями и операционными системами, ожидающими " "другого поведения терминала. " #: ../terminatorlib/preferences.glade.h:131 @@ -1072,7 +1081,7 @@ msgstr "Укажите шрифт заголовка" #: ../terminatorlib/preferences.glade.h:141 msgid "Titlebar" -msgstr "" +msgstr "Заголовок окна" #: ../terminatorlib/preferences.glade.h:142 #: ../terminatorlib/terminal_popup_menu.py:204 @@ -1113,11 +1122,11 @@ msgstr "Комбинации клавиш" #: ../terminatorlib/preferences.glade.h:153 msgid "Plugin" -msgstr "Надстройка" +msgstr "Модуль" #: ../terminatorlib/preferences.glade.h:154 msgid "This plugin has no configuration options" -msgstr "Этот плагин не имеет параметров конфигурации" +msgstr "Этот модуль не имеет параметров конфигурации" #: ../terminatorlib/preferences.glade.h:155 msgid "Plugins" @@ -1125,32 +1134,17 @@ msgstr "Модули" #: ../terminatorlib/preferences.glade.h:158 msgid "Version: 2.1.1" -msgstr "" +msgstr "Версия: 2.1.1" #: ../terminatorlib/preferences.glade.h:159 msgid "" -"The goal of this project is to produce a useful tool for arranging " -"terminals. It is inspired by programs such as gnome-multi-term, quadkonsole, " -"etc. in that the main focus is arranging terminals in grids (tabs is the " -"most common default method, which Terminator also supports).\n" +"The goal of this project is to produce a useful tool for arranging terminals. It is inspired by programs such as gnome-multi-term, quadkonsole, etc. in that the main focus is arranging terminals in grids (tabs is the most common default method, which Terminator also supports).\n" "\n" -"Much of the behavior of Terminator is based on GNOME Terminal, and we are " -"adding more features from that as time goes by, but we also want to extend " -"out in different directions with useful features for sysadmins and other " -"users. If you have any suggestions, please file wishlist bugs! (see left for " -"the Development link)" +"Much of the behavior of Terminator is based on GNOME Terminal, and we are adding more features from that as time goes by, but we also want to extend out in different directions with useful features for sysadmins and other users. If you have any suggestions, please file wishlist bugs! (see left for the Development link)" msgstr "" -"Задачей данного проекта является создание удобного инструмента для " -"совмещения терминалов. Вдохновленный такими программами как gnome-multi-" -"term, quadkonsole и подобных, он прежде всего нацелен собирать терминалы в " -"сетки (которые в свою очередь могут разноситься по вкладкам, которые, кстати " -"Terminator так же поддерживает).\n" +"Задачей данного проекта является создание удобного инструмента для совмещения терминалов. Вдохновлённый такими программами как gnome-multi-term, quadkonsole и подобных, он прежде всего нацелен собирать терминалы в сетки (которые в свою очередь могут разноситься по вкладкам, которые, кстати Terminator так же поддерживает).\n" "\n" -"Большая часть функционала заимствована из GNOME Terminal и мы со временем " -"добавляем больше разных плюшек оттуда. Но хотелось бы как-то еще расширить " -"его возможности для сисадминов и прочих пользователей. Если у вас есть какие-" -"либо предложения, пожалуйста озвучьте их на багтрекере (wishlist bugs)! (см. " -"сайт разработчиков)" +"Большая часть функционала заимствована из GNOME Terminal и мы со временем добавляем больше разных плюшек оттуда. Но хотелось бы как-то еще расширить его возможности для сисадминов и прочих пользователей. Если у вас есть какие-либо предложения, пожалуйста озвучьте их на багтрекере (wishlist bugs)! (см. сайт разработчиков)" #: ../terminatorlib/preferences.glade.h:162 msgid "The Manual" @@ -1159,9 +1153,10 @@ msgstr "Руководство" #: ../terminatorlib/preferences.glade.h:163 msgid "" "Development\n" -"Bugs / " -"Enhancements" +"Bugs / Enhancements" msgstr "" +"Разработка\n" +"Ошибки / Улучшения" #: ../terminatorlib/preferences.glade.h:165 msgid "About" @@ -1181,15 +1176,15 @@ msgstr "Восстановить размер шрифта" #: ../terminatorlib/prefseditor.py:107 msgid "Increase font size on all terminals" -msgstr "" +msgstr "Увеличить размер шрифта во всех терминалах" #: ../terminatorlib/prefseditor.py:108 msgid "Decrease font size on all terminals" -msgstr "" +msgstr "Уменьшить размер шрифта во всех терминалах" #: ../terminatorlib/prefseditor.py:109 msgid "Restore original font size on all terminals" -msgstr "" +msgstr "Восстановить исходный размер шрифта во всех терминалах" #: ../terminatorlib/prefseditor.py:110 msgid "Create a new tab" @@ -1249,7 +1244,7 @@ msgstr "Вставить из буфера обмена" #: ../terminatorlib/prefseditor.py:126 msgid "Paste primary selection" -msgstr "" +msgstr "Вставить первоначальный выделенный фрагмент" #: ../terminatorlib/prefseditor.py:127 msgid "Show/Hide the scrollbar" @@ -1385,7 +1380,7 @@ msgstr "Показать/Скрыть окно" #: ../terminatorlib/prefseditor.py:160 msgid "Create new group" -msgstr "" +msgstr "Создать новую группу" #: ../terminatorlib/prefseditor.py:161 msgid "Group all terminals" @@ -1393,7 +1388,7 @@ msgstr "Группировать все терминалы" #: ../terminatorlib/prefseditor.py:162 msgid "Group/Ungroup all terminals" -msgstr "Группировать/разрознить все терминалы" +msgstr "Группировать/разделить все терминалы" #: ../terminatorlib/prefseditor.py:163 msgid "Ungroup all terminals" @@ -1401,15 +1396,15 @@ msgstr "Разгруппировать все терминалы" #: ../terminatorlib/prefseditor.py:164 msgid "Group terminals in window" -msgstr "" +msgstr "Группировать терминалы в окне" #: ../terminatorlib/prefseditor.py:165 msgid "Group/Ungroup terminals in window" -msgstr "" +msgstr "Группировать/разделить терминалы в окне" #: ../terminatorlib/prefseditor.py:166 msgid "Ungroup terminals in window" -msgstr "" +msgstr "Разгруппировать терминалы в окне" #: ../terminatorlib/prefseditor.py:167 msgid "Group terminals in tab" @@ -1417,7 +1412,7 @@ msgstr "Группировать терминалы во вкладке" #: ../terminatorlib/prefseditor.py:168 msgid "Group/Ungroup terminals in tab" -msgstr "Группировать/разрознить терминалы во вкладке" +msgstr "Группировать/разделить терминалы во вкладке" #: ../terminatorlib/prefseditor.py:169 msgid "Ungroup terminals in tab" @@ -1429,7 +1424,7 @@ msgstr "Создать новое окно" #: ../terminatorlib/prefseditor.py:171 msgid "Spawn a new Terminator process" -msgstr "Создать новый процесс Terminator'а" +msgstr "Создать новый процесс Terminator" #: ../terminatorlib/prefseditor.py:172 msgid "Don't broadcast key presses" @@ -1449,7 +1444,7 @@ msgstr "Вставить номер терминала" #: ../terminatorlib/prefseditor.py:176 msgid "Insert padded terminal number" -msgstr "Вставить номер терминала" +msgstr "Вставить дополненный номер терминала" #: ../terminatorlib/prefseditor.py:177 msgid "Edit window title" @@ -1477,17 +1472,17 @@ msgstr "Переключиться на предыдущий профиль" #: ../terminatorlib/prefseditor.py:183 msgid "Open the Preferences window" -msgstr "" +msgstr "Открыть окно параметров" #: ../terminatorlib/prefseditor.py:184 msgid "Open the manual" msgstr "Открыть руководство" -#: ../terminatorlib/prefseditor.py:1370 +#: ../terminatorlib/prefseditor.py:1366 msgid "New Profile" msgstr "Создать профиль" -#: ../terminatorlib/prefseditor.py:1413 ../terminatorlib/prefseditor.py:1418 +#: ../terminatorlib/prefseditor.py:1409 ../terminatorlib/prefseditor.py:1414 msgid "New Layout" msgstr "Новое расположение" @@ -1534,7 +1529,7 @@ msgstr "Вст_авить" #: ../terminatorlib/terminal_popup_menu.py:112 msgid "Set W_indow Title" -msgstr "" +msgstr "Указать название о_кна" #: ../terminatorlib/terminal_popup_menu.py:117 msgid "Split H_orizontally" @@ -1574,7 +1569,7 @@ msgstr "Группирование" #: ../terminatorlib/terminal_popup_menu.py:186 msgid "Relaunch Command" -msgstr "" +msgstr "Перезапустить команду" #: ../terminatorlib/terminal_popup_menu.py:191 msgid "Show _scrollbar" @@ -1582,7 +1577,7 @@ msgstr "Показать полосу прокрутки" #: ../terminatorlib/terminal_popup_menu.py:248 msgid "_Layouts..." -msgstr "" +msgstr "_Шаблоны..." #: ../terminatorlib/terminal.py:481 msgid "N_ew group..." @@ -1599,11 +1594,11 @@ msgstr "Удалить группу %s" #: ../terminatorlib/terminal.py:512 msgid "G_roup all in window" -msgstr "" +msgstr "С_группировать всё в окне" #: ../terminatorlib/terminal.py:517 msgid "Ungro_up all in window" -msgstr "" +msgstr "Раз_группировать всё в окне" #: ../terminatorlib/terminal.py:522 msgid "G_roup all in tab" @@ -1611,7 +1606,7 @@ msgstr "С_группировать всё во вкладке" #: ../terminatorlib/terminal.py:527 msgid "Ungro_up all in tab" -msgstr "Раз_рознить терминалы во вкладке" +msgstr "Раз_группировать терминалы во вкладке" #: ../terminatorlib/terminal.py:532 msgid "Remove all groups" @@ -1650,25 +1645,25 @@ msgstr "_Добавить номер терминала" msgid "Insert _padded terminal number" msgstr "Вставить _номер терминала" -#: ../terminatorlib/terminal.py:1490 +#: ../terminatorlib/terminal.py:1492 msgid "Unable to find a shell" msgstr "Не удается найти оболочку (shell)" -#: ../terminatorlib/terminal.py:1521 +#: ../terminatorlib/terminal.py:1546 msgid "Unable to start shell:" msgstr "Не удается запустить оболочку:" -#: ../terminatorlib/terminal.py:1975 +#: ../terminatorlib/terminal.py:2000 msgid "Rename Window" msgstr "Переименование окна" -#: ../terminatorlib/terminal.py:1983 +#: ../terminatorlib/terminal.py:2008 msgid "Enter a new title for the Terminator window..." msgstr "Введите новое название для окна Terminator..." #: ../terminatorlib/titlebar.py:112 msgid "[INACTIVE: Right-Click for Relaunch option] " -msgstr "" +msgstr "[НЕАКТИВНО: Щёлкните правой кнопкой мыши для функции перезапуска] " #: ../terminatorlib/titlebar.py:258 msgid "Alpha" @@ -1773,117 +1768,9 @@ msgstr "окно" #: ../terminatorlib/window.py:773 #, python-format msgid "Window group %s" -msgstr "" +msgstr "Группа окон %s" #: ../terminatorlib/window.py:799 #, python-format msgid "Tab %d" msgstr "Вкладка %d" - -#~ msgid "Current Locale" -#~ msgstr "Текущая языковая настройка" - -#~ msgid "Western" -#~ msgstr "Западная" - -#~ msgid "Central European" -#~ msgstr "Центрально-европейская" - -#~ msgid "South European" -#~ msgstr "Южно-европейская" - -#~ msgid "Baltic" -#~ msgstr "Балтийская" - -#~ msgid "Cyrillic" -#~ msgstr "Кириллица" - -#~ msgid "Arabic" -#~ msgstr "Арабская" - -#~ msgid "Greek" -#~ msgstr "Греческая" - -#~ msgid "Hebrew Visual" -#~ msgstr "Иврит (Визуальный)" - -#~ msgid "Hebrew" -#~ msgstr "Иврит" - -#~ msgid "Turkish" -#~ msgstr "Турецкая" - -#~ msgid "Nordic" -#~ msgstr "Скандинавская" - -#~ msgid "Celtic" -#~ msgstr "Кельтская" - -#~ msgid "Romanian" -#~ msgstr "Румынская" - -#~ msgid "Unicode" -#~ msgstr "Юникод" - -#~ msgid "Armenian" -#~ msgstr "Армянская" - -#~ msgid "Chinese Traditional" -#~ msgstr "Традиционная китайская" - -#~ msgid "Cyrillic/Russian" -#~ msgstr "Кириллица/Русская" - -#~ msgid "Japanese" -#~ msgstr "Японская" - -#~ msgid "Korean" -#~ msgstr "Корейская" - -#~ msgid "Chinese Simplified" -#~ msgstr "Упрощенная китайская" - -#~ msgid "Georgian" -#~ msgstr "Грузинская" - -#~ msgid "Cyrillic/Ukrainian" -#~ msgstr "Кириллица/Украина" - -#~ msgid "Croatian" -#~ msgstr "Хорватская" - -#~ msgid "Hindi" -#~ msgstr "Хинди" - -#~ msgid "Persian" -#~ msgstr "Персидская" - -#~ msgid "Gujarati" -#~ msgstr "Гуарати (Индия)" - -#~ msgid "Gurmukhi" -#~ msgstr "Гармукхи" - -#~ msgid "Icelandic" -#~ msgstr "Исладнский" - -#~ msgid "Vietnamese" -#~ msgstr "Вьетнамский" - -#~ msgid "Thai" -#~ msgstr "Тайский" - -#~ msgid "Encoding:" -#~ msgstr "Кодировка:" - -#~ msgid "Encodings" -#~ msgstr "Кодировки" - -#~ msgid "Default" -#~ msgstr "По умолчанию" - -#~ msgid "User defined" -#~ msgstr "Пользовательский" - -#~ msgid "Other Encodings" -#~ msgstr "Другие кодировки" From d36f148a26fed4492c70eaa385d6aff761af9d46 Mon Sep 17 00:00:00 2001 From: mark doerr Date: Sat, 3 Feb 2024 12:26:15 +0100 Subject: [PATCH 4/9] feat: plugin - save current user layout --- .../plugins/save_user_session_layout.py | 69 +++++++++++++++++++ terminatorlib/version.py | 2 +- 2 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 terminatorlib/plugins/save_user_session_layout.py diff --git a/terminatorlib/plugins/save_user_session_layout.py b/terminatorlib/plugins/save_user_session_layout.py new file mode 100644 index 00000000..a640a4cd --- /dev/null +++ b/terminatorlib/plugins/save_user_session_layout.py @@ -0,0 +1,69 @@ +import os +import sys + +# Fix imports when testing this file directly +if __name__ == '__main__': + sys.path.append( os.path.join(os.path.dirname(__file__), "../..")) + +from gi.repository import Gtk,Vte + +from terminatorlib.config import Config +import terminatorlib.plugin as plugin +from terminatorlib.translation import _ +from terminatorlib.util import get_config_dir, err, dbg, gerr +from terminatorlib.terminator import Terminator +from terminatorlib import util + + +# AVAILABLE must contain a list of all the classes that you want exposed +AVAILABLE = ['SaveUserSessionLayout'] + +class SaveUserSessionLayout(plugin.MenuItem): + capabilities = ['terminal_menu', 'session'] + + config = None + conf_file = os.path.join(get_config_dir(),"save_last_session_cwd") + conf_sessions = [] + emit_close_count = 0 + + vte_version = Vte.get_minor_version() + + def __init__(self): + dbg("SaveUserSessionLayout Init") + plugin.MenuItem.__init__(self) + + def callback(self, menuitems, menu, terminal): + """ Add save menu item to the menu""" + vte_terminal = terminal.get_vte() + item = Gtk.MenuItem.new_with_mnemonic(_('Save _UserSessionLayout')) + item.connect("activate", self.save_all_session_layouts, terminal) + menuitems.append(item) + + def save_all_session_layouts(self, menuitem, terminal): + for term in Terminator().terminals: + self.save_session_layout("", "") + + #not used, but capability can be used to load automatically + def load_session_layout(self, debugtab=False, widget=None, cwd=None, metadata=None, profile=None): + dbg("SaveUserSessionLayout load layout") + terminator = Terminator() + util.spawn_new_terminator(terminator.origcwd, ['-u', '-l', 'SaveUserSessionLayout']) + + def save_session_layout(self, debugtab=False, widget=None, cwd=None, metadata=None, profile=None): + + config = Config() + terminator = Terminator() + current_layout = terminator.describe_layout(save_cwd = True) + dbg("SaveUserSessionLayout: save layout(%s)" % current_layout) + res = config.replace_layout("SaveUserSessionLayout", current_layout) + if (not res): + r = config.add_layout("SaveUserSessionLayout", current_layout) + config.save() + return True + + + def close(self, term, event, arg1 = None): + if (self.emit_close_count == 0): + self.emit_close_count = self.emit_close_count + 1 + self.save_session_layout("", "") + diff --git a/terminatorlib/version.py b/terminatorlib/version.py index e0aa5fcb..d9e093e2 100644 --- a/terminatorlib/version.py +++ b/terminatorlib/version.py @@ -20,4 +20,4 @@ """ APP_NAME = 'terminator' -APP_VERSION = '2.1.3' +APP_VERSION = '2.1.4' From 36b3602292fc1321dbd03aaff6215ec4a232d7f0 Mon Sep 17 00:00:00 2001 From: Matt Rose Date: Thu, 15 Feb 2024 21:44:14 -0500 Subject: [PATCH 5/9] fix syntax warnings Got the following Warnings when running under python 3.12.1 home/mattrose/Code/terminator/terminatorlib/terminal.py:324: SyntaxWarning: invalid escape sequence '\[' hostchars = "-A-Za-z0-9:\[\]" /home/mattrose/Code/terminator/terminatorlib/terminal.py:348: SyntaxWarning: invalid escape sequence '\.' "(www|ftp)[" + hostchars + "]*\.[" + hostchars + /home/mattrose/Code/terminator/terminatorlib/terminal.py:354: SyntaxWarning: invalid escape sequence '\.' "[a-zA-Z0-9-]*\.[a-zA-Z0-9][a-zA-Z0-9-]+" + /home/mattrose/Code/terminator/terminatorlib/terminal.py:359: SyntaxWarning: invalid escape sequence '\^' """news:[-A-Z\^_a-z{|}~!"#$%&'()*+,./0-9;:=?`]+@""" + To fix this, I changed the strings to raw strings to pass to the regex --- terminatorlib/terminal.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/terminatorlib/terminal.py b/terminatorlib/terminal.py index 3c82deda..c34b7ab6 100644 --- a/terminatorlib/terminal.py +++ b/terminatorlib/terminal.py @@ -321,7 +321,7 @@ def update_url_matches(self): """Update the regexps used to match URLs""" userchars = "-A-Za-z0-9" passchars = "-A-Za-z0-9,?;.:/!%$^*&~\"#'" - hostchars = "-A-Za-z0-9:\[\]" + hostchars = r"-A-Za-z0-9:\[\]" pathchars = "-A-Za-z0-9_$.+!*(),;:@&=?/~#%'" schemes = "(news:|telnet:|nntp:|file:/|https?:|ftps?:|webcal:|ssh:)" user = "[" + userchars + "]+(:[" + passchars + "]+)?" @@ -345,18 +345,18 @@ def update_url_matches(self): self._add_regex('voip', re) re = (lboundry + - "(www|ftp)[" + hostchars + "]*\.[" + hostchars + + "(www|ftp)[" + hostchars + r"]*\.[" + hostchars + ".]+(:[0-9]+)?(" + urlpath + ")?" + rboundry + "/?") self._add_regex('addr_only', re) re = (lboundry + "(mailto:)?[a-zA-Z0-9][a-zA-Z0-9.+-]*@[a-zA-Z0-9]" + - "[a-zA-Z0-9-]*\.[a-zA-Z0-9][a-zA-Z0-9-]+" + + r"[a-zA-Z0-9-]*\.[a-zA-Z0-9][a-zA-Z0-9-]+" + "[.a-zA-Z0-9-]*" + rboundry) self._add_regex('email', re) re = (lboundry + - """news:[-A-Z\^_a-z{|}~!"#$%&'()*+,./0-9;:=?`]+@""" + + r"""news:[-A-Z\^_a-z{|}~!"#$%&'()*+,./0-9;:=?`]+@""" + "[-A-Za-z0-9.]+(:[0-9]+)?" + rboundry) self._add_regex('nntp', re) From 7f581fde86cb8d8b5014fa6a2e929f7d73e3dbb7 Mon Sep 17 00:00:00 2001 From: Matt Rose Date: Thu, 15 Feb 2024 22:16:33 -0500 Subject: [PATCH 6/9] fix traceback in layout_done Got this while testing layouts. Easy fix Traceback (most recent call last): File "/home/mattrose/Code/terminator/./terminator", line 137, in TERMINATOR.layout_done() File "/home/mattrose/Code/terminator/terminatorlib/terminator.py", line 341, in layout_done term.ensure_visible_and_focussed() ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AttributeError: 'NoneType' object has no attribute 'ensure_visible_and_focussed' --- terminatorlib/terminator.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/terminatorlib/terminator.py b/terminatorlib/terminator.py index 4da976f7..8b5ac0d9 100644 --- a/terminatorlib/terminator.py +++ b/terminatorlib/terminator.py @@ -338,7 +338,8 @@ def layout_done(self): # For windows without a notebook ensure Terminal is visible and focused if window_last_active_term_mapping[window]: term = self.find_terminal_by_uuid(window_last_active_term_mapping[window].urn) - term.ensure_visible_and_focussed() + if term: + term.ensure_visible_and_focussed() # Build list of new windows using prelayout list new_win_list = [] From 5d0904b619bf852008227cd7e2e34e0c06dbb7db Mon Sep 17 00:00:00 2001 From: mark doerr Date: Fri, 16 Feb 2024 19:36:41 +0100 Subject: [PATCH 7/9] fix: version re-set to 2.1.3 --- terminatorlib/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terminatorlib/version.py b/terminatorlib/version.py index d9e093e2..e0aa5fcb 100644 --- a/terminatorlib/version.py +++ b/terminatorlib/version.py @@ -20,4 +20,4 @@ """ APP_NAME = 'terminator' -APP_VERSION = '2.1.4' +APP_VERSION = '2.1.3' From 4e6e2937713766a29918a8f327bf285cbc8954f7 Mon Sep 17 00:00:00 2001 From: Matt Rose Date: Mon, 11 Mar 2024 15:17:51 -0400 Subject: [PATCH 8/9] Properly parse file:/// URIs Previously, file URIs were lumped in with full uris that would be used for HTTP, FTP, etc. This caused file:/// uri parser to ignore any file with a root dir that had a character that was not a valid hostname character to be ignored. --- terminatorlib/terminal.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/terminatorlib/terminal.py b/terminatorlib/terminal.py index c34b7ab6..2aaf58d7 100644 --- a/terminatorlib/terminal.py +++ b/terminatorlib/terminal.py @@ -300,6 +300,7 @@ def load_plugins(self, force = False): registry.load_plugins(force) def _add_regex(self, name, re): + dbg(f"adding regex: {re}") match = -1 if regex.FLAGS_PCRE2: try: @@ -323,19 +324,23 @@ def update_url_matches(self): passchars = "-A-Za-z0-9,?;.:/!%$^*&~\"#'" hostchars = r"-A-Za-z0-9:\[\]" pathchars = "-A-Za-z0-9_$.+!*(),;:@&=?/~#%'" - schemes = "(news:|telnet:|nntp:|file:/|https?:|ftps?:|webcal:|ssh:)" + schemes = "(news:|telnet:|nntp:|https?:|ftps?:|webcal:|ssh:)" user = "[" + userchars + "]+(:[" + passchars + "]+)?" urlpath = "/[" + pathchars + "]*[^]'.}>) \t\r\n,\\\"]" lboundry = "\\b" rboundry = "\\b" + re = (lboundry + "file:/" + "//?(:[0-9]+)?(" + urlpath + ")" + + rboundry + "/?") + self._add_regex('file', re) + re = (lboundry + schemes + "//(" + user + "@)?[" + hostchars +".]+(:[0-9]+)?(" + urlpath + ")?" + rboundry + "/?") self._add_regex('full_uri', re) - if self.matches['full_uri'] == -1: + if self.matches['full_uri'] == -1 or self.matches['file'] == -1: err ('Terminal::update_url_matches: Failed adding URL matches') else: re = (lboundry + From 520fd1f6f79d10a57d925baebaf9f1d42c31381b Mon Sep 17 00:00:00 2001 From: Rick Calixte <10281587+rcalixte@users.noreply.github.com> Date: Wed, 10 Jan 2024 15:28:49 -0500 Subject: [PATCH 9/9] Add support for a second keybind per action Modifying code: - terminatorlib/config.py - terminatorlib/keybindings.py - terminatorlib/plugin.py - terminatorlib/preferences.glade - terminatorlib/prefseditor.py - terminatorlib/terminal_popup_menu.py - terminatorlib/window.py Modifying test: - tests/test_prefseditor_keybindings.py Closes #371 --- terminatorlib/config.py | 731 +++++++++++++------------- terminatorlib/keybindings.py | 38 +- terminatorlib/plugin.py | 124 +++-- terminatorlib/preferences.glade | 31 +- terminatorlib/prefseditor.py | 575 +++++++++++--------- terminatorlib/terminal_popup_menu.py | 107 ++-- terminatorlib/window.py | 244 ++++----- tests/test_prefseditor_keybindings.py | 67 ++- 8 files changed, 996 insertions(+), 921 deletions(-) diff --git a/terminatorlib/config.py b/terminatorlib/config.py index 008d1cfa..3f93a140 100644 --- a/terminatorlib/config.py +++ b/terminatorlib/config.py @@ -66,7 +66,7 @@ >>> config.options_set({}) >>> config.options_get() {} ->>> +>>> """ @@ -81,211 +81,211 @@ from gi.repository import Gio DEFAULTS = { - 'global_config': { - 'dbus' : True, - 'focus' : 'click', - 'handle_size' : -1, - 'geometry_hinting' : False, - 'window_state' : 'normal', - 'borderless' : False, - 'extra_styling' : True, - 'tab_position' : 'top', - 'broadcast_default' : 'group', - 'close_button_on_tab' : True, - 'scroll_tabbar' : False, - 'homogeneous_tabbar' : True, - 'hide_from_taskbar' : False, - 'always_on_top' : False, - 'hide_on_lose_focus' : False, - 'sticky' : False, - 'use_custom_url_handler': False, - 'custom_url_handler' : '', - 'inactive_color_offset': 0.8, - 'inactive_bg_color_offset': 1.0, - 'enabled_plugins' : ['LaunchpadBugURLHandler', - 'LaunchpadCodeURLHandler', - 'APTURLHandler'], - 'ask_before_closing' : 'multiple_terminals', - 'always_split_with_profile': False, - 'putty_paste_style' : False, - 'putty_paste_style_source_clipboard': False, - 'disable_mouse_paste' : False, - 'smart_copy' : True, - 'clear_select_on_copy' : False, - 'cell_width' : 1.0, - 'cell_height' : 1.0, - 'case_sensitive' : True, - 'invert_search' : False, - 'link_single_click' : False, - 'title_at_bottom' : False, - 'detachable_tabs' : True, - - 'new_tab_after_current_tab': False, - }, - 'keybindings': { - 'zoom_in' : 'plus', - 'zoom_out' : 'minus', - 'zoom_normal' : '0', - 'zoom_in_all' : '', - 'zoom_out_all' : '', - 'zoom_normal_all' : '', - 'new_tab' : 't', - 'cycle_next' : 'Tab', - 'cycle_prev' : 'Tab', - 'go_next' : 'n', - 'go_prev' : 'p', - 'go_up' : 'Up', - 'go_down' : 'Down', - 'go_left' : 'Left', - 'go_right' : 'Right', - 'rotate_cw' : 'r', - 'rotate_ccw' : 'r', - 'split_auto' : 'a', - 'split_horiz' : 'o', - 'split_vert' : 'e', - 'close_term' : 'w', - 'copy' : 'c', - 'paste' : 'v', - 'paste_selection' : '', - 'toggle_scrollbar' : 's', - 'search' : 'f', - 'page_up' : '', - 'page_down' : '', - 'page_up_half' : '', - 'page_down_half' : '', - 'line_up' : '', - 'line_down' : '', - 'close_window' : 'q', - 'resize_up' : 'Up', - 'resize_down' : 'Down', - 'resize_left' : 'Left', - 'resize_right' : 'Right', - 'move_tab_right' : 'Page_Down', - 'move_tab_left' : 'Page_Up', - 'toggle_zoom' : 'x', - 'scaled_zoom' : 'z', - 'next_tab' : 'Page_Down', - 'prev_tab' : 'Page_Up', - 'switch_to_tab_1' : '', - 'switch_to_tab_2' : '', - 'switch_to_tab_3' : '', - 'switch_to_tab_4' : '', - 'switch_to_tab_5' : '', - 'switch_to_tab_6' : '', - 'switch_to_tab_7' : '', - 'switch_to_tab_8' : '', - 'switch_to_tab_9' : '', - 'switch_to_tab_10' : '', - 'full_screen' : 'F11', - 'reset' : 'r', - 'reset_clear' : 'g', - 'hide_window' : 'a', - 'create_group' : '', - 'group_all' : 'g', - 'group_all_toggle' : '', - 'ungroup_all' : 'g', - 'group_win' : '', - 'group_win_toggle' : '', - 'ungroup_win' : 'w', - 'group_tab' : 't', - 'group_tab_toggle' : '', - 'ungroup_tab' : 't', - 'new_window' : 'i', - 'new_terminator' : 'i', - 'broadcast_off' : '', - 'broadcast_group' : '', - 'broadcast_all' : '', - 'insert_number' : '1', - 'insert_padded' : '0', - 'edit_window_title': 'w', - 'edit_tab_title' : 'a', - 'edit_terminal_title': 'x', - 'layout_launcher' : 'l', - 'next_profile' : '', - 'previous_profile' : '', - 'preferences' : '', - 'preferences_keybindings' : 'k', - 'help' : 'F1' - }, - 'profiles': { - 'default': { - 'allow_bold' : True, - 'audible_bell' : False, - 'visible_bell' : False, - 'urgent_bell' : False, - 'icon_bell' : True, - 'background_color' : '#000000', - 'background_darkness' : 0.5, - 'background_type' : 'solid', - 'background_image' : '', - 'background_image_mode' : 'stretch_and_fill', - 'background_image_align_horiz': 'center', - 'background_image_align_vert' : 'middle', - 'backspace_binding' : 'ascii-del', - 'delete_binding' : 'escape-sequence', - 'cursor_blink' : True, - 'cursor_shape' : 'block', - 'cursor_fg_color' : '', - 'cursor_bg_color' : '', - 'cursor_color_default' : True, - 'term' : 'xterm-256color', - 'colorterm' : 'truecolor', - 'font' : 'Mono 10', - 'foreground_color' : '#aaaaaa', - 'show_titlebar' : True, - 'scrollbar_position' : "right", - 'scroll_on_keystroke' : True, - 'scroll_on_output' : False, - 'scrollback_lines' : 500, - 'scrollback_infinite' : False, - 'disable_mousewheel_zoom': False, - 'exit_action' : 'close', - 'palette' : '#2e3436:#cc0000:#4e9a06:#c4a000:\ + 'global_config': { + 'dbus': True, + 'focus': 'click', + 'handle_size': -1, + 'geometry_hinting': False, + 'window_state': 'normal', + 'borderless': False, + 'extra_styling': True, + 'tab_position': 'top', + 'broadcast_default': 'group', + 'close_button_on_tab': True, + 'scroll_tabbar': False, + 'homogeneous_tabbar': True, + 'hide_from_taskbar': False, + 'always_on_top': False, + 'hide_on_lose_focus': False, + 'sticky': False, + 'use_custom_url_handler': False, + 'custom_url_handler': '', + 'inactive_color_offset': 0.8, + 'inactive_bg_color_offset': 1.0, + 'enabled_plugins': ['LaunchpadBugURLHandler', + 'LaunchpadCodeURLHandler', + 'APTURLHandler'], + 'ask_before_closing': 'multiple_terminals', + 'always_split_with_profile': False, + 'putty_paste_style': False, + 'putty_paste_style_source_clipboard': False, + 'disable_mouse_paste': False, + 'smart_copy': True, + 'clear_select_on_copy': False, + 'cell_width': 1.0, + 'cell_height': 1.0, + 'case_sensitive': True, + 'invert_search': False, + 'link_single_click': False, + 'title_at_bottom': False, + 'detachable_tabs': True, + 'new_tab_after_current_tab': False, + }, + 'keybindings': { + 'zoom_in': ['plus', ''], + 'zoom_out': ['minus', ''], + 'zoom_normal': ['0', ''], + 'zoom_in_all': ['', ''], + 'zoom_out_all': ['', ''], + 'zoom_normal_all': ['', ''], + 'new_tab': ['t', ''], + 'cycle_next': ['Tab', ''], + 'cycle_prev': ['Tab', ''], + 'go_next': ['n', ''], + 'go_prev': ['p', ''], + 'go_up': ['Up', ''], + 'go_down': ['Down', ''], + 'go_left': ['Left', ''], + 'go_right': ['Right', ''], + 'rotate_cw': ['r', ''], + 'rotate_ccw': ['r', ''], + 'split_auto': ['a', ''], + 'split_horiz': ['o', ''], + 'split_vert': ['e', ''], + 'close_term': ['w', ''], + 'copy': ['c', ''], + 'paste': ['v', ''], + 'paste_selection': ['', ''], + 'toggle_scrollbar': ['s', ''], + 'search': ['f', ''], + 'page_up': ['', ''], + 'page_down': ['', ''], + 'page_up_half': ['', ''], + 'page_down_half': ['', ''], + 'line_up': ['', ''], + 'line_down': ['', ''], + 'close_window': ['q', ''], + 'resize_up': ['Up', ''], + 'resize_down': ['Down', ''], + 'resize_left': ['Left', ''], + 'resize_right': ['Right', ''], + 'move_tab_right': ['Page_Down', ''], + 'move_tab_left': ['Page_Up', ''], + 'toggle_zoom': ['x', ''], + 'scaled_zoom': ['z', ''], + 'next_tab': ['Page_Down', ''], + 'prev_tab': ['Page_Up', ''], + 'switch_to_tab_1': ['', ''], + 'switch_to_tab_2': ['', ''], + 'switch_to_tab_3': ['', ''], + 'switch_to_tab_4': ['', ''], + 'switch_to_tab_5': ['', ''], + 'switch_to_tab_6': ['', ''], + 'switch_to_tab_7': ['', ''], + 'switch_to_tab_8': ['', ''], + 'switch_to_tab_9': ['', ''], + 'switch_to_tab_10': ['', ''], + 'full_screen': ['F11', ''], + 'reset': ['r', ''], + 'reset_clear': ['g', ''], + 'hide_window': ['a', ''], + 'create_group': ['', ''], + 'group_all': ['g', ''], + 'group_all_toggle': ['', ''], + 'ungroup_all': ['g', ''], + 'group_win': ['', ''], + 'group_win_toggle': ['', ''], + 'ungroup_win': ['w', ''], + 'group_tab': ['t', ''], + 'group_tab_toggle': ['', ''], + 'ungroup_tab': ['t', ''], + 'new_window': ['i', ''], + 'new_terminator': ['i', ''], + 'broadcast_off': ['', ''], + 'broadcast_group': ['', ''], + 'broadcast_all': ['', ''], + 'insert_number': ['1', ''], + 'insert_padded': ['0', ''], + 'edit_window_title': ['w', ''], + 'edit_tab_title': ['a', ''], + 'edit_terminal_title': ['x', ''], + 'layout_launcher': ['l', ''], + 'next_profile': ['', ''], + 'previous_profile': ['', ''], + 'preferences': ['', ''], + 'preferences_keybindings': ['k', ''], + 'help': ['F1', ''] + }, + 'profiles': { + 'default': { + 'allow_bold': True, + 'audible_bell': False, + 'visible_bell': False, + 'urgent_bell': False, + 'icon_bell': True, + 'background_color': '#000000', + 'background_darkness': 0.5, + 'background_type': 'solid', + 'background_image': '', + 'background_image_mode': 'stretch_and_fill', + 'background_image_align_horiz': 'center', + 'background_image_align_vert': 'middle', + 'backspace_binding': 'ascii-del', + 'delete_binding': 'escape-sequence', + 'cursor_blink': True, + 'cursor_shape': 'block', + 'cursor_fg_color': '', + 'cursor_bg_color': '', + 'cursor_color_default': True, + 'term': 'xterm-256color', + 'colorterm': 'truecolor', + 'font': 'Mono 10', + 'foreground_color': '#aaaaaa', + 'show_titlebar': True, + 'scrollbar_position': "right", + 'scroll_on_keystroke': True, + 'scroll_on_output': False, + 'scrollback_lines': 500, + 'scrollback_infinite': False, + 'disable_mousewheel_zoom': False, + 'exit_action': 'close', + 'palette': '#2e3436:#cc0000:#4e9a06:#c4a000:\ #3465a4:#75507b:#06989a:#d3d7cf:#555753:#ef2929:#8ae234:#fce94f:\ #729fcf:#ad7fa8:#34e2e2:#eeeeec', - 'word_chars' : '-,./?%&#:_', - 'mouse_autohide' : True, - 'login_shell' : False, - 'use_custom_command' : False, - 'custom_command' : '', - 'use_system_font' : True, - 'use_theme_colors' : False, - 'bold_is_bright' : False, - 'cell_height' : 1.0, - 'cell_width' : 1.0, - 'force_no_bell' : False, - 'copy_on_selection' : False, - 'split_to_group' : False, - 'autoclean_groups' : True, - 'http_proxy' : '', - # Titlebar - 'title_hide_sizetext' : False, - 'title_transmit_fg_color' : '#ffffff', - 'title_transmit_bg_color' : '#c80003', - 'title_receive_fg_color' : '#ffffff', - 'title_receive_bg_color' : '#0076c9', - 'title_inactive_fg_color' : '#000000', - 'title_inactive_bg_color' : '#c0bebf', - 'title_use_system_font' : True, - 'title_font' : 'Sans 9' - }, - }, - 'layouts': { - 'default': { - 'window0': { - 'type': 'Window', - 'parent': '' - }, - 'child1': { - 'type': 'Terminal', - 'parent': 'window0' - } - } - }, - 'plugins': { + 'word_chars': '-,./?%&#:_', + 'mouse_autohide': True, + 'login_shell': False, + 'use_custom_command': False, + 'custom_command': '', + 'use_system_font': True, + 'use_theme_colors': False, + 'bold_is_bright': False, + 'cell_height': 1.0, + 'cell_width': 1.0, + 'force_no_bell': False, + 'copy_on_selection': False, + 'split_to_group': False, + 'autoclean_groups': True, + 'http_proxy': '', + # Titlebar + 'title_hide_sizetext': False, + 'title_transmit_fg_color': '#ffffff', + 'title_transmit_bg_color': '#c80003', + 'title_receive_fg_color': '#ffffff', + 'title_receive_bg_color': '#0076c9', + 'title_inactive_fg_color': '#000000', + 'title_inactive_bg_color': '#c0bebf', + 'title_use_system_font': True, + 'title_font': 'Sans 9' }, + }, + 'layouts': { + 'default': { + 'window0': { + 'type': 'Window', + 'parent': '' + }, + 'child1': { + 'type': 'Terminal', + 'parent': 'window0' + } + } + }, + 'plugins': { + }, } + class Config(object): """Class to provide a slightly richer config API above ConfigBase""" base = None @@ -293,8 +293,9 @@ class Config(object): system_mono_font = None system_prop_font = None system_focus = None + system_font = None inhibited = None - + def __init__(self, profile='default'): self.base = ConfigBase() self.set_profile(profile) @@ -303,45 +304,45 @@ def __init__(self, profile='default'): def __getitem__(self, key, default=None): """Look up a configuration item""" - return(self.base.get_item(key, self.profile, default=default)) + return self.base.get_item(key, self.profile, default=default) def __setitem__(self, key, value): """Set a particular configuration item""" - return(self.base.set_item(key, value, self.profile)) + return self.base.set_item(key, value, self.profile) def get_profile(self): """Get our profile""" - return(self.profile) + return self.profile def get_profile_by_name(self, profile): """Get the profile with the specified name""" - return(self.base.profiles[profile]) + return self.base.profiles[profile] def set_profile(self, profile, force=False): """Set our profile (which usually means change it)""" options = self.options_get() if not force and options and options.profile and profile == 'default': - dbg('overriding default profile to %s' % options.profile) + dbg(f'overriding default profile to {options.profile}') profile = options.profile - dbg('Changing profile to %s' % profile) + dbg(f'Changing profile to {profile}') self.profile = profile if profile not in self.base.profiles: - dbg('%s does not exist, creating' % profile) + dbg(f'{profile} does not exist, creating') self.base.profiles[profile] = copy(DEFAULTS['profiles']['default']) def add_profile(self, profile, toclone): """Add a new profile""" - return(self.base.add_profile(profile, toclone)) + return self.base.add_profile(profile, toclone) def del_profile(self, profile): """Delete a profile""" if profile == self.profile: # FIXME: We should solve this problem by updating terminals when we # remove a profile - err('Config::del_profile: Deleting in-use profile %s.' % profile) + err(f'Config::del_profile: Deleting in-use profile {profile}.') self.set_profile('default') if profile in self.base.profiles: - del(self.base.profiles[profile]) + del self.base.profiles[profile] options = self.options_get() if options and options.profile == profile: options.profile = None @@ -351,89 +352,86 @@ def rename_profile(self, profile, newname): """Rename a profile""" if profile in self.base.profiles: self.base.profiles[newname] = self.base.profiles[profile] - del(self.base.profiles[profile]) + del self.base.profiles[profile] if profile == self.profile: self.profile = newname def list_profiles(self): """List all configured profiles""" - return(list(self.base.profiles.keys())) + return list(self.base.profiles.keys()) def add_layout(self, name, layout): """Add a new layout""" - return(self.base.add_layout(name, layout)) + return self.base.add_layout(name, layout) def replace_layout(self, name, layout): """Replace an existing layout""" - return(self.base.replace_layout(name, layout)) + return self.base.replace_layout(name, layout) def del_layout(self, layout): """Delete a layout""" if layout in self.base.layouts: - del(self.base.layouts[layout]) + del self.base.layouts[layout] def rename_layout(self, layout, newname): """Rename a layout""" if layout in self.base.layouts: self.base.layouts[newname] = self.base.layouts[layout] - del(self.base.layouts[layout]) + del self.base.layouts[layout] def list_layouts(self): """List all configured layouts""" - return(list(self.base.layouts.keys())) + return list(self.base.layouts.keys()) def connect_gsetting_callbacks(self): """Get system settings and create callbacks for changes""" dbg("GSetting connects for system changes") # Have to preserve these to self, or callbacks don't happen - self.gsettings_interface=Gio.Settings.new('org.gnome.desktop.interface') + self.gsettings_interface = Gio.Settings.new('org.gnome.desktop.interface') self.gsettings_interface.connect("changed::font-name", self.on_gsettings_change_event) self.gsettings_interface.connect("changed::monospace-font-name", self.on_gsettings_change_event) - self.gsettings_wm=Gio.Settings.new('org.gnome.desktop.wm.preferences') + self.gsettings_wm = Gio.Settings.new('org.gnome.desktop.wm.preferences') self.gsettings_wm.connect("changed::focus-mode", self.on_gsettings_change_event) def get_system_prop_font(self): """Look up the system font""" if self.system_prop_font is not None: - return(self.system_prop_font) - elif 'org.gnome.desktop.interface' not in Gio.Settings.list_schemas(): - return + return self.system_prop_font + if 'org.gnome.desktop.interface' not in Gio.Settings.list_schemas(): + return '' + gsettings = Gio.Settings.new('org.gnome.desktop.interface') + value = gsettings.get_value('font-name') + if value: + self.system_prop_font = value.get_string() else: - gsettings=Gio.Settings.new('org.gnome.desktop.interface') - value = gsettings.get_value('font-name') - if value: - self.system_prop_font = value.get_string() - else: - self.system_prop_font = "Sans 10" - return(self.system_prop_font) + self.system_prop_font = "Sans 10" + return self.system_prop_font def get_system_mono_font(self): """Look up the system font""" if self.system_mono_font is not None: - return(self.system_mono_font) - elif 'org.gnome.desktop.interface' not in Gio.Settings.list_schemas(): - return + return self.system_mono_font + if 'org.gnome.desktop.interface' not in Gio.Settings.list_schemas(): + return '' + gsettings = Gio.Settings.new('org.gnome.desktop.interface') + value = gsettings.get_value('monospace-font-name') + if value: + self.system_mono_font = value.get_string() else: - gsettings=Gio.Settings.new('org.gnome.desktop.interface') - value = gsettings.get_value('monospace-font-name') - if value: - self.system_mono_font = value.get_string() - else: - self.system_mono_font = "Mono 10" - return(self.system_mono_font) + self.system_mono_font = "Mono 10" + return self.system_mono_font def get_system_focus(self): """Look up the system focus setting""" if self.system_focus is not None: - return(self.system_focus) - elif 'org.gnome.desktop.interface' not in Gio.Settings.list_schemas(): - return - else: - gsettings=Gio.Settings.new('org.gnome.desktop.wm.preferences') - value = gsettings.get_value('focus-mode') - if value: - self.system_focus = value.get_string() - return(self.system_focus) + return self.system_focus + if 'org.gnome.desktop.interface' not in Gio.Settings.list_schemas(): + return '' + gsettings = Gio.Settings.new('org.gnome.desktop.wm.preferences') + value = gsettings.get_value('focus-mode') + if value: + self.system_focus = value.get_string() + return self.system_focus def on_gsettings_change_event(self, settings, key): """Handle a gsetting change event""" @@ -449,9 +447,8 @@ def on_gsettings_change_event(self, settings, key): def save(self): """Cause ConfigBase to save our config to file""" if self.inhibited is True: - return(True) - else: - return(self.base.save()) + return True + return self.base.save() def inhibit_save(self): """Prevent calls to save() being honoured""" @@ -467,64 +464,64 @@ def options_set(self, options): def options_get(self): """Get the command line options""" - return(self.base.command_line_options) + return self.base.command_line_options def plugin_get(self, pluginname, key, default=None): """Get a plugin config value, if doesn't exist return default if specified """ - return(self.base.get_item(key, plugin=pluginname, default=default)) + return self.base.get_item(key, plugin=pluginname, default=default) def plugin_set(self, pluginname, key, value): """Set a plugin config value""" - return(self.base.set_item(key, value, plugin=pluginname)) + return self.base.set_item(key, value, plugin=pluginname) def plugin_get_config(self, plugin): """Return a whole config tree for a given plugin""" - return(self.base.get_plugin(plugin)) + return self.base.get_plugin(plugin) def plugin_set_config(self, plugin, tree): """Set a whole config tree for a given plugin""" - return(self.base.set_plugin(plugin, tree)) + return self.base.set_plugin(plugin, tree) def plugin_del_config(self, plugin): """Delete a whole config tree for a given plugin""" - return(self.base.del_plugin(plugin)) + return self.base.del_plugin(plugin) def layout_get_config(self, layout): """Return a layout""" - return(self.base.get_layout(layout)) + return self.base.get_layout(layout) def layout_set_config(self, layout, tree): """Set a layout""" - return(self.base.set_layout(layout, tree)) + return self.base.set_layout(layout, tree) def copy_layout_item(self, src_layout, dst_layout, item): + """Copy a layout item""" items = {} for child in src_layout: - section = src_layout[child] - sec_type = section.get('type', None) + section = src_layout[child] + sec_type = section.get('type', None) if sec_type != 'Terminal': continue cp_item = section.get(item, None) - uuid = str(section.get('uuid', None)) + uuid = str(section.get('uuid', None)) if cp_item: items[uuid] = cp_item - dbg("items to be copied:%s" % items) + dbg(f"items to be copied:{items}") for child in dst_layout: - section = dst_layout[child] - sec_type = section.get('type', None) + section = dst_layout[child] + sec_type = section.get('type', None) if sec_type != 'Terminal': continue - uuid = str(section.get('uuid', None)) + uuid = str(section.get('uuid', None)) update_item = items.get(uuid, None) if uuid and update_item: - dbg("update layout item:(%s) with value:(%s)" - % (item, update_item)) - section[item] = update_item + dbg(f"update layout item:({item}) with value:({update_item})") + section[item] = update_item class ConfigBase(Borg): @@ -591,9 +588,9 @@ def defaults_to_configspec(self): if keytype in keymap: keytype = keymap[keytype] elif keytype == 'list': - value = 'list(%s)' % ','.join(value) + value = f"list({','.join(value)})" - keytype = '%s(default=%s)' % (keytype, value) + keytype = f'{keytype}(default={value})' if key == 'custom_url_handler': keytype = 'string(default="")' @@ -602,11 +599,16 @@ def defaults_to_configspec(self): configspecdata['global_config'] = section section = {} - for key in DEFAULTS['keybindings']: - value = DEFAULTS['keybindings'][key] - if value is None or value == '': - continue - section[key] = 'string(default=%s)' % value + for key, val in DEFAULTS['keybindings'].items(): + if isinstance(val, str): + value = list(val, '') + if value is None or value == '': + continue + elif isinstance(val, list): + if len(val) < 2: + val.append('') + value = val + section[key] = f'list(default=list{tuple(value)})' configspecdata['keybindings'] = section section = {} @@ -616,11 +618,11 @@ def defaults_to_configspec(self): if keytype in keymap: keytype = keymap[keytype] elif keytype == 'list': - value = 'list(%s)' % ','.join(value) + value = f"list({','.join(value)})" if keytype == 'string': - value = '"%s"' % value + value = f'"{value}"' - keytype = '%s(default=%s)' % (keytype, value) + keytype = f'{keytype}(default={value})' section[key] = keytype configspecdata['profiles'] = {} @@ -640,9 +642,9 @@ def defaults_to_configspec(self): configspecdata['plugins'] = {} configspec = ConfigObj(configspecdata) - if DEBUG == True: + if DEBUG: configspec.write(open('/tmp/terminator_configspec_debug.txt', 'wb')) - return(configspec) + return configspec def load(self): """Load configuration data from our various sources""" @@ -651,7 +653,7 @@ def load(self): return filename = self.get_config_filename() - dbg('looking for config file: %s' % filename) + dbg(f'looking for config file: {filename}') try: # # Make sure we attempt to update the ‘cell_height’ config @@ -661,10 +663,10 @@ def load(self): update_config_to_cell_height(filename) self.config_file_updated_to_cell_height = True - configfile = open(filename, 'r') + configfile = open(filename, 'r', encoding='utf-8') except Exception as ex: if not self.whined: - err('ConfigBase::load: Unable to open %s (%s)' % (filename, ex)) + err(f'ConfigBase::load: Unable to open {filename} ({ex})') self.whined = True return # If we have successfully loaded a config, allow future whining @@ -676,25 +678,25 @@ def load(self): validator = Validator() result = parser.validate(validator, preserve_errors=True) except Exception as ex: - err('Unable to load configuration: %s' % ex) + err(f'Unable to load configuration: {ex}') return - if result != True: + if result is not True: err('ConfigBase::load: config format is not valid') for (section_list, key, _other) in flatten_errors(parser, result): if key is not None: - err('[%s]: %s is invalid' % (','.join(section_list), key)) + err(f"[{','.join(section_list)}]: {key} is invalid") else: - err('[%s] missing' % ','.join(section_list)) + err(f"[{','.join(section_list)}] missing") else: dbg('config validated successfully') for section_name in self.sections: - dbg('Processing section: %s' % section_name) + dbg(f'Processing section: {section_name}') section = getattr(self, section_name) if section_name == 'profiles': for profile in parser[section_name]: - dbg('Processing profile: %s' % profile) + dbg(f'Processing profile: {profile}') if section_name not in section: # FIXME: Should this be outside the loop? section[profile] = copy(DEFAULTS['profiles']['default']) @@ -703,20 +705,20 @@ def load(self): if section_name not in parser: continue for part in parser[section_name]: - dbg('Processing %s: %s' % (section_name, part)) + dbg(f'Processing {section_name}: {part}') section[part] = parser[section_name][part] elif section_name == 'layouts': for layout in parser[section_name]: - dbg('Processing %s: %s' % (section_name, layout)) + dbg(f'Processing {section_name}: {layout}') if layout == 'default' and \ parser[section_name][layout] == {}: - continue + continue section[layout] = parser[section_name][layout] elif section_name == 'keybindings': if section_name not in parser: continue for part in parser[section_name]: - dbg('Processing %s: %s' % (section_name, part)) + dbg(f'Processing {section_name}: {part}') if parser[section_name][part] == 'None': section[part] = None else: @@ -724,13 +726,12 @@ def load(self): else: try: section.update(parser[section_name]) - except KeyError as ex: - dbg('skipping missing section %s' % section_name) + except KeyError: + dbg(f'skipping missing section {section_name}') self.loaded = True def get_config_filename(self): - filename = '' if self.command_line_options and self.command_line_options.config: filename = self.command_line_options.config else: @@ -744,52 +745,50 @@ def save_config_with_suffix(self, suffix): try: filename = self.get_config_filename() - #save the current config, to revert any changes make in preferences - #save the current config to config_dir path which is at least writable - cfg_filename = os.path.join(get_config_dir(), 'config') + # save the current config to revert any changes make in preferences + # save the current config to config_dir path which is at least writable + cfg_filename = os.path.join(get_config_dir(), 'config') cur_loaded_file = cfg_filename + suffix if os.path.exists(filename) and cur_loaded_file: - dbg('copy file:%s to' \ - ' file:%s' % (filename, cur_loaded_file)) + dbg(f'copy file:{filename} to file:{cur_loaded_file}') shutil.copy2(filename, cur_loaded_file) elif cur_loaded_file: - open(cur_loaded_file, 'a').close() + open(cur_loaded_file, 'a', encoding='utf-8').close() else: err('ConfigBase:: Unable to get filename to save') except Exception as ex: - err('ConfigBase::save_config_with_suffix' \ - ' Unable to save config: %s' % ex) + err('ConfigBase::save_config_with_suffix' + f' Unable to save config: {ex}') def restore_config_with_suffix(self, suffix): try: filename = self.get_config_filename() - cfg_filename = os.path.join(get_config_dir(), 'config') + cfg_filename = os.path.join(get_config_dir(), 'config') cur_loaded_file = cfg_filename + suffix if os.path.exists(cur_loaded_file): if not os.access(filename, os.W_OK): - dbg('path:%s not writable' \ - ' restoring to path:%s' % (filename,cfg_filename)) + dbg(f'path:{filename} not writable' + f' restoring to path:{cfg_filename}') filename = cfg_filename - dbg('restore from file:%s to file:%s' - % (cur_loaded_file, filename)) + dbg(f'restore from file:{cur_loaded_file} to file:{filename}') shutil.copy2(cur_loaded_file, filename) except Exception as ex: - err('ConfigBase::restore_config_with_suffix' \ - ' Unable to restore config: %s' % ex) + err('ConfigBase::restore_config_with_suffix' + f' Unable to restore config: {ex}') def remove_config_with_suffix(self, suffix): try: - cfg_filename = os.path.join(get_config_dir(), 'config') + cfg_filename = os.path.join(get_config_dir(), 'config') cur_loaded_file = cfg_filename + suffix if os.path.exists(cur_loaded_file): - dbg('remove file:%s' % (cur_loaded_file)) + dbg(f'remove file:{cur_loaded_file}') os.remove(cur_loaded_file) except Exception as ex: - err('ConfigBase::remove_config_with_suffix' \ - ' Unable to remove config: %s' % ex) + err('ConfigBase::remove_config_with_suffix' + f' Unable to remove config: {ex}') def reload(self): """Force a reload of the base config""" @@ -803,16 +802,16 @@ def save(self): parser.indent_type = ' ' for section_name in ['global_config', 'keybindings']: - dbg('Processing section: %s' % section_name) + dbg(f'Processing section: {section_name}') section = getattr(self, section_name) if section_name == 'keybindings': from terminatorlib.plugin import KeyBindUtil # for plugin KeyBindUtil assist in plugin_util - keybindutil = KeyBindUtil(); - keyb_keys = keybindutil.get_all_act_to_keys() + keybindutil = KeyBindUtil() + keyb_keys = keybindutil.get_all_act_to_keys() # we only need keys as a reference so to match them # against new values - keyb_keys = dict.fromkeys(keyb_keys, "") + keyb_keys = dict.fromkeys(keyb_keys, "") default_merged_section = {**keyb_keys, **DEFAULTS[section_name]} merged_section = {**keyb_keys, **section} @@ -823,24 +822,24 @@ def save(self): from .configjson import JSON_PROFILE_NAME, JSON_LAYOUT_NAME parser['profiles'] = {} - for profile in self.profiles: + for profile, profile_value in self.profiles.items(): if profile == JSON_PROFILE_NAME: continue - dbg('Processing profile: %s' % profile) + dbg(f'Processing profile: {profile}') parser['profiles'][profile] = dict_diff( - DEFAULTS['profiles']['default'], self.profiles[profile]) + DEFAULTS['profiles']['default'], profile_value) parser['layouts'] = {} - for layout in self.layouts: + for layout, layout_value in self.layouts.items(): if layout == JSON_LAYOUT_NAME: continue - dbg('Processing layout: %s' % layout) - parser['layouts'][layout] = self.layouts[layout] + dbg(f'Processing layout: {layout}') + parser['layouts'][layout] = layout_value parser['plugins'] = {} - for plugin in self.plugins: - dbg('Processing plugin: %s' % plugin) - parser['plugins'][plugin] = self.plugins[plugin] + for plugin, plugin_value in self.plugins.items(): + dbg(f'Processing plugin: {plugin}') + parser['plugins'][plugin] = plugin_value config_dir = get_config_dir() if not os.path.isdir(config_dir): @@ -849,22 +848,22 @@ def save(self): try: if self.command_line_options.config: filename = self.command_line_options.config - else: - filename = os.path.join(config_dir,'config') + else: + filename = os.path.join(config_dir, 'config') if not os.path.isfile(filename): - open(filename, 'a').close() + open(filename, 'a', encoding='utf-8').close() backup_file = filename + '~' if os.path.exists(filename): shutil.copy2(filename, backup_file) - with open(filename, 'wb') as fh: - parser.write(fh) + with open(filename, 'wb') as file: + parser.write(file) os.remove(backup_file) except Exception as ex: - err('ConfigBase::save: Unable to save config: %s' % ex) + err(f'ConfigBase::save: Unable to save config: {ex}') def get_item(self, key, profile='default', plugin=None, default=None): """Look up a configuration item""" @@ -873,28 +872,23 @@ def get_item(self, key, profile='default', plugin=None, default=None): profile = 'default' if key in self.global_config: - dbg('%s found in globals: %s' % - (key, self.global_config[key])) - return(self.global_config[key]) - elif key in self.profiles[profile]: - dbg('%s found in profile %s: %s' % ( - key, profile, self.profiles[profile][key])) - return(self.profiles[profile][key]) - elif key == 'keybindings': - return(self.keybindings) - elif plugin and plugin in self.plugins and key in self.plugins[plugin]: - dbg('%s found in plugin %s: %s' % ( - key, plugin, self.plugins[plugin][key])) - return(self.plugins[plugin][key]) - elif default: + dbg(f'{key} found in globals: {self.global_config[key]}') + return self.global_config[key] + if key in self.profiles[profile]: + dbg(f'{key} found in profile {profile}: {self.profiles[profile][key]}') + return self.profiles[profile][key] + if key == 'keybindings': + return self.keybindings + if plugin and plugin in self.plugins and key in self.plugins[plugin]: + dbg(f'{key} found in plugin {plugin}: {self.plugins[plugin][key]}') + return self.plugins[plugin][key] + if default: return default - else: - raise KeyError('ConfigBase::get_item: unknown key %s' % key) + raise KeyError(f'ConfigBase::get_item: unknown key {key}') def set_item(self, key, value, profile='default', plugin=None): """Set a configuration item""" - dbg('Setting %s=%s (profile=%s, plugin=%s)' % - (key, value, profile, plugin)) + dbg(f'Setting {key}={value} ({profile=}, {plugin=})') if key in self.global_config: self.global_config[key] = value @@ -907,14 +901,13 @@ def set_item(self, key, value, profile='default', plugin=None): self.plugins[plugin] = {} self.plugins[plugin][key] = value else: - raise KeyError('ConfigBase::set_item: unknown key %s' % key) + raise KeyError(f'ConfigBase::set_item: unknown key {key}') - return(True) + return True def get_plugin(self, plugin): """Return a whole tree for a plugin""" - if plugin in self.plugins: - return(self.plugins[plugin]) + return self.plugins.get(plugin) def set_plugin(self, plugin, tree): """Set a whole tree for a plugin""" @@ -928,34 +921,34 @@ def del_plugin(self, plugin): def add_profile(self, profile, toclone): """Add a new profile""" if profile in self.profiles: - return(False) + return False if toclone is not None: newprofile = copy(toclone) else: newprofile = copy(DEFAULTS['profiles']['default']) self.profiles[profile] = newprofile - return(True) + return True def add_layout(self, name, layout): """Add a new layout""" if name in self.layouts: - return(False) + return False self.layouts[name] = layout - return(True) + return True def replace_layout(self, name, layout): """Replaces a layout with the given name""" - if not name in self.layouts: - return(False) + if name not in self.layouts: + return False self.layouts[name] = layout - return(True) + return True def get_layout(self, layout): """Return a layout""" if layout in self.layouts: - return(self.layouts[layout]) - else: - err('layout does not exist: %s' % layout) + return self.layouts[layout] + err(f'layout does not exist: {layout}') + return '' def set_layout(self, layout, tree): """Set a layout""" diff --git a/terminatorlib/keybindings.py b/terminatorlib/keybindings.py index 61551c8e..24e72ff8 100644 --- a/terminatorlib/keybindings.py +++ b/terminatorlib/keybindings.py @@ -16,19 +16,23 @@ """Terminator by Chris Jones -Validator and functions for dealing with Terminator's customisable +Validator and functions for dealing with Terminator's customisable keyboard shortcuts. """ import re -from gi.repository import Gtk, Gdk +from gi.repository import Gdk from .util import err + class KeymapError(Exception): """Custom exception for errors in keybinding configurations""" + MODIFIER = re.compile('<([^<]+)>') + + class Keybindings: """Class to handle loading and lookup of Terminator keybindings""" @@ -37,7 +41,7 @@ class Keybindings: 'control': Gdk.ModifierType.CONTROL_MASK, 'primary': Gdk.ModifierType.CONTROL_MASK, 'shift': Gdk.ModifierType.SHIFT_MASK, - 'alt': Gdk.ModifierType.MOD1_MASK, + 'alt': Gdk.ModifierType.MOD1_MASK, # Gdk.ModifierType.ALT_MASK ? 'super': Gdk.ModifierType.SUPER_MASK, 'hyper': Gdk.ModifierType.HYPER_MASK, 'mod2': Gdk.ModifierType.MOD2_MASK @@ -62,19 +66,19 @@ def reload(self): self._lookup = {} self._masks = 0 for action, bindings in list(self.keys.items()): - if not isinstance(bindings, tuple): - bindings = (bindings,) + if not isinstance(bindings, list): + bindings = [bindings, ''] for binding in bindings: if not binding or binding == "None": continue try: - keyval, mask = self._parsebinding(binding) + keyval, mask = self.parsebinding(binding) # Does much the same, but with poorer error handling. - #keyval, mask = Gtk.accelerator_parse(binding) - except KeymapError as e: - err ("keybindings.reload failed to parse binding '%s': %s" % (binding, e)) + # keyval, mask = Gtk.accelerator_parse(binding) + except KeymapError as exc: + err(f"keybindings.reload failed to parse binding '{binding}': {exc}") else: if mask & Gdk.ModifierType.SHIFT_MASK: if keyval == Gdk.KEY_Tab: @@ -91,7 +95,7 @@ def reload(self): self._lookup[mask][keyval] = action self._masks |= mask - def _parsebinding(self, binding): + def parsebinding(self, binding): """Parse an individual binding using gtk's binding function""" mask = 0 modifiers = re.findall(MODIFIER, binding) @@ -103,27 +107,25 @@ def _parsebinding(self, binding): raise KeymapError('No key found') keyval = Gdk.keyval_from_name(key) if keyval == 0: - raise KeymapError("Key '%s' is unrecognised" % key) + raise KeymapError(f"Key '{key}' is unrecognised") return (keyval, mask) def _lookup_modifier(self, modifier): """Map modifier names to gtk values""" try: return self.modifiers[modifier.lower()] - except KeyError: - raise KeymapError("Unhandled modifier '<%s>'" % modifier) + except KeyError as exc: + raise KeymapError(f"Unhandled modifier '<{modifier}>'") from exc def lookup(self, event): """Translate a keyboard event into a mapped key""" try: - _found, keyval, _egp, _lvl, consumed = self.keymap.translate_keyboard_state( - event.hardware_keycode, + _, keyval, _, _, consumed = self.keymap.translate_keyboard_state( + event.hardware_keycode, Gdk.ModifierType(event.get_state() & ~Gdk.ModifierType.LOCK_MASK), event.group) except TypeError: - err ("keybindings.lookup failed to translate keyboard event: %s" % - dir(event)) + err(f"keybindings.lookup failed to translate keyboard event: {dir(event)}") return None mask = (event.get_state() & ~consumed) & self._masks return self._lookup.get(mask, self.empty).get(keyval, None) - diff --git a/terminatorlib/plugin.py b/terminatorlib/plugin.py index 49a972e7..1eb328f0 100644 --- a/terminatorlib/plugin.py +++ b/terminatorlib/plugin.py @@ -30,17 +30,17 @@ from .util import dbg, err, get_config_dir from .terminator import Terminator -class Plugin(object): + +class Plugin: """Definition of our base plugin class""" capabilities = None def __init__(self): """Class initialiser.""" - pass def unload(self): """Prepare to be unloaded""" - pass + class PluginRegistry(borg.Borg): """Definition of a class to store plugin instances""" @@ -63,7 +63,7 @@ def prepare_attributes(self): (head, _tail) = os.path.split(borg.__file__) self.path.append(os.path.join(head, 'plugins')) self.path.append(os.path.join(get_config_dir(), 'plugins')) - dbg('Plugin path: %s' % self.path) + dbg(f'Plugin path: {self.path}') if not self.done: self.done = False if not self.available_plugins: @@ -75,7 +75,7 @@ def load_plugins(self, force=False): dbg('Already loaded') return - dbg('loading plugins, force:(%s)' % force) + dbg(f'loading plugins, force:({force})') config = Config() @@ -91,73 +91,73 @@ def load_plugins(self, force=False): continue pluginpath = os.path.join(plugindir, plugin) if os.path.isfile(pluginpath) and plugin[-3:] == '.py': - dbg('Importing plugin %s' % plugin) + dbg(f'Importing plugin {plugin}') try: module = __import__(plugin[:-3], None, None, ['']) for item in getattr(module, 'AVAILABLE'): func = getattr(module, item) - if item not in list(self.available_plugins.keys()): + if item not in self.available_plugins: self.available_plugins[item] = func if item not in config['enabled_plugins']: - dbg('plugin %s not enabled, skipping' % item) + dbg(f'plugin {item} not enabled, skipping') continue if item not in self.instances: self.instances[item] = func() elif force: - #instead of multiple copies of loaded - #plugin objects, unload where plugins - #can clean up and then re-init so there - #is one plugin object + # instead of multiple copies of loaded + # plugin objects, unload where plugins + # can clean up and then re-init so there + # is one plugin object self.instances[item].unload() self.instances.pop(item, None) self.instances[item] = func() except Exception as ex: - err('PluginRegistry::load_plugins: Importing plugin %s \ -failed: %s' % (plugin, ex)) + err(f'PluginRegistry::load_plugins: Importing plugin {plugin} \ +failed: {ex}') self.done = True def get_plugins_by_capability(self, capability): """Return a list of plugins with a particular capability""" result = [] - dbg('searching %d plugins \ -for %s' % (len(self.instances), capability)) - for plugin in self.instances: - if capability in self.instances[plugin].capabilities: - result.append(self.instances[plugin]) + dbg(f'searching {len(self.instances)} plugins for {capability}') + for plugin_value in self.instances.values(): + if capability in plugin_value.capabilities: + result.append(plugin_value) return result def get_all_plugins(self): """Return all plugins""" - return(self.instances) + return self.instances def get_available_plugins(self): """Return a list of all available plugins whether they are enabled or disabled""" - return(list(self.available_plugins.keys())) + return list(self.available_plugins.keys()) def is_enabled(self, plugin): """Return a boolean value indicating whether a plugin is enabled or not""" - return(plugin in self.instances) + return plugin in self.instances def enable(self, plugin): """Enable a plugin""" if plugin in self.instances: - err("Cannot enable plugin %s, already enabled" % plugin) - dbg("Enabling %s" % plugin) + err(f"Cannot enable plugin {plugin}, already enabled") + dbg(f"Enabling {plugin}") self.instances[plugin] = self.available_plugins[plugin]() def disable(self, plugin): """Disable a plugin""" - dbg("Disabling %s" % plugin) + dbg(f"Disabling {plugin}") self.instances[plugin].unload() - del(self.instances[plugin]) + del self.instances[plugin] # This is where we should define a base class for each type of plugin we # support + # URLHandler - This adds a regex match to the Terminal widget and provides a # callback to turn that into a URL. class URLHandler(Plugin): @@ -188,6 +188,7 @@ def unload(self): for terminal in terminator.terminals: terminal.match_remove(self.handler_name) + # MenuItem - This is able to execute code during the construction of the # context menu of a Terminal. class MenuItem(Plugin): @@ -199,32 +200,31 @@ def callback(self, menuitems, menu, terminal): raise NotImplementedError -""" --Basic plugin util for key-press handling, has all mapping to be used -in layout keybindings - -Vishweshwar Saran Singh Deo vssdeo@gmail.com -""" - -from gi.repository import Gtk, Gdk -from terminatorlib.keybindings import Keybindings, KeymapError +from gi.repository import Gdk +from terminatorlib.keybindings import Keybindings PLUGIN_UTIL_DESC = 0 -PLUGIN_UTIL_ACT = 1 +PLUGIN_UTIL_ACT = 1 PLUGIN_UTIL_KEYS = 2 + class KeyBindUtil: + """ + -Basic plugin util for key-press handling, has all mapping to be used + in layout keybindings + Vishweshwar Saran Singh Deo vssdeo@gmail.com + """ keybindings = Keybindings() - map_key_to_act = {} + map_key_to_act = {} map_act_to_keys = {} map_act_to_desc = {} def __init__(self, config=None): self.config = config - #Example + # Example # bind # first param is desc, second is action str # self.keyb.bindkey([PluginUrlFindNext , PluginUrlActFindNext, "j"]) @@ -234,68 +234,63 @@ def __init__(self, config=None): # if act == "url_find_next": - - #check map key_val_mask -> action def _check_keybind_change(self, key): + '''Check map key_val_mask -> action ''' act = key[PLUGIN_UTIL_ACT] - for key_val_mask in self.map_key_to_act: - old_act = self.map_key_to_act[key_val_mask] - if act == old_act: + for key_val_mask, mask_value in self.map_key_to_act.items(): + if act == mask_value: return key_val_mask return None - #check in config before binding def bindkey_check_config(self, key): + '''Check in config before binding''' if not self.config: raise Warning("bindkey_check_config called without config init") actstr = key[PLUGIN_UTIL_ACT] kbsect = self.config.base.get_item('keybindings') keystr = kbsect[actstr] if actstr in kbsect else "" - dbg("bindkey_check_config:action (%s) key str:(%s)" % (actstr, keystr)) + dbg(f"bindkey_check_config:action ({actstr}) key str:({keystr})") if len(keystr): key[PLUGIN_UTIL_KEYS] = keystr - dbg("found new Action->KeyVal in config: (%s, %s)" - % (actstr, keystr)); + dbg(f"found new Action->KeyVal in config: ({actstr}, {keystr})") self.bindkey(key) def bindkey(self, key): - (keyval, mask) = self.keybindings._parsebinding(key[PLUGIN_UTIL_KEYS]) + keyval, mask = self.keybindings.parsebinding(key[PLUGIN_UTIL_KEYS]) keyval = Gdk.keyval_to_lower(keyval) mask = Gdk.ModifierType(mask) ret = (keyval, mask) - dbg("bindkey: (%s) (%s)" % (key[PLUGIN_UTIL_KEYS], str(ret))) + dbg(f"bindkey: ({key[PLUGIN_UTIL_KEYS]}) ({str(ret)})") - #remove if any old key_val_mask + # remove if any old key_val_mask old_key_val_mask = self._check_keybind_change(key) if old_key_val_mask: - dbg("found old key binding, removing: (%s)" % str(old_key_val_mask)) + dbg(f"found old key binding, removing: ({str(old_key_val_mask)})") del self.map_key_to_act[old_key_val_mask] - #map key-val-mask to action, used to ease key-press management + # map key-val-mask to action, used to ease key-press management self.map_key_to_act[ret] = key[PLUGIN_UTIL_ACT] - - #map action to key-combo-str, used in preferences->keybinding - self.map_act_to_keys[key[PLUGIN_UTIL_ACT]] = key[PLUGIN_UTIL_KEYS] - #map action to key-combo description, in used preferences->keybinding + # map action to key-combo-str, used in preferences->keybinding + self.map_act_to_keys[key[PLUGIN_UTIL_ACT]] = key[PLUGIN_UTIL_KEYS] + # map action to key-combo description, in used preferences->keybinding self.map_act_to_desc[key[PLUGIN_UTIL_ACT]] = key[PLUGIN_UTIL_DESC] def unbindkey(self, key): - # Suppose user changed the key-combo and its diff from # what the plugin had set by default, we need to get # current key-combo - act = key[PLUGIN_UTIL_ACT] + act = key[PLUGIN_UTIL_ACT] act_keys = self.map_act_to_keys[act] - (keyval, mask) = self.keybindings._parsebinding(act_keys) + keyval, mask = self.keybindings.parsebinding(act_keys) keyval = Gdk.keyval_to_lower(keyval) mask = Gdk.ModifierType(mask) ret = (keyval, mask) - dbg("unbindkey: (%s) (%s)" % (key[PLUGIN_UTIL_KEYS], str(ret))) + dbg(f"unbindkey: ({key[PLUGIN_UTIL_KEYS]}) ({str(ret)})") # FIXME keys should always be there, can also use .pop(key, None) # lets do it after testing @@ -303,14 +298,13 @@ def unbindkey(self, key): del self.map_act_to_keys[key[PLUGIN_UTIL_ACT]] del self.map_act_to_desc[key[PLUGIN_UTIL_ACT]] - def keyaction(self, event): - #FIXME MOD2 mask comes in the event, remove - event.state &= ~Gdk.ModifierType.MOD2_MASK + # FIXME MOD2 mask comes in the event, remove + event.state &= ~Gdk.ModifierType.MOD2_MASK keyval = Gdk.keyval_to_lower(event.keyval) ret = (keyval, event.state) - dbg("keyaction: (%s)" % str(ret)) + dbg(f"keyaction: ({str(ret)})") return self.map_key_to_act.get(ret, None) def get_act_to_keys(self, key): @@ -325,7 +319,7 @@ def get_all_act_to_desc(self): def get_act_to_desc(self, act): return self.map_act_to_desc.get(act) - #get action to key binding from config + # get action to key binding from config def get_act_to_keys_config(self, act): if not self.config: raise Warning("get_keyvalmask_for_act called without config init") diff --git a/terminatorlib/preferences.glade b/terminatorlib/preferences.glade index fbfb4c76..839bbf55 100644 --- a/terminatorlib/preferences.glade +++ b/terminatorlib/preferences.glade @@ -231,9 +231,13 @@ - + - + + + + + @@ -4129,13 +4133,13 @@ - Keybinding + Keybinding 1 True other - - + + 2 @@ -4144,6 +4148,23 @@ + + + Keybinding 2 + + + True + other + + + + + 4 + 5 + + + + diff --git a/terminatorlib/prefseditor.py b/terminatorlib/prefseditor.py index db22f6a0..7cea859b 100755 --- a/terminatorlib/prefseditor.py +++ b/terminatorlib/prefseditor.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -"""Preferences Editor for Terminator. +"""Preferences Editor for Terminator. Load a UIBuilder config file, display it, populate it with our current config, then optionally read that back out and @@ -8,7 +8,8 @@ """ import os -from gi.repository import GObject, Gtk, Gdk +import re +from gi.repository import Gdk, Gtk from .util import dbg, err from . import config @@ -20,19 +21,29 @@ from .plugin import KeyBindUtil + def get_color_string(widcol): - return('#%02x%02x%02x' % (widcol.red>>8, widcol.green>>8, widcol.blue>>8)) + if widcol: + red_bit = widcol.red >> 8 + green_bit = widcol.green >> 8 + blue_bit = widcol.blue >> 8 + return f'#{red_bit:02x}{green_bit:02x}{blue_bit:02x}' + return '' + def color2hex(widget): """Pull the colour values out of a Gtk ColorPicker widget and return them - as 8bit hex values, sinces its default behaviour is to give 16bit values""" + as 8bit hex values, since its default behaviour is to give 16bit values""" return get_color_string(widget.get_color()) + def rgba2hex(widget): return get_color_string(widget.get_rgba().to_color()) + NUM_PALETTE_COLORS = 16 + # FIXME: We need to check that we have represented all of Config() below class PrefsEditor: """Class implementing the various parts of the preferences editor""" @@ -47,6 +58,8 @@ class PrefsEditor: layouteditor = None previous_layout_selection = None previous_profile_selection = None + previous_selection = None + previous_plugin_selection = None colorschemevalues = {'black_on_yellow': 0, 'black_on_white': 1, 'grey_on_black': 2, @@ -103,92 +116,92 @@ class PrefsEditor: 'gruvbox_dark': '#282828:#cc241d:#98971a:#d79921:\ #458588:#b16286:#689d6a:#a89984:#928374:#fb4934:#b8bb26:#fabd2f:\ #83a598:#d3869b:#8ec07c:#ebdbb2'} - keybindingnames = { 'zoom_in' : _('Increase font size'), - 'zoom_out' : _('Decrease font size'), - 'zoom_normal' : _('Restore original font size'), - 'zoom_in_all' : _('Increase font size on all terminals'), - 'zoom_out_all' : _('Decrease font size on all terminals'), - 'zoom_normal_all' : _('Restore original font size on all terminals'), - 'new_tab' : _('Create a new tab'), - 'cycle_next' : _('Focus the next terminal'), - 'cycle_prev' : _('Focus the previous terminal'), - 'go_next' : _('Focus the next terminal'), - 'go_prev' : _('Focus the previous terminal'), - 'go_up' : _('Focus the terminal above'), - 'go_down' : _('Focus the terminal below'), - 'go_left' : _('Focus the terminal left'), - 'go_right' : _('Focus the terminal right'), - 'rotate_cw' : _('Rotate terminals clockwise'), - 'rotate_ccw' : _('Rotate terminals counter-clockwise'), - 'split_auto' : _('Split automatically'), - 'split_horiz' : _('Split horizontally'), - 'split_vert' : _('Split vertically'), - 'close_term' : _('Close terminal'), - 'copy' : _('Copy selected text'), - 'paste' : _('Paste clipboard'), - 'paste_selection' : _('Paste primary selection'), - 'toggle_scrollbar' : _('Show/Hide the scrollbar'), - 'search' : _('Search terminal scrollback'), - 'page_up' : _('Scroll upwards one page'), - 'page_down' : _('Scroll downwards one page'), - 'page_up_half' : _('Scroll upwards half a page'), - 'page_down_half' : _('Scroll downwards half a page'), - 'line_up' : _('Scroll upwards one line'), - 'line_down' : _('Scroll downwards one line'), - 'close_window' : _('Close window'), - 'resize_up' : _('Resize the terminal up'), - 'resize_down' : _('Resize the terminal down'), - 'resize_left' : _('Resize the terminal left'), - 'resize_right' : _('Resize the terminal right'), - 'move_tab_right' : _('Move the tab right'), - 'move_tab_left' : _('Move the tab left'), - 'toggle_zoom' : _('Maximize terminal'), - 'scaled_zoom' : _('Zoom terminal'), - 'next_tab' : _('Switch to the next tab'), - 'prev_tab' : _('Switch to the previous tab'), - 'switch_to_tab_1' : _('Switch to the first tab'), - 'switch_to_tab_2' : _('Switch to the second tab'), - 'switch_to_tab_3' : _('Switch to the third tab'), - 'switch_to_tab_4' : _('Switch to the fourth tab'), - 'switch_to_tab_5' : _('Switch to the fifth tab'), - 'switch_to_tab_6' : _('Switch to the sixth tab'), - 'switch_to_tab_7' : _('Switch to the seventh tab'), - 'switch_to_tab_8' : _('Switch to the eighth tab'), - 'switch_to_tab_9' : _('Switch to the ninth tab'), - 'switch_to_tab_10' : _('Switch to the tenth tab'), - 'full_screen' : _('Toggle fullscreen'), - 'reset' : _('Reset the terminal'), - 'reset_clear' : _('Reset and clear the terminal'), - 'hide_window' : _('Toggle window visibility'), - 'create_group' : _('Create new group'), - 'group_all' : _('Group all terminals'), - 'group_all_toggle' : _('Group/Ungroup all terminals'), - 'ungroup_all' : _('Ungroup all terminals'), - 'group_win' : _('Group terminals in window'), - 'group_win_toggle' : _('Group/Ungroup terminals in window'), - 'ungroup_win' : _('Ungroup terminals in window'), - 'group_tab' : _('Group terminals in tab'), - 'group_tab_toggle' : _('Group/Ungroup terminals in tab'), - 'ungroup_tab' : _('Ungroup terminals in tab'), - 'new_window' : _('Create a new window'), - 'new_terminator' : _('Spawn a new Terminator process'), - 'broadcast_off' : _('Don\'t broadcast key presses'), - 'broadcast_group' : _('Broadcast key presses to group'), - 'broadcast_all' : _('Broadcast key events to all'), - 'insert_number' : _('Insert terminal number'), - 'insert_padded' : _('Insert zero padded terminal number'), - 'edit_window_title': _('Edit window title'), - 'edit_terminal_title': _('Edit terminal title'), - 'edit_tab_title' : _('Edit tab title'), - 'layout_launcher' : _('Open layout launcher window'), - 'next_profile' : _('Switch to next profile'), - 'previous_profile' : _('Switch to previous profile'), - 'preferences' : _('Open the Preferences window'), - 'preferences_keybindings' : _('Open the Preferences-Keybindings window'), - 'help' : _('Open the manual') - } - - def __init__ (self, term, cur_page=0): + keybindingnames = {'zoom_in': _('Increase font size'), + 'zoom_out': _('Decrease font size'), + 'zoom_normal': _('Restore original font size'), + 'zoom_in_all': _('Increase font size on all terminals'), + 'zoom_out_all': _('Decrease font size on all terminals'), + 'zoom_normal_all': _('Restore original font size on all terminals'), + 'new_tab': _('Create a new tab'), + 'cycle_next': _('Focus the next terminal'), + 'cycle_prev': _('Focus the previous terminal'), + 'go_next': _('Focus the next terminal'), + 'go_prev': _('Focus the previous terminal'), + 'go_up': _('Focus the terminal above'), + 'go_down': _('Focus the terminal below'), + 'go_left': _('Focus the terminal left'), + 'go_right': _('Focus the terminal right'), + 'rotate_cw': _('Rotate terminals clockwise'), + 'rotate_ccw': _('Rotate terminals counter-clockwise'), + 'split_auto': _('Split automatically'), + 'split_horiz': _('Split horizontally'), + 'split_vert': _('Split vertically'), + 'close_term': _('Close terminal'), + 'copy': _('Copy selected text'), + 'paste': _('Paste clipboard'), + 'paste_selection': _('Paste primary selection'), + 'toggle_scrollbar': _('Show/Hide the scrollbar'), + 'search': _('Search terminal scrollback'), + 'page_up': _('Scroll upwards one page'), + 'page_down': _('Scroll downwards one page'), + 'page_up_half': _('Scroll upwards half a page'), + 'page_down_half': _('Scroll downwards half a page'), + 'line_up': _('Scroll upwards one line'), + 'line_down': _('Scroll downwards one line'), + 'close_window': _('Close window'), + 'resize_up': _('Resize the terminal up'), + 'resize_down': _('Resize the terminal down'), + 'resize_left': _('Resize the terminal left'), + 'resize_right': _('Resize the terminal right'), + 'move_tab_right': _('Move the tab right'), + 'move_tab_left': _('Move the tab left'), + 'toggle_zoom': _('Maximize terminal'), + 'scaled_zoom': _('Zoom terminal'), + 'next_tab': _('Switch to the next tab'), + 'prev_tab': _('Switch to the previous tab'), + 'switch_to_tab_1': _('Switch to the first tab'), + 'switch_to_tab_2': _('Switch to the second tab'), + 'switch_to_tab_3': _('Switch to the third tab'), + 'switch_to_tab_4': _('Switch to the fourth tab'), + 'switch_to_tab_5': _('Switch to the fifth tab'), + 'switch_to_tab_6': _('Switch to the sixth tab'), + 'switch_to_tab_7': _('Switch to the seventh tab'), + 'switch_to_tab_8': _('Switch to the eighth tab'), + 'switch_to_tab_9': _('Switch to the ninth tab'), + 'switch_to_tab_10': _('Switch to the tenth tab'), + 'full_screen': _('Toggle fullscreen'), + 'reset': _('Reset the terminal'), + 'reset_clear': _('Reset and clear the terminal'), + 'hide_window': _('Toggle window visibility'), + 'create_group': _('Create new group'), + 'group_all': _('Group all terminals'), + 'group_all_toggle': _('Group/Ungroup all terminals'), + 'ungroup_all': _('Ungroup all terminals'), + 'group_win': _('Group terminals in window'), + 'group_win_toggle': _('Group/Ungroup terminals in window'), + 'ungroup_win': _('Ungroup terminals in window'), + 'group_tab': _('Group terminals in tab'), + 'group_tab_toggle': _('Group/Ungroup terminals in tab'), + 'ungroup_tab': _('Ungroup terminals in tab'), + 'new_window': _('Create a new window'), + 'new_terminator': _('Spawn a new Terminator process'), + 'broadcast_off': _('Don\'t broadcast key presses'), + 'broadcast_group': _('Broadcast key presses to group'), + 'broadcast_all': _('Broadcast key events to all'), + 'insert_number': _('Insert terminal number'), + 'insert_padded': _('Insert zero padded terminal number'), + 'edit_window_title': _('Edit window title'), + 'edit_terminal_title': _('Edit terminal title'), + 'edit_tab_title': _('Edit tab title'), + 'layout_launcher': _('Open layout launcher window'), + 'next_profile': _('Switch to next profile'), + 'previous_profile': _('Switch to previous profile'), + 'preferences': _('Open the Preferences window'), + 'preferences_keybindings': _('Open the Preferences-Keybindings window'), + 'help': _('Open the manual') + } + + def __init__(self, term, cur_page=0): self.config = config.Config() self.config.base.reload() self.term = term @@ -198,15 +211,15 @@ def __init__ (self, term, cur_page=0): self.builder.set_translation_domain(APP_NAME) self.keybindings = Keybindings() self.active_message_dialog = None + self.keybind_filter_str = None try: # Figure out where our library is on-disk so we can open our (head, _tail) = os.path.split(config.__file__) librarypath = os.path.join(head, 'preferences.glade') - gladefile = open(librarypath, 'r') + gladefile = open(librarypath, 'r', encoding='utf-8') gladedata = gladefile.read() except Exception as ex: - print("Failed to find preferences.glade") - print(ex) + print(f"Failed to find preferences.glade:\n{ex}") return self.builder.add_from_string(gladedata) @@ -227,13 +240,13 @@ def __init__ (self, term, cur_page=0): try: self.config.inhibit_save() self.set_values() - except Exception as e: - err('Unable to set values: %s' % e) + except Exception as exc: + err(f'Unable to set values: {exc}') self.config.uninhibit_save() guiget = self.builder.get_object - nb = guiget('notebook1') - nb.set_current_page(cur_page) + notebook = guiget('notebook1') + notebook.set_current_page(cur_page) self.config.base.save_config_with_suffix('_cur') @@ -247,7 +260,7 @@ def on_closebutton_clicked(self, _button): terminator.reconfigure() self.window.destroy() self.calling_window.preventHide = False - del(self) + del self def on_restoreconfigbutton_clicked(self, _button): """restore config to load time""" @@ -259,7 +272,7 @@ def set_values(self): Config()""" guiget = self.builder.get_object - ## Global tab + # GLOBAL TAB # Mouse focus focus = self.config['focus'] active = 0 @@ -278,7 +291,7 @@ def set_values(self): # Cell Height cellheightsize = self.config['cell_height'] - cellheightsize = round(float(cellheightsize),1) + cellheightsize = round(float(cellheightsize), 1) widget = guiget('cellheight') widget.set_value(cellheightsize) widget = guiget('cellheight_value_label') @@ -286,7 +299,7 @@ def set_values(self): # Cell Width cellwidthsize = self.config['cell_width'] - cellwidthsize = round(float(cellwidthsize),1) + cellwidthsize = round(float(cellwidthsize), 1) widget = guiget('cellwidth') widget.set_value(cellwidthsize) widget = guiget('cellwidth_value_label') @@ -317,7 +330,7 @@ def set_values(self): elif option == 'always': active = 2 else: - active = 1 + active = 1 widget = guiget('askbeforeclose') widget.set_active(active) # Window borders @@ -365,16 +378,16 @@ def set_values(self): # Detachable tabs widget = guiget('detachable_tabs') widget.set_active(self.config['detachable_tabs']) - #Hide from taskbar + # Hide from taskbar widget = guiget('hidefromtaskbcheck') widget.set_active(self.config['hide_from_taskbar']) - #Always on top + # Always on top widget = guiget('alwaysontopcheck') widget.set_active(self.config['always_on_top']) - #Hide on lose focus + # Hide on lose focus widget = guiget('hideonlosefocuscheck') widget.set_active(self.config['hide_on_lose_focus']) - #Show on all workspaces + # Show on all workspaces widget = guiget('stickycheck') widget.set_active(self.config['sticky']) @@ -386,7 +399,7 @@ def set_values(self): widget = guiget('new_tab_after_current_checkbutton') widget.set_active(self.config['new_tab_after_current_tab']) - #Always split with profile + # Always split with profile widget = guiget('always_split_with_profile') widget.set_active(self.config['always_split_with_profile']) # Putty paste style @@ -408,7 +421,7 @@ def set_values(self): widget = guiget('disable_mouse_paste') widget.set_active(self.config['disable_mouse_paste']) - ## Profile tab + # PROFILE TAB # Populate the profile list widget = guiget('profilelist') liststore = widget.get_model() @@ -426,7 +439,7 @@ def set_values(self): selection.connect('changed', self.on_profile_selection_changed) selection.select_iter(self.profileiters['default']) - ## Layouts tab + # LAYOUTS TAB widget = guiget('layoutlist') liststore = widget.get_model() layouts = self.config.list_layouts() @@ -452,39 +465,43 @@ def set_values(self): selection = widget.get_selection() selection.connect('changed', self.on_layout_item_selection_changed) - ## Keybindings tab - widget = guiget('keybindingtreeview') + # KEYBINDINGS TAB + widget = guiget('keybindingtreeview') kbsearch = guiget('keybindingsearchentry') self.keybind_filter_str = "" - #lets hide whatever we can in nested scope + # let's hide whatever we can in nested scope def filter_visible(model, treeiter, data): - act = model[treeiter][0] + act = model[treeiter][0] keys = data[act] if act in data else "" desc = model[treeiter][1] - kval = model[treeiter][2] - mask = model[treeiter][3] - #so user can search for disabled keys also - if not (len(keys) and kval and mask): + kval0 = model[treeiter][2] + mask0 = model[treeiter][3] + kval1 = model[treeiter][4] + mask1 = model[treeiter][5] + # so user can search for disabled keys also + if isinstance(keys, str): + keys = [keys, ''] + if not (any(keys) and any([kval0, mask0, kval1, mask1])): act = "Disabled" self.keybind_filter_str = self.keybind_filter_str.lower() - searchtxt = (act + " " + keys + " " + desc).lower() + keys_str = re.sub(r'[\[,\]]', '', str(keys)) + searchtxt = f'{act} {keys_str} {desc}'.lower() pos = searchtxt.find(self.keybind_filter_str) - if (pos >= 0): - dbg("filter find:%s in search text: %s" % - (self.keybind_filter_str, searchtxt)) + if pos >= 0: + dbg(f"filter find:{self.keybind_filter_str} in search text: {searchtxt}") return True return False def on_search(widget, text): - MAX_SEARCH_LEN = 10 + max_search_len = 10 self.keybind_filter_str = widget.get_text() - ln = len(self.keybind_filter_str) - #its a small list & we are eager for quick search, but limit - if (ln >=2 and ln < MAX_SEARCH_LEN): - dbg("filter search str: %s" % self.keybind_filter_str) + length = len(self.keybind_filter_str) + # it's a small list & we are eager for quick search, but limit + if 2 <= length < max_search_len: + dbg(f"filter search str: {self.keybind_filter_str}") self.treemodelfilter.refilter() def on_search_refilter(widget): @@ -498,31 +515,44 @@ def on_search_refilter(widget): liststore.set_sort_column_id(0, Gtk.SortType.ASCENDING) keybindings = self.config['keybindings'] - keybindutil = KeyBindUtil() - plugin_keyb_act = keybindutil.get_all_act_to_keys() - plugin_keyb_desc = keybindutil.get_all_act_to_desc() - #merge give preference to main bindings over plugin - keybindings = {**plugin_keyb_act, **keybindings} + keybindutil = KeyBindUtil() + plugin_keyb_act = keybindutil.get_all_act_to_keys() + plugin_keyb_desc = keybindutil.get_all_act_to_desc() + # merge give preference to main bindings over plugin + keybindings = {**plugin_keyb_act, **keybindings} self.keybindingnames = {**plugin_keyb_desc, **self.keybindingnames} - #dbg("appended actions %s names %s" % (keybindings, self.keybindingnames)) + # dbg(f"appended actions {keybindings} names {self.keybindingnames}") - for keybinding in keybindings: + for keybinding, value in keybindings.items(): keyval = 0 mask = 0 - value = keybindings[keybinding] - if value is not None and value != '': - try: - (keyval, mask) = self.keybindings._parsebinding(value) - except KeymapError: - pass - liststore.append([keybinding, self.keybindingnames[keybinding], - keyval, mask]) + if isinstance(value, str): + if value is not None and value != '': + try: + keyval, mask = self.keybindings.parsebinding(value) + except KeymapError: + pass + liststore.append([keybinding, self.keybindingnames[keybinding], + keyval, mask, 0, 0]) + self.config['keybindings'][keybinding] = [value, ''] + if isinstance(value, list): + bindings_list = [(0, 0), (0, 0)] + for index, val in enumerate(value): + if val is not None and val != '': + try: + keyval, mask = self.keybindings.parsebinding(val) + bindings_list[index] = (keyval, mask) + except KeymapError: + pass + liststore.append([keybinding, self.keybindingnames[keybinding], + bindings_list[0][0], bindings_list[0][1], + bindings_list[1][0], bindings_list[1][1]]) self.treemodelfilter = liststore.filter_new() self.treemodelfilter.set_visible_func(filter_visible, keybindings) widget.set_model(self.treemodelfilter) - ## Plugins tab + # PLUGINS TAB # Populate the plugin list widget = guiget('pluginlist') liststore = widget.get_model() @@ -531,12 +561,11 @@ def on_search_refilter(widget): pluginlist = self.registry.get_available_plugins() self.plugins = {} for plugin in pluginlist: - if plugin[0] != "_": # Do not display hidden plugins + if plugin[0] != "_": # Do not display hidden plugins self.plugins[plugin] = self.registry.is_enabled(plugin) - for plugin in self.plugins: - self.pluginiters[plugin] = liststore.append([plugin, - self.plugins[plugin]]) + for plugin, plugin_value in self.plugins.items(): + self.pluginiters[plugin] = liststore.append([plugin, plugin_value]) selection = widget.get_selection() selection.connect('changed', self.on_plugin_selection_changed) if len(self.pluginiters) > 0: @@ -547,9 +576,9 @@ def set_profile_values(self, profile): self.config.set_profile(profile) guiget = self.builder.get_object - dbg('Setting profile %s' % profile) + dbg(f'Setting profile {profile}') - ## General tab + # GENERAL TAB # Use system font widget = guiget('system_font_checkbutton') widget.set_active(self.config['use_system_font']) @@ -557,7 +586,7 @@ def set_profile_values(self, profile): # Font selector widget = guiget('font_selector') - if self.config['use_system_font'] == True: + if self.config['use_system_font'] is True: fontname = self.config.get_system_mono_font() if fontname is not None: widget.set_font_name(fontname) @@ -624,7 +653,7 @@ def set_profile_values(self, profile): except: widget.set_color(Gdk.color_parse('#ffffff')) - ## Command tab + # COMMAND TAB # Login shell widget = guiget('login_shell_checkbutton') widget.set_active(self.config['login_shell']) @@ -645,7 +674,7 @@ def set_profile_values(self, profile): # Default is to close the terminal widget.set_active(0) - ## Colors tab + # COLORS TAB # Use system colors widget = guiget('use_theme_colors_checkbutton') widget.set_active(self.config['use_theme_colors']) @@ -655,9 +684,9 @@ def set_profile_values(self, profile): # Colorscheme widget = guiget('color_scheme_combobox') scheme = None - for ascheme in self.colourschemes: - forecol = self.colourschemes[ascheme][0] - backcol = self.colourschemes[ascheme][1] + for ascheme, scheme_value in self.colourschemes.items(): + forecol = scheme_value[0] + backcol = scheme_value[1] if self.config['foreground_color'].lower() == forecol and \ self.config['background_color'].lower() == backcol: scheme = ascheme @@ -690,8 +719,8 @@ def set_profile_values(self, profile): # Palette scheme widget = guiget('palette_combobox') palette = None - for apalette in self.palettes: - if self.config['palette'].lower() == self.palettes[apalette]: + for apalette, palette_value in self.palettes.items(): + if self.config['palette'].lower() == palette_value: palette = apalette if palette not in self.palettevalues: if self.config['palette'] in [None, '']: @@ -703,6 +732,7 @@ def set_profile_values(self, profile): for palette_id in range(0, NUM_PALETTE_COLORS): widget = self.get_palette_widget(palette_id) widget.set_events(Gdk.EventMask.BUTTON_PRESS_MASK) + def on_palette_click(event, data, widget=widget): self.edit_palette_button(widget) widget.connect('button-press-event', on_palette_click) @@ -714,11 +744,11 @@ def on_palette_click(event, data, widget=widget): widget = guiget('inactive_color_offset') widget.set_value(float(self.config['inactive_color_offset'])) widget = guiget('inactive_color_offset_value_label') - widget.set_text('%d%%' % (int(float(self.config['inactive_color_offset'])*100))) + widget.set_text(f"{int(float(self.config['inactive_color_offset'])*100)}%%") widget = guiget('inactive_bg_color_offset') widget.set_value(float(self.config['inactive_bg_color_offset'])) widget = guiget('inactive_bg_color_offset_value_label') - widget.set_text('%d%%' % (int(float(self.config['inactive_bg_color_offset'])*100))) + widget.set_text(f"{int(float(self.config['inactive_bg_color_offset'])*100)}%%") # Open links with a single click (instead of a Ctrl-left click) widget = guiget('link_single_click') widget.set_active(self.config['link_single_click']) @@ -730,7 +760,7 @@ def on_palette_click(event, data, widget=widget): widget = guiget('custom_url_handler_entry') widget.set_text(self.config['custom_url_handler']) - ## Background tab + # BACKGROUND TAB # Radio values if self.config['background_type'] == 'solid': guiget('solid_radiobutton').set_active(True) @@ -775,8 +805,8 @@ def on_palette_click(event, data, widget=widget): # Background shading widget = guiget('background_darkness_scale') widget.set_value(float(self.config['background_darkness'])) - - ## Scrolling tab + + # SCROLLING TAB # Scrollbar position widget = guiget('scrollbar_position_combobox') value = self.config['scrollbar_position'] @@ -799,7 +829,7 @@ def on_palette_click(event, data, widget=widget): widget = guiget('scroll_on_keystroke_checkbutton') widget.set_active(self.config['scroll_on_keystroke']) - ## Compatibility tab + # COMPATIBILITY TAB # Backspace key widget = guiget('backspace_binding_combobox') value = self.config['backspace_binding'] @@ -823,11 +853,11 @@ def on_palette_click(event, data, widget=widget): else: widget.set_active(0) - ## Titlebar tab + # TITLEBAR TAB # Titlebar colors for bit in ['title_transmit_fg_color', 'title_transmit_bg_color', - 'title_receive_fg_color', 'title_receive_bg_color', - 'title_inactive_fg_color', 'title_inactive_bg_color']: + 'title_receive_fg_color', 'title_receive_bg_color', + 'title_inactive_fg_color', 'title_inactive_bg_color']: widget = guiget(bit) widget.set_color(Gdk.color_parse(self.config[bit])) # Hide size text from the title bar @@ -839,7 +869,7 @@ def on_palette_click(event, data, widget=widget): self.on_title_system_font_checkbutton_toggled(widget) # Font selector widget = guiget('title_font_selector') - if self.config['title_use_system_font'] == True: + if self.config['title_use_system_font'] is True: fontname = self.config.get_system_prop_font() if fontname is not None: widget.set_font_name(fontname) @@ -878,6 +908,7 @@ def on_dbuscheck_toggled(self, widget): self.config.save() def on_detachable_tabs_toggled(self, widget): + """Detachable tabs setting changed""" self.config['detachable_tabs'] = widget.get_active() self.config.save() @@ -968,7 +999,7 @@ def on_smart_copy_toggled(self, widget): self.config['smart_copy'] = widget.get_active() self.config.save() - def on_clear_select_on_copy_toggled(self,widget): + def on_clear_select_on_copy_toggled(self, widget): """Clear selection on smart copy""" self.config['clear_select_on_copy'] = widget.get_active() self.config.save() @@ -1056,7 +1087,7 @@ def on_scrollback_infinite_toggled(self, widget): """Scrollback infiniteness changed""" spinbutton = self.builder.get_object('scrollback_lines_spinbutton') value = widget.get_active() - if value == True: + if value is True: spinbutton.set_sensitive(False) else: spinbutton.set_sensitive(True) @@ -1075,11 +1106,13 @@ def on_scrollbar_position_combobox_changed(self, widget): self.config['scrollbar_position'] = value self.config.save() - def on_background_image_file_set(self,widget): + def on_background_image_file_set(self, widget): + """Background image setting changed""" self.config['background_image'] = widget.get_filename() self.config.save() def on_background_image_mode_changed(self, widget): + """Background image mode setting changed""" selected = widget.get_active() if selected == 1: value = 'scale_and_fit' @@ -1093,6 +1126,7 @@ def on_background_image_mode_changed(self, widget): self.config.save() def on_background_image_align_horiz_changed(self, widget): + """Background image horizontal alignment setting changed""" selected = widget.get_active() if selected == 1: value = 'center' @@ -1104,6 +1138,7 @@ def on_background_image_align_horiz_changed(self, widget): self.config.save() def on_background_image_align_vert_changed(self, widget): + """Background image vertical alignment setting changed""" selected = widget.get_active() if selected == 1: value = 'middle' @@ -1117,8 +1152,7 @@ def on_background_image_align_vert_changed(self, widget): def on_darken_background_scale_value_changed(self, widget): """Background darkness setting changed""" value = widget.get_value() # This one is rounded according to the UI. - if value > 1.0: - value = 1.0 + value = min(value, 1.0) self.config['background_darkness'] = value self.config.save() @@ -1145,7 +1179,7 @@ def on_palette_combobox_changed(self, widget): palettebits.append(get_color_string(self.get_palette_color(palette_id))) palette = ':'.join(palettebits) else: - err('Unknown palette value: %s' % value) + err(f'Unknown palette value: {value}') return self.config['palette'] = palette @@ -1163,7 +1197,7 @@ def on_foreground_colorbutton_draw(self, widget, cr): cr.fill() def on_foreground_colorbutton_click(self, event, data): - dialog = Gtk.ColorChooserDialog("Choose Terminal Text Color") + dialog = Gtk.ColorChooserDialog(_("Choose Terminal Text Color")) fg = self.config['foreground_color'] dialog.set_rgba(Gdk.RGBA.from_color(Gdk.color_parse(self.config['foreground_color']))) dialog.connect('notify::rgba', self.on_foreground_colorpicker_color_change) @@ -1216,7 +1250,7 @@ def on_background_colorpicker_color_change(self, widget, color): def get_palette_widget(self, palette_id): """Returns the palette widget for the given palette ID.""" guiget = self.builder.get_object - return guiget('palette_colorpicker_%d' % (palette_id + 1)) + return guiget(f'palette_colorpicker_{palette_id + 1}') def get_palette_id(self, widget): """Returns the palette ID for the given palette widget.""" @@ -1401,31 +1435,28 @@ def on_title_transmit_fg_color_color_set(self, widget): def on_inactive_color_offset_value_changed(self, widget): """Inactive color offset setting changed""" value = widget.get_value() # This one is rounded according to the UI. - if value > 1.0: - value = 1.0 + value = min(value, 1.0) self.config['inactive_color_offset'] = value self.config.save() guiget = self.builder.get_object label_widget = guiget('inactive_color_offset_value_label') - label_widget.set_text('%d%%' % (int(value * 100))) + label_widget.set_text(f'{int(value * 100)}%%') def on_inactive_bg_color_offset_value_changed(self, widget): """Inactive background color offset setting changed""" value = widget.get_value() # This one is rounded according to the UI. - if value > 1.0: - value = 1.0 + value = min(value, 1.0) self.config['inactive_bg_color_offset'] = value self.config.save() guiget = self.builder.get_object label_widget = guiget('inactive_bg_color_offset_value_label') - label_widget.set_text('%d%%' % (int(value * 100))) + label_widget.set_text(f'{int(value * 100)}%%') def on_handlesize_value_changed(self, widget): """Handle size changed""" value = widget.get_value() # This one is rounded according to the UI. value = int(value) # Cast to int. - if value > 20: - value = 20 + value = min(value, 20) self.config['handle_size'] = value self.config.save() guiget = self.builder.get_object @@ -1436,8 +1467,7 @@ def on_cellheight_value_changed(self, widget): """Handles cell height changed""" value = widget.get_value() value = round(float(value), 1) - if value > 2.0: - value = 2.0 + value = min(value, 2.0) self.config['cell_height'] = value self.config.save() guiget = self.builder.get_object @@ -1448,8 +1478,7 @@ def on_cellwidth_value_changed(self, widget): """Handles cell width changed""" value = widget.get_value() value = round(float(value), 1) - if value > 2.0: - value = 2.0 + value = min(value, 2.0) self.config['cell_width'] = value self.config.save() guiget = self.builder.get_object @@ -1509,6 +1538,7 @@ def on_winstatecombo_changed(self, widget): value = 'normal' self.config['window_state'] = value self.config.save() + def on_askbeforeclose_changed(self, widget): """Ask Before Close changed""" selected = widget.get_active() @@ -1516,13 +1546,14 @@ def on_askbeforeclose_changed(self, widget): value = 'Never' elif selected == 1: value = 'Multiple Terminals' - elif selected == 2: + elif selected == 2: value = 'Always' else: value = 'Multiple Terminals' - configval = value.lower().replace(" ","_") + configval = value.lower().replace(" ", "_") self.config['ask_before_closing'] = configval self.config.save() + # helper function, not a signal def addprofile(self, name, toclone): """Add a profile""" @@ -1530,14 +1561,14 @@ def addprofile(self, name, toclone): treeview = guiget('profilelist') model = treeview.get_model() - values = [ r[0] for r in model ] + values = [r[0] for r in model] newprofile = name if newprofile in values: i = 1 while newprofile in values: i = i + 1 - newprofile = '%s %d' % (name, i) + newprofile = f'{name} {i}' if self.config.add_profile(newprofile, toclone): res = model.append([newprofile, True]) @@ -1591,14 +1622,15 @@ def on_layoutaddbutton_clicked(self, _button): treeview = guiget('layoutlist') model = treeview.get_model() - values = [ r[0] for r in model ] + values = [r[0] for r in model] name = _('New Layout') if name in values: i = 0 while name in values: i = i + 1 - name = '%s %d' % (_('New Layout'), i) + new_layout_str = _('New Layout') + name = f'{new_layout_str} {i}' if self.config.add_layout(name, current_layout): res = model.append([name, True]) @@ -1628,7 +1660,9 @@ def on_layoutrefreshbutton_clicked(self, _button): dbg("updated layout from terminator:(%s)" % current_layout) if self.config.replace_layout(name, current_layout): - treeview.set_cursor(model.get_path(rowiter), column=treeview.get_column(0), start_editing=False) + treeview.set_cursor(model.get_path(rowiter), + column=treeview.get_column(0), + start_editing=False) self.config.save() self.layouteditor.set_layout(name) @@ -1690,8 +1724,8 @@ def on_system_font_checkbutton_toggled(self, checkbox): widget.set_sensitive(not value) self.config['use_system_font'] = value self.config.save() - - if self.config['use_system_font'] == True: + + if self.config['use_system_font'] is True: fontname = self.config.get_system_mono_font() if fontname is not None: widget.set_font_name(fontname) @@ -1709,7 +1743,7 @@ def on_title_system_font_checkbutton_toggled(self, checkbox): self.config['title_use_system_font'] = value self.config.save() - if self.config['title_use_system_font'] == True: + if self.config['title_use_system_font'] is True: fontname = self.config.get_system_prop_font() if fontname is not None: widget.set_font_name(fontname) @@ -1739,9 +1773,9 @@ def update_background_tab(self): imagewidget = guiget('image_radiobutton') transwidget = guiget('transparent_radiobutton') - if imagewidget.get_active() == True: + if imagewidget.get_active() is True: backtype = 'image' - elif transwidget.get_active() == True: + elif transwidget.get_active() is True: backtype = 'transparent' else: backtype = 'solid' @@ -1792,7 +1826,7 @@ def on_plugin_selection_changed(self, selection): self.set_plugin(plugin) self.previous_plugin_selection = plugin - widget = self.builder.get_object('plugintogglebutton') + # widget = self.builder.get_object('plugintogglebutton') def on_plugin_toggled(self, cell, path): """A plugin has been enabled or disabled""" @@ -1811,22 +1845,22 @@ def on_plugin_toggled(self, cell, path): # Update the treeview model[path][1] = self.plugins[plugin] - enabled_plugins = [x for x in self.plugins if self.plugins[x] == True] + enabled_plugins = [x for x, val in self.plugins.items() if val is True] self.config['enabled_plugins'] = enabled_plugins self.config.save() def set_plugin(self, plugin): """Show the preferences for the selected plugin, if any""" - pluginpanelabel = self.builder.get_object('pluginpanelabel') - pluginconfig = self.config.plugin_get_config(plugin) + # pluginpanelabel = self.builder.get_object('pluginpanelabel') + # pluginconfig = self.config.plugin_get_config(plugin) # FIXME: Implement this, we need to auto-construct a UI for the plugin def on_profile_name_edited(self, cell, path, newtext): """Update a profile name""" oldname = cell.get_property('text') - if oldname == newtext or oldname == 'default': + if oldname in (newtext, 'default'): return - dbg('Changing %s to %s' % (oldname, newtext)) + dbg(f'Changing {oldname} to {newtext}') self.config.rename_profile(oldname, newtext) self.config.save() @@ -1861,9 +1895,9 @@ def on_layout_profile_workingdir_changed(self, widget): def on_layout_name_edited(self, cell, path, newtext): """Update a layout name""" oldname = cell.get_property('text') - if oldname == newtext or oldname == 'default': + if oldname in (newtext, 'default'): return - dbg('Changing %s to %s' % (oldname, newtext)) + dbg(f'Changing {oldname} to {newtext}') self.config.rename_layout(oldname, newtext) self.config.save() @@ -1912,8 +1946,8 @@ def on_use_theme_colors_checkbutton_toggled(self, widget): back = guiget('background_colorbutton') if active: - for widget in [scheme, fore, back]: - widget.set_sensitive(False) + for _widget in [scheme, fore, back]: + _widget.set_sensitive(False) else: scheme.set_sensitive(True) self.on_color_scheme_combobox_changed(scheme) @@ -1926,15 +1960,26 @@ def on_bold_text_is_bright_checkbutton_toggled(self, widget): self.config['bold_is_bright'] = widget.get_active() self.config.save() - def on_cellrenderer_accel_edited(self, liststore, path, key, mods, _code): - inpath = path #save for debugging - trpath = Gtk.TreePath.new_from_string(inpath) - path = str(self.treemodelfilter.convert_path_to_child_path(trpath)) - dbg("convert path with filter from: %s to: %s" % - (inpath, path)) + def on_cellrenderer_accel_edited_keybind1(self, liststore, path, key, mods, _code): + """Handle an edited keybinding 1""" + self.on_cellrenderer_accel_edited(liststore, path, key, mods, _code, + 2, 3, 0) + + def on_cellrenderer_accel_edited_keybind2(self, liststore, path, key, mods, _code): + """Handle an edited keybinding 2""" + self.on_cellrenderer_accel_edited(liststore, path, key, mods, _code, + 4, 5, 1) + + def on_cellrenderer_accel_edited(self, liststore, path, key, mods, _code, key_index, mod_index, binding_index): """Handle an edited keybinding""" + inpath = path # save for debugging + trpath = Gtk.TreePath.new_from_string(inpath) + path = str(self.treemodelfilter.convert_path_to_child_path(trpath)) + dbg(f"convert path with filter from: {inpath} to: {path}") + # Ignore `Gdk.KEY_Tab` so that `Shift+Tab` is displayed as `Shift+Tab` - # in `Preferences>Keybindings` and NOT `Left Tab` (see `Gdk.KEY_ISO_Left_Tab`). + # in `Preferences>Keybindings` and NOT `Left Tab` + # (see `Gdk.KEY_ISO_Left_Tab`). if mods & Gdk.ModifierType.SHIFT_MASK and key != Gdk.KEY_Tab: key_with_shift = Gdk.Keymap.translate_keyboard_state( self.keybindings.keymap, @@ -1944,7 +1989,7 @@ def on_cellrenderer_accel_edited(self, liststore, path, key, mods, _code): ) keyval_lower, keyval_upper = Gdk.keyval_convert_case(key) - # Remove the Shift modifier from `mods` if a new key binding doesn't + # Remove the Shift modifier from `mods` if a new keybinding doesn't # contain a letter and its key value (`key`) can't be modified by a # Shift key. if key_with_shift.level != 0 and keyval_lower == keyval_upper: @@ -1955,24 +2000,22 @@ def on_cellrenderer_accel_edited(self, liststore, path, key, mods, _code): current_binding = liststore.get_value(liststore.get_iter(path), 0) parsed_accel = Gtk.accelerator_parse(accel) - keybindutil = KeyBindUtil() - keybindings = self.config["keybindings"] - #merge give preference to main bindings over plugin - plugin_keyb_act = keybindutil.get_all_act_to_keys() - keybindings = {**plugin_keyb_act, **keybindings} + keybindutil = KeyBindUtil() + keybindings = self.config["keybindings"] + # merge give preference to main bindings over plugin + plugin_keyb_act = keybindutil.get_all_act_to_keys() + keybindings = {**plugin_keyb_act, **keybindings} duplicate_bindings = [] - for conf_binding, conf_accel in keybindings.items(): - if conf_accel is None: - continue + for conf_binding, conf_accels in keybindings.items(): + for conf_accel in conf_accels: + if conf_accel is None: + continue - parsed_conf_accel = Gtk.accelerator_parse(conf_accel) + parsed_conf_accel = Gtk.accelerator_parse(conf_accel) - if ( - parsed_accel == parsed_conf_accel - and current_binding != conf_binding - ): - duplicate_bindings.append((conf_binding, conf_accel)) + if (parsed_accel == parsed_conf_accel and current_binding != conf_binding): + duplicate_bindings.append((conf_binding, conf_accel)) if duplicate_bindings: dialog = Gtk.MessageDialog( @@ -1980,16 +2023,14 @@ def on_cellrenderer_accel_edited(self, liststore, path, key, mods, _code): flags=Gtk.DialogFlags.MODAL, message_type=Gtk.MessageType.ERROR, buttons=Gtk.ButtonsType.CLOSE, - text="Duplicate Key Bindings Are Not Allowed", + text=_("Duplicate keybindings are not allowed."), ) accel_label = Gtk.accelerator_get_label(key, mods) # get the first found duplicate duplicate_keybinding_name = duplicate_bindings[0][0] - message = ( - "Key binding `{0}` is already in use to trigger the `{1}` action." - ).format(accel_label, self.keybindingnames[duplicate_keybinding_name]) + message = f"Key binding `{accel_label}` is already in use to trigger the `{self.keybindingnames[duplicate_keybinding_name]}` action." dialog.format_secondary_text(message) self.active_message_dialog = dialog @@ -1999,43 +2040,66 @@ def on_cellrenderer_accel_edited(self, liststore, path, key, mods, _code): return - celliter = liststore.get_iter_from_string(path) - liststore.set(celliter, 2, key, 3, mods) - binding = liststore.get_value(liststore.get_iter(path), 0) accel = Gtk.accelerator_name(key, mods) - self.config['keybindings'][binding] = accel + alt_index = abs(binding_index - 1) + if self.config['keybindings'][binding][alt_index] != accel: + self.config['keybindings'][binding][binding_index] = accel + else: + dialog = Gtk.MessageDialog( + transient_for=self.window, + flags=Gtk.DialogFlags.MODAL, + message_type=Gtk.MessageType.ERROR, + buttons=Gtk.ButtonsType.CLOSE, + text=_("Duplicate keybindings are not allowed."), + ) + + self.active_message_dialog = dialog + dialog.run() + dialog.destroy() + self.active_message_dialog = None + + return + + celliter = liststore.get_iter_from_string(path) + liststore.set(celliter, key_index, key, mod_index, mods) plugin_keyb_desc = keybindutil.get_act_to_desc(binding) if plugin_keyb_desc: - dbg("modifying plugin binding: %s, %s, %s" % - (plugin_keyb_desc, binding, accel)) + dbg(f"modifying plugin binding: %{plugin_keyb_desc}, {binding}, {accel}") keybindutil.bindkey([plugin_keyb_desc, binding, accel]) else: - dbg("skipping: %s" % binding) - pass + dbg(f"skipping: {binding}") self.config.save() - def on_cellrenderer_accel_cleared(self, liststore, path): - inpath = path #save for debugging - trpath = Gtk.TreePath.new_from_string(inpath) - path = str(self.treemodelfilter.convert_path_to_child_path(trpath)) - dbg("convert path with filter from: %s to: %s" % - (inpath, path)) + def on_cellrenderer_accel_cleared_keybind1(self, liststore, path): + """Handle the clearing of keybinding 1 accelerator""" + self.on_cellrenderer_accel_cleared(liststore, path, 2, 3, 0) + + def on_cellrenderer_accel_cleared_keybind2(self, liststore, path): + """Handle the clearing of keybinding 2 accelerator""" + self.on_cellrenderer_accel_cleared(liststore, path, 4, 5, 1) + def on_cellrenderer_accel_cleared(self, liststore, path, key_index, mod_index, binding_index): """Handle the clearing of a keybinding accelerator""" + inpath = path # save for debugging + trpath = Gtk.TreePath.new_from_string(inpath) + path = str(self.treemodelfilter.convert_path_to_child_path(trpath)) + dbg(f"convert path with filter from: {inpath} to: {path}") + celliter = liststore.get_iter_from_string(path) - liststore.set(celliter, 2, 0, 3, 0) + liststore.set(celliter, key_index, 0, mod_index, 0) binding = liststore.get_value(liststore.get_iter(path), 0) - self.config['keybindings'][binding] = "" + self.config['keybindings'][binding][binding_index] = '' self.config.save() - def on_open_manual(self, widget): + def on_open_manual(self, widget): """Open the fine manual""" self.term.key_help() + class LayoutEditor: profile_ids_to_profile = None profile_profile_to_ids = None @@ -2045,6 +2109,7 @@ class LayoutEditor: treeview = None treestore = None config = None + previous_layout_selection = None def __init__(self, builder): """Initialise ourself""" @@ -2068,15 +2133,14 @@ def set_layout(self, layout_name): store.clear() children = list(layout.keys()) - i = 0 - while children != []: + while children: child = children.pop() child_type = layout[child]['type'] parent = layout[child]['parent'] if child_type != 'Window' and parent not in layout: # We have an orphan! - err('%s is an orphan in this layout. Discarding' % child) + err(f'{child} is an orphan in this layout. Discarding') continue try: parentiter = listitems[parent] @@ -2101,7 +2165,7 @@ def set_layout(self, layout_name): def update_profiles(self): """Update the list of profiles""" self.profile_ids_to_profile = {} - self.profile_profile_to_ids= {} + self.profile_profile_to_ids = {} chooser = self.builder.get_object('layout_profile_chooser') profiles = self.config.list_profiles() @@ -2201,6 +2265,7 @@ def on_layout_profile_workingdir_activate(self, widget): layout[self.layout_item]['directory'] = workdir self.config.save() + if __name__ == '__main__': from . import util util.DEBUG = True diff --git a/terminatorlib/terminal_popup_menu.py b/terminatorlib/terminal_popup_menu.py index d23406b7..3f897bf8 100644 --- a/terminatorlib/terminal_popup_menu.py +++ b/terminatorlib/terminal_popup_menu.py @@ -1,6 +1,6 @@ # Terminator by Chris Jones # GPL v2 only -"""terminal_popup_menu.py - classes necessary to provide a terminal context +"""terminal_popup_menu.py - classes necessary to provide a terminal context menu""" from gi.repository import Gtk, Gdk @@ -13,12 +13,14 @@ from .prefseditor import PrefsEditor from . import plugin -class TerminalPopupMenu(object): + +class TerminalPopupMenu: """Class implementing the Terminal context menu""" terminal = None terminator = None config = None accelgrp = None + popup_menu = None def __init__(self, terminal): """Class initialiser""" @@ -34,53 +36,55 @@ def get_menu_item_mask(self, maskstr): maskstr = maskstr.lower() if maskstr.find(''.lower()) >= 0: mask = mask | Gdk.ModifierType.SHIFT_MASK - dbg("adding mask %s" % mask) + dbg(f"adding mask {mask}") ctrl = (maskstr.find(''.lower()) >= 0 or maskstr.find(''.lower()) >= 0) if ctrl: mask = mask | Gdk.ModifierType.CONTROL_MASK - dbg("adding mask %s" % mask) + dbg(f"adding mask {mask}") if maskstr.find(''.lower()) >= 0: mask = mask | Gdk.ModifierType.MOD1_MASK - dbg("adding mask %s" % mask) + dbg(f"adding mask {mask}") mask = Gdk.ModifierType(mask) - dbg("menu_item_mask :%d" % mask) + dbg(f"menu_item_mask :{mask}") return mask def menu_item(self, menutype, actstr, menustr): - act = self.config.base.get_item('keybindings', actstr) - maskstr = act[actstr] if actstr in act else "" - mask = self.get_menu_item_mask(maskstr) + act = self.config.base.get_item('keybindings', actstr) + maskstr = '' + for keymask in act.get(actstr, []): + if keymask: + maskstr = keymask + break + mask = self.get_menu_item_mask(maskstr) accelchar = "" pos = menustr.lower().find("_") if (pos >= 0 and pos+1 < len(menustr)): accelchar = menustr.lower()[pos+1] - #this may require tweak. what about shortcut function keys ? + # this may require tweak. what about shortcut function keys ? if maskstr: mpos = maskstr.rfind(">") - #can't have a char at 0 position as <> is len 2 + # can't have a char at 0 position as <> is len 2 if mpos >= 0 and mpos+1 < len(maskstr): configaccelchar = maskstr[mpos+1:] - #ensure to take only 1 char else ignore + # ensure to take only 1 char else ignore if len(configaccelchar) == 1: - dbg("found accelchar in config:%s override:%s" - % (configaccelchar, accelchar)) + dbg(f"found accelchar in config:{configaccelchar} override:{accelchar}") accelchar = configaccelchar - dbg("action from config:%s for item:%s with shortcut accelchar:(%s)" - % (maskstr, menustr, accelchar)) + dbg(f"action from config:{maskstr} for item:{menustr} with shortcut accelchar:({accelchar})") item = menutype.new_with_mnemonic(_(menustr)) if mask: item.add_accelerator("activate", - self.accelgrp, - Gdk.keyval_from_name(accelchar), - mask, - Gtk.AccelFlags.VISIBLE) + self.accelgrp, + Gdk.keyval_from_name(accelchar), + mask, + Gtk.AccelFlags.VISIBLE) return item def show(self, widget, event=None): @@ -90,24 +94,17 @@ def show(self, widget, event=None): menu = Gtk.Menu() self.popup_menu = menu url = None - button = None - time = None self.config.set_profile(terminal.get_profile()) if event: url = terminal.vte.match_check_event(event) - button = event.button - time = event.time - else: - time = 0 - button = 3 if url and url[0]: - dbg("URL matches id: %d" % url[1]) + dbg(f"URL matches id: {url[1]}") if not url[1] in list(terminal.matches.values()): - err("Unknown URL match id: %d" % url[1]) - dbg("Available matches: %s" % terminal.matches) + err(f"Unknown URL match id: {url[1]}") + dbg(f"Available matches: {terminal.matches}") nameopen = None namecopy = None @@ -119,19 +116,19 @@ def show(self, widget, event=None): namecopy = _('_Copy VoIP address') elif url[1] in list(terminal.matches.values()): # This is a plugin match + plugin_name = '' for pluginname in terminal.matches: if terminal.matches[pluginname] == url[1]: + dbg(f"Found match ID ({url[1]}) in terminal.matches plugin {pluginname}") + plugin_name = pluginname break - dbg("Found match ID (%d) in terminal.matches plugin %s" % - (url[1], pluginname)) registry = plugin.PluginRegistry() registry.load_plugins() plugins = registry.get_plugins_by_capability('url_handler') for urlplugin in plugins: - if urlplugin.handler_name == pluginname: - dbg("Identified matching plugin: %s" % - urlplugin.handler_name) + if urlplugin.handler_name == plugin_name: + dbg(f"Identified matching plugin: {urlplugin.handler_name}") nameopen = _(urlplugin.nameopen) namecopy = _(urlplugin.namecopy) break @@ -149,7 +146,7 @@ def show(self, widget, event=None): menu.append(item) item = Gtk.MenuItem.new_with_mnemonic(namecopy) - item.connect('activate', + item.connect('activate', lambda x: terminal.clipboard.set_text(terminal.prepare_url(url), len(terminal.prepare_url(url)))) menu.append(item) @@ -171,22 +168,14 @@ def show(self, widget, event=None): 'Set _Window Title') item.connect('activate', lambda x: terminal.key_edit_window_title()) menu.append(item) - + if not terminal.is_zoomed(): item = self.menu_item(Gtk.ImageMenuItem, 'split_auto', 'Split _Auto') - """ - image = Gtk.Image() - image.set_from_icon_name(APP_NAME + '_auto', Gtk.IconSize.MENU) - item.set_image(image) - if hasattr(item, 'set_always_show_image'): - item.set_always_show_image(True) - """ item.connect('activate', lambda x: terminal.emit('split-auto', - self.terminal.get_cwd())) + self.terminal.get_cwd())) menu.append(item) - item = self.menu_item(Gtk.ImageMenuItem, 'split_horiz', 'Split H_orizontally') image = Gtk.Image() @@ -195,7 +184,7 @@ def show(self, widget, event=None): if hasattr(item, 'set_always_show_image'): item.set_always_show_image(True) item.connect('activate', lambda x: terminal.emit('split-horiz', - self.terminal.get_cwd())) + self.terminal.get_cwd())) menu.append(item) item = self.menu_item(Gtk.ImageMenuItem, 'split_vert', @@ -206,18 +195,18 @@ def show(self, widget, event=None): if hasattr(item, 'set_always_show_image'): item.set_always_show_image(True) item.connect('activate', lambda x: terminal.emit('split-vert', - self.terminal.get_cwd())) + self.terminal.get_cwd())) menu.append(item) item = self.menu_item(Gtk.MenuItem, 'new_tab', 'Open _Tab') item.connect('activate', lambda x: terminal.emit('tab-new', False, - terminal)) + terminal)) menu.append(item) if self.terminator.debug_address is not None: item = Gtk.MenuItem.new_with_mnemonic(_('Open _Debug Tab')) item.connect('activate', lambda x: - terminal.emit('tab-new', True, terminal)) + terminal.emit('tab-new', True, terminal)) menu.append(item) menu.append(Gtk.SeparatorMenuItem()) @@ -249,7 +238,7 @@ def show(self, widget, event=None): menu.append(Gtk.SeparatorMenuItem()) - if self.config['show_titlebar'] == False: + if self.config['show_titlebar'] is False: item = Gtk.MenuItem.new_with_mnemonic(_('Grouping')) submenu = self.terminal.populate_group_menu() submenu.show_all() @@ -264,12 +253,12 @@ def show(self, widget, event=None): menu.append(Gtk.SeparatorMenuItem()) item = self.menu_item(Gtk.CheckMenuItem, 'toggle_readonly', '_Read only') - item.set_active(not(terminal.vte.get_input_enabled())) + item.set_active(not terminal.vte.get_input_enabled()) item.connect('toggled', lambda x: terminal.do_readonly_toggle()) menu.append(item) item = self.menu_item(Gtk.CheckMenuItem, 'toggle_scrollbar', - 'Show _scrollbar') + 'Show _scrollbar') item.set_active(terminal.scrollbar.get_property('visible')) item.connect('toggled', lambda x: terminal.do_scrollbar_toggle()) menu.append(item) @@ -311,19 +300,19 @@ def show(self, widget, event=None): plugins = registry.get_plugins_by_capability('terminal_menu') for menuplugin in plugins: menuplugin.callback(menuitems, menu, terminal) - + if len(menuitems) > 0: menu.append(Gtk.SeparatorMenuItem()) for menuitem in menuitems: menu.append(menuitem) except Exception as ex: - err('TerminalPopupMenu::show: %s' % ex) + err(f'TerminalPopupMenu::show: {ex}') menu.show_all() menu.popup_at_pointer(None) - return(True) + return True def add_layout_launcher(self, menu): """Add the layout list to the menu""" @@ -333,6 +322,6 @@ def add_layout_launcher(self, menu): item.set_submenu(submenu) layouts = self.config.list_layouts() for layout in layouts: - item = Gtk.MenuItem(layout) - item.connect('activate', lambda x: spawn_new_terminator(self.terminator.origcwd, ['-u', '-l', x.get_label()])) - submenu.append(item) + item = Gtk.MenuItem(layout) + item.connect('activate', lambda x: spawn_new_terminator(self.terminator.origcwd, ['-u', '-l', x.get_label()])) + submenu.append(item) diff --git a/terminatorlib/window.py b/terminatorlib/window.py index d0a15b4e..1f684a94 100644 --- a/terminatorlib/window.py +++ b/terminatorlib/window.py @@ -4,7 +4,6 @@ import copy import time -import uuid import gi from gi.repository import GObject from gi.repository import Gtk, Gdk @@ -33,6 +32,7 @@ err('Unable to load Keybinder module. This means the \ hide_window shortcut will be unavailable') + # pylint: disable-msg=R0904 class Window(Container, Gtk.Window): """Class implementing a top-level Terminator window""" @@ -50,7 +50,7 @@ class Window(Container, Gtk.Window): set_pos_by_ratio = None last_active_term = None preventHide = None - + cached_maker = None zoom_data = None term_zoomed = False @@ -74,15 +74,16 @@ def __init__(self): self.get_style_context().add_class("terminator-terminal-window") -# self.set_property('allow-shrink', True) # FIXME FOR GTK3, or do we need this actually? - icon_to_apply='' + # FIXME FOR GTK3, or do we need this actually? + # self.set_property('allow-shrink', True) + icon_to_apply = '' self.register_callbacks() self.apply_config() self.title = WindowTitle(self) self.title.update() - + self.preventHide = False options = self.config.options_get() @@ -92,14 +93,13 @@ def __init__(self): if options.role: self.set_role(options.role) - + if options.forcedicon is not None: icon_to_apply = options.forcedicon if options.geometry: if not self.parse_geometry(options.geometry): - err('Window::__init__: Unable to parse geometry: %s' % - options.geometry) + err(f'Window::__init__: Unable to parse geometry: {options.geometry}') self.apply_icon(icon_to_apply) self.pending_set_rough_geometry_hint = False @@ -108,16 +108,15 @@ def __init__(self): def do_get_property(self, prop): """Handle gobject getting a property""" if prop.name in ['term_zoomed', 'term-zoomed']: - return(self.term_zoomed) - else: - raise AttributeError('unknown property %s' % prop.name) + return self.term_zoomed + raise AttributeError(f'unknown property {prop.name}') def do_set_property(self, prop, value): """Handle gobject setting a property""" if prop.name in ['term_zoomed', 'term-zoomed']: self.term_zoomed = value else: - raise AttributeError('unknown property %s' % prop.name) + raise AttributeError(f'unknown property {prop.name}') def register_callbacks(self): """Connect the GTK+ signals we care about""" @@ -131,20 +130,21 @@ def register_callbacks(self): # Attempt to grab a global hotkey for hiding the window. # If we fail, we'll never hide the window, iconifying instead. - if self.config['keybindings']['hide_window'] not in ('', None): - if display_manager() == 'X11': - try: - self.hidebound = Keybinder.bind( - self.config['keybindings']['hide_window'], - self.on_hide_window) - except (KeyError, NameError): - pass - - if not self.hidebound: - err('Unable to bind hide_window key, another instance/window has it.') - self.hidefunc = self.iconify - else: - self.hidefunc = self.hide + for binding in self.config['keybindings']['hide_window']: + if binding: + if display_manager() == 'X11': + try: + self.hidebound = Keybinder.bind( + binding, + self.on_hide_window) + except (KeyError, NameError): + pass + + if not self.hidebound: + err('Unable to bind hide_window key, another instance/window has it.') + self.hidefunc = self.iconify + else: + self.hidefunc = self.hide def apply_config(self): """Apply various configuration options""" @@ -189,7 +189,7 @@ def apply_icon(self, requested_icon): self.set_icon_from_file(requested_icon) return except (NameError, GObject.GError): - dbg('Unable to load %s icon as file' % (repr(requested_icon))) + dbg(f'Unable to load {repr(requested_icon)} icon as file') icon_name_list.insert(0, requested_icon) @@ -197,39 +197,36 @@ def apply_icon(self, requested_icon): # Test if the icon is available first if icon_theme.lookup_icon(icon_name, 48, 0): self.set_icon_name(icon_name) - return # Success! We're done. - else: - dbg('Unable to load %s icon' % (icon_name)) + return # Success! We're done. + dbg(f'Unable to load {icon_name} icon') icon = self.render_icon(Gtk.STOCK_DIALOG_INFO, Gtk.IconSize.BUTTON) self.set_icon(icon) def on_key_press(self, window, event): """Handle a keyboard event""" - maker = Factory() - self.set_urgency_hint(False) mapping = self.terminator.keybindings.lookup(event) if mapping: - dbg('looked up %r' % mapping) + dbg(rf'looked up {mapping}') if mapping == 'full_screen': self.set_fullscreen(not self.isfullscreen) elif mapping == 'close_window': if not self.on_delete_event(window, - Gdk.Event.new(Gdk.EventType.DELETE)): + Gdk.Event.new(Gdk.EventType.DELETE)): self.on_destroy_event(window, - Gdk.Event.new(Gdk.EventType.DESTROY)) + Gdk.Event.new(Gdk.EventType.DESTROY)) else: - return(False) - return(True) + return False + return True def on_button_press(self, window, event): """Handle a mouse button event. Mainly this is just a clean way to cancel any urgency hints that are set.""" self.set_urgency_hint(False) - return(False) + return False def on_focus_out(self, window, event): """Focus has left the window""" @@ -255,7 +252,7 @@ def on_focus_in(self, window, event): def is_child_notebook(self): """Returns True if this Window's child is a Notebook""" maker = Factory() - return(maker.isinstance(self.get_child(), 'Notebook')) + return maker.isinstance(self.get_child(), 'Notebook') def tab_new(self, widget=None, debugtab=False, _param1=None, _param2=None): """Make a new tab""" @@ -272,7 +269,7 @@ def tab_new(self, widget=None, debugtab=False, _param1=None, _param2=None): maker = Factory() if not self.is_child_notebook(): dbg('Making a new Notebook') - notebook = maker.make('Notebook', window=self) + _ = maker.make('Notebook', window=self) self.show() self.present() return self.get_child().newtab(debugtab, cwd=cwd, profile=profile) @@ -280,15 +277,13 @@ def tab_new(self, widget=None, debugtab=False, _param1=None, _param2=None): def on_delete_event(self, window, event, data=None): """Handle a window close request""" maker = Factory() - child = self.get_child() if (maker.isinstance(child, 'Terminal') or - maker.isinstance(child, 'Container')): + maker.isinstance(child, 'Container')): confirm_close = self.construct_confirm_close(window, child) - return (confirm_close != Gtk.ResponseType.ACCEPT) - else: - dbg('unknown child: %s' % child) - return False # close anyway + return confirm_close != Gtk.ResponseType.ACCEPT + dbg(f'unknown child: {child}') + return False # close anyway def on_destroy_event(self, widget, data=None): """Handle window destruction""" @@ -300,24 +295,25 @@ def on_destroy_event(self, widget, data=None): # terminal.describe_layout() while terminal is closing. # Also while receiving event on Plugins Side, if connected to term # we can't use close-term as it starts to close terminal, so we - # send a pre-close-term before Example: Plugin SaveLastSessionLayout + # send a pre-close-term before + # Example: Plugin SaveLastSessionLayout terminal.emit('pre-close-term') terminal.close() self.cnxids.remove_all() self.terminator.deregister_window(self) self.isDestroyed = True self.destroy() - del(self) + del self def on_hide_window(self, data=None): """Handle a request to hide/show the window""" if not self.isDestroyed: if not self.get_property('visible'): - #Don't show if window has just been hidden because of - #lost focus + # Don't show if window has just been hidden because of + # lost focus if (time.time() - self.losefocus_time < 0.1) and \ - self.config['hide_on_lose_focus']: + self.config['hide_on_lose_focus']: return if self.position: self.move(self.position[0], self.position[1]) @@ -335,43 +331,42 @@ def on_hide_window(self, data=None): # pylint: disable-msg=W0613 def on_window_state_changed(self, window, event): """Handle the state of the window changing""" - self.isfullscreen = bool(event.new_window_state & + self.isfullscreen = bool(event.new_window_state & Gdk.WindowState.FULLSCREEN) self.ismaximised = bool(event.new_window_state & - Gdk.WindowState.MAXIMIZED) - dbg('fullscreen=%s, maximised=%s' \ - % (self.isfullscreen, self.ismaximised)) + Gdk.WindowState.MAXIMIZED) + dbg(f'fullscreen={self.isfullscreen}, maximised={self.ismaximised}') - return(False) + return False def set_maximised(self, value): """Set the maximised state of the window from the supplied value""" - if value == True: + if value is True: self.maximize() else: self.unmaximize() def set_fullscreen(self, value): """Set the fullscreen state of the window from the supplied value""" - if value == True: + if value is True: self.fullscreen() else: self.unfullscreen() def set_borderless(self, value): """Set the state of the window border from the supplied value""" - self.set_decorated (not value) + self.set_decorated(not value) def set_hidden(self, value): """Set the visibility of the window from the supplied value""" - if value == True: + if value is True: self.ignore_startup_show = True else: self.ignore_startup_show = False def set_iconified(self, value): """Set the minimised state of the window from the supplied value""" - if value == True: + if value is True: self.iconify() def set_always_on_top(self, value): @@ -380,12 +375,12 @@ def set_always_on_top(self, value): def set_sticky(self, value): """Set the sticky hint from the supplied value""" - if value == True: + if value is True: self.stick() def set_real_transparency(self, value=True): """Enable RGBA if supported on the current screen""" - if self.is_composited() == False: + if self.is_composited() is False: value = False screen = self.get_screen() @@ -394,21 +389,20 @@ def set_real_transparency(self, value=True): visual = screen.get_rgba_visual() if visual: self.set_visual(visual) - + def show(self, startup=False): """Undo the startup show request if started in hidden mode""" - #Present is necessary to grab focus when window is hidden from taskbar. - #It is important to call present() before show(), otherwise the window - #won't be brought to front if an another application has the focus. - #Last note: present() will implicitly call Gtk.Window.show() + # Present is necessary to grab focus when window is hidden from taskbar. + # It is important to call present() before show(), otherwise the window + # won't be brought to front if an another application has the focus. + # Last note: present() will implicitly call Gtk.Window.show() self.present() - #Window must be shown, then hidden for the hotkeys to be registered - if (self.ignore_startup_show and startup == True): + # Window must be shown, then hidden for the hotkeys to be registered + if self.ignore_startup_show and startup is True: self.position = self.get_position() self.hide() - def add(self, widget, metadata=None): """Add a widget to the window by way of Gtk.Window.add()""" maker = Factory() @@ -437,9 +431,8 @@ def add(self, widget, metadata=None): 'rotate-cw': [self.rotate, True], 'rotate-ccw': [self.rotate, False]} - for signal in signals: + for signal, handler in signals.items(): args = [] - handler = signals[signal] if isinstance(handler, list): args = handler[1:] handler = handler[0] @@ -451,13 +444,13 @@ def remove(self, widget): """Remove our child widget by way of Gtk.Window.remove()""" Gtk.Window.remove(self, widget) self.disconnect_child(widget) - return(True) + return True def get_children(self): """Return a single list of our child""" children = [] children.append(self.get_child()) - return(children) + return children def hoover(self): """Ensure we still have a reason to exist""" @@ -484,7 +477,7 @@ def split_axis(self, widget, vertical=True, cwd=None, sibling=None, widgetfirst= container = maker.make('VPaned') else: container = maker.make('HPaned') - + self.set_pos_by_ratio = True if not sibling: @@ -508,7 +501,7 @@ def split_axis(self, widget, vertical=True, cwd=None, sibling=None, widgetfirst= for term in order: container.add(term) container.show_all() - + while Gtk.events_pending(): Gtk.main_iteration_do(False) sibling.grab_focus() @@ -530,7 +523,7 @@ def is_zoomed(self): err('failed to get "term_zoomed" property') return False - def zoom(self, widget, font_scale=True): + def zoom(self, widget, fontscale=True): """Zoom a terminal widget""" maker = Factory() children = self.get_children() @@ -543,7 +536,7 @@ def zoom(self, widget, font_scale=True): self.zoom_data = widget.get_zoom_data() self.zoom_data['widget'] = widget self.zoom_data['old_child'] = children[0] - self.zoom_data['font_scale'] = font_scale + self.zoom_data['font_scale'] = fontscale old_parent = self.zoom_data['old_parent'] if maker.isinstance(old_parent, 'Notebook'): @@ -555,9 +548,9 @@ def zoom(self, widget, font_scale=True): self.add(widget) self.set_property('term_zoomed', True) - if font_scale: - widget.cnxids.new(widget, 'size-allocate', - widget.zoom_scale, self.zoom_data) + if fontscale: + widget.cnxids.new(widget, 'size-allocate', + widget.zoom_scale, self.zoom_data) widget.grab_focus() @@ -628,19 +621,19 @@ def rotate(self, widget, clockwise): self.set_pos_by_ratio = False def get_terminals(self): - return(util.enumerate_descendants(self)[1]) - + return util.enumerate_descendants(self)[1] + def get_visible_terminals(self): """Walk down the widget tree to find all of the visible terminals. Mostly using Container::get_visible_terminals()""" terminals = {} - if not hasattr(self, 'cached_maker'): + if not hasattr(self, 'cached_maker') or self.cached_maker is None: self.cached_maker = Factory() maker = self.cached_maker child = self.get_child() if not child: - return([]) + return [] # If our child is a Notebook, reset to work from its visible child if maker.isinstance(child, 'Notebook'): @@ -652,22 +645,22 @@ def get_visible_terminals(self): elif maker.isinstance(child, 'Terminal'): terminals[child] = child.get_allocation() else: - err('Unknown child type %s' % type(child)) + err(f'Unknown child type {type(child)}') - return(terminals) + return terminals def get_focussed_terminal(self): """Find which terminal we want to have focus""" terminals = self.get_visible_terminals() for terminal in terminals: if terminal.vte.is_focus(): - return(terminal) - return(None) + return terminal + return None def deferred_set_rough_geometry_hints(self): # no parameters are used in set_rough_geometry_hints, so we can # use the set_rough_geometry_hints - if self.pending_set_rough_geometry_hint == True: + if self.pending_set_rough_geometry_hint is True: return self.pending_set_rough_geometry_hint = True GObject.idle_add(self.do_deferred_set_rough_geometry_hints) @@ -679,9 +672,9 @@ def do_deferred_set_rough_geometry_hints(self): def set_rough_geometry_hints(self): """Walk all the terminals along the top and left edges to fake up how many columns/rows we sort of have""" - if self.ismaximised == True: + if self.ismaximised is True: return - if not hasattr(self, 'cached_maker'): + if not hasattr(self, 'cached_maker') or self.cached_maker is None: self.cached_maker = Factory() maker = self.cached_maker if maker.isinstance(self.get_child(), 'Notebook'): @@ -700,17 +693,17 @@ def set_rough_geometry_hints(self): if rect.y == 0: cols, rows = terminal.get_size() column_sum = column_sum + cols + _terminal = terminal if column_sum == 0 or row_sum == 0: - dbg('column_sum=%s,row_sum=%s. No terminals found in >=1 axis' % - (column_sum, row_sum)) + dbg(f'{column_sum=},{row_sum=}. No terminals found in >=1 axis') return # FIXME: I don't think we should just use whatever font size info is on # the last terminal we inspected. Looking up the default profile font # size and calculating its character sizes would be rather expensive # though. - font_width, font_height = terminal.get_font_size() + font_width, font_height = _terminal.get_font_size() total_font_width = font_width * column_sum total_font_height = font_height * row_sum @@ -718,9 +711,8 @@ def set_rough_geometry_hints(self): extra_width = win_width - total_font_width extra_height = win_height - total_font_height - dbg('setting geometry hints: (ewidth:%s)(eheight:%s),\ -(fwidth:%s)(fheight:%s)' % (extra_width, extra_height, - font_width, font_height)) + dbg(f'setting geometry hints: ({extra_width=})({extra_height}),\ +({font_width=})({font_height=})') geometry = Gdk.Geometry() geometry.base_width = extra_width geometry.base_height = extra_height @@ -796,7 +788,8 @@ def group_win(self, widget): """Group all terminals in the current window""" # FIXME: Why isn't this being done by Terminator() ? dbg("Group Windows") - group = _('Window group %s' % (len(self.terminator.groups) + 1)) + group_str = _('Window group') + group = f'{group_str} {len(self.terminator.groups) + 1}' self.terminator.create_group(group) self.set_groups(group, self.get_terminals()) @@ -843,7 +836,7 @@ def ungroup_tab(self, widget): if not maker.isinstance(notebook, 'Notebook'): dbg('note in a notebook, refusing to ungroup tab') return - + self.set_groups(None, self.get_visible_terminals()) def move_tab(self, widget, direction): @@ -855,10 +848,10 @@ def move_tab(self, widget, direction): notebook = self.get_child() if not maker.isinstance(notebook, 'Notebook'): - dbg('not in a notebook, refusing to move tab %s' % direction) + dbg(f'not in a notebook, refusing to move tab {direction}') return - dbg('moving tab %s' % direction) + dbg(f'moving tab {direction}') numpages = notebook.get_n_pages() page = notebook.get_current_page() child = notebook.get_nth_page(page) @@ -874,9 +867,9 @@ def move_tab(self, widget, direction): else: page = page + 1 else: - err('unknown direction: %s' % direction) + err(f'unknown direction: {direction}') return - + notebook.reorder_child(child, page) def navigate_terminal(self, terminal, direction): @@ -890,7 +883,7 @@ def navigate_terminal(self, terminal, direction): visibles = self.get_visible_terminals() current = terminals.index(terminal) length = len(terminals) - next = None + _next = None if length <= 1 or len(visibles) <= 1: return @@ -903,11 +896,11 @@ def navigate_terminal(self, terminal, direction): if direction == 'next': tmpterms.reverse() - next = 0 + _next = 0 while len(tmpterms) > 0: tmpitem = tmpterms.pop() if tmpitem in visibles: - next = terminals.index(tmpitem) + _next = terminals.index(tmpitem) break elif direction in ['left', 'right', 'up', 'down']: layout = self.get_visible_terminals() @@ -937,7 +930,7 @@ def navigate_terminal(self, terminal, direction): keys = list(offsets.values()) keys.sort() winners = [k for k, v in offsets.items() if v == keys[0]] - next = terminals.index(winners[0]) + _next = terminals.index(winners[0]) if len(winners) > 1: # Break an n-way tie using the cursor position @@ -947,29 +940,29 @@ def navigate_terminal(self, terminal, direction): for term in winners: rect = layout[term] if util.get_nav_tiebreak(direction, cursor_x, cursor_y, - rect): - next = terminals.index(term) - break; + rect): + _next = terminals.index(term) + break else: - err('Unknown navigation direction: %s' % direction) + err(f'Unknown navigation direction: {direction}') - if next is not None: - terminals[next].grab_focus() + if _next is not None: + terminals[_next].grab_focus() def create_layout(self, layout): """Apply any config items from our layout""" if 'children' not in layout: - err('layout describes no children: %s' % layout) + err(f'layout describes no children: {layout}') return children = layout['children'] if len(children) != 1: # We're a Window, we can only have one child - err('incorrect number of children for Window: %s' % layout) + err(f'incorrect number of children for Window: {layout}') return child = children[list(children.keys())[0]] terminal = self.get_children()[0] - dbg('Making a child of type: %s' % child['type']) + dbg(f"Making a child of type: {child['type']}") if child['type'] == 'VPaned': self.split_axis(terminal, True) elif child['type'] == 'HPaned': @@ -983,17 +976,18 @@ def create_layout(self, layout): elif child['type'] == 'Terminal': pass else: - err('unknown child type: %s' % child['type']) + err(f"unknown child type: {child['type']}") return self.get_children()[0].create_layout(child) - if 'last_active_term' in layout and layout['last_active_term'] not in ['', None]: + if layout.get('last_active_term'): self.last_active_term = make_uuid(layout['last_active_term']) - if 'last_active_window' in layout and layout['last_active_window'] == 'True': + if layout.get('last_active_window') == 'True': self.terminator.last_active_window = self.uuid + class WindowTitle(object): """Class to handle the setting of the window title""" @@ -1022,13 +1016,7 @@ def force_title(self, newtext): def update(self): """Update the title automatically""" - title = None - - # FIXME: What the hell is this for?! - if self.forced: - title = self.text - else: - title = "%s" % self.text + title = str(self.text) self.window.set_title(title) diff --git a/tests/test_prefseditor_keybindings.py b/tests/test_prefseditor_keybindings.py index 56ee3b74..11f492be 100644 --- a/tests/test_prefseditor_keybindings.py +++ b/tests/test_prefseditor_keybindings.py @@ -57,7 +57,8 @@ def test_non_empty_default_keybinding_accels_are_distinct(): all_default_accelerators = [ Gtk.accelerator_parse(accel) - for accel in config.DEFAULTS["keybindings"].values() + for accels in config.DEFAULTS["keybindings"].values() + for accel in accels if accel != "" # ignore empty key bindings ] @@ -108,7 +109,8 @@ def test_message_dialog_is_shown_on_duplicate_accel_assignment( # Replace default accelerator with a test one prefs_editor.on_cellrenderer_accel_edited( - liststore=liststore, path=path, key=key, mods=mods, _code=code + liststore=liststore, path=path, key=key, mods=mods, _code=code, + key_index=2, mod_index=3, binding_index=0 ) assert message_dialog.has_appeared == expected @@ -121,12 +123,12 @@ def test_message_dialog_is_shown_on_duplicate_accel_assignment( [ # 1) 'edit_tab_title' Ctrl+Alt+A ("9", 97, CONTROL_ALT_MOD, 38), - # 2) 'edit_terminal_title' Ctrl+Alt+A - ("10", 97, CONTROL_ALT_MOD, 38), + # 2) 'move_tab_right' Ctrl+Alt+A + ("33", 97, CONTROL_ALT_MOD, 38), # 3) 'edit_window_title' F11 ("11", 65480, Gdk.ModifierType(0), 95), - # 4) 'zoom_in' Shift+Ctrl+Z - ("70", 122, CONTROL_SHIFT_MOD, 52), + # 4) 'move_tab_left' Shift+Ctrl+Z + ("32", 122, CONTROL_SHIFT_MOD, 52), ], ) def test_duplicate_accels_not_possible_to_set(accel_params): @@ -157,23 +159,25 @@ def test_duplicate_accels_not_possible_to_set(accel_params): all_default_accelerators = { Gtk.accelerator_parse(accel) - for accel in config.DEFAULTS["keybindings"].values() + for accels in config.DEFAULTS["keybindings"].values() + for accel in accels if accel != "" # ignore empty key bindings } # Check that a test accelerator is indeed a duplicate assert (key, mods) in all_default_accelerators default_accelerator = Gtk.accelerator_parse( - config.DEFAULTS["keybindings"][binding] + config.DEFAULTS["keybindings"][binding][0] ) # Replace default accelerator with a test one prefs_editor.on_cellrenderer_accel_edited( - liststore=liststore, path=path, key=key, mods=mods, _code=code + liststore=liststore, path=path, key=key, mods=mods, _code=code, + key_index=2, mod_index=3, binding_index=0 ) new_accelerator = Gtk.accelerator_parse( - prefs_editor.config["keybindings"][binding] + prefs_editor.config["keybindings"][binding][0] ) # Key binding accelerator value shouldn't have changed @@ -195,17 +199,20 @@ def test_duplicate_accels_not_possible_to_set(accel_params): (Gdk.KEY_a, Gdk.ModifierType.CONTROL_MASK, 38), (Gdk.KEY_a, Gdk.ModifierType.CONTROL_MASK), ), - # 3) `Ctrl+Shift+a` shouldn't change - #((Gdk.KEY_a, CONTROL_SHIFT_MOD, 38), (Gdk.KEY_a, CONTROL_SHIFT_MOD),), - # 4) `Ctrl+Shift+Alt+F1` shouldn't change + # 3) `Ctrl+Shift+y` shouldn't change ( - (Gdk.KEY_F1, CONTROL_ALT_SHIFT_MOD, 67), - (Gdk.KEY_F1, CONTROL_ALT_SHIFT_MOD), + (Gdk.KEY_y, CONTROL_SHIFT_MOD, 38), + (Gdk.KEY_y, CONTROL_SHIFT_MOD), ), - # 5) `Shift+Up` shouldn't change + # 4) `Ctrl+Shift+Alt+U` shouldn't change ( - (Gdk.KEY_Up, Gdk.ModifierType.SHIFT_MASK, 111), - (Gdk.KEY_Up, Gdk.ModifierType.SHIFT_MASK), + (Gdk.KEY_u, CONTROL_ALT_SHIFT_MOD, 67), + (Gdk.KEY_u, CONTROL_ALT_SHIFT_MOD), + ), + # 5) `Ctrl+Alt+S` shouldn't change + ( + (Gdk.KEY_s, CONTROL_ALT_MOD, 111), + (Gdk.KEY_s, CONTROL_ALT_MOD), ), # 6) `Ctrl+Shift+[` should become `Ctrl+{` ( @@ -231,6 +238,11 @@ def test_keybinding_edit_produce_expected_accels( term = terminal.Terminal() prefs_editor = prefseditor.PrefsEditor(term=term) + message_dialog = MessageDialogToken() + # Check for an active message dialog every second + GLib.timeout_add_seconds( + 1, detect_close_message_dialog, prefs_editor, message_dialog + ) widget = prefs_editor.builder.get_object("keybindingtreeview") treemodelfilter = widget.get_model() @@ -245,6 +257,9 @@ def test_keybinding_edit_produce_expected_accels( key=key, mods=mods, _code=hardware_keycode, + key_index=2, + mod_index=3, + binding_index=0, ) liststore_iter = liststore.get_iter(path) @@ -284,23 +299,31 @@ def test_keybinding_successfully_reassigned_after_clearing(accel_params): liststore = treemodelfilter.get_model() path, key, mods, hardware_keycode = accel_params - # Assign a key binding + # Assign a key binding to keybinding 1 prefs_editor.on_cellrenderer_accel_edited( liststore=liststore, path=path, key=key, mods=mods, _code=hardware_keycode, + key_index=2, + mod_index=3, + binding_index=0, ) - # Clear the key binding - prefs_editor.on_cellrenderer_accel_cleared(liststore=liststore, path=path) - # Reassign the key binding + # Clear the key binding for keybinding 1 + prefs_editor.on_cellrenderer_accel_cleared(liststore=liststore, path=path, + key_index=2, mod_index=3, + binding_index=0) + # Reassign the key binding for keybinding 1 prefs_editor.on_cellrenderer_accel_edited( liststore=liststore, path=path, key=key, mods=mods, _code=hardware_keycode, + key_index=2, + mod_index=3, + binding_index=0, ) reset_config_keybindings()